lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Internal/CRC32C.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.Database.LSMTree.Internal.CRC32C (
-- * Main test tree
tests
) where
import Control.Monad
import qualified Control.Monad.IOSim as IOSim
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Short as SBS
import qualified Data.Digest.CRC32C
import qualified Data.Foldable as Fold
import Database.LSMTree.Extras (showPowersOf)
import Database.LSMTree.Internal.CRC32C
import System.FS.API
import System.FS.API.Lazy
import System.FS.API.Strict
import qualified System.FS.Sim.Error as FsSim
import qualified System.FS.Sim.MockFS as FsSim
import qualified System.FS.Sim.Stream as FsSim.Stream
import System.Posix.Types
import Test.QuickCheck
import Test.QuickCheck.Instances ()
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)
import Test.Util.FS (withTempIOHasFS)
tests :: TestTree
tests = testGroup "Database.LSMTree.Internal.CRC32C" [
testProperty "incremental" prop_incremental
, testProperty "all splits" prop_splits
, testProperty "put/get" prop_putGet
, testProperty "write/read" prop_writeRead
, testProperty "prop_hGetExactlyCRC32C_SBS" prop_hGetExactlyCRC32C_SBS
, testProperty "prop_hGetAllCRC32C'" prop_hGetAllCRC32C'
]
{-------------------------------------------------------------------------------
Properties
-------------------------------------------------------------------------------}
prop_incremental :: [BS.ByteString] -> Bool
prop_incremental bss =
Fold.foldl' (flip updateCRC32C) initialCRC32C bss
== CRC32C (Data.Digest.CRC32C.crc32c (BS.concat bss))
prop_splits :: BS.ByteString -> Bool
prop_splits bs =
and [ updateCRC32C b2 (updateCRC32C b1 initialCRC32C) == expected
| let expected = CRC32C (Data.Digest.CRC32C.crc32c bs)
, n <- [0 .. BS.length bs]
, let (b1, b2) = BS.splitAt n bs ]
prop_putGet :: PartialIOErrors -> [BS.ByteString] -> Bool
prop_putGet (PartialIOErrors errs) bss =
fst $ IOSim.runSimOrThrow $
FsSim.runSimErrorFS FsSim.empty errs $ \_ fs -> do
let path = mkFsPath ["foo"]
crc1 <- withFile fs path (WriteMode MustBeNew) $ \h ->
foldM (\crc bs -> snd <$> hPutAllCRC32C fs h bs crc)
initialCRC32C bss
crc2 <- withFile fs path ReadMode $ \h ->
let go crc = do
(bs,crc') <- hGetSomeCRC32C fs h 42 crc
if BS.null bs then pure crc' else go crc'
in go initialCRC32C
crc3 <- readFileCRC32C fs path
let expected = CRC32C (Data.Digest.CRC32C.crc32c (BS.concat bss))
pure (all (expected==) [crc1, crc2, crc3])
-- TODO: test like prop_putGet but with put corruption, to detect that the
-- crc doesn't match, to detect the corruption.
prop_writeRead :: PartialIOErrors -> ChecksumsFile -> Bool
prop_writeRead (PartialIOErrors errs) chks =
fst $ IOSim.runSimOrThrow $
FsSim.runSimErrorFS FsSim.empty errs $ \_ fs -> do
let path = mkFsPath ["foo.checksums"]
writeChecksumsFile fs path chks
chks' <- readChecksumsFile fs path
pure (chks == chks')
instance Arbitrary CRC32C where
arbitrary = CRC32C <$> arbitraryBoundedIntegral
instance Arbitrary ChecksumsFileName where
arbitrary = ChecksumsFileName <$> name
where
name = fmap BS.pack (listOf nameChar)
nameChar = arbitrary `suchThat` \w8 ->
let c = toEnum (fromIntegral w8)
in c /= '(' && c /= ')' && c /= '\n'
-- | File system simulated errors for partial reads and writes. No other
-- silent corruption or noisy exceptions. Used to test that handling of
-- partial read\/writes is robust.
--
newtype PartialIOErrors = PartialIOErrors FsSim.Errors
deriving stock Show
instance Arbitrary PartialIOErrors where
arbitrary = do
hGetSomeE <- genPartialErrorStream
hPutSomeE <- genPartialErrorStream
pure $ PartialIOErrors FsSim.emptyErrors {
FsSim.hGetSomeE,
FsSim.hPutSomeE
}
shrink (PartialIOErrors errs) = map PartialIOErrors (shrink errs)
genPartialErrorStream :: Gen (FsSim.Stream.Stream (Either a FsSim.Partial))
genPartialErrorStream =
FsSim.Stream.genInfinite
. FsSim.Stream.genMaybe 2 1 -- 2:1 normal:partial
. fmap Right
$ arbitrary
{-------------------------------------------------------------------------------
Properties for functions with user-supplied buffers
-------------------------------------------------------------------------------}
newtype BS = BS BS.ByteString
deriving stock (Show, Eq)
instance Arbitrary BS where
arbitrary = BS . BS.pack <$> scale (2*) arbitrary
shrink = fmap (BS . BS.pack) . shrink . BS.unpack . unBS
where unBS (BS x) = x
prop_hGetExactlyCRC32C_SBS :: BS -> Property
prop_hGetExactlyCRC32C_SBS (BS bs) =
ioProperty $ withTempIOHasFS "tempDir" $ \hfs -> do
withFile hfs (mkFsPath ["temp.file"]) (WriteMode MustBeNew) $ \h ->
void $ hPutAllStrict hfs h bs
x <- withFile hfs (mkFsPath ["temp.file"]) ReadMode $ \h ->
simpl hfs h initialCRC32C
y <- withFile hfs (mkFsPath ["temp.file"]) ReadMode $ \h ->
hGetExactlyCRC32C_SBS hfs h (fromIntegral $ BS.length bs) initialCRC32C
pure $ x === y
where
simpl fs h crc = do
lbs <- hGetAll fs h
let !crc' = BSL.foldlChunks (flip updateCRC32C) crc lbs
pure (SBS.toShort (BS.toStrict lbs), crc')
prop_hGetAllCRC32C':: Positive (Small ByteCount) -> BS -> Property
prop_hGetAllCRC32C' (Positive (Small chunkSize)) (BS bs) =
ioProperty $ withTempIOHasFS "tempDir" $ \hfs -> do
withFile hfs (mkFsPath ["temp.file"]) (WriteMode MustBeNew) $ \h ->
void $ hPutAllStrict hfs h bs
x <- withFile hfs (mkFsPath ["temp.file"]) ReadMode $ \h ->
simpl hfs h initialCRC32C
y <- withFile hfs (mkFsPath ["temp.file"]) ReadMode $ \h ->
hGetAllCRC32C' hfs h (ChunkSize chunkSize) initialCRC32C
pure
$ tabulate "number of chunks"
[ showPowersOf 2 $ ceiling @Double $
fromIntegral (BS.length bs) / (fromIntegral chunkSize) ]
$ x === y
where
simpl fs h crc = do
lbs <- hGetAll fs h
let !crc' = BSL.foldlChunks (flip updateCRC32C) crc lbs
pure crc'