{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Test.IORef.Full (tests) where
import Control.Monad.Reader
import Data.Bifunctor
import Data.Constraint (Dict(..))
import Data.IORef
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Proxy
import Test.QuickCheck
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck (testProperty)
import Test.QuickCheck qualified as QC
import Test.QuickCheck.StateModel
import Test.QuickCheck.StateModel.Lockstep
import Test.QuickCheck.StateModel.Lockstep.Defaults qualified as Lockstep
import Test.QuickCheck.StateModel.Lockstep.Run qualified as Lockstep
import Test.QuickCheck.StateModel.Lockstep.Run (labelActions)
{-------------------------------------------------------------------------------
Model "M"
-------------------------------------------------------------------------------}
type MRef = Int
data M = M {
-- | Value of every var
mValues :: Map MRef Int
-- | Often did we write each value to each var?
--
-- This is used for tagging.
, mWrites :: Map (MRef, Int) Int
}
deriving (Show)
initModel :: M
initModel = M {
mValues = Map.empty
, mWrites = Map.empty
}
modelNew :: M -> (MRef, M)
modelNew M{..} = (
mockRef
, M { mValues = Map.insert mockRef 0 mValues
, mWrites = mWrites
}
)
where
mockRef :: MRef
mockRef = Map.size mValues
modelWrite :: MRef -> Int -> M -> (Int, M)
modelWrite v x M{..} = (
x
, M { mValues = Map.insert v x mValues
, mWrites = Map.alter recordWrite (v, x) mWrites
}
)
where
recordWrite :: Maybe Int -> Maybe Int
recordWrite Nothing = Just 1 -- First write
recordWrite (Just n) = Just (n + 1)
modelRead :: MRef -> M -> (Int, M)
modelRead v m@M{..} = (mValues Map.! v, m)
{-------------------------------------------------------------------------------
Instances
-------------------------------------------------------------------------------}
instance StateModel (Lockstep M) where
data Action (Lockstep M) a where
-- | Create new IORef
New :: Action (Lockstep M) (IORef Int)
-- | Write value to IORef
--
-- The value is specified either as a concrete value or by reference
-- to a previous write.
Write ::
ModelVar M (IORef Int)
-> Either Int (ModelVar M Int)
-> Action (Lockstep M) Int
-- | Read IORef
Read ::
ModelVar M (IORef Int)
-> Action (Lockstep M) Int
initialState = Lockstep.initialState initModel
nextState = Lockstep.nextState
precondition = Lockstep.precondition
arbitraryAction = Lockstep.arbitraryAction
shrinkAction = Lockstep.shrinkAction
instance RunModel (Lockstep M) RealMonad where
perform = \_state -> runIO
postcondition = Lockstep.postcondition
monitoring = Lockstep.monitoring (Proxy @RealMonad)
instance InLockstep M where
data ModelValue M a where
MRef :: MRef -> ModelValue M (IORef Int)
MInt :: Int -> ModelValue M Int
data Observable M a where
ORef :: Observable M (IORef Int)
OId :: (Show a, Eq a) => a -> Observable M a
observeModel (MRef _) = ORef
observeModel (MInt x) = OId x
usedVars New = []
usedVars (Write v (Left _)) = [SomeGVar v]
usedVars (Write v (Right v')) = [SomeGVar v, SomeGVar v']
usedVars (Read v) = [SomeGVar v]
modelNextState = runModel
arbitraryWithVars findVars _mock = oneof $ concat [
withoutVars
, case findVars (Proxy @(IORef Int)) of
[] -> []
vars -> withVars (elements vars)
]
where
withoutVars :: [Gen (Any (LockstepAction M))]
withoutVars = [return $ Some New]
withVars ::
Gen (ModelVar M (IORef Int))
-> [Gen (Any (LockstepAction M))]
withVars genVar = [
fmap Some $ Write <$> genVar <*> (Left <$> choose (0, 10))
, fmap Some $ Read <$> genVar
]
shrinkWithVars findVars _mock = \case
New -> []
Write v (Left x) -> concat [
Some . Write v . Left <$> shrink x
, Some . Write v . Right <$> findVars (Proxy @Int)
]
Write _ (Right _) -> []
Read _ -> []
tagStep (_stBefore, stAfter) _action _result = concat [
[ "WriteSameVarTwice"
| not
. Map.null
. Map.filter (\numWrites -> numWrites > 1)
$ mWrites stAfter
]
]
instance RunLockstep M RealMonad where
observeReal _ action result =
case (action, result) of
(New , _) -> ORef
(Write{} , x) -> OId x
(Read{} , x) -> OId x
showRealResponse _ = \case
New{} -> Nothing
Write{} -> Just Dict
Read{} -> Just Dict
deriving instance Show (Action (Lockstep M) a)
deriving instance Show (Observable M a)
deriving instance Show (ModelValue M a)
deriving instance Eq (Action (Lockstep M) a)
deriving instance Eq (Observable M a)
deriving instance Eq (ModelValue M a)
{-------------------------------------------------------------------------------
Interpreters against the real system and against the model
-------------------------------------------------------------------------------}
data Buggy = Buggy | NotBuggy
deriving (Show, Eq)
instance Arbitrary Buggy where
arbitrary = elements [Buggy, NotBuggy]
shrink Buggy = [NotBuggy]
shrink NotBuggy = []
-- | We trigger a bug if we write the /same/ value to the /same/ variable
type BrokenRef = [(IORef Int, Int)]
type RealMonad = ReaderT (Buggy, IORef BrokenRef) IO
runIO :: Action (Lockstep M) a -> LookUp RealMonad -> RealMonad a
runIO action lookUp = ReaderT $ \(buggy, brokenRef) ->
case action of
New -> newIORef 0
Write v x -> brokenWrite buggy brokenRef (lookUpRef v) (lookUpInt x)
Read v -> readIORef (lookUpRef v)
where
lookUpRef :: ModelVar M (IORef Int) -> IORef Int
lookUpRef = lookUpGVar (Proxy @RealMonad) lookUp
lookUpInt :: Either Int (ModelVar M Int) -> Int
lookUpInt (Left x) = x
lookUpInt (Right v) = lookUpGVar (Proxy @RealMonad) lookUp v
-- | The second write to the same variable will be broken
brokenWrite :: Buggy -> IORef BrokenRef -> IORef Int -> Int -> IO Int
brokenWrite NotBuggy _ v x = writeIORef v x >> return x
brokenWrite Buggy brokenRef v x = do
broken <- readIORef brokenRef
if (v, x) `elem` broken then
writeIORef v 123456789
else
writeIORef v x
modifyIORef brokenRef ((v, x) :)
return x
runModel ::
Action (Lockstep M) a
-> ModelLookUp M
-> M -> (ModelValue M a, M)
runModel action lookUp =
case action of
New -> first MRef . modelNew
Write v x -> first MInt . modelWrite (lookUpRef v) (lookUpInt x)
Read v -> first MInt . modelRead (lookUpRef v)
where
lookUpRef :: ModelVar M (IORef Int) -> MRef
lookUpRef var = case lookUp var of MRef r -> r
lookUpInt :: Either Int (ModelVar M Int) -> Int
lookUpInt (Left x) = x
lookUpInt (Right v) = case lookUp v of MInt x -> x
{-------------------------------------------------------------------------------
Top-level tests
-------------------------------------------------------------------------------}
propIORef :: Buggy -> Actions (Lockstep M) -> Property
propIORef buggy actions =
(if triggersBug then expectFailure else id)
$ Lockstep.runActionsBracket
(Proxy @M)
initBroken
(\_ -> return ())
runReaderT
actions
where
triggersBug :: Bool
triggersBug =
buggy == Buggy
&& "WriteSameVarTwice" `elem` labelActions actions
initBroken :: IO (Buggy, IORef BrokenRef)
initBroken = (buggy,) <$> newIORef []
tests :: TestTree
tests = testGroup "Test.IORef.Full" [
testCase "labelledExamples" $
QC.labelledExamples $ Lockstep.tagActions (Proxy @M)
, testProperty "runActions" propIORef
]