delta-store-1.0.0.0: src/Test/Data/Store.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ViewPatterns #-}
{-|
Copyright : © 2022-2023 IOHK, 2023-2025 Cardano Foundation
License : Apache-2.0
Description : Utilities for testing 'Store' implementations.
This module provides utilities for testing 'Store' implementations.
* 'prop_StoreUpdate' is a general property test that tests
the laws for 'updateS'.
* 'genChain' generates random sequences of deltas.
* 'StoreUnitTest' provides a monadic DSL
for writing example test cases for 'Store'.
-}
module Test.Data.Store
( -- * Store laws
GenDelta
, prop_StoreUpdate
-- * Generators
, Chain (..)
, genChain
, shrinkChain
-- * Unit test DSL for developing a Store
, StoreUnitTest
, unitTestStore
, applyS
, checkLaw
, reset
, context
, observe
, ignore
) where
import Prelude
import Control.Exception
( throwIO
)
import Control.Monad
( forM_
)
import Control.Monad.Trans.Class
( lift
)
import Control.Monad.Trans.RWS
( RWST
, ask
, censor
, evalRWST
, get
, listen
, put
, tell
)
import Data.Delta
( Delta (..)
)
import Data.Either
( isRight
)
import Data.Store
( Store (loadS, updateS, writeS)
)
import Test.QuickCheck
( Gen
, Property
, conjoin
, counterexample
, forAll
, forAllShrink
, getSize
, (===)
)
import Test.QuickCheck.Monadic
( assert
, monadicIO
, monitor
, run
)
{-----------------------------------------------------------------------------
Store laws
------------------------------------------------------------------------------}
-- | Given a value, generate a random delta that applies to this value.
type GenDelta da = Base da -> Gen da
-- | Chain of deltas and the results of their application.
--
-- The delta that is applied *last* appears in the list *first*.
data Chain da = Chain [(Base da, da)] (Base da)
instance Show da => Show (Chain da) where
show (Chain adas _) = show . map snd $ adas
-- | Randomly generate a chain of deltas.
genChain :: Delta da => Gen (Base da) -> GenDelta da -> Gen (Chain da)
genChain gen0 more = do
n <- getSize
a0 <- gen0
go n a0 [] a0
where
go 0 _ das a0 = pure $ Chain das a0
go n alast das a0 = do
da <- more alast
let a = apply da alast
go (n - 1) a ((a, da) : das) a0
-- | Shrink a chain of deltas.
shrinkChain :: Chain da -> [Chain da]
shrinkChain (Chain [] _) = []
shrinkChain (Chain das a0) =
[ Chain [] a0, Chain [last das] a0, Chain (tail das) a0 ]
-- | Test whether the law on 'updateS' is satisfied.
--
-- Subsumes test for the law on 'writeS' / 'loadS'.
prop_StoreUpdate
:: (Monad m, Delta da, Eq (Base da), Show da, Show (Base da))
=> (forall b. m b -> IO b)
-- ^ Function to embed the monad in 'IO'
-> m (Store m qa da)
-- ^ Creation for 'Store' that is to be tested.
-> Gen (Base da)
-- ^ Generator for the initial value.
-> GenDelta da
-- ^ Generator for deltas.
-> Property
prop_StoreUpdate toIO mkStore gen0 more =
forAll gen0 $ \a0' ->
forAllShrink (genChain (pure a0') more) shrinkChain $ \chain ->
let Chain adas a0 = chain
as = map fst adas ++ [a0]
das = map snd adas
in counterexample ("\nUpdates applied:\n" <> unlines (map show as))
$ monadicIO $ do
ea <- run . toIO $ do
store <- mkStore
writeS store a0
-- first update is applied last!
let updates = reverse $ zip das (drop 1 as)
forM_ updates $ \(da, a) -> updateS store (Just a) da
loadS store
case ea of
Left err -> run $ throwIO err
Right a -> do
monitor $ counterexample
$ "\nExpected:\n" <> show (head as)
monitor $ counterexample
$ "\nGot:\n" <> show a
assert $ a == head as
{-----------------------------------------------------------------------------
DSL for developing
------------------------------------------------------------------------------}
-- | A monadic DSL to unit test a 'Store'.
newtype StoreUnitTest m qa da r = StoreUnitTest
{ runStoreUnitTest :: RWST
(Store m qa da)
[Property]
(Base da, Base da, [da])
m
r
} deriving (Functor, Applicative, Monad)
-- | Apply a delta to the current value.
applyS :: (Monad m, Delta da) => da -> StoreUnitTest m qa da ()
applyS r = StoreUnitTest $ do
s <- ask
(q, x, ds) <- get
put (q, apply r x, r : ds)
lift $ updateS s (Just x) r
-- | Check the store laws.
checkLaw
:: (Monad m, Eq (Base da), Show (Base da), Show da)
=> StoreUnitTest m qa da ()
checkLaw = StoreUnitTest $ do
(_, x, reverse -> ds) <- get
x' <- ask >>= lift . loadS
tell
[ counterexample (show (ds, leftOf x')) (isRight x')
, counterexample (show ds) $ rightOf x' === x
]
where
leftOf (Left x) = x
leftOf _ = undefined
rightOf (Right x) = x
rightOf _ = undefined
-- | Reset the store state to the initial value.
reset :: Monad m => StoreUnitTest m qa da ()
reset = StoreUnitTest $ do
s <- ask
(q, _, _) <- get
lift $ writeS s q
put (q, q, [])
-- | Run a unit test for a 'Store'.
unitTestStore
:: (Monad m, Eq (Base da), Show (Base da), Show da)
=> Base da
-> Store m qa da
-> StoreUnitTest m qa da a
-> m Property
unitTestStore x s f =
conjoin . snd
<$> evalRWST (runStoreUnitTest (f >> checkLaw)) s (x, x, [])
-- | Add a context to test.
context
:: Monad m
=> (Property -> Property)
-> StoreUnitTest m qa da x
-> StoreUnitTest m qa da x
context d f = StoreUnitTest $ do
(x, w) <- listen $ runStoreUnitTest f
tell $ fmap d w
pure x
-- | Observe a property on the current value of the store.
observe :: Monad m => (Base da -> Property) -> StoreUnitTest m qa da ()
observe f = StoreUnitTest $ do
(_, s, _) <- get
tell [f s]
-- | Ignore the properties of a sub-test.
ignore :: Monad m => StoreUnitTest m qa da x -> StoreUnitTest m qa da x
ignore = StoreUnitTest . censor (const []) . runStoreUnitTest