Skip to content

Commit ae5d642

Browse files
author
Arnaud Bailly
authored
Refactor runActions to distinguish more clearly various cases (#71)
* Refactor runActions to distinguish more clearly various cases As I was looking for ways to implement parallel testing in q-d, I tried to understand what was going on with actions execution and I have attempted to expose more explicitly the various execution branches. This lead some simplification in the ways post-condition are computed. * Use full words instead of abbreviations in smart shrinker definition * Simplify runActions * Consolidate various counter models in a single module
1 parent cf5273f commit ae5d642

File tree

7 files changed

+280
-127
lines changed

7 files changed

+280
-127
lines changed

quickcheck-dynamic/quickcheck-dynamic.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ test-suite quickcheck-dynamic-test
9090
main-is: Spec.hs
9191
hs-source-dirs: test
9292
other-modules:
93-
Spec.DynamicLogic.CounterModel
93+
Spec.DynamicLogic.Counters
9494
Spec.DynamicLogic.Registry
9595
Spec.DynamicLogic.RegistryModel
9696
Test.QuickCheck.DynamicLogic.QuantifySpec
97+
Test.QuickCheck.StateModelSpec
9798

9899
ghc-options: -rtsopts
99100
build-depends:
@@ -104,4 +105,5 @@ test-suite quickcheck-dynamic-test
104105
, quickcheck-dynamic
105106
, stm
106107
, tasty
108+
, tasty-hunit
107109
, tasty-quickcheck

quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic/SmartShrinking.hs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ module Test.QuickCheck.DynamicLogic.SmartShrinking (shrinkSmart) where
33
import Test.QuickCheck
44

55
-- | This combinator captures the 'smart shrinking' implemented for the
6-
-- Smart type wrapper in Test.QuickCheck.Modifiers.
6+
-- `Smart` type wrapper in [Test.QuickCheck.Modifiers](https://hackage.haskell.org/package/QuickCheck-2.14.3/docs/Test-QuickCheck-Modifiers.html#t:Smart).
7+
-- It interleaves the output of the given shrinker to try to converge to more
8+
-- interesting values faster.
79
shrinkSmart :: (a -> [a]) -> Smart a -> [Smart a]
8-
shrinkSmart shr (Smart i x) = take i' ys `ilv` drop i' ys
10+
shrinkSmart shrinker (Smart i x) = take i' ys `interleave` drop i' ys
911
where
10-
ys = [Smart j y | (j, y) <- [0 ..] `zip` shr x]
12+
ys = [Smart j y | (j, y) <- [0 ..] `zip` shrinker x]
13+
1114
i' = 0 `max` (i - 2)
12-
[] `ilv` bs = bs
13-
as `ilv` [] = as
14-
(a : as) `ilv` (b : bs) = a : b : (as `ilv` bs)
15+
16+
[] `interleave` bs = bs
17+
as `interleave` [] = as
18+
(a : as) `interleave` (b : bs) = a : b : (as `interleave` bs)

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

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{-# LANGUAGE QuantifiedConstraints #-}
33
{-# LANGUAGE UndecidableInstances #-}
44

5-
-- | Simple (stateful) Model-Based Testing library for use with Haskell QuickCheck.
5+
-- | Model-Based Testing library for use with Haskell QuickCheck.
66
--
77
-- This module provides the basic machinery to define a `StateModel` from which /traces/ can
88
-- be generated and executed against some /actual/ implementation code to define monadic `Property`
@@ -32,6 +32,7 @@ module Test.QuickCheck.StateModel (
3232
runActions,
3333
lookUpVar,
3434
lookUpVarMaybe,
35+
viewAtType,
3536
initialAnnotatedState,
3637
computeNextState,
3738
computePrecondition,
@@ -192,6 +193,13 @@ newtype PostconditionM m a = PostconditionM {runPost :: WriterT (Endo Property,
192193
instance MonadTrans PostconditionM where
193194
lift = PostconditionM . lift
194195

196+
evaluatePostCondition :: Monad m => PostconditionM m Bool -> PropertyM m ()
197+
evaluatePostCondition post = do
198+
(b, (Endo mon, Endo onFail)) <- run . runWriterT . runPost $ post
199+
monitor mon
200+
unless b $ monitor onFail
201+
assert b
202+
195203
-- | Apply the property transformation to the property after evaluating
196204
-- the postcondition. Useful for collecting statistics while avoiding
197205
-- duplication between `monitoring` and `postcondition`.
@@ -239,23 +247,6 @@ class (forall a. Show (Action state a), Monad m) => RunModel state m where
239247
monitoringFailure :: state -> Action state a -> LookUp m -> Error state -> Property -> Property
240248
monitoringFailure _ _ _ _ prop = prop
241249

242-
computePostcondition
243-
:: forall m state a
244-
. RunModel state m
245-
=> (state, state)
246-
-> ActionWithPolarity state a
247-
-> LookUp m
248-
-> Either (Error state) (Realized m a)
249-
-> PostconditionM m Bool
250-
computePostcondition ss (ActionWithPolarity a p) l r
251-
| p == PosPolarity = case r of
252-
Right ra -> postcondition ss a l ra
253-
-- NOTE: this is actually redundant as this handled
254-
-- at the call site for this function, but this is
255-
-- good hygiene?
256-
Left _ -> pure False
257-
| otherwise = postconditionOnFailure ss a l r
258-
259250
type LookUp m = forall a. Typeable a => Var a -> Realized m a
260251

261252
type Env m = [EnvEntry m]
@@ -515,47 +506,82 @@ runActions
515506
)
516507
=> Actions state
517508
-> PropertyM m (Annotated state, Env m)
518-
runActions (Actions_ rejected (Smart _ actions)) = loop initialAnnotatedState [] actions
509+
runActions (Actions_ rejected (Smart _ actions)) = do
510+
(finalState, env) <- runSteps initialAnnotatedState [] actions
511+
unless (null rejected) $
512+
monitor $
513+
tabulate "Actions rejected by precondition" rejected
514+
return (finalState, env)
515+
516+
-- | Core function to execute a sequence of `Step` given some initial `Env`ironment
517+
-- and `Annotated` state.
518+
runSteps
519+
:: forall state m e
520+
. ( StateModel state
521+
, RunModel state m
522+
, e ~ Error state
523+
, forall a. IsPerformResult e a
524+
)
525+
=> Annotated state
526+
-> Env m
527+
-> [Step state]
528+
-> PropertyM m (Annotated state, Env m)
529+
runSteps s env [] = return (s, reverse env)
530+
runSteps s env ((v := act) : as) = do
531+
pre $ computePrecondition s act
532+
ret <- run $ performResultToEither <$> perform (underlyingState s) action (lookUpVar env)
533+
let name = show polar ++ actionName action
534+
monitor $ tabulate "Actions" [name]
535+
monitor $ tabulate "Action polarity" [show polar]
536+
case (polar, ret) of
537+
(PosPolarity, Left err) ->
538+
positiveActionFailed err
539+
(PosPolarity, Right val) -> do
540+
(s', env') <- positiveActionSucceeded ret val
541+
runSteps s' env' as
542+
(NegPolarity, _) -> do
543+
(s', env') <- negativeActionResult ret
544+
runSteps s' env' as
519545
where
520-
loop :: Annotated state -> Env m -> [Step state] -> PropertyM m (Annotated state, Env m)
521-
loop _s env [] = do
522-
unless (null rejected) $
523-
monitor $
524-
tabulate "Actions rejected by precondition" rejected
525-
return (_s, reverse env)
526-
loop s env ((v := act) : as) = do
527-
pre $ computePrecondition s act
528-
ret <- run $ performResultToEither <$> perform (underlyingState s) (polarAction act) (lookUpVar env)
529-
let name = show (polarity act) ++ actionName (polarAction act)
530-
monitor $ tabulate "Actions" [name]
531-
monitor $ tabulate "Action polarity" [show $ polarity act]
532-
if
533-
| polarity act == PosPolarity
534-
, Left err <- ret -> do
535-
monitor $
536-
monitoringFailure @state @m
537-
(underlyingState s)
538-
(polarAction act)
539-
(lookUpVar env)
540-
err
541-
stop False
542-
| otherwise -> do
543-
let var = unsafeCoerceVar v
544-
s' = computeNextState s act var
545-
env'
546-
| Right val <- ret = (var :== val) : env
547-
| otherwise = env
548-
monitor $ monitoring @state @m (underlyingState s, underlyingState s') (polarAction act) (lookUpVar env') ret
549-
(b, (Endo mon, Endo onFail)) <-
550-
run
551-
. runWriterT
552-
. runPost
553-
$ computePostcondition @m
554-
(underlyingState s, underlyingState s')
555-
act
556-
(lookUpVar env)
557-
ret
558-
monitor mon
559-
unless b $ monitor onFail
560-
assert b
561-
loop s' env' as
546+
polar = polarity act
547+
548+
action = polarAction act
549+
550+
positiveActionFailed err = do
551+
monitor $
552+
monitoringFailure @state @m
553+
(underlyingState s)
554+
action
555+
(lookUpVar env)
556+
err
557+
stop False
558+
559+
positiveActionSucceeded ret val = do
560+
(s', env', stateTransition) <- computeNewState ret
561+
evaluatePostCondition $
562+
postcondition
563+
stateTransition
564+
action
565+
(lookUpVar env)
566+
val
567+
pure (s', env')
568+
569+
negativeActionResult ret = do
570+
(s', env', stateTransition) <- computeNewState ret
571+
evaluatePostCondition $
572+
postconditionOnFailure
573+
stateTransition
574+
action
575+
(lookUpVar env)
576+
ret
577+
pure (s', env')
578+
579+
computeNewState ret = do
580+
let var = unsafeCoerceVar v
581+
s' = computeNextState s act var
582+
env'
583+
| Right val <- ret = (var :== val) : env
584+
| otherwise = env
585+
stateTransition = (underlyingState s, underlyingState s')
586+
monitor $ monitoring @state @m stateTransition action (lookUpVar env') ret
587+
pure (s', env', stateTransition)

quickcheck-dynamic/test/Spec.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
module Main (main) where
44

5-
import Spec.DynamicLogic.CounterModel qualified
65
import Spec.DynamicLogic.RegistryModel qualified
76
import Test.QuickCheck.DynamicLogic.QuantifySpec qualified
7+
import Test.QuickCheck.StateModelSpec qualified
88
import Test.Tasty
99

1010
main :: IO ()
@@ -15,6 +15,6 @@ tests =
1515
testGroup
1616
"dynamic logic"
1717
[ Spec.DynamicLogic.RegistryModel.tests
18-
, Spec.DynamicLogic.CounterModel.tests
1918
, Test.QuickCheck.DynamicLogic.QuantifySpec.tests
19+
, Test.QuickCheck.StateModelSpec.tests
2020
]

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

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{-# LANGUAGE NamedFieldPuns #-}
2+
3+
-- | Define several variant models of /counters/ which are useful to
4+
-- test or use examples for various behaviours of the runtime.
5+
module Spec.DynamicLogic.Counters where
6+
7+
import Control.Monad.Reader
8+
import Data.IORef
9+
import Test.QuickCheck
10+
import Test.QuickCheck.StateModel
11+
12+
-- A very simple model with a single action that always succeed in
13+
-- predictable way. This model is useful for testing the runtime.
14+
newtype SimpleCounter = SimpleCounter {count :: Int}
15+
deriving (Eq, Show, Generic)
16+
17+
deriving instance Eq (Action SimpleCounter a)
18+
deriving instance Show (Action SimpleCounter a)
19+
instance HasVariables (Action SimpleCounter a) where
20+
getAllVariables _ = mempty
21+
22+
instance StateModel SimpleCounter where
23+
data Action SimpleCounter a where
24+
IncSimple :: Action SimpleCounter Int
25+
26+
arbitraryAction _ _ = pure $ Some IncSimple
27+
28+
initialState = SimpleCounter 0
29+
30+
nextState SimpleCounter{count} IncSimple _ = SimpleCounter (count + 1)
31+
32+
instance RunModel SimpleCounter (ReaderT (IORef Int) IO) where
33+
perform _ IncSimple _ = do
34+
ref <- ask
35+
lift $ atomicModifyIORef' ref (\count -> (succ count, count))
36+
37+
-- A very simple model with a single action whose postcondition fails in a
38+
-- predictable way. This model is useful for testing the runtime.
39+
newtype FailingCounter = FailingCounter {failingCount :: Int}
40+
deriving (Eq, Show, Generic)
41+
42+
deriving instance Eq (Action FailingCounter a)
43+
deriving instance Show (Action FailingCounter a)
44+
instance HasVariables (Action FailingCounter a) where
45+
getAllVariables _ = mempty
46+
47+
instance StateModel FailingCounter where
48+
data Action FailingCounter a where
49+
Inc' :: Action FailingCounter Int
50+
51+
arbitraryAction _ _ = pure $ Some Inc'
52+
53+
initialState = FailingCounter 0
54+
55+
nextState FailingCounter{failingCount} Inc' _ = FailingCounter (failingCount + 1)
56+
57+
instance RunModel FailingCounter (ReaderT (IORef Int) IO) where
58+
perform _ Inc' _ = do
59+
ref <- ask
60+
lift $ atomicModifyIORef' ref (\count -> (succ count, count))
61+
62+
postcondition (_, FailingCounter{failingCount}) _ _ _ = pure $ failingCount < 4
63+
64+
-- A generic but simple counter model
65+
data Counter = Counter Int
66+
deriving (Show, Generic)
67+
68+
deriving instance Show (Action Counter a)
69+
deriving instance Eq (Action Counter a)
70+
instance HasVariables (Action Counter a) where
71+
getAllVariables _ = mempty
72+
73+
instance StateModel Counter where
74+
data Action Counter a where
75+
Inc :: Action Counter ()
76+
Reset :: Action Counter Int
77+
78+
initialState = Counter 0
79+
80+
arbitraryAction _ _ = frequency [(5, pure $ Some Inc), (1, pure $ Some Reset)]
81+
82+
nextState (Counter n) Inc _ = Counter (n + 1)
83+
nextState _ Reset _ = Counter 0
84+
85+
instance RunModel Counter (ReaderT (IORef Int) IO) where
86+
perform _ Inc _ = do
87+
ref <- ask
88+
lift $ modifyIORef ref succ
89+
perform _ Reset _ = do
90+
ref <- ask
91+
lift $ do
92+
n <- readIORef ref
93+
writeIORef ref 0
94+
pure n
95+
96+
postcondition (Counter n, _) Reset _ res = pure $ n == res
97+
postcondition _ _ _ _ = pure True

0 commit comments

Comments
 (0)