Skip to content

Commit 5ebbf12

Browse files
Adding errors to actions to help negative testing (#58)
Also: * Better error message for `lookUpVar` * Fix bug in computePostcondition * remove `failureResult` as it is no longer relevant * A monitoring callback called when a positive action fails * More human-friendly semantics for negative actions * Add simplest possible model for a counter example (not a counterexample) * updated changelog --------- Co-authored-by: Ulf Norell <ulf.norell@gmail.com>
1 parent c309099 commit 5ebbf12

File tree

7 files changed

+160
-49
lines changed

7 files changed

+160
-49
lines changed

quickcheck-dynamic/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ changes.
1010
## UNRELEASED
1111

1212
* Added some lightweight negative-shrinking based on a simple dependency analysis.
13+
* Added the option to return errors from actions by defining `type Error state`.
14+
When this is defined `perform` has return type `m (Either (Error state) (Realized m a))`,
15+
when it is left as the default the type remains `m (Realized m a)`.
1316

1417
## 3.3.1
1518

quickcheck-dynamic/quickcheck-dynamic.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ common lang
4646
ImportQualifiedPost
4747
LambdaCase
4848
MultiParamTypeClasses
49+
MultiWayIf
4950
PatternSynonyms
5051
QuantifiedConstraints
5152
RankNTypes
@@ -89,6 +90,7 @@ test-suite quickcheck-dynamic-test
8990
main-is: Spec.hs
9091
hs-source-dirs: test
9192
other-modules:
93+
Spec.DynamicLogic.CounterModel
9294
Spec.DynamicLogic.Registry
9395
Spec.DynamicLogic.RegistryModel
9496

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ instance StateModel s => HasVariables (FailingAction s) where
162162

163163
instance StateModel s => Eq (FailingAction s) where
164164
ErrorFail s == ErrorFail s' = s == s'
165-
ActionFail (a :: ActionWithPolarity s a) == ActionFail (a' :: ActionWithPolarity s a')
165+
ActionFail (a :: ActionWithPolarity s a) == ActionFail (a' :: ActionWithPolarity s' a')
166166
| Just Refl <- eqT @a @a' = a == a'
167167
_ == _ = False
168168

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

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ module Test.QuickCheck.StateModel (
3636
computePrecondition,
3737
computeArbitraryAction,
3838
computeShrinkAction,
39-
failureResult,
4039
) where
4140

4241
import Control.Monad
@@ -49,8 +48,8 @@ import Data.Kind
4948
import Data.List
5049
import Data.Monoid (Endo (..))
5150
import Data.Set qualified as Set
51+
import Data.Void
5252
import GHC.Generics
53-
import GHC.Stack
5453
import Test.QuickCheck as QC
5554
import Test.QuickCheck.DynamicLogic.SmartShrinking
5655
import Test.QuickCheck.Monadic
@@ -97,14 +96,21 @@ class
9796
-- @
9897
-- data Action RegState a where
9998
-- Spawn :: Action RegState ThreadId
100-
-- Register :: String -> Var ThreadId -> Action RegState (Either ErrorCall ())
99+
-- Register :: String -> Var ThreadId -> Action RegState ()
101100
-- KillThread :: Var ThreadId -> Action RegState ()
102101
-- @
103102
--
104103
-- The @Spawn@ action should produce a @ThreadId@, whereas the @KillThread@ action does not return
105104
-- anything.
106105
data Action state a
107106

107+
-- | The type of errors that actions can throw. If this is defined as anything
108+
-- other than `Void` `perform` is required to return `Either (Error state) a`
109+
-- instead of `a`.
110+
type Error state
111+
112+
type Error state = Void
113+
108114
-- | Display name for `Action`.
109115
-- This is useful to provide sensible statistics about the distribution of `Action`s run
110116
-- when checking a property.
@@ -154,6 +160,22 @@ class
154160

155161
deriving instance (forall a. Show (Action state a)) => Show (Any (Action state))
156162

163+
-- | The result required of `perform` depending on the `Error` type
164+
-- of a state model. If there are no errors, `Error state = Void`, and
165+
-- so we don't need to specify if the action failed or not.
166+
type family PerformResult e a where
167+
PerformResult Void a = a
168+
PerformResult e a = Either e a
169+
170+
class IsPerformResult e a where
171+
performResultToEither :: PerformResult e a -> Either e a
172+
173+
instance {-# OVERLAPPING #-} IsPerformResult Void a where
174+
performResultToEither = Right
175+
176+
instance {-# OVERLAPPABLE #-} (PerformResult e a ~ Either e a) => IsPerformResult e a where
177+
performResultToEither = id
178+
157179
-- TODO: maybe it makes sense to write
158180
-- out a long list of these instances
159181
type family Realized (m :: Type -> Type) a :: Type
@@ -179,7 +201,7 @@ monitorPost m = PostconditionM $ tell (Endo m, mempty)
179201
counterexamplePost :: Monad m => String -> PostconditionM m ()
180202
counterexamplePost c = PostconditionM $ tell (mempty, Endo $ counterexample c)
181203

182-
class Monad m => RunModel state m where
204+
class (forall a. Show (Action state a), Monad m) => RunModel state m where
183205
-- | Perform an `Action` in some `state` in the `Monad` `m`. This
184206
-- is the function that's used to exercise the actual stateful
185207
-- implementation, usually through various side-effects as permitted
@@ -190,40 +212,47 @@ class Monad m => RunModel state m where
190212
--
191213
-- The `Lookup` parameter provides an /environment/ to lookup `Var
192214
-- a` instances from previous steps.
193-
perform :: forall a. Typeable a => state -> Action state a -> LookUp m -> m (Realized m a)
215+
perform :: Typeable a => state -> Action state a -> LookUp m -> m (PerformResult (Error state) (Realized m a))
194216

195217
-- | Postcondition on the `a` value produced at some step.
196218
-- The result is `assert`ed and will make the property fail should it be `False`. This is useful
197219
-- to check the implementation produces expected values.
198-
postcondition :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> PostconditionM m Bool
220+
postcondition :: (state, state) -> Action state a -> LookUp m -> Realized m a -> PostconditionM m Bool
199221
postcondition _ _ _ _ = pure True
200222

201223
-- | Postcondition on the result of running a _negative_ `Action`.
202224
-- The result is `assert`ed and will make the property fail should it be `False`. This is useful
203225
-- to check the implementation produces e.g. the expected errors or to check that the SUT hasn't
204226
-- been updated during the execution of the negative action.
205-
postconditionOnFailure :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> PostconditionM m Bool
227+
postconditionOnFailure :: (state, state) -> Action state a -> LookUp m -> Either (Error state) (Realized m a) -> PostconditionM m Bool
206228
postconditionOnFailure _ _ _ _ = pure True
207229

208230
-- | Allows the user to attach additional information to the `Property` at each step of the process.
209231
-- This function is given the full transition that's been executed, including the start and ending
210232
-- `state`, the `Action`, the current environment to `Lookup` and the value produced by `perform`
211233
-- while executing this step.
212-
monitoring :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> Property -> Property
234+
monitoring :: (state, state) -> Action state a -> LookUp m -> Either (Error state) (Realized m a) -> Property -> Property
213235
monitoring _ _ _ _ prop = prop
214236

215-
-- | Indicate that the result of an action (in `perform`)
216-
-- should not be inspected by the postcondition or appear
217-
-- in a positive test. Useful when we want to give a type
218-
-- for an `Action` like `SomeAct :: Action SomeState SomeType`
219-
-- instead of `SomeAct :: Action SomeState (Either SomeError SomeType)`
220-
-- but still need to return something in `perform` in the failure case.
221-
failureResult :: HasCallStack => a
222-
failureResult = error "A result of a failing action has been erronesouly inspected"
237+
-- | Allows the user to attach additional information to the `Property` if a positive action fails.
238+
monitoringFailure :: state -> Action state a -> LookUp m -> Error state -> Property -> Property
239+
monitoringFailure _ _ _ _ prop = prop
223240

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

229258
type LookUp m = forall a. Typeable a => Var a -> Realized m a
@@ -252,7 +281,7 @@ lookUpVarMaybe (((v' :: Var b) :== a) : env) v =
252281

253282
lookUpVar :: Typeable a => Env m -> Var a -> Realized m a
254283
lookUpVar env v = case lookUpVarMaybe env v of
255-
Nothing -> error $ "Variable " ++ show v ++ " is not bound!"
284+
Nothing -> error $ "Variable " ++ show v ++ " is not bound at type " ++ show (typeRep v) ++ "!"
256285
Just a -> a
257286

258287
data WithUsedVars a = WithUsedVars VarContext a
@@ -477,8 +506,12 @@ stateAfter (Actions actions) = loop initialAnnotatedState actions
477506
loop s ((var := act) : as) = loop (computeNextState @state s act var) as
478507

479508
runActions
480-
:: forall state m
481-
. (StateModel state, RunModel state m)
509+
:: forall state m e
510+
. ( StateModel state
511+
, RunModel state m
512+
, e ~ Error state
513+
, forall a. IsPerformResult e a
514+
)
482515
=> Actions state
483516
-> PropertyM m (Annotated state, Env m)
484517
runActions (Actions_ rejected (Smart _ actions)) = loop initialAnnotatedState [] actions
@@ -491,24 +524,37 @@ runActions (Actions_ rejected (Smart _ actions)) = loop initialAnnotatedState []
491524
return (_s, reverse env)
492525
loop s env ((v := act) : as) = do
493526
pre $ computePrecondition s act
494-
ret <- run $ perform (underlyingState s) (polarAction act) (lookUpVar env)
527+
ret <- run $ performResultToEither <$> perform (underlyingState s) (polarAction act) (lookUpVar env)
495528
let name = show (polarity act) ++ actionName (polarAction act)
496529
monitor $ tabulate "Actions" [name]
497-
let var = unsafeCoerceVar v
498-
s' = computeNextState s act var
499-
env' = (var :== ret) : env
500530
monitor $ tabulate "Action polarity" [show $ polarity act]
501-
monitor $ monitoring @state @m (underlyingState s, underlyingState s') (polarAction act) (lookUpVar env') ret
502-
(b, (Endo mon, Endo onFail)) <-
503-
run
504-
. runWriterT
505-
. runPost
506-
$ computePostcondition @m
507-
(underlyingState s, underlyingState s')
508-
act
509-
(lookUpVar env)
510-
ret
511-
monitor mon
512-
unless b $ monitor onFail
513-
assert b
514-
loop s' env' as
531+
if
532+
| polarity act == PosPolarity
533+
, Left err <- ret -> do
534+
monitor $
535+
monitoringFailure @state @m
536+
(underlyingState s)
537+
(polarAction act)
538+
(lookUpVar env)
539+
err
540+
stop False
541+
| otherwise -> do
542+
let var = unsafeCoerceVar v
543+
s' = computeNextState s act var
544+
env'
545+
| Right val <- ret = (var :== val) : env
546+
| otherwise = env
547+
monitor $ monitoring @state @m (underlyingState s, underlyingState s') (polarAction act) (lookUpVar env') ret
548+
(b, (Endo mon, Endo onFail)) <-
549+
run
550+
. runWriterT
551+
. runPost
552+
$ computePostcondition @m
553+
(underlyingState s, underlyingState s')
554+
act
555+
(lookUpVar env)
556+
ret
557+
monitor mon
558+
unless b $ monitor onFail
559+
assert b
560+
loop s' env' as

quickcheck-dynamic/test/Spec.hs

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

33
module Main (main) where
44

5+
import Spec.DynamicLogic.CounterModel qualified
56
import Spec.DynamicLogic.RegistryModel qualified
67
import Test.Tasty
78

@@ -13,4 +14,5 @@ tests =
1314
testGroup
1415
"dynamic logic"
1516
[ Spec.DynamicLogic.RegistryModel.tests
17+
, Spec.DynamicLogic.CounterModel.tests
1618
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
module Spec.DynamicLogic.CounterModel where
2+
3+
import Control.Monad.Reader
4+
import Data.IORef
5+
import Test.QuickCheck
6+
import Test.QuickCheck.Extras
7+
import Test.QuickCheck.Monadic
8+
import Test.QuickCheck.StateModel
9+
import Test.Tasty hiding (after)
10+
import Test.Tasty.QuickCheck
11+
12+
data Counter = Counter Int
13+
deriving (Show, Generic)
14+
15+
deriving instance Show (Action Counter a)
16+
deriving instance Eq (Action Counter a)
17+
instance HasVariables (Action Counter a) where
18+
getAllVariables _ = mempty
19+
20+
instance StateModel Counter where
21+
data Action Counter a where
22+
Inc :: Action Counter ()
23+
Reset :: Action Counter Int
24+
25+
initialState = Counter 0
26+
27+
arbitraryAction _ _ = frequency [(5, pure $ Some Inc), (1, pure $ Some Reset)]
28+
29+
nextState (Counter n) Inc _ = Counter (n + 1)
30+
nextState _ Reset _ = Counter 0
31+
32+
instance RunModel Counter (ReaderT (IORef Int) IO) where
33+
perform _ Inc _ = do
34+
ref <- ask
35+
lift $ modifyIORef ref succ
36+
perform _ Reset _ = do
37+
ref <- ask
38+
lift $ do
39+
n <- readIORef ref
40+
writeIORef ref 0
41+
pure n
42+
43+
postcondition (Counter n, _) Reset _ res = pure $ n == res
44+
postcondition _ _ _ _ = pure True
45+
46+
prop_counter :: Actions Counter -> Property
47+
prop_counter as = monadicIO $ do
48+
ref <- lift $ newIORef (0 :: Int)
49+
runPropertyReaderT (runActions as) ref
50+
assert True
51+
52+
tests :: TestTree
53+
tests =
54+
testGroup
55+
"counter tests"
56+
[ testProperty "prop_conter" $ prop_counter
57+
]

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ instance StateModel RegState where
4040
data Action RegState a where
4141
Spawn :: Action RegState ThreadId
4242
WhereIs :: String -> Action RegState (Maybe ThreadId)
43-
Register :: String -> Var ThreadId -> Action RegState (Either SomeException ())
44-
Unregister :: String -> Action RegState (Either SomeException ())
43+
Register :: String -> Var ThreadId -> Action RegState ()
44+
Unregister :: String -> Action RegState ()
4545
KillThread :: Var ThreadId -> Action RegState ()
4646

47+
type Error RegState = SomeException
48+
4749
precondition s (Register name tid) =
4850
name `Map.notMember` regs s
4951
&& tid `notElem` Map.elems (regs s)
@@ -109,7 +111,8 @@ type RegM = ReaderT Registry IO
109111

110112
instance RunModel RegState RegM where
111113
perform _ Spawn _ = do
112-
lift $ forkIO (threadDelay 10000000)
114+
tid <- lift $ forkIO (threadDelay 10000000)
115+
pure $ Right tid
113116
perform _ (Register name tid) env = do
114117
reg <- ask
115118
lift $ try $ register reg name (env tid)
@@ -118,24 +121,22 @@ instance RunModel RegState RegM where
118121
lift $ try $ unregister reg name
119122
perform _ (WhereIs name) _ = do
120123
reg <- ask
121-
lift $ whereis reg name
124+
res <- lift $ whereis reg name
125+
pure $ Right res
122126
perform _ (KillThread tid) env = do
123127
lift $ killThread (env tid)
124128
lift $ threadDelay 100
129+
pure $ Right ()
125130

126131
postcondition (s, _) (WhereIs name) env mtid = do
127132
pure $ (env <$> Map.lookup name (regs s)) == mtid
128-
postcondition _ Register{} _ res = do
129-
pure $ isRight res
130133
postcondition _ _ _ _ = pure True
131134

132135
postconditionOnFailure (s, _) act@Register{} _ res = do
133136
monitorPost $
134137
tabulate
135138
"Reason for -Register"
136-
[ why s act
137-
| Left{} <- [res]
138-
]
139+
[why s act]
139140
pure $ isLeft res
140141
postconditionOnFailure _s _ _ _ = pure True
141142

0 commit comments

Comments
 (0)