delta-store-1.0.0.0: test/unit/Data/DBVarSpec.hs
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-|
Copyright : © 2022-2023 IOHK, 2023-2025 Cardano Foundation
License : Apache-2.0
-}
module Data.DBVarSpec
( spec
) where
import Prelude
import Control.Concurrent.Class.MonadSTM
( atomically
, newTVarIO
, readTVar
, writeTVar
)
import Control.Exception
( toException
)
import Control.Monad.Class.MonadAsync
( concurrently_
, race
)
import Control.Monad.Class.MonadThrow
( throwIO
)
import Control.Monad.Class.MonadTimer
( threadDelay
)
import Control.Monad.IOSim
( IOSim
, runSimOrThrow
)
import Data.Delta
( Delta (..)
)
import Data.DBVar
( initDBVar
, updateDBVar
, readDBVar
)
import Data.Store
( NotInitialized (..)
, UpdateStore
, mkUpdateStore
)
import Test.Hspec
( Spec
, describe
, it
)
import Test.QuickCheck
( Arbitrary (arbitrary)
, Property
, Testable (property)
, elements
, oneof
, vectorOf
, (===)
)
spec :: Spec
spec = do
describe "operations" $ do
it "updateDBVar >> readDBVar" $ property $
\(da :: DeltaA) a0 ->
propertyIOSim $ do
store <- newDelayedStore 0
var <- initDBVar store a0
updateDBVar var da
a1 <- readDBVar var
pure $ a1 === apply da a0
describe "concurrency" $ do
it "readDBVar does not block" $ property $
\(da :: DeltaA) a0 ->
propertyIOSim $ do
store <- newDelayedStore 1000
var <- initDBVar store a0
Right a1 <- race
(updateDBVar var da)
(threadDelay 500 >> readDBVar var)
pure $ a1 === a0
it "updateDBVar blocks" $ property $
\(da10 :: DeltaA) da21 a0 ->
propertyIOSim $ do
store <- newDelayedStore 1000
var <- initDBVar store a0
concurrently_
(updateDBVar var da10)
(threadDelay 500 >> updateDBVar var da21)
a2 <- readDBVar var
pure $ a2 === apply [da21, da10] a0
{-----------------------------------------------------------------------------
Test setup
------------------------------------------------------------------------------}
propertyIOSim :: Testable p => (forall s. IOSim s p) -> Property
propertyIOSim action = property $ runSimOrThrow action
-- | 'Store' in volatile memory that invalidates the value
-- while performing an expensive update operation.
newDelayedStore
:: forall da s. Delta da => Int -> IOSim s (UpdateStore (IOSim s) da)
newDelayedStore delay = do
let broken = Left $ toException NotInitialized
ref <- newTVarIO broken
let load = atomically (readTVar ref)
write = atomically . writeTVar ref . Right
update :: Maybe (Base da) -> da -> IOSim s ()
update _ da = do
ea <- atomically $ do
ea <- readTVar ref
writeTVar ref broken -- invalidate the value
pure ea
threadDelay delay -- wait for a long time
case ea of
Left e -> throwIO e
Right a -> atomically . writeTVar ref . Right $ apply da a
pure $ mkUpdateStore load write update
{-----------------------------------------------------------------------------
Delta type for testing
------------------------------------------------------------------------------}
newtype A = A { unA :: [Int] }
deriving (Show, Eq)
data DeltaA
= AddOne
| AddTwo
| Drop
deriving (Show, Eq)
instance Delta DeltaA where
type Base DeltaA = A
apply AddOne = A . (1:) . unA
apply AddTwo = A . (2:) . unA
apply Drop = A . (drop 1) . unA
instance Arbitrary A where
arbitrary = A <$> oneof (map (\n -> vectorOf n arbitrary) [0,1,2])
instance Arbitrary DeltaA where
arbitrary = elements [AddOne, AddTwo, Drop]