packages feed

lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Generators.hs

module Test.Database.LSMTree.Generators (
    tests
  ) where

import           Data.Bifoldable (bifoldMap)
import           Data.Coerce (Coercible, coerce)
import qualified Data.Map.Strict as Map
import qualified Data.Vector.Primitive as VP
import           Data.Word (Word64, Word8)
import           Database.LSMTree.Extras (showPowersOf)
import           Database.LSMTree.Extras.Generators
import           Database.LSMTree.Extras.MergingRunData
import           Database.LSMTree.Extras.MergingTreeData
import           Database.LSMTree.Extras.ReferenceImpl
import           Database.LSMTree.Extras.RunData
import           Database.LSMTree.Internal.BlobRef (BlobSpan)
import qualified Database.LSMTree.Internal.BloomFilter as Bloom
import           Database.LSMTree.Internal.Entry
import qualified Database.LSMTree.Internal.Index as Index
import qualified Database.LSMTree.Internal.MergingRun as MR
import           Database.LSMTree.Internal.PageAcc (entryWouldFitInPage,
                     sizeofEntry)
import           Database.LSMTree.Internal.Paths (RunFsPaths (..))
import           Database.LSMTree.Internal.RawBytes (RawBytes (..))
import qualified Database.LSMTree.Internal.RawBytes as RB
import qualified Database.LSMTree.Internal.RunAcc as RunAcc
import qualified Database.LSMTree.Internal.RunBuilder as RunBuilder
import           Database.LSMTree.Internal.RunNumber (RunNumber (..))
import           Database.LSMTree.Internal.Serialise
import           Database.LSMTree.Internal.UniqCounter
import qualified System.FS.API as FS
import qualified System.FS.BlockIO.API as FS
import qualified System.FS.Sim.MockFS as MockFS
import qualified Test.QuickCheck as QC
import           Test.QuickCheck (Property)
import           Test.Tasty (TestTree, localOption, testGroup)
import           Test.Tasty.QuickCheck (QuickCheckMaxSize (..), testProperty,
                     (===))
import           Test.Util.Arbitrary
import           Test.Util.FS (propNoOpenHandles, withSimHasBlockIO)

tests :: TestTree
tests = testGroup "Test.Database.LSMTree.Generators" [
      testGroup "PageContentFits" $
        prop_arbitraryAndShrinkPreserveInvariant noTags
          pageContentFitsInvariant
    , testGroup "PageContentOrdered" $
        prop_arbitraryAndShrinkPreserveInvariant noTags
          pageContentOrderedInvariant
    , localOption (QuickCheckMaxSize 20) $ -- takes too long!
      testGroup "LogicalPageSummaries" $
        prop_arbitraryAndShrinkPreserveInvariant noTags $
          pagesInvariant @Word64
    , testGroup "Chunk size" $
        prop_arbitraryAndShrinkPreserveInvariant noTags
          chunkSizeInvariant
    , testGroup "RawBytes" $
        [ testProperty "packRawBytesPinnedOrUnpinned"
            prop_packRawBytesPinnedOrUnpinned
        ]
     ++ prop_arbitraryAndShrinkPreserveInvariant labelRawBytes
          (deepseqInvariant @RawBytes)
    , testGroup "LargeRawBytes" $
        prop_arbitraryAndShrinkPreserveInvariant
          (\(LargeRawBytes rb) -> labelRawBytes rb)
          (deepseqInvariant @LargeRawBytes)
    , testGroup "BiasedKey" $
          prop_arbitraryAndShrinkPreserveInvariant
            (labelTestKOps @BiasedKey)
            deepseqInvariant
    , testGroup "helpers"
        [ testProperty "prop_shrinkVec" $ \vec ->
            shrinkVec (QC.shrink @Int) vec === map VP.fromList (QC.shrink (VP.toList vec))
        ]
    , testGroup "RunData" $
        prop_arbitraryAndShrinkPreserveInvariant
          labelRunData
          noInvariant
     ++ [ testProperty "withRun doesn't leak resources" $ \rd ->
            QC.ioProperty $
              withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ ->
                prop_withRunDoesntLeak hfs hbio rd
        ]
    , testGroup "NonEmptyRunData" $
        prop_arbitraryAndShrinkPreserveInvariant
          labelNonEmptyRunData
          noInvariant
    , testGroup "MergingRunData" $
        prop_arbitraryAndShrinkPreserveInvariant
          @(SerialisedMergingRunData MR.LevelMergeType)
          labelMergingRunData
          ((=== Right ()) . mergingRunDataInvariant)
     ++ [ testProperty "withMergingRun doesn't leak resources" $ \mrd ->
            QC.ioProperty $
              withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ ->
                prop_withMergingRunDoesntLeak hfs hbio mrd
        ]
    , testGroup "MergingTreeData" $
        prop_arbitraryAndShrinkPreserveInvariant
          labelMergingTreeData
          ((=== Right ()) . mergingTreeDataInvariant)
     ++ [ testProperty "withMergingTree doesn't leak resources" $ \mtd ->
            QC.ioProperty $
              withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _ ->
                prop_withMergingTreeDoesntLeak hfs hbio mtd
        ]
    ]

