Skip to content

Commit 9627852

Browse files
Make it work!
1 parent 9fdad99 commit 9627852

File tree

3 files changed

+88
-43
lines changed

3 files changed

+88
-43
lines changed

quickcheck-dynamic/src/Test/QuickCheck/StateModel.hs

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import Test.QuickCheck.Monadic
6868
import Test.QuickCheck.StateModel.Variables
6969
import Control.Concurrent
7070
import Control.Monad.Identity
71+
import Control.Arrow (first)
7172

7273
-- | The typeclass users implement to define a model against which to validate some implementation.
7374
--
@@ -259,6 +260,9 @@ class RunModel state m => RunModelPar state m where
259260

260261
postconditionPar :: (state, state) -> Action state a -> LookUp -> a -> PostconditionM Identity Bool
261262

263+
postconditionOnFailurePar :: (state, state) -> Action state a -> LookUp -> Either (Error state m) a -> PostconditionM Identity Bool
264+
postconditionOnFailurePar _ _ _ _ = pure True
265+
262266
monitoringPar :: Action state a -> LookUp -> Either (Error state m) a -> Property -> Property
263267
monitoringPar _ _ _ = id
264268

@@ -423,10 +427,10 @@ moreActions r p =
423427

424428
-- NOTE: indexed on state for forwards compatibility, e.g. when we
425429
-- want to give an explicit initial state
426-
data Options state = Options {actionLengthMultiplier :: Rational, negativeActions :: Bool}
430+
data Options state = Options {actionLengthMultiplier :: Rational}
427431

428432
defaultOptions :: Options state
429-
defaultOptions = Options{actionLengthMultiplier = 1, negativeActions = True}
433+
defaultOptions = Options{actionLengthMultiplier = 1}
430434

431435
-- | Generate arbitrary actions with the `GenActionsOptions`. More flexible than using the type-based
432436
-- modifiers.
@@ -454,15 +458,14 @@ generateActionsWithOptions Options{..} = do
454458
return (reverse steps, rejected)
455459
else return (reverse steps, rejected)
456460
where
457-
correctPolarity polarity = negativeActions || polarity == PosPolarity
458461
satisfyPrecondition = sized $ \n -> go n (2 * n) [] -- idea copied from suchThatMaybe
459462
go m n rej
460463
| m > n = return (Nothing, rej)
461464
| otherwise = do
462465
a <- resize m $ computeArbitraryAction s
463466
case a of
464467
Some act@ActionWithPolarity{..} ->
465-
if computePrecondition s act && correctPolarity polarity
468+
if computePrecondition s act
466469
then return (Just (Some act), rej)
467470
else go (m + 1) n (actionName polarAction : rej)
468471

@@ -693,8 +696,6 @@ instance StateModel state => Show (ParallelActions state) where
693696
, show threadA
694697
, "-- Thread B"
695698
, show threadB
696-
, "-- Indices"
697-
, show (threadATags pas, threadBTags pas)
698699
]
699700
where
700701
common = Actions $ commonActions pas
@@ -705,13 +706,24 @@ instance StateModel state => Arbitrary (ParallelActions state) where
705706
arbitrary = genParActions
706707

707708
shrink (ParallelActions actions as bs) =
708-
[ ParallelActions actions as bs | actions <- shrinkActionsWithOptions opts actions ]
709-
where opts = defaultOptions{ negativeActions = False }
709+
filter checkParallelActions [ ParallelActions actions as bs | actions <- QC.shrink actions ]
710+
711+
checkParallelActions :: StateModel state => ParallelActions state -> Bool
712+
checkParallelActions pas = checkWellTypedness commonCtx threadA && checkWellTypedness commonCtx threadB
713+
where
714+
commonCtx = allVariables common
715+
common = Actions $ commonActions pas
716+
threadA = threadAActions pas
717+
threadB = threadBActions pas
718+
719+
checkWellTypedness :: StateModel state => VarContext -> [Step state] -> Bool
720+
checkWellTypedness _ [] = True
721+
checkWellTypedness ctx ((v := a) : ss) = a `wellTypedIn` ctx && checkWellTypedness (extendContext ctx v) ss
710722

