crdt-7.0: test/Laws.hs
{-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Laws
( cmrdtLaw
, cvrdtLaws
, opCommutativity
) where
import Control.Monad.State.Strict (MonadState, get, modify)
import Control.Monad.Trans (lift)
import Data.Functor.Identity (runIdentity)
import Data.Maybe (isJust)
import Data.Semigroup (Semigroup, (<>))
import QuickCheck.GenT (MonadGen, liftGen, runGenT)
import qualified QuickCheck.GenT as GenT
import Test.QuickCheck (Arbitrary (..), Property, choose,
counterexample, discard, forAll, getSize,
property, (.&&.), (===), (==>))
import Test.Tasty (TestTree)
import Test.Tasty.QuickCheck (testProperty)
import CRDT.Cm (CmRDT (..), concurrent)
import CRDT.Cv (CvRDT)
import CRDT.LamportClock (Clock)
import CRDT.LamportClock.Simulation (ProcessSim, ProcessSimT,
runLamportClockSimT,
runProcessSimT)
import Data.Semilattice (Semilattice, merge)
import ArbitraryOrphans ()
semigroupLaw :: forall a. (Arbitrary a, Semigroup a, Eq a, Show a) => TestTree
semigroupLaw = testProperty "associativity" associativity
where
associativity x y (z :: a) = (x <> y) <> z === x <> (y <> z)
semilatticeLaws
:: forall a. (Arbitrary a, Semilattice a, Eq a, Show a) => [TestTree]
semilatticeLaws =
[ semigroupLaw @a
, testProperty "commutativity" commutativity
, testProperty "idempotency" idempotency
]
where
idempotency (x :: a) = x `merge` x === x
commutativity x (y :: a) = x `merge` y === y `merge` x
cvrdtLaws :: forall a. (Arbitrary a, CvRDT a, Eq a, Show a) => [TestTree]
cvrdtLaws = semilatticeLaws @a
-- | CmRDT law: concurrent ops commute
cmrdtLaw
:: forall op.
( CmRDT op, Show op
, Arbitrary (Intent op), Show (Intent op)
, Arbitrary (Payload op), Show (Payload op)
)
=> Property
cmrdtLaw = property concurrentOpsCommute
where
concurrentOpsCommute payload pid1 pid2 pid3 =
pid1 < pid2 && pid2 < pid3 ==>
forAll genOps $ \case
Right ((in1, op1), (in2, op2), state3) ->
concurrent op1 op2 ==>
opCommutativity (in1, op1) (in2, op2) state3
Left _ -> discard
where
genOps = fmap runIdentity $ runGenT $ runLamportClockSimT payload $ (,,)
<$> runProcessSimT pid1 (do
_ <- genAndApplyOps @op
genAndApplyOp @op)
<*> runProcessSimT pid2 (do
_ <- genAndApplyOps @op
genAndApplyOp @op)
<*> runProcessSimT pid3 (do
_ <- genAndApplyOps @op
get)
opCommutativity
:: forall op.
(CmRDT op, Show op, Show (Intent op), Show (Payload op))
=> (Intent op, op) -- ^ the op must be made from the intent
-> (Intent op, op) -- ^ the op must be made from the intent
-> Payload op -- ^ any reachable state
-> Property
opCommutativity (in1, op1) (in2, op2) state =
isJust (makeOp' in1 state) ==>
isJust (makeOp' in2 state) ==>
counterexample
( show in2 ++ " must be valid after " ++ show op1 ++
" applied to " ++ show state )
(isJust $ makeOp' in2 $ apply op1 state)
.&&.
(apply op1 . apply op2) state === (apply op2 . apply op1) state
where
makeOp' = makeOp @op @(ProcessSim (Payload op))
genAndApplyOp
:: ( CmRDT op, Arbitrary (Intent op)
, Clock m, MonadState (Payload op) m, MonadGen m
, Show op, Show (Intent op), Show (Payload op)
)
=> m (Intent op, op)
genAndApplyOp = do
payload <- get
intent <- liftGen arbitrary
case makeOp intent payload of
Nothing -> genAndApplyOp
Just opAction -> do
op <- opAction
modify $ apply op
pure (intent, op)
genAndApplyOps
:: ( CmRDT op, Arbitrary (Intent op)
, Clock m, MonadState (Payload op) m, MonadGen m
, Show op, Show (Intent op), Show (Payload op)
)
=> m [(Intent op, op)]
genAndApplyOps = GenT.listOf genAndApplyOp
instance MonadGen m => MonadGen (ProcessSimT s m) where
liftGen = lift . liftGen
variant = undefined
sized f = do
size <- liftGen getSize
f size
resize = undefined
choose = liftGen . choose