{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module IORefs (prop_IORefs_sequential) where
import Control.Concurrent
import Data.Coerce
(coerce)
import Data.Foldable
(toList)
import Data.IORef
import Data.Map.Strict
(Map)
import GHC.Generics
(Generic)
import Prelude
import Test.QuickCheck
import Test.StateMachine
import qualified Data.Map.Strict as Map
import Test.StateMachine.Lockstep.Simple
import Test.StateMachine.TreeDiff
{-------------------------------------------------------------------------------
Instantiate the simple API
-------------------------------------------------------------------------------}
data T a
data instance Cmd (T _) h = New | Read h | Update h
deriving stock (Show, Functor, Foldable, Traversable)
data instance Resp (T a) h = Var h | Val a | Unit ()
deriving stock (Show, Eq, Functor, Foldable, Traversable)
data instance MockHandle (T _) = MV Int
deriving stock (Show, Eq, Ord, Generic)
newtype instance RealHandle (T a) = RealVar (Opaque (IORef a))
deriving stock (Eq, Show, Generic)
type instance MockState (T a) = Map (MockHandle (T a)) a
instance ToExpr (MockHandle (T a))
instance ToExpr (RealHandle (T a))
type instance Tag (T _) = TagCmd
{-------------------------------------------------------------------------------
Interpreters
-------------------------------------------------------------------------------}
runMock :: a
-> (a -> a)
-> Cmd (T a) (MockHandle (T a))
-> MockState (T a) -> (Resp (T a) (MockHandle (T a)), MockState (T a))
runMock e f cmd m =
case cmd of
New -> let v = MV (Map.size m) in (Var v, Map.insert v e m)
Read v -> (Val (m Map.! v), m)
Update v -> (Unit (), Map.adjust f v m)
runReal :: a
-> (a -> a)
-> Cmd (T a) (RealHandle (T a))
-> IO (Resp (T a) (RealHandle (T a)))
runReal e f cmd =
case cmd of
New -> Var <$> coerce <$> newIORef e
Read r -> Val <$> readIORef (coerce r)
Update r -> Unit <$> slowModify (coerce r) f
slowModify :: IORef a -> (a -> a) -> IO ()
slowModify r f = readIORef r >>= \a -> threadDelay 1000 >> writeIORef r (f a)
{-------------------------------------------------------------------------------
Generator
-------------------------------------------------------------------------------}
generator :: forall a.
Model (T a) Symbolic
-> Maybe (Gen (Cmd (T a) :@ Symbolic))
generator (Model _ hs) = Just $ oneof $ concat [
withoutHandle
, if null hs then [] else withHandle
]
where
withoutHandle :: [Gen (Cmd (T a) :@ Symbolic)]
withoutHandle = [return $ At New]
withHandle :: [Gen (Cmd (T a) :@ Symbolic)]
withHandle = [
fmap At $ Update <$> genHandle
, fmap At $ Read <$> genHandle
]
genHandle :: Gen (Reference (RealHandle (T a)) Symbolic)
genHandle = elements (map fst hs)
{-------------------------------------------------------------------------------
Tagging
We just label with the name of the command, for now.
-------------------------------------------------------------------------------}
data TagCmd = TagNew | TagRead | TagUpdate
deriving stock (Show)
tagCmds :: [Event (T Int) Symbolic] -> [TagCmd]
tagCmds = map (aux . unAt . cmd)
where
aux :: Cmd (T Int) h -> TagCmd
aux New = TagNew
aux (Read _) = TagRead
aux (Update _) = TagUpdate
{-------------------------------------------------------------------------------
Wrapping it all up
NOTE: The parallel property will fail (intentional race condition).
-------------------------------------------------------------------------------}
ioRefTest :: StateMachineTest (T Int)
ioRefTest = StateMachineTest {
initMock = Map.empty
, generator = IORefs.generator
, shrinker = \_ _ -> []
, newHandles = toList
, runMock = IORefs.runMock 0 (+1)
, runReal = IORefs.runReal 0 (+1)
, cleanup = \_ -> return ()
, tag = tagCmds
}
prop_IORefs_sequential :: Property
prop_IORefs_sequential = prop_sequential ioRefTest Nothing