711723
genParActions :: forall state. StateModel state => Gen (ParallelActions state)
712724
genParActions = do
713725
-- The ~ works around a bug in ghc (https://gitlab.haskell.org/ghc/ghc/-/issues/22004) (which is not in all ghc versions ?!)
714-
as@(~(Actions steps)) <- generateActionsWithOptions @state $ defaultOptions { negativeActions = False }
726+
as@(~(Actions steps)) <- QC.arbitrary
715727
split <- QC.choose (0, length steps - 1)
716728
let (common, post) = splitAt split steps
717729
commonCtx = allVariables common
@@ -746,15 +758,14 @@ type Trace state m = [TraceStep state m]
746758
runTracing :: ( RunModelPar state m
747759
, e ~ Error state m
748760
, forall a. IsPerformResult e a
749-
) => Env -> [Step state] -> m (Trace state m)
750-
runTracing env [] = return []
761+
) => Env -> [Step state] -> m (Trace state m, Env)
762+
runTracing env [] = return ([], env)
751763
runTracing env ((v := ap):as) = do
752764
r <- performResultToEither <$> performPar (polarAction ap) (lookUpVar env)
753765
let step = TraceStep r v ap
754-
case r of
755-
-- Fail early because we don't support negative actions
756-
Left err -> return [step]
757-
Right res -> (step :) <$> runTracing ((v :== res) : env) as
766+
env' | Right val <- r = (v :== val) : env
767+
| otherwise = env
768+
(first (step :)) <$> runTracing env' as
758769

759770
class Monad m => ForkYou m where
760771
forkThread :: m a -> m (m a)
@@ -777,40 +788,51 @@ runParActions :: ( StateModel state
777788
, ForkYou m
778789
) => ParallelActions state -> PropertyM m ()
779790
runParActions pas = do
780-
(s, env) <- runActions $ Actions $ commonActions pas
791+
monitor $ counterexample "-- Monitoring main thread"
792+
(trC, env) <- run $ runTracing mempty $ commonActions pas
781793
joinA <- run $ forkThread $ runTracing env (threadAActions pas)
782794
joinB <- run $ forkThread $ runTracing env (threadBActions pas)
783-
trA <- run joinA
784-
trB <- run joinB
785-
monitor $ monitorTrace env trA
786-
monitor $ monitorTrace env trB
787-
let ilvs = interleavings trA trB
788-
assert $ any (checkTrace s env) ilvs
795+
(trA, _) <- run joinA
796+
(trB, _) <- run joinB
797+
monitor $ counterexample "-- Monitoring thread A"
798+
monitorTrace env trA
799+
monitor $ counterexample "-- Monitoring thread B"
800+
monitorTrace env trB
801+
let ilvs = map (trC ++) $ interleavings trA trB
802+
assert $ any (checkTrace initialAnnotatedState mempty) ilvs
789803
-- TODO: stats collection and cleanup
790804

791805
monitorTrace :: forall state m. (StateModel state, RunModelPar state m)
792-
=> Env -> Trace state m -> Property -> Property
793-
monitorTrace _env [] = id
794-
monitorTrace env (TraceStep r v step : tr) =
795-
monitorTrace env' tr . monitoringPar @state @m (polarAction step) (lookUpVar env) r
806+
=> Env -> Trace state m -> PropertyM m ()
807+
monitorTrace _env [] = pure ()
808+
monitorTrace env (TraceStep r v step : tr) = do
809+
monitor $ monitoringPar @state @m (polarAction step) (lookUpVar env) r
810+
monitorTrace env' tr
796811
where
797812
env' | Right val <- r = (v :== val) : env
798813
| otherwise = env
799814

