@@ -36,7 +36,6 @@ module Test.QuickCheck.StateModel (
3636 computePrecondition ,
3737 computeArbitraryAction ,
3838 computeShrinkAction ,
39- failureResult ,
4039) where
4140
4241import Control.Monad
@@ -49,8 +48,8 @@ import Data.Kind
4948import Data.List
5049import Data.Monoid (Endo (.. ))
5150import Data.Set qualified as Set
51+ import Data.Void
5252import GHC.Generics
53- import GHC.Stack
5453import Test.QuickCheck as QC
5554import Test.QuickCheck.DynamicLogic.SmartShrinking
5655import 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
155161deriving 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
159181type family Realized (m :: Type -> Type ) a :: Type
@@ -179,7 +201,7 @@ monitorPost m = PostconditionM $ tell (Endo m, mempty)
179201counterexamplePost :: Monad m => String -> PostconditionM m ()
180202counterexamplePost 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
225249computePostcondition 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
229258type LookUp m = forall a . Typeable a = > Var a -> Realized m a
@@ -252,7 +281,7 @@ lookUpVarMaybe (((v' :: Var b) :== a) : env) v =
252281
253282lookUpVar :: Typeable a => Env m -> Var a -> Realized m a
254283lookUpVar 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
258287data 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
479508runActions
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 )
484517runActions (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
0 commit comments