lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Internal/Chunk.hs
module Test.Database.LSMTree.Internal.Chunk (tests) where
import Prelude hiding (concat, drop, length)
import Control.Category ((>>>))
import Control.Monad.ST.Strict (runST)
import qualified Data.List as List (concat, drop, length)
import Data.Maybe (catMaybes, fromJust, isJust, isNothing)
import Data.Vector.Primitive (Vector, concat, length)
import Data.Word (Word8)
import Database.LSMTree.Extras.Generators ()
-- for @Arbitrary@ instantiation of @Vector@
import Database.LSMTree.Internal.Chunk (Chunk, createBaler, feedBaler,
toByteVector, unsafeEndBaler)
import Test.QuickCheck (Arbitrary (arbitrary, shrink),
NonEmptyList (NonEmpty), Positive (Positive, getPositive),
Property, Small (Small, getSmall), Testable, scale,
shrinkMap, tabulate, (===), (==>))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)
-- * Tests
tests :: TestTree
tests = testGroup "Test.Database.LSMTree.Internal.Chunk" $
[
testProperty "Content is preserved"
prop_contentIsPreserved,
testProperty "No remnant after output"
prop_noRemnantAfterOutput,
testProperty "Common chunks are large"
prop_commonChunksAreLarge,
testProperty "Remnant chunk is non-empty"
prop_remnantChunkIsNonEmpty,
testProperty "Remnant chunk is small"
prop_remnantChunkIsSmall
]
-- * Properties to test
{-|
Feeds a freshly created baler a sequence of data portions and ends it
afterwards, yielding all output.
-}
balingOutput :: Int -- Minimum chunk size
-> [[Vector Word8]] -- Data portions to be fed
-> ([Maybe Chunk], Maybe Chunk) -- Feeding output and remnant
balingOutput minChunkSize food = runST $ do
baler <- createBaler minChunkSize
commonChunks <- mapM (flip feedBaler baler) food
remnant <- unsafeEndBaler baler
pure (commonChunks, remnant)
{-|
Supplies the output of a complete baler run for constructing a property.
The resulting property additionally provides statistics about the lengths of
buildup phases, where a buildup phase is a sequence of feedings that does
not result in chunks and is followed by an ultimate chunk production, which
happens either due to another feeding or due to the baler run ending and
producing a remnant chunk.
-}
withBalingOutput ::
Testable prop
=> Int -- Minimum chunk size
-> [[Vector Word8]] -- Data portions to be fed
-> ([Maybe Chunk] -> Maybe Chunk -> prop) -- Property from baler output
-> Property -- Resulting property
withBalingOutput minChunkSize food consumer
= tabulate "Lengths of buildup phases"
(map show (buildupPhasesLengths commonChunks))
(consumer commonChunks remnant)
where
commonChunks :: [Maybe Chunk]
remnant :: Maybe Chunk
(commonChunks, remnant) = balingOutput minChunkSize food
buildupPhasesLengths :: [Maybe Chunk] -> [Int]
buildupPhasesLengths [] = []
buildupPhasesLengths chunks = List.length buildupOutput :
buildupPhasesLengths (List.drop 1 followUp)
where
buildupOutput, followUp :: [Maybe Chunk]
(buildupOutput, followUp) = span isNothing chunks
prop_contentIsPreserved :: MinChunkSize -> [[Vector Word8]] -> Property
prop_contentIsPreserved (MinChunkSize minChunkSize) food
= withBalingOutput minChunkSize food $ \ commonChunks remnant ->
let
input :: Vector Word8
input = concat (List.concat food)
output :: Vector Word8
output = concat $
toByteVector <$> catMaybes (commonChunks ++ [remnant])
in input === output
prop_noRemnantAfterOutput :: MinChunkSize
-> NonEmptyList [Vector Word8]
-> Property
prop_noRemnantAfterOutput (MinChunkSize minChunkSize) (NonEmpty food)
= withBalingOutput minChunkSize food $ \ commonChunks remnant ->
isJust (last commonChunks) ==> isNothing remnant
prop_commonChunksAreLarge :: MinChunkSize -> [[Vector Word8]] -> Property
prop_commonChunksAreLarge (MinChunkSize minChunkSize) food
= withBalingOutput minChunkSize food $ \ commonChunks _ ->
all (toByteVector >>> length >>> (>= minChunkSize)) $
catMaybes commonChunks
remnantChunkSizeIs :: (Int -> Bool) -> Int -> [[Vector Word8]] -> Property
remnantChunkSizeIs constraint minChunkSize food
= withBalingOutput minChunkSize food $ \ _ remnant ->
isJust remnant ==> constraint (length (toByteVector (fromJust remnant)))
prop_remnantChunkIsNonEmpty :: MinChunkSize -> [[Vector Word8]] -> Property
prop_remnantChunkIsNonEmpty (MinChunkSize minChunkSize)
= remnantChunkSizeIs (> 0) minChunkSize
prop_remnantChunkIsSmall :: MinChunkSize -> [[Vector Word8]] -> Property
prop_remnantChunkIsSmall (MinChunkSize minChunkSize)
= remnantChunkSizeIs (< minChunkSize) minChunkSize
-- * Test case generation and shrinking
{-|
The type of minimum chunk sizes.
This type is isomorphic to 'Int' but has a different way of generating test
cases. Only small, positive integers are generated, and they are generated
using \(2 \cdot s^{2}\) as the size parameter, where \(s\) refers to the
original size parameter.
The reasons for the modification of the size parameter in the
above-mentioned way are somewhat subtle.
First, we want the ratio between the average minimum chunk size and the
average size of a data portion that we feed to a baler to be independent of
the size parameter. Each data portion is a list of primitive vectors of
bytes, and arbitrarily generated lists and byte vectors have lengths that
are small, positive integers. Such integers are \(s/2\) on average. As a
result, the average size of data fed to a baler is \(s^{2}/4\). By
generating minimum chunk sizes with \(a \cdot s^{2}\) as the size parameter
for some constant \(a\), the average minimum chunk size is \(a/2 \cdot
s^{2}\) and therefore $2a$ times the average size of a data portion fed,
independently of \(s\).
Second, we want prompt chunk generation as well as chunk generation after
only two or more feedings to occur reasonably often. To achieve this to some
degree, we can tune the parameter \(a\). It appears that \(a\) being \(2\)
leads to reasonable results.
-}
newtype MinChunkSize = MinChunkSize Int deriving stock Show
fromMinChunkSize :: MinChunkSize -> Int
fromMinChunkSize (MinChunkSize minChunkSize) = minChunkSize
instance Arbitrary MinChunkSize where
arbitrary = scale (\ size -> 2 * size ^ (2 :: Int)) $
MinChunkSize <$> getSmall <$> getPositive <$> arbitrary
shrink = shrinkMap (MinChunkSize . getSmall . getPositive)
(Positive . Small . fromMinChunkSize)