lsm-tree-1.0.0.1: test/Test/Database/LSMTree/Internal/RunReader.hs
module Test.Database.LSMTree.Internal.RunReader (
-- * Main test tree
tests,
-- * Utilities
readKOps,
) where
import Control.RefCount
import Data.Coerce (coerce)
import qualified Data.Map as Map
import Database.LSMTree.Extras.Generators (BiasedKey (..))
import Database.LSMTree.Extras.RunData
import Database.LSMTree.Internal.BlobRef
import qualified Database.LSMTree.Internal.BloomFilter as Bloom
import Database.LSMTree.Internal.Entry (Entry)
import qualified Database.LSMTree.Internal.Index as Index (IndexType (Ordinary))
import Database.LSMTree.Internal.Run (Run)
import qualified Database.LSMTree.Internal.RunAcc as RunAcc
import qualified Database.LSMTree.Internal.RunBuilder as RunBuilder
import qualified Database.LSMTree.Internal.RunReader as Reader
import Database.LSMTree.Internal.Serialise
import qualified System.FS.API as FS
import qualified System.FS.BlockIO.API as FS
import qualified System.FS.Sim.MockFS as MockFS
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck
import Test.Util.FS (propNoOpenHandles, withSimHasBlockIO,
withTempIOHasBlockIO)
import Test.Util.Orphans ()
tests :: TestTree
tests = testGroup "Database.LSMTree.Internal.RunReader"
[ testGroup "MockFS"
[ testProperty "prop_read" $ \wb ->
ioProperty $ withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ -> do
prop_readAtOffset hfs hbio wb Nothing
, testProperty "prop_readAtOffset" $ \wb offset ->
ioProperty $ withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ -> do
prop_readAtOffset hfs hbio wb (Just offset)
, testProperty "prop_readAtOffsetExisting" $ \wb i ->
ioProperty $ withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ -> do
prop_readAtOffsetExisting hfs hbio wb i
, testProperty "prop_readAtOffsetIdempotence" $ \wb i ->
ioProperty $ withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ -> do
prop_readAtOffsetIdempotence hfs hbio wb i
, testProperty "prop_readAtOffsetReadHead" $ \wb ->
ioProperty $ withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ -> do
prop_readAtOffsetReadHead hfs hbio wb
]
, testGroup "RealFS"
[ testProperty "prop_read" $ \wb ->
ioProperty $ withTempIOHasBlockIO "tmp_RunReader" $ \hfs hbio -> do
prop_readAtOffset hfs hbio wb Nothing
, testProperty "prop_readAtOffset" $ \wb offset ->
ioProperty $ withTempIOHasBlockIO "tmp_RunReader" $ \hfs hbio -> do
prop_readAtOffset hfs hbio wb (Just offset)
, testProperty "prop_readAtOffsetExisting" $ \wb i ->
ioProperty $ withTempIOHasBlockIO "tmp_RunReader" $ \hfs hbio -> do
prop_readAtOffsetExisting hfs hbio wb i
, testProperty "prop_readAtOffsetIdempotence" $ \wb i ->
ioProperty $ withTempIOHasBlockIO "tmp_RunReader" $ \hfs hbio -> do
prop_readAtOffsetIdempotence hfs hbio wb i
, testProperty "prop_readAtOffsetReadHead" $ \wb ->
ioProperty $ withTempIOHasBlockIO "tmp_RunReader" $ \hfs hbio -> do
prop_readAtOffsetReadHead hfs hbio wb
]
]
runParams :: RunBuilder.RunParams
runParams =
RunBuilder.RunParams {
runParamCaching = RunBuilder.CacheRunData,
runParamAlloc = RunAcc.RunAllocFixed 10,
runParamIndex = Index.Ordinary
}
testSalt :: Bloom.Salt
testSalt = 4
-- | Creating a run from a write buffer and reading from the run yields the
-- original elements.
--
-- @id === read . new . flush@
--
-- If there is an offset:
--
-- @dropWhile ((< offset) . key) === read . newAtOffset offset . flush@
--
prop_readAtOffset ::
FS.HasFS IO h
-> FS.HasBlockIO IO h
-> RunData BiasedKey SerialisedValue SerialisedBlob
-> Maybe BiasedKey
-> IO Property
prop_readAtOffset fs hbio rd offsetKey =
withRefCtx $ \refCtx ->
withRunAt fs hbio refCtx testSalt runParams (simplePath 42) rd' $ \run -> do
rhs <- readKOps (coerce offsetKey) run
pure . labelRunData rd' $
counterexample ("entries expected: " <> show (length lhs)) $
counterexample ("entries found: " <> show (length rhs)) $
lhs === rhs
where
rd' = serialiseRunData rd
kops = Map.toList (unRunData rd')
lhs = case offsetKey of
Nothing -> kops
Just k -> dropWhile ((< coerce k) . fst) kops
-- | A version of 'prop_readAtOffset' where the offset key is always one
-- of the keys that exist in the run.
prop_readAtOffsetExisting ::
FS.HasFS IO h
-> FS.HasBlockIO IO h
-> RunData BiasedKey SerialisedValue SerialisedBlob
-> NonNegative Int
-> IO Property
prop_readAtOffsetExisting fs hbio rd (NonNegative index)
| null kops = pure discard
| otherwise =
prop_readAtOffset fs hbio rd (Just (keys !! (index `mod` length keys)))
where
keys :: [BiasedKey]
keys = coerce (fst <$> kops)
kops = Map.toList (unRunData rd)
-- | Idempotence of 'readAtOffset'.
-- Reading at an offset should not perform any stateful effects which alter
-- the result of a subsequent read at the same offset.
--
-- @readAtOffset offset *> readAtOffset offset === readAtOffset offset@
--
prop_readAtOffsetIdempotence ::
FS.HasFS IO h
-> FS.HasBlockIO IO h
-> RunData BiasedKey SerialisedValue SerialisedBlob
-> Maybe BiasedKey
-> IO Property
prop_readAtOffsetIdempotence fs hbio rd offsetKey =
withRefCtx $ \refCtx ->
withRunAt fs hbio refCtx testSalt runParams (simplePath 42) rd' $ \run -> do
lhs <- readKOps (coerce offsetKey) run
rhs <- readKOps (coerce offsetKey) run
pure . labelRunData rd' $
counterexample ("entries expected: " <> show (length lhs)) $
counterexample ("entries found: " <> show (length rhs)) $
lhs === rhs
where
rd' = serialiseRunData rd
-- | Head of 'read' equals 'readAtOffset' of head of 'read'.
-- Reading the first key from the run initialized without an offset
-- should be the same as reading from a run initialized at the first key.
-- the result of a subsequent read at the same offset.
--
-- @read === readAtOffset (head read)@
--
prop_readAtOffsetReadHead ::
FS.HasFS IO h
-> FS.HasBlockIO IO h
-> RunData BiasedKey SerialisedValue SerialisedBlob
-> IO Property
prop_readAtOffsetReadHead fs hbio rd =
withRefCtx $ \refCtx ->
withRunAt fs hbio refCtx testSalt runParams (simplePath 42) rd' $ \run -> do
lhs <- readKOps Nothing run
rhs <- case lhs of
[] -> pure []
(key,_):_ -> readKOps (Just key) run
pure . labelRunData rd' $
counterexample ("entries expected: " <> show (length lhs)) $
counterexample ("entries found: " <> show (length rhs)) $
lhs === rhs
where
rd' = serialiseRunData rd
{-------------------------------------------------------------------------------
Utilities
-------------------------------------------------------------------------------}
type SerialisedEntry = Entry SerialisedValue SerialisedBlob
type SerialisedKOp = (SerialisedKey, SerialisedEntry)
readKOps ::
Maybe (SerialisedKey) -- ^ offset
-> Ref (Run IO h)
-> IO [SerialisedKOp]
readKOps offset run = do
reader <- Reader.new offsetKey run
go reader
where
offsetKey = maybe Reader.NoOffsetKey (Reader.OffsetKey . coerce) offset
go reader = do
Reader.next reader >>= \case
Reader.Empty -> pure []
Reader.ReadEntry key e -> do
let fs = Reader.readerHasFS reader
e' <- traverse (readRawBlobRef fs) $ Reader.toFullEntry e
((key, e') :) <$> go reader