packages feed

crdt-5.0: test/Laws.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Laws
    ( cmrdtLaw
    , cvrdtLaws
    ) where

import           Data.Maybe (fromMaybe, isJust)
import           Data.Semigroup (Semigroup, (<>))
import           Test.QuickCheck (Arbitrary (..), Property, counterexample,
                                  discard, property, (.&&.), (===), (==>))
import           Test.Tasty (TestTree)
import           Test.Tasty.QuickCheck (testProperty)

import           CRDT.Cm (CmRDT (..), concurrent)
import           CRDT.Cm.Counter (Counter)
import           CRDT.Cm.GSet (GSet)
import           CRDT.Cm.TwoPSet (TwoPSet)
import           CRDT.Cv (CvRDT)
import           CRDT.LamportClock (Clock, ProcessSim, runLamportClockSim,
                                    runProcessSim)
import           CRDT.LWW (LWW)
import qualified CRDT.LWW as LWW
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

class Initialize op where
    type Initial op
    type Initial op = Payload op

    initialize :: Clock m => Initial op -> m (Payload op)
    default initialize
        :: (Applicative m, Initial op ~ Payload op)
        => Initial op -> m (Payload op)
    initialize = pure

instance Initialize (Counter a)

instance Initialize (GSet a)

instance Initialize (LWW a) where
    type Initial (LWW a) = a
    initialize = LWW.initial

instance Initialize (TwoPSet a)

-- | CmRDT law: concurrent ops commute
cmrdtLaw
    :: forall op.
    ( CmRDT op
    , Arbitrary op, Show op
    , Arbitrary (Intent  op), Show (Intent  op)
    , Arbitrary (Payload op), Show (Payload op)
    , Initialize op
    , Arbitrary (Initial op), Show (Initial op)
    )
    => Property
cmrdtLaw = property concurrentOpsCommute
  where
    concurrentOpsCommute st1 st2 seed3 in1 in2 pid1 pid2 pid3 =
        let (op1, op2, state) = runLamportClockSim $ (,,)
                <$> runProcessSim pid1 (makeOp @op in1 st1 `orElse` discard)
                <*> runProcessSim pid2 (makeOp @op in2 st2 `orElse` discard)
                <*> runProcessSim pid3 (initialize @op seed3)
        in  concurrent op1 op2 ==> opCommutativity (in1, op1) (in2, op2) state
    opCommutativity (in1, op1) (in2, op2) state =
        isJust (makeOp @op @ProcessSim in1 state) ==>
        isJust (makeOp @op @ProcessSim in2 state) ==>
            counterexample
                ( show in1 ++ " must be valid after " ++ show op2 ++
                  " applied to " ++ show state )
                (isJust $ makeOp @op @ProcessSim in1 $ apply op2 state)
            .&&.
            (apply op1 . apply op2) state === (apply op2 . apply op1) state

orElse :: Maybe a -> a -> a
orElse = flip fromMaybe