800815
checkTrace :: forall state m. (StateModel state, RunModelPar state m) => Annotated state -> Env -> Trace state m -> Bool
801816
checkTrace _ _ [] = True
802-
-- NOTE that we don't support negative actions
803-
checkTrace _ _ (TraceStep Left{} _ _ : _) = False
804-
checkTrace s env (TraceStep (Right val) v act : tr) =
805-
let pre = computePrecondition s act
806-
s' = computeNextState s act v
807-
env' = (v :== val) : env
808-
(b, _) = runWriter . runPost $ postconditionPar @state @m
809-
(underlyingState s, underlyingState s')
810-
(polarAction act)
811-
(lookUpVar env')
812-
val
813-
in pre && b && checkTrace s' env' tr
817+
checkTrace s env (TraceStep r v (ActionWithPolarity a _) : tr) =
818+
-- NOTE: we need to re-compute the polarity of `a` here because it may be that the failure can be explained,
819+
-- but only by the action failing when it was previous successful
820+
let act = actionWithPolarity s a
821+
s' = computeNextState s act v
822+
env' | Right val <- r = (v :== val) : env
823+
| otherwise = env
824+
checkPost | Right val <- r, polarity act == PosPolarity = fst . runWriter . runPost $ postconditionPar @state @m
825+
(underlyingState s, underlyingState s')
826+
(polarAction act)
827+
(lookUpVar env')
828+
val
829+
| Left{} <- r, polarity act == PosPolarity = False
830+
| otherwise = fst . runWriter . runPost $ postconditionOnFailurePar @state @m
831+
(underlyingState s, underlyingState s')
832+
(polarAction act)
833+
(lookUpVar env')
834+
r
835+
in computePrecondition s act && checkPost && checkTrace s' env' tr
814836

815837
interleavings :: [a] -> [a] -> [[a]]
816838
interleavings [] bs = [bs]
@@ -822,6 +844,4 @@ interleavings (a:as) (b:bs) =
822844
-- TODO:
823845
-- - Negative actions (because the actual interleaving might involve legitimate failures
824846
-- even if the linear sequence doesn't)
825-
-- - Remove negativeActions flag
826-
-- - Shrinking
827-
-- - Check scoping after shrinking linear actions
847+
-- - Check negative postcondition and correct precondition in `checkTrace`

quickcheck-dynamic/test/Spec/DynamicLogic/Counters.hs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
-- test or use examples for various behaviours of the runtime.
55
module Spec.DynamicLogic.Counters where
66

7+
import Control.Concurrent
78
import Control.Monad.Reader
89
import Data.IORef
9-
import Test.QuickCheck (frequency)
10+
import Test.QuickCheck (frequency, Property)
1011
import Test.QuickCheck.StateModel
12+
import Test.QuickCheck.Extras
13+
import Test.QuickCheck.Monadic
1114

1215
-- A very simple model with a single action that always succeed in
1316
-- predictable way. This model is useful for testing the runtime.
@@ -85,13 +88,27 @@ instance StateModel Counter where
8588
instance RunModel Counter (ReaderT (IORef Int) IO) where
8689
perform _ Inc _ = do
8790
ref <- ask
88-
lift $ modifyIORef ref succ
91+
lift $ do
92+
n <- readIORef ref
93+
threadDelay 1000
94+
writeIORef ref (n + 1)
8995
perform _ Reset _ = do
9096
ref <- ask
9197
lift $ do
9298
n <- readIORef ref
99+
threadDelay 1000
93100
writeIORef ref 0
94101
pure n
95102

96103
postcondition (Counter n, _) Reset _ res = pure $ n == res
97104
postcondition _ _ _ _ = pure True
105+
106+
107+
instance RunModelPar Counter (ReaderT (IORef Int) IO) where
108+
postconditionPar (Counter n, _) Reset _ res = pure $ n == res
109+
postconditionPar _ _ _ _ = pure True
110+
111+
prop_counter_par :: ParallelActions Counter -> Property
112+
prop_counter_par as = monadicIO $ do
113+
ref <- lift $ newIORef (0 :: Int)
114+
runPropertyReaderT (runParActions as) ref

quickcheck-dynamic/test/Spec/DynamicLogic/RegistryModel.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ instance RunModelPar RegState RegM where
151151
return $ (env <$> Map.lookup name (regs s)) == mtid
152152
postconditionPar _ _ _ _ = return True
153153

154+
postconditionOnFailurePar (s, _) act@Register{} _ res = do
155+
monitorPost $
156+
QC.tabulate
157+
"Reason for -Register"
158+
[why s act]
159+
pure $ isLeft res
160+
postconditionOnFailurePar _ _ _ _ = pure True
161+
154162
monitoringPar act@(showDictAction -> ShowDict) _ res =
155163
QC.counterexample (show res ++ " <- " ++ show act)
156164

0 commit comments

Comments
 (0)