testSalt :: Bloom.Salt
testSalt = 4

runParams :: Index.IndexType -> RunBuilder.RunParams
runParams indexType =
    RunBuilder.RunParams {
      runParamCaching = RunBuilder.CacheRunData,
      runParamAlloc   = RunAcc.RunAllocFixed 10,
      runParamIndex   = indexType
    }

prop_packRawBytesPinnedOrUnpinned :: Bool -> [Word8] -> Bool
prop_packRawBytesPinnedOrUnpinned pinned ws =
    packRawBytesPinnedOrUnpinned pinned ws == RawBytes (VP.fromList ws)

labelRawBytes :: RawBytes -> Property -> Property
labelRawBytes rb =
    QC.tabulate "size" [showPowersOf 2 (RB.size rb)]

type TestEntry = Entry SerialisedValue BlobSpan
type TestKOp k = (k, TestEntry)

labelTestKOps ::
     Coercible k SerialisedKey
  => [TestKOp k]
  -> Property
  -> Property
labelTestKOps kops' =
      QC.tabulate "key occurrences (>1 is collision)" (map (show . snd) (Map.assocs keyCounts))
    . QC.tabulate "key sizes" (map (showPowersOf 4 . sizeofKey) keys)
    . QC.tabulate "value sizes" (map (showPowersOf 4 . sizeofValue) values)
    . QC.tabulate "k/op sizes" (map (showPowersOf 4 . uncurry sizeofEntry) kops)
    . QC.tabulate "k/op is large" (map (show . isLarge) kops)
    . QC.checkCoverage
    . QC.cover 50 (any isLarge kops) "any k/op is large"
    . QC.cover  1 (ratioUniqueKeys < (0.9 :: Double)) ">10% of keys collide"
    . QC.cover  5 (any (> 2) keyCounts) "has key with >2 collisions"
  where
    kops = coerce kops' :: [(SerialisedKey, Entry SerialisedValue BlobSpan)]
    keys = map fst kops
    keyCounts = Map.fromListWith (+) [(k, (1 :: Int)) | k <- keys]
    uniqueKeys = Map.keys keyCounts
    ratioUniqueKeys = fromIntegral (length uniqueKeys) / fromIntegral (length keys)
    values = foldMap (bifoldMap pure mempty . snd) kops

    isLarge = not . uncurry entryWouldFitInPage

prop_withRunDoesntLeak ::
     FS.HasFS IO h
  -> FS.HasBlockIO IO h
  -> SerialisedRunData
  -> IO Property
prop_withRunDoesntLeak hfs hbio rd = do
    let indexType = Index.Ordinary
    let path = FS.mkFsPath ["something-1"]
    let fsPaths = RunFsPaths path (RunNumber 0)
    FS.createDirectory hfs path
    withRunAt hfs hbio testSalt (runParams indexType) fsPaths rd $ \_run -> do
      pure (QC.property True)

prop_withMergingRunDoesntLeak ::
     FS.HasFS IO h
  -> FS.HasBlockIO IO h
  -> SerialisedMergingRunData MR.LevelMergeType
  -> IO Property
prop_withMergingRunDoesntLeak hfs hbio mrd = do
    let indexType = Index.Ordinary
    let path = FS.mkFsPath ["something-2"]
    FS.createDirectory hfs path
    counter <- newUniqCounter 0
    withMergingRun hfs hbio resolveVal testSalt (runParams indexType) path counter mrd $
      \_mr -> do
        pure (QC.property True)

-- TODO: This only tests the happy path. For everything else, we'd need to
-- inject errors, e.g. with @simErrorHasBlockIO@.
prop_withMergingTreeDoesntLeak ::
     FS.HasFS IO h
  -> FS.HasBlockIO IO h
  -> SerialisedMergingTreeData
  -> IO Property
prop_withMergingTreeDoesntLeak hfs hbio mrd = do
    let indexType = Index.Ordinary
    let path = FS.mkFsPath ["something-3"]
    FS.createDirectory hfs path
    counter <- newUniqCounter 0
    withMergingTree hfs hbio resolveVal testSalt (runParams indexType) path counter mrd $
      \_tree -> do
        pure (QC.property True)

resolveVal :: SerialisedValue -> SerialisedValue -> SerialisedValue
resolveVal (SerialisedValue x) (SerialisedValue y) = SerialisedValue (x <> y)