packages feed

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

{-# LANGUAGE OverloadedStrings #-}
module Test.Database.LSMTree.Internal.PageAcc (tests) where

import           Control.Exception
import           Control.Monad (forM_)
import           Control.Monad.ST.Strict (ST, runST, stToIO)
import qualified Data.ByteString as BS
import           Data.Maybe (isJust)
import           Data.Word

import           Database.LSMTree.Internal.BlobRef (BlobSpan (..))
import           Database.LSMTree.Internal.Entry (Entry (..))
import           Database.LSMTree.Internal.PageAcc
import           Database.LSMTree.Internal.PageAcc1
import           Database.LSMTree.Internal.RawPage (RawPage)
import           Database.LSMTree.Internal.Serialise

import           Database.LSMTree.Extras.NoThunks (propNoThunks)
import qualified Database.LSMTree.Extras.ReferenceImpl as Ref
import           Test.Util.RawPage (propEqualRawPages)

import           Test.QuickCheck
import           Test.QuickCheck.Instances ()
import           Test.Tasty (TestTree, testGroup)
import           Test.Tasty.QuickCheck (testProperty)

tests :: TestTree
tests =
  testGroup "Database.LSMTree.Internal.PageAcc" $
    [ testProperty "vs reference impl" prop_vsReferenceImpl
    , testProperty "maxPageKeys == 759" (maxPageKeys === 759)
    ]

 ++ [ testProperty
        ("example-" ++ show (n :: Int) ++ [ a | length exs > 1 ])
        (prop_vsReferenceImpl (Ref.PageContentMaybeOverfull kops))
    | (n, exs)  <- zip [0..] examples
    , (a, kops) <- zip ['a'..] exs
    ]

 ++ [ testProperty "+PageAcc1" prop_vsRefWithPageAcc1
    ]

 ++ [ testProperty "prop_noThunks_newPageAcc" prop_noThunks_newPageAcc
    , testProperty "prop_noThunks_pageAccAddElem" prop_noThunks_pageAccAddElem
    , testProperty "prop_noThunks_resetPageAcc" prop_noThunks_resetPageAcc
    ]

  where
    examples = example0123 ++ [example4s, example5s, example6s, example7s]

    example0123 =
      map (:[])
      [ []
      , [(Ref.Key "foobar", Ref.Delete)]
      , [(Ref.Key "foobar", Ref.Insert (Ref.Value "value")
                                       (Just (Ref.BlobRef 111 333)))]
      , [(Ref.Key "\NUL", Ref.Delete), (Ref.Key "\SOH", Ref.Delete)]
      ]

    example4s = [ [(Ref.Key "", Ref.Insert (Ref.Value (BS.replicate sz 120))
                                           Nothing)]
                | sz <- [4063..4065] ]

    example5s = [ [ (Ref.Key "",Ref.Delete)
                  , (Ref.Key "k", Ref.Insert (Ref.Value (BS.replicate sz 120))
                                             Nothing) ]
                | sz <- [4060..4062] ]

    example6s = [ [(Ref.Key "", Ref.Insert (Ref.Value (BS.replicate sz 120))
                                           (Just (Ref.BlobRef 111 333))) ]
                | sz <- [4051..4053] ]

    example7s = [ (replicate maxPageKeys     (Ref.Key " ",Ref.Delete))
                , (replicate (maxPageKeys+1) (Ref.Key " ",Ref.Delete))
                , (replicate (maxPageKeys+1) (Ref.Key "", Ref.Delete))
                ]

maxPageKeys :: Int
maxPageKeys =
    go 0 (Ref.pageSizeEmpty Ref.DiskPage4k)
  where
    go s ps =
      case Ref.pageSizeAddElem (Ref.Key " ", Ref.Delete) ps of
        Nothing  -> s
        Just ps' -> go (s + 1) ps'

prop_vsReferenceImpl :: Ref.PageContentMaybeOverfull -> Property
prop_vsReferenceImpl (Ref.PageContentMaybeOverfull kops) =
    case (refImpl, realImpl) of
      (Just (lhs, _), Just rhs) -> propEqualRawPages lhs rhs
      (Nothing,       Nothing)  -> label "overflow" $
                                   property True

      -- Special cases where we allow a test pass.
      (Just _,        Nothing)
        -- The PageAcc does not support single-key/op pairs that overflow onto
        -- multiple pages. That case is handled by PageAcc1.
        | [_]       <- kops
        , Just page <- Ref.encodePage Ref.DiskPage4k kops
        , Ref.pageDiskPages page > 1
                                -> label "PageAcc1 special case" $
                                   property True

        -- PageAcc (quite reasonably) assumes that keys are not all empty
        -- (since in practice they'll be distinct) and thus it can impose an
        -- upper bound on the number of keys in a page. It's possible to
        -- construct test cases with empty keys that exceed the buffer size.
        | length kops >= maxPageKeys
                                -> label "max number of keys reached" $
                                   property True

      _                         -> property False
  where
    refImpl  = Ref.toRawPageMaybeOverfull (Ref.PageContentMaybeOverfull kops)
    realImpl = toRawPageViaPageAcc [ (Ref.toSerialisedKey k, Ref.toEntry op)
                                   | (k,op) <- kops ]


-- | This is like 'prop_vsReferenceImpl' bus used _both_ @PageAcc@ and
-- @PageAcc1@ together to fill in the special cases.
--
prop_vsRefWithPageAcc1 :: Ref.PageContentMaybeOverfull -> Property
prop_vsRefWithPageAcc1 (Ref.PageContentMaybeOverfull kops) =
    case (refImpl, realImpl) of
      (Just (lhs, loverflow),
       Just (rhs, roverflow)) ->
            label (show (length loverflow) ++ " overflow pages") $
            (if isJust (pageAcc1SpecialCase kops) then label "PageAcc1" else id)
              (propEqualRawPages lhs rhs)
       .&&. counterexample "overflow pages do not match"
              (loverflow === roverflow)

      (Nothing, Nothing) ->
        label "overfull" $ property True

      -- Special cases are a subset of those above in 'prop_vsReferenceImpl'.
      (Just _, Nothing) | length kops >= maxPageKeys ->
        label "max number of keys reached" $ property True

      _ -> property False
  where
    refImpl  = Ref.toRawPageMaybeOverfull (Ref.PageContentMaybeOverfull kops)

    -- Use whichever implementation is appropriate:
    realImpl
      | Just (k,op) <- pageAcc1SpecialCase kops
      = Just (singletonPage (Ref.toSerialisedKey k) (Ref.toEntry op))

      | otherwise
      = (\rp -> (rp, [])) <$>
        toRawPageViaPageAcc [ (Ref.toSerialisedKey k, Ref.toEntry op)
                            | (k,op) <- kops ]

    pageAcc1SpecialCase [(k, op)] | op /= Ref.Delete = Just (k, op)
    pageAcc1SpecialCase _                            = Nothing


-- | Use a 'PageAcc' to try to make a 'RawPage' from key\/op pairs. It will
-- return @Nothing@ if the key\/op pairs would not all fit in a page.
--
toRawPageViaPageAcc :: [(SerialisedKey, Entry SerialisedValue BlobSpan)]
                    -> Maybe RawPage
toRawPageViaPageAcc kops0 =
    runST $ do
      acc <- newPageAcc
      go acc kops0
  where
    go acc []            = Just <$> serialisePageAcc acc
    go acc ((k,op):kops) = do
      added <- pageAccAddElem acc k op
      if added
        then go acc kops
        else pure Nothing

{-------------------------------------------------------------------------------
  NoThunks
-------------------------------------------------------------------------------}

prop_noThunks_newPageAcc :: Property
prop_noThunks_newPageAcc = once $ ioProperty $ do
    pa <- stToIO newPageAcc
    propNoThunks pa

prop_noThunks_pageAccAddElem :: Property
prop_noThunks_pageAccAddElem = once $ ioProperty $ do
    pa <- stToIO $ do
      pa <- newPageAcc
      pageAccAddElemN pa 10
      pure pa
    propNoThunks pa

prop_noThunks_resetPageAcc :: Property
prop_noThunks_resetPageAcc = once $ ioProperty $ do
    pa <- stToIO $ do
      pa <- newPageAcc
      pageAccAddElemN pa 10
      resetPageAcc pa
      pure pa
    propNoThunks pa

pageAccAddElemN :: PageAcc s -> Word64 -> ST s ()
pageAccAddElemN pa n = do
    forM_ [1..n] $ \(x :: Word64) -> do
        b <- pageAccAddElem pa (serialiseKey x) (Insert (serialiseValue x))
        assert b $ pure ()