lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Internal/Entry.hs
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Test.Database.LSMTree.Internal.Entry (tests) where
import Data.List.NonEmpty (NonEmpty)
import Data.Semigroup hiding (First)
import qualified Data.Semigroup as S
import qualified Database.LSMTree as Full
import Database.LSMTree.Extras.Generators ()
import Database.LSMTree.Internal.Entry
import Test.QuickCheck
import Test.QuickCheck.Classes (semigroupLaws)
import Test.Tasty
import Test.Tasty.QuickCheck (QuickCheckMaxSize (QuickCheckMaxSize),
QuickCheckTests (QuickCheckTests), testProperty)
import Test.Util.QC
tests :: TestTree
tests = adjustOption (\_ -> QuickCheckTests 10000) $
adjustOption (\_ -> QuickCheckMaxSize 1000) $
testGroup "Test.Database.LSMTree.Internal.Entry" [
-- * Class laws
testClassLaws "Regular Entry" $
semigroupLaws (Proxy @(Regular (Entry (Sum Int) String)))
, testClassLaws "Regular Update" $
semigroupLaws (Proxy @(Regular (Full.Update (Sum Int) String)))
, testClassLaws "Union Entry" $
semigroupLaws (Proxy @(Union (Entry (Sum Int) String)))
, testClassLaws "Union Update" $
semigroupLaws (Proxy @(Union (Full.Update (Sum Int) String)))
-- * Semantics
, testProperty "prop_regularSemantics" $
prop_regularSemantics @(Sum Int) @String
, testProperty "prop_unionSemantics" $
prop_unionSemantics @(Sum Int) @String
]
-- TODO: it would be nice to write down how the semantic tests below relate to
-- the semantics of operations on the public API.
-- | @sconcat == fromEntry . sconcat . toEntry@ with regular semantics.
prop_regularSemantics ::
(Show v, Show b, Eq v, Eq b, Semigroup v)
=> NonEmpty (Full.Update v b)
-> Property
prop_regularSemantics es = expected === real
where
expected = from . sconcat . fmap to $ es
where
to :: Full.Update v b -> Regular (Full.Update v b)
to = Regular
from :: Regular (Full.Update v b) -> Full.Update v b
from = unRegular
real = from . sconcat . fmap to $ es
where
to :: Full.Update v b -> Regular (Entry v b)
to = Regular . updateToEntry
from :: Regular (Entry v b) -> Full.Update v b
from = entryToUpdate . unRegular
-- | @sconcat == fromEntry . sconcat . toEntry@ with union semantics.
prop_unionSemantics ::
(Show v, Show b, Eq v, Eq b, Semigroup v)
=> NonEmpty (Full.Update v b)
-> Property
prop_unionSemantics es = expected === real
where
expected = from . sconcat . fmap to $ es
where
to :: Full.Update v b -> Union (Full.Update v b)
to = Union
from :: Union (Full.Update v b) -> Full.Update v b
from = unUnion
real = from . sconcat . fmap to $ es
where
to :: Full.Update v b -> Union (Entry v b)
to = Union . updateToEntry
from :: Union (Entry v b) -> Full.Update v b
from = entryToUpdate . unUnion
{-------------------------------------------------------------------------------
Regular semantics
-------------------------------------------------------------------------------}
newtype Regular a = Regular { unRegular :: a }
deriving stock (Show, Eq)
--
-- Update
--
deriving newtype instance (Arbitrary v, Arbitrary b)
=> Arbitrary (Regular (Full.Update v b))
instance Semigroup v => Semigroup (Regular (Full.Update v b)) where
Regular up1 <> Regular up2 = Regular $ case (up1, up2) of
(Full.Delete , _ ) -> up1
(Full.Insert{} , _ ) -> up1
(Full.Upsert v1 , Full.Delete ) -> Full.Insert v1 Nothing
(Full.Upsert v1 , Full.Insert v2 _) -> Full.Insert (v1 <> v2) Nothing
(Full.Upsert v1 , Full.Upsert v2 ) -> Full.Upsert (v1 <> v2)
--
-- Entry
--
deriving via Uniform (Entry v b)
instance (Arbitrary v, Arbitrary b) => Arbitrary (Regular (Entry v b))
-- | Semigroup instance using 'combine'.
instance Semigroup v => Semigroup (Regular (Entry v b)) where
Regular e1 <> Regular e2 = Regular $ combine (<>) e1 e2
{-------------------------------------------------------------------------------
Union semantics
-------------------------------------------------------------------------------}
newtype Union a = Union { unUnion :: a }
deriving stock (Show, Eq)
--
-- Update
--
deriving newtype instance (Arbitrary v, Arbitrary b)
=> Arbitrary (Union (Full.Update v b))
instance Semigroup v => Semigroup (Union (Full.Update v b)) where
Union up1 <> Union up2 = Union $ fromModel $ toModel up1 <> toModel up2
where
toModel :: Full.Update v b -> Maybe (v, S.First (Maybe b))
toModel Full.Delete = Nothing
toModel (Full.Insert v mb) = Just (v, S.First mb)
toModel (Full.Upsert v) = Just (v, S.First Nothing)
fromModel :: Maybe (v, S.First (Maybe b)) -> Full.Update v b
fromModel Nothing = Full.Delete
fromModel (Just (v, S.First mb)) = Full.Insert v mb
--
-- Entry
--
deriving via Uniform (Entry v b)
instance (Arbitrary v, Arbitrary b) => Arbitrary (Union (Entry v b))
-- | Semigroup instance using 'combineUnion'.
instance Semigroup v => Semigroup (Union (Entry v b)) where
Union e1 <> Union e2 = Union $ combineUnion (<>) e1 e2
{-------------------------------------------------------------------------------
Utility
-------------------------------------------------------------------------------}
-- | A wrapper type with a 'Semigroup' instance that always throws an error.
newtype Unlawful a = Unlawful a
deriving stock (Show, Eq)
deriving newtype Arbitrary
-- | A 'Semigroup' instance that always throws an error.
instance Semigroup (Unlawful a) where
_ <> _ = error "unlawful"
newtype Uniform a = Uniform a
-- | We do not use the @'Arbitrary' 'Entry'@ instance here, because we want to
-- generate each constructor with equal probability.
instance (Arbitrary v, Arbitrary b) => Arbitrary (Uniform (Entry v b)) where
arbitrary = Uniform <$> liftArbitrary2Entry arbitrary arbitrary
shrink (Uniform x) = Uniform <$> liftShrink2Entry shrink shrink x
liftArbitrary2Entry :: Gen v -> Gen b -> Gen (Entry v b)
liftArbitrary2Entry genVal genBlob = frequency
[ (1, Insert <$> genVal)
, (1, InsertWithBlob <$> genVal <*> genBlob)
, (1, Upsert <$> genVal)
, (1, pure Delete)
]
liftShrink2Entry :: (v -> [v]) -> (b -> [b]) -> Entry v b -> [Entry v b]
liftShrink2Entry shrinkVal shrinkBlob = \case
Insert v -> Delete : (Insert <$> shrinkVal v)
InsertWithBlob v b -> [Delete, Insert v]
++ [ InsertWithBlob v' b'
| (v', b') <- liftShrink2 shrinkVal shrinkBlob (v, b)
]
Upsert v -> Delete : Insert v : (Upsert <$> shrinkVal v)
Delete -> []
{-------------------------------------------------------------------------------
Injections/projections
-------------------------------------------------------------------------------}
updateToEntry :: Full.Update v b -> Entry v b
updateToEntry = \case
Full.Insert v Nothing -> Insert v
Full.Insert v (Just b) -> InsertWithBlob v b
Full.Upsert v -> Upsert v
Full.Delete -> Delete
entryToUpdate :: Entry v b -> Full.Update v b
entryToUpdate = \case
Insert v -> Full.Insert v Nothing
InsertWithBlob v b -> Full.Insert v (Just b)
Upsert v -> Full.Upsert v
Delete -> Full.Delete