quickcheck-dynamic 3.1.1 → 3.2.0
raw patch · 10 files changed
+667/−180 lines, 10 filesdep +quickcheck-dynamicdep +stmdep +tasty
Dependencies added: quickcheck-dynamic, stm, tasty, tasty-quickcheck, tasty-test-reporter
Files
- CHANGELOG.md +5/−0
- quickcheck-dynamic.cabal +67/−47
- src/Test/QuickCheck/DynamicLogic.hs +23/−19
- src/Test/QuickCheck/DynamicLogic/Internal.hs +83/−57
- src/Test/QuickCheck/DynamicLogic/Quantify.hs +2/−2
- src/Test/QuickCheck/StateModel.hs +146/−52
- src/Test/QuickCheck/StateModel/Variables.hs +3/−3
- test/Spec.hs +17/−0
- test/Spec/DynamicLogic/Registry.hs +50/−0
- test/Spec/DynamicLogic/RegistryModel.hs +271/−0
CHANGELOG.md view
@@ -9,6 +9,11 @@ ## UNRELEASED +## 3.2.0++* Added support for negative testing via `validFailingAction` and `postconditionOnFailure`+ callbacks in `StateModel` and `RunModel`.+ ## 3.1.1 - 2023-06-26 * Added instances for `HasVariables` with custom error messages to avoid the issue of
quickcheck-dynamic.cabal view
@@ -1,16 +1,15 @@-cabal-version: 2.2-name: quickcheck-dynamic-version: 3.1.1-license: Apache-2.0+cabal-version: 2.2+name: quickcheck-dynamic+version: 3.2.0+license: Apache-2.0 license-files: LICENSE NOTICE -maintainer: arnaud.bailly@iohk.io-author: Ulf Norell-category: Testing-synopsis:- A library for stateful property-based testing+maintainer: arnaud.bailly@iohk.io+author: Ulf Norell+category: Testing+synopsis: A library for stateful property-based testing homepage: https://github.com/input-output-hk/quickcheck-dynamic#readme @@ -20,8 +19,8 @@ description: Please see the README on GitHub at <https://github.com/input-output-hk/quickcheck-dynamic#readme> -build-type: Simple-extra-doc-files: README.md+build-type: Simple+extra-doc-files: README.md extra-source-files: CHANGELOG.md source-repository head@@ -32,54 +31,75 @@ default-language: Haskell2010 default-extensions: ConstraintKinds- QuantifiedConstraints DataKinds- DeriveFunctor+ DefaultSignatures+ DeriveDataTypeable DeriveFoldable+ DeriveFunctor+ DeriveGeneric DeriveTraversable- DeriveDataTypeable- StandaloneDeriving+ DerivingVia+ FlexibleContexts+ FlexibleInstances+ GADTs+ GeneralizedNewtypeDeriving ImportQualifiedPost- TupleSections LambdaCase+ MultiParamTypeClasses PatternSynonyms- GADTs- TypeApplications+ QuantifiedConstraints+ RankNTypes ScopedTypeVariables+ StandaloneDeriving+ TupleSections+ TypeApplications TypeFamilies- FlexibleContexts- FlexibleInstances- MultiParamTypeClasses- RankNTypes- ViewPatterns- DefaultSignatures TypeOperators- DerivingVia- DeriveGeneric- GeneralizedNewtypeDeriving+ ViewPatterns ghc-options: -Wall -Wnoncanonical-monad-instances -Wunused-packages -Wincomplete-uni-patterns -Wincomplete-record-updates- -Wredundant-constraints -Widentities- -Wno-unused-do-bind+ -Wredundant-constraints -Widentities -Wno-unused-do-bind library- import: lang- hs-source-dirs: src- exposed-modules:- Test.QuickCheck.DynamicLogic- Test.QuickCheck.DynamicLogic.CanGenerate- Test.QuickCheck.DynamicLogic.Internal- Test.QuickCheck.DynamicLogic.Quantify- Test.QuickCheck.DynamicLogic.SmartShrinking- Test.QuickCheck.DynamicLogic.Utils- Test.QuickCheck.Extras- Test.QuickCheck.StateModel- Test.QuickCheck.StateModel.Variables- build-depends:- QuickCheck -any,- base >= 4.7 && <5,- random -any,- mtl -any,- containers -any,+ import: lang+ hs-source-dirs: src+ exposed-modules:+ Test.QuickCheck.DynamicLogic+ Test.QuickCheck.DynamicLogic.CanGenerate+ Test.QuickCheck.DynamicLogic.Internal+ Test.QuickCheck.DynamicLogic.Quantify+ Test.QuickCheck.DynamicLogic.SmartShrinking+ Test.QuickCheck.DynamicLogic.Utils+ Test.QuickCheck.Extras+ Test.QuickCheck.StateModel+ Test.QuickCheck.StateModel.Variables++ build-depends:+ , base >=4.7 && <5+ , containers+ , mtl+ , QuickCheck+ , random++test-suite quickcheck-dynamic-test+ import: lang+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test+ other-modules:+ Spec.DynamicLogic.Registry+ Spec.DynamicLogic.RegistryModel++ ghc-options: -rtsopts+ build-depends:+ , base >=4.7 && <5+ , containers+ , mtl+ , QuickCheck+ , quickcheck-dynamic+ , stm+ , tasty+ , tasty-quickcheck+ , tasty-test-reporter
src/Test/QuickCheck/DynamicLogic.hs view
@@ -9,6 +9,7 @@ module Test.QuickCheck.DynamicLogic ( DL, action,+ failingAction, anyAction, anyActions, anyActions_,@@ -62,6 +63,9 @@ action :: (Typeable a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s (Var a) action cmd = DL $ \_ k -> DL.after cmd k +failingAction :: (Typeable a, Eq (Action s a), Show (Action s a)) => Action s a -> DL s ()+failingAction cmd = DL $ \_ k -> DL.afterNegative cmd (k ())+ anyAction :: DL s () anyAction = DL $ \_ k -> DL.afterAny $ k () @@ -129,28 +133,28 @@ runDL :: Annotated s -> DL s () -> DL.DynFormula s runDL s dl = unDL dl s $ \_ _ -> DL.passTest -forAllUniqueDL ::- (DL.DynLogicModel s, Testable a) =>- Annotated s ->- DL s () ->- (Actions s -> a) ->- Property+forAllUniqueDL+ :: (DL.DynLogicModel s, Testable a)+ => Annotated s+ -> DL s ()+ -> (Actions s -> a)+ -> Property forAllUniqueDL initState d = DL.forAllUniqueScripts initState (runDL initState d) -forAllDL ::- (DL.DynLogicModel s, Testable a) =>- DL s () ->- (Actions s -> a) ->- Property+forAllDL+ :: (DL.DynLogicModel s, Testable a)+ => DL s ()+ -> (Actions s -> a)+ -> Property forAllDL d = DL.forAllScripts (runDL initialAnnotatedState d) -forAllMappedDL ::- (DL.DynLogicModel s, Testable a) =>- (rep -> DL.DynLogicTest s) ->- (DL.DynLogicTest s -> rep) ->- (Actions s -> srep) ->- DL s () ->- (srep -> a) ->- Property+forAllMappedDL+ :: (DL.DynLogicModel s, Testable a)+ => (rep -> DL.DynLogicTest s)+ -> (DL.DynLogicTest s -> rep)+ -> (Actions s -> srep)+ -> DL s ()+ -> (srep -> a)+ -> Property forAllMappedDL to from fromScript d prop = DL.forAllMappedScripts to from (runDL initialAnnotatedState d) (prop . fromScript)
src/Test/QuickCheck/DynamicLogic/Internal.hs view
@@ -33,7 +33,7 @@ | -- | After a specific action the predicate should hold forall a. (Eq (Action s a), Show (Action s a), Typeable a) =>- After (Action s a) (Var a -> DynPred s)+ After (ActionWithPolarity s a) (Var a -> DynPred s) | Error String (DynPred s) | -- | Adjust the probability of picking a branch Weight Double (DynLogic s)@@ -64,15 +64,32 @@ afterAny :: (Annotated s -> DynFormula s) -> DynFormula s afterAny f = DynFormula $ \n -> AfterAny $ \s -> unDynFormula (f s) n +afterPolar+ :: (Typeable a, Eq (Action s a), Show (Action s a))+ => ActionWithPolarity s a+ -> (Var a -> Annotated s -> DynFormula s)+ -> DynFormula s+afterPolar act f = DynFormula $ \n -> After act $ \x s -> unDynFormula (f x s) n+ -- | Given `f` must be `True` after /some/ action. -- `f` is passed the state resulting from executing the `Action`.-after ::- (Typeable a, Eq (Action s a), Show (Action s a)) =>- Action s a ->- (Var a -> Annotated s -> DynFormula s) ->- DynFormula s-after act f = DynFormula $ \n -> After act $ \x s -> unDynFormula (f x s) n+after+ :: (Typeable a, Eq (Action s a), Show (Action s a))+ => Action s a+ -> (Var a -> Annotated s -> DynFormula s)+ -> DynFormula s+after act f = afterPolar (ActionWithPolarity act PosPolarity) f +-- | Given `f` must be `True` after /some/ negative action.+-- `f` is passed the state resulting from executing the `Action`+-- as a negative action.+afterNegative+ :: (Typeable a, Eq (Action s a), Show (Action s a))+ => Action s a+ -> (Annotated s -> DynFormula s)+ -> DynFormula s+afterNegative act f = afterPolar (ActionWithPolarity act NegPolarity) (const f)+ -- | Disjunction for DL formulae. -- Is `True` if either formula is `True`. The choice is /angelic/, ie. it is -- always made by the "caller". This is mostly important in case a test is@@ -88,11 +105,11 @@ -- for those values. `Quantifiable` values are thus values that can be generated -- and checked and the `Test.QuickCheck.DynamicLogic.Quantify` module defines -- basic combinators to build those from building blocks.-forAllQ ::- Quantifiable q =>- q ->- (Quantifies q -> DynFormula s) ->- DynFormula s+forAllQ+ :: Quantifiable q+ => q+ -> (Quantifies q -> DynFormula s)+ -> DynFormula s forAllQ q f | isEmptyQ q' = ignore | otherwise = DynFormula $ \n -> ForAll q' $ ($ n) . unDynFormula . f@@ -137,7 +154,7 @@ data FailingAction s = ErrorFail String- | forall a. (Typeable a, Eq (Action s a)) => ActionFail (Action s a)+ | forall a. (Typeable a, Eq (Action s a)) => ActionFail (ActionWithPolarity s a) instance StateModel s => HasVariables (FailingAction s) where getAllVariables ErrorFail{} = mempty@@ -145,13 +162,13 @@ instance StateModel s => Eq (FailingAction s) where ErrorFail s == ErrorFail s' = s == s'- ActionFail (a :: Action s a) == ActionFail (a' :: Action s a')+ ActionFail (a :: ActionWithPolarity s a) == ActionFail (a' :: ActionWithPolarity s a') | Just Refl <- eqT @a @a' = a == a' _ == _ = False instance StateModel s => Show (FailingAction s) where show (ErrorFail s) = "Error " ++ show s- show (ActionFail a) = show a+ show (ActionFail (ActionWithPolarity a pol)) = show pol ++ " : " ++ show a data DynLogicTest s = BadPrecondition (TestSequence s) (FailingAction s) (Annotated s)@@ -193,8 +210,10 @@ type TestStep s = Witnesses (Step s) newtype TestSequence s = TestSeq (Witnesses (TestContinuation s))- deriving (Show, Eq) +deriving instance StateModel s => Show (TestSequence s)+deriving instance StateModel s => Eq (TestSequence s)+ data TestContinuation s = ContStep (Step s) (TestSequence s) | ContStop@@ -285,7 +304,11 @@ where prettyBad :: FailingAction s -> String prettyBad (ErrorFail e) = "assert " ++ show e ++ " False"- prettyBad (ActionFail a) = "action $ " ++ show a ++ " -- Failed precondition\n pure ()"+ prettyBad (ActionFail (ActionWithPolarity a p)) = f ++ " $ " ++ show a ++ " -- Failed precondition\n pure ()"+ where+ f+ | p == PosPolarity = "action"+ | otherwise = "failingAction" show (Looping ss) = prettyTestSequence (usedVariables ss) ss ++ "\n pure ()\n -- Looping" show (Stuck ss s) = prettyTestSequence (usedVariables ss) ss ++ "\n pure ()\n -- Stuck in state " ++ show s show (DLScript ss) = prettyTestSequence (usedVariables ss) ss ++ "\n pure ()\n"@@ -309,26 +332,29 @@ restricted :: Action s a -> Bool restricted _ = False +restrictedPolar :: DynLogicModel s => ActionWithPolarity s a -> Bool+restrictedPolar (ActionWithPolarity a _) = restricted a+ -- * Generate Properties -- | Simplest "execution" function for `DynFormula`. -- Turns a given a `DynFormula` paired with an interpreter function to produce some result from an --- `Actions` sequence into a proper `Property` than can then be run by QuickCheck.-forAllScripts ::- (DynLogicModel s, Testable a) =>- DynFormula s ->- (Actions s -> a) ->- Property+forAllScripts+ :: (DynLogicModel s, Testable a)+ => DynFormula s+ -> (Actions s -> a)+ -> Property forAllScripts = forAllMappedScripts id id -- | `Property` function suitable for formulae without choice.-forAllUniqueScripts ::- (DynLogicModel s, Testable a) =>- Annotated s ->- DynFormula s ->- (Actions s -> a) ->- Property+forAllUniqueScripts+ :: (DynLogicModel s, Testable a)+ => Annotated s+ -> DynFormula s+ -> (Actions s -> a)+ -> Property forAllUniqueScripts s f k = QC.withSize $ \sz -> let d = unDynFormula f sz@@ -338,13 +364,13 @@ Just test -> validDLTest d test . applyMonitoring d test . property $ k (scriptFromDL test) -- | Creates a `Property` from `DynFormula` with some specialised isomorphism for shrinking purpose.-forAllMappedScripts ::- (DynLogicModel s, Testable a) =>- (rep -> DynLogicTest s) ->- (DynLogicTest s -> rep) ->- DynFormula s ->- (Actions s -> a) ->- Property+forAllMappedScripts+ :: (DynLogicModel s, Testable a)+ => (rep -> DynLogicTest s)+ -> (DynLogicTest s -> rep)+ -> DynFormula s+ -> (Actions s -> a)+ -> Property forAllMappedScripts to from f k = QC.withSize $ \n -> let d = unDynFormula f n@@ -383,14 +409,14 @@ addW Do{} ss = ss addW (Witness a w') ss = TestSeqWitness a (addW w' ss) -generate ::- (Monad m, DynLogicModel s) =>- (Annotated s -> Int -> DynLogic s -> m (NextStep s)) ->- DynLogic s ->- Int ->- Annotated s ->- Int ->- m (DynLogicTest s)+generate+ :: (Monad m, DynLogicModel s)+ => (Annotated s -> Int -> DynLogic s -> m (NextStep s))+ -> DynLogic s+ -> Int+ -> Annotated s+ -> Int+ -> m (DynLogicTest s) generate chooseNextStepFun d n s size = if n > sizeLimit size then return $ Looping TestSeqStop@@ -504,10 +530,10 @@ AfterAny k -> do m <- keepTryingUntil 100 (computeArbitraryAction s) $ \case- Some act -> computePrecondition s act && not (restricted act)+ Some act -> computePrecondition s act && not (restrictedPolar act) case m of Nothing -> return NoStep- Just (Some a) ->+ Just (Some a@ActionWithPolarity{}) -> return $ Stepping (Do $ mkVar n := a)@@ -563,19 +589,19 @@ nextStateStep :: StateModel s => Step s -> Annotated s -> Annotated s nextStateStep (var := act) s = computeNextState s act var -shrinkScript :: DynLogicModel s => DynLogic s -> TestSequence s -> [TestSequence s]+shrinkScript :: forall s. DynLogicModel s => DynLogic s -> TestSequence s -> [TestSequence s] shrinkScript = shrink' initialAnnotatedState where- shrink' :: DynLogicModel s => Annotated s -> DynLogic s -> TestSequence s -> [TestSequence s]+ shrink' :: Annotated s -> DynLogic s -> TestSequence s -> [TestSequence s] shrink' s d ss = structural s d ss ++ nonstructural s d ss nonstructural s d (TestSeqWitness a ss) = [ TestSeqWitness a' ss'- | a' <- shrinkWitness d a+ | a' <- shrinkWitness @s d a , ss' <- ss : shrink' s (stepDLSeq d s $ TestSeqWitness a TestSeqStop) ss ] nonstructural s d (TestSeqStep step@(var := act) ss) =- [TestSeqStep (unsafeCoerceVar var := act') ss | Some act' <- computeShrinkAction s act]+ [TestSeqStep (unsafeCoerceVar var := act') ss | Some act'@ActionWithPolarity{} <- computeShrinkAction s act] ++ [ TestSeqStep step ss' | ss' <- shrink'@@ -608,13 +634,13 @@ -- The result of pruning a list of actions is a prefix of a list of actions that -- could have been generated by the dynamic logic.-pruneDLTest :: DynLogicModel s => DynLogic s -> TestSequence s -> TestSequence s+pruneDLTest :: forall s. DynLogicModel s => DynLogic s -> TestSequence s -> TestSequence s pruneDLTest dl = prune [dl] initialAnnotatedState where prune [] _ _ = TestSeqStop prune _ _ TestSeqStop = TestSeqStop prune ds s (TestSeqWitness a ss) =- case [d' | d <- ds, d' <- stepDLW d a] of+ case [d' | d <- ds, d' <- stepDLW @s d a] of [] -> prune ds s ss ds' -> TestSeqWitness a $ prune ds' s ss prune ds s (TestSeqStep step@(_ := act) ss)@@ -630,7 +656,7 @@ -- the type variables cleanly and do `Just Refl <- eqT ...` here instead. | Some a == Some act = [k (unsafeCoerceVar var) (computeNextState s act (unsafeCoerceVar var))] stepDL (AfterAny k) s (Do (var := act))- | not (restricted act) = [k (computeNextState s act var)]+ | not (restrictedPolar act) = [k (computeNextState s act var)] stepDL (Alt _ d d') s step = stepDL d s step ++ stepDL d' s step stepDL (Stopping d) s step = stepDL d s step stepDL (Weight _ d) s step = stepDL d s step@@ -641,7 +667,7 @@ stepDL (Monitor _f d) s step = stepDL d s step stepDL _ _ _ = [] -stepDLW :: forall a s. (DynLogicModel s, Typeable a) => DynLogic s -> a -> [DynLogic s]+stepDLW :: forall s a. (DynLogicModel s, Typeable a) => DynLogic s -> a -> [DynLogic s] stepDLW (ForAll (q :: Quantification a') k) a = case eqT @a @a' of Just Refl -> [k a | isaQ q a]@@ -684,11 +710,11 @@ getScript (Stuck s _) = s getScript (DLScript s) = s -makeTestFromPruned :: DynLogicModel s => DynLogic s -> TestSequence s -> DynLogicTest s+makeTestFromPruned :: forall s. DynLogicModel s => DynLogic s -> TestSequence s -> DynLogicTest s makeTestFromPruned dl = make dl initialAnnotatedState where make d s TestSeqStop- | b : _ <- badActions d s = BadPrecondition TestSeqStop b s+ | b : _ <- badActions @s d s = BadPrecondition TestSeqStop b s | stuck d s = Stuck TestSeqStop s | otherwise = DLScript TestSeqStop make d s (TestSeqWitness a ss) =@@ -728,7 +754,7 @@ ( \case Some act -> computePrecondition s act- && not (restricted act)+ && not (restrictedPolar act) ) stuck (Alt Angelic d d') s = stuck d s && stuck d' s stuck (Alt Demonic d d') s = stuck d s || stuck d' s@@ -799,7 +825,7 @@ where s' = computeNextState s a' (unsafeCoerceVar var) findMonitoring (AfterAny k) s as@(TestSeqStep (_var := a) _)- | not (restricted a) = findMonitoring (After a $ const k) s as+ | not (restrictedPolar a) = findMonitoring (After a $ const k) s as findMonitoring (Alt _b d d') s as = -- Give priority to monitoring matches to the left. Combining both -- results in repeated monitoring from always, which is unexpected.
src/Test/QuickCheck/DynamicLogic/Quantify.hs view
@@ -205,8 +205,8 @@ from (a, b, c, d) = (a, (b, (c, d))) instance- (Quantifiable a, Quantifiable b, Quantifiable c, Quantifiable d, Quantifiable e) =>- Quantifiable (a, b, c, d, e)+ (Quantifiable a, Quantifiable b, Quantifiable c, Quantifiable d, Quantifiable e)+ => Quantifiable (a, b, c, d, e) where type Quantifies (a, b, c, d, e) =
src/Test/QuickCheck/StateModel.hs view
@@ -15,6 +15,8 @@ WithUsedVars (..), Annotated (..), Step (..),+ Polarity (..),+ ActionWithPolarity (..), LookUp, Actions (..), pattern Actions,@@ -34,6 +36,7 @@ computePrecondition, computeArbitraryAction, computeShrinkAction,+ failureResult, ) where import Control.Monad@@ -46,6 +49,7 @@ import Data.List import Data.Set qualified as Set import GHC.Generics+import GHC.Stack import Test.QuickCheck as QC import Test.QuickCheck.DynamicLogic.SmartShrinking import Test.QuickCheck.Monadic@@ -69,6 +73,12 @@ -- the action is /rejected/ and a new one is tried. This is also useful when shrinking a trace -- in order to ensure that removing some `Action` still produces a valid trace. The `precondition` can be -- somewhat redundant with the generator's conditions,+-- * `validFailingAction`: Specifies when an action that fails it's `precondition` can still run as what is+-- called a _negative_ action. This means that the action is (1) expected to fail and (2) not expected to+-- change the model state. This is very useful for testing the checks and failure conditions in the SUT+-- are implemented correctly. Should it be necessary to update the model state with e.g. book-keeping for+-- a negative action one can define `failureNextState` - but it is generally recommended to let this be+-- as simple an action as possible. class ( forall a. Show (Action state a) , forall a. HasVariables (Action state a)@@ -123,12 +133,24 @@ nextState :: Typeable a => state -> Action state a -> Var a -> state nextState s _ _ = s + -- | Transition function for negative actions. Note that most negative testing applications+ -- should not require an implementation of this function!+ failureNextState :: Typeable a => state -> Action state a -> state+ failureNextState s _ = s+ -- | Precondition for filtering generated `Action`. -- This function is applied before the action is performed, it is useful to refine generators that -- can produce more values than are useful. precondition :: state -> Action state a -> Bool precondition _ _ = True + -- | Precondition for filtering an `Action` that can meaningfully run but is supposed to fail.+ -- An action will run as a _negative_ action if the `precondition` fails and `validFailingAction` succeeds.+ -- A negative action should have _no effect_ on the model state. This may not be desierable in all+ -- situations - in which case one can override this semantics for book-keeping in `failureNextState`.+ validFailingAction :: state -> Action state a -> Bool+ validFailingAction _ _ = False+ deriving instance (forall a. Show (Action state a)) => Show (Any (Action state)) -- TODO: maybe it makes sense to write@@ -172,6 +194,13 @@ postcondition :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> PostconditionM m Bool postcondition _ _ _ _ = pure True + -- | Postcondition on the result of running a _negative_ `Action`.+ -- The result is `assert`ed and will make the property fail should it be `False`. This is useful+ -- to check the implementation produces e.g. the expected errors or to check that the SUT hasn't+ -- been updated during the execution of the negative action.+ postconditionOnFailure :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> PostconditionM m Bool+ postconditionOnFailure _ _ _ _ = pure True+ -- | Allows the user to attach additional information to the `Property` at each step of the process. -- This function is given the full transition that's been executed, including the start and ending -- `state`, the `Action`, the current environment to `Lookup` and the value produced by `perform`@@ -179,6 +208,20 @@ monitoring :: forall a. (state, state) -> Action state a -> LookUp m -> Realized m a -> Property -> Property monitoring _ _ _ _ prop = prop +-- | Indicate that the result of an action (in `perform`)+-- should not be inspected by the postcondition or appear+-- in a positive test. Useful when we want to give a type+-- for an `Action` like `SomeAct :: Action SomeState SomeType`+-- instead of `SomeAct :: Action SomeState (Either SomeError SomeType)`+-- but still need to return something in `perform` in the failure case.+failureResult :: HasCallStack => a+failureResult = error "A result of a failing action has been erronesouly inspected"++computePostcondition :: forall m state a. RunModel state m => (state, state) -> ActionWithPolarity state a -> LookUp m -> Realized m a -> PostconditionM m Bool+computePostcondition ss (ActionWithPolarity a p) l r+ | p == PosPolarity = postcondition ss a l r+ | otherwise = postconditionOnFailure ss a l r+ type LookUp m = forall a. Typeable a => Var a -> Realized m a type Env m = [EnvEntry m]@@ -210,26 +253,50 @@ data WithUsedVars a = WithUsedVars VarContext a +data Polarity+ = PosPolarity+ | NegPolarity+ deriving (Ord, Eq)++instance Show Polarity where+ show PosPolarity = "+"+ show NegPolarity = "-"++data ActionWithPolarity state a = Eq (Action state a) =>+ ActionWithPolarity+ { polarAction :: Action state a+ , polarity :: Polarity+ }++instance HasVariables (Action state a) => HasVariables (ActionWithPolarity state a) where+ getAllVariables = getAllVariables . polarAction++deriving instance Eq (Action state a) => Eq (ActionWithPolarity state a)+ data Step state where- (:=) ::- (Typeable a, Eq (Action state a), Show (Action state a)) =>- Var a ->- Action state a ->- Step state+ (:=)+ :: (Typeable a, Eq (Action state a), Show (Action state a))+ => Var a+ -> ActionWithPolarity state a+ -> Step state infix 5 := instance (forall a. HasVariables (Action state a)) => HasVariables (Step state) where- getAllVariables (var := act) = Set.insert (Some var) $ getAllVariables act+ getAllVariables (var := act) = Set.insert (Some var) $ getAllVariables (polarAction act) +funName :: Polarity -> String+funName PosPolarity = "action"+funName _ = "failingAction"+ instance Show (Step state) where- show (var := act) = show var ++ " <- action $ " ++ show act+ show (var := act) = show var ++ " <- " ++ funName (polarity act) ++ " $ " ++ show (polarAction act) instance Show (WithUsedVars (Step state)) where show (WithUsedVars ctx (var := act)) = if isWellTyped var ctx- then show var ++ " <- action $ " ++ show act- else "action $ " ++ show act+ then show var ++ " <- " ++ funName (polarity act) ++ " $ " ++ show (polarAction act)+ else funName (polarity act) ++ " $ " ++ show (polarAction act) instance Eq (Step state) where (v := act) == (v' := act') =@@ -270,7 +337,7 @@ go :: Annotated state -> [Step state] -> VarContext go aState [] = allVariables (underlyingState aState) go aState ((var := act) : steps) =- allVariables act+ allVariables (polarAction act) <> allVariables (underlyingState aState) <> go (computeNextState aState act var) steps @@ -289,7 +356,7 @@ , do (mact, rej) <- satisfyPrecondition case mact of- Just (Some act) -> do+ Just (Some act@ActionWithPolarity{}) -> do let var = mkVar step (as, rejected) <- arbActions (computeNextState s act var) (step + 1) return ((var := act) : as, rej ++ rejected)@@ -307,12 +374,12 @@ Some act -> if computePrecondition s act then return (Just (Some act), rej)- else go (m + 1) n (actionName act : rej)+ else go (m + 1) n (actionName (polarAction act) : rej) shrink (Actions_ rs as) = map (Actions_ rs) (shrinkSmart (map (prune . map fst) . shrinkList shrinker . withStates) as) where- shrinker (v := act, s) = [(unsafeCoerceVar v := act', s) | Some act' <- computeShrinkAction s act]+ shrinker (v := act, s) = [(unsafeCoerceVar v := act', s) | Some act'@ActionWithPolarity{} <- computeShrinkAction s act] -- Running state models @@ -327,77 +394,104 @@ initialAnnotatedState :: StateModel state => Annotated state initialAnnotatedState = Metadata mempty initialState -computePrecondition :: StateModel state => Annotated state -> Action state a -> Bool-computePrecondition s a =- all (\(Some v) -> v `isWellTyped` vars s) (getAllVariables a)- && precondition (underlyingState s) a+actionWithPolarity :: (StateModel state, Eq (Action state a)) => Annotated state -> Action state a -> ActionWithPolarity state a+actionWithPolarity s a =+ let p+ | precondition (underlyingState s) a = PosPolarity+ | validFailingAction (underlyingState s) a = NegPolarity+ | otherwise = PosPolarity+ in ActionWithPolarity a p -computeNextState ::- (StateModel state, Typeable a) =>- Annotated state ->- Action state a ->- Var a ->- Annotated state-computeNextState s a v = Metadata (extendContext (vars s) v) (nextState (underlyingState s) a v)+computePrecondition :: StateModel state => Annotated state -> ActionWithPolarity state a -> Bool+computePrecondition s (ActionWithPolarity a p) =+ let polarPrecondition+ | p == PosPolarity = precondition (underlyingState s) a+ | otherwise = validFailingAction (underlyingState s) a && not (precondition (underlyingState s) a)+ in all (\(Some v) -> v `isWellTyped` vars s) (getAllVariables a)+ && polarPrecondition -computeArbitraryAction ::- StateModel state =>- Annotated state ->- Gen (Any (Action state))-computeArbitraryAction s = arbitraryAction (vars s) (underlyingState s)+computeNextState+ :: (StateModel state, Typeable a)+ => Annotated state+ -> ActionWithPolarity state a+ -> Var a+ -> Annotated state+computeNextState s a v+ | polarity a == PosPolarity = Metadata (extendContext (vars s) v) (nextState (underlyingState s) (polarAction a) v)+ | otherwise = Metadata (vars s) (failureNextState (underlyingState s) (polarAction a)) -computeShrinkAction ::- (Typeable a, StateModel state) =>- Annotated state ->- Action state a ->- [Any (Action state)]-computeShrinkAction s = shrinkAction (vars s) (underlyingState s)+computeArbitraryAction+ :: StateModel state+ => Annotated state+ -> Gen (Any (ActionWithPolarity state))+computeArbitraryAction s = do+ Some a <- arbitraryAction (vars s) (underlyingState s)+ pure $ Some $ actionWithPolarity s a -prune :: StateModel state => [Step state] -> [Step state]+computeShrinkAction+ :: forall state a+ . (Typeable a, StateModel state)+ => Annotated state+ -> ActionWithPolarity state a+ -> [Any (ActionWithPolarity state)]+computeShrinkAction s (ActionWithPolarity a _) =+ [Some (actionWithPolarity s a') | Some a' <- shrinkAction (vars s) (underlyingState s) a]++prune :: forall state. StateModel state => [Step state] -> [Step state] prune = loop initialAnnotatedState where loop _s [] = [] loop s ((var := act) : as)- | computePrecondition s act =+ | computePrecondition @state s act = (var := act) : loop (computeNextState s act var) as | otherwise = loop s as -withStates :: StateModel state => [Step state] -> [(Step state, Annotated state)]+withStates :: forall state. StateModel state => [Step state] -> [(Step state, Annotated state)] withStates = loop initialAnnotatedState where loop _s [] = [] loop s ((var := act) : as) =- (var := act, s) : loop (computeNextState s act var) as+ (var := act, s) : loop (computeNextState @state s act var) as -stateAfter :: StateModel state => Actions state -> Annotated state+stateAfter :: forall state. StateModel state => Actions state -> Annotated state stateAfter (Actions actions) = loop initialAnnotatedState actions where loop s [] = s- loop s ((var := act) : as) = loop (computeNextState s act var) as+ loop s ((var := act) : as) = loop (computeNextState @state s act var) as -runActions ::- forall state m.- (StateModel state, RunModel state m) =>- Actions state ->- PropertyM m (Annotated state, Env m)+runActions+ :: forall state m+ . (StateModel state, RunModel state m)+ => Actions state+ -> PropertyM m (Annotated state, Env m) runActions (Actions_ rejected (Smart _ actions)) = loop initialAnnotatedState [] actions where loop :: Annotated state -> Env m -> [Step state] -> PropertyM m (Annotated state, Env m) loop _s env [] = do unless (null rejected) $- monitor (tabulate "Actions rejected by precondition" rejected)+ monitor $+ tabulate "Actions rejected by precondition" rejected return (_s, reverse env) loop s env ((v := act) : as) = do pre $ computePrecondition s act- ret <- run (perform (underlyingState s) act (lookUpVar env))- let name = actionName act- monitor (tabulate "Actions" [name])+ ret <- run $ perform (underlyingState s) (polarAction act) (lookUpVar env)+ let name = show (polarity act) ++ actionName (polarAction act)+ monitor $ tabulate "Actions" [name] let var = unsafeCoerceVar v s' = computeNextState s act var env' = (var :== ret) : env- monitor (monitoring @state @m (underlyingState s, underlyingState s') act (lookUpVar env') ret)- (b, (Endo mon, Endo onFail)) <- run . runWriterT . runPost $ postcondition @state @m (underlyingState s, underlyingState s') act (lookUpVar env) ret+ monitor $ tabulate "Action polarity" [show $ polarity act]+ monitor $ monitoring @state @m (underlyingState s, underlyingState s') (polarAction act) (lookUpVar env') ret+ (b, (Endo mon, Endo onFail)) <-+ run+ . runWriterT+ . runPost+ $ computePostcondition @m+ (underlyingState s, underlyingState s')+ act+ (lookUpVar env)+ ret monitor mon unless b $ monitor onFail assert b
src/Test/QuickCheck/StateModel/Variables.hs view
@@ -141,12 +141,12 @@ instance {-# OVERLAPPABLE #-} ( Break- (TypeError ( 'Text "Missing instance of HasVariables for non-Generic type " ':<>: 'ShowType a))+ (TypeError ('Text "Missing instance of HasVariables for non-Generic type " ':<>: 'ShowType a)) (Rep a) , Generic a , GenericHasVariables (Rep a)- ) =>- HasVariables a+ )+ => HasVariables a where getAllVariables = genericGetAllVariables . from
+ test/Spec.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Spec.DynamicLogic.RegistryModel qualified+import Test.Tasty+import Test.Tasty.Runners.Reporter qualified as Reporter++main :: IO ()+main = defaultMainWithIngredients [Reporter.ingredient] tests++tests :: TestTree+tests =+ testGroup+ "dynamic logic"+ [ Spec.DynamicLogic.RegistryModel.tests+ ]
+ test/Spec/DynamicLogic/Registry.hs view
@@ -0,0 +1,50 @@+-- A simple local name service for threads... behaves like the Erlang+-- process registry.+module Spec.DynamicLogic.Registry where++import Control.Concurrent.STM+import Control.Monad+import GHC.Conc++type Registry = TVar [(String, ThreadId)]++isAlive :: ThreadId -> IO Bool+isAlive tid = do+ s <- threadStatus tid+ return $ s /= ThreadFinished && s /= ThreadDied++setupRegistry :: IO Registry+setupRegistry = atomically $ newTVar []++whereis :: Registry -> String -> IO (Maybe ThreadId)+whereis registry name = do+ reg <- readRegistry registry+ return $ lookup name reg++register :: Registry -> String -> ThreadId -> IO ()+register registry name tid = do+ ok <- isAlive tid+ reg <- readRegistry registry+ if ok && name `notElem` map fst reg && tid `notElem` map snd reg+ then atomically $ do+ reg' <- readTVar registry+ if name `notElem` map fst reg' && tid `notElem` map snd reg'+ then writeTVar registry ((name, tid) : reg')+ else error "badarg"+ else error "badarg"++unregister :: Registry -> String -> IO ()+unregister registry name = do+ reg <- readRegistry registry+ when (name `elem` map fst reg) $ do+ atomically $ modifyTVar registry $ filter ((/= name) . fst)++readRegistry :: Registry -> IO [(String, ThreadId)]+readRegistry registry = garbageCollect registry *> atomically (readTVar registry)++garbageCollect :: Registry -> IO ()+garbageCollect registry = do+ reg <- atomically $ readTVar registry+ garbage <- filterM (fmap not . isAlive) (map snd reg)+ atomically $ modifyTVar registry $ filter ((`notElem` garbage) . snd)+ return ()
+ test/Spec/DynamicLogic/RegistryModel.hs view
@@ -0,0 +1,271 @@+module Spec.DynamicLogic.RegistryModel where++import Control.Concurrent+import Control.Exception++import GHC.Generics++import Control.Monad.Reader+import Data.Either+import Data.List+import Data.Map (Map)+import Data.Map qualified as Map+import Test.QuickCheck+import Test.QuickCheck.Monadic hiding (assert)+import Test.QuickCheck.Monadic qualified as QC+import Test.Tasty hiding (after)++import Test.Tasty.QuickCheck (testProperty)++import Spec.DynamicLogic.Registry+import Test.QuickCheck.DynamicLogic+import Test.QuickCheck.Extras+import Test.QuickCheck.StateModel++data RegState = RegState+ { regs :: Map String (Var ThreadId)+ , dead :: [Var ThreadId]+ }+ deriving (Show, Generic)++deriving instance Show (Action RegState a)+deriving instance Eq (Action RegState a)++instance HasVariables (Action RegState a) where+ getAllVariables (Register _ v) = getAllVariables v+ getAllVariables (KillThread v) = getAllVariables v+ getAllVariables _ = mempty++instance StateModel RegState where+ data Action RegState a where+ Spawn :: Action RegState ThreadId+ WhereIs :: String -> Action RegState (Maybe ThreadId)+ Register :: String -> Var ThreadId -> Action RegState (Either SomeException ())+ Unregister :: String -> Action RegState (Either SomeException ())+ KillThread :: Var ThreadId -> Action RegState ()++ precondition s (Register name tid) =+ name `Map.notMember` regs s+ && tid `notElem` Map.elems (regs s)+ && tid `notElem` dead s+ precondition s (Unregister name) =+ name `Map.member` regs s+ precondition _ _ = True++ validFailingAction _ _ = True++ arbitraryAction ctx s =+ frequency $+ [+ ( max 1 $ 10 - length (ctxAtType @ThreadId ctx)+ , return $ Some Spawn+ )+ ,+ ( 2 * Map.size (regs s)+ , Some <$> (Unregister <$> probablyRegistered s)+ )+ ,+ ( 10+ , Some <$> (WhereIs <$> probablyRegistered s)+ )+ ]+ ++ [ ( max 1 $ 3 - length (dead s)+ , Some <$> (KillThread <$> arbitraryVar ctx)+ )+ | not . null $ ctxAtType @ThreadId ctx+ ]+ ++ [ ( max 1 $ 10 - Map.size (regs s)+ , Some <$> (Register <$> probablyUnregistered s <*> arbitraryVar ctx)+ )+ | not . null $ ctxAtType @ThreadId ctx+ ]++ shrinkAction ctx _ (Register name tid) =+ [Some (Unregister name)]+ ++ [Some (Register name' tid) | name' <- shrinkName name]+ ++ [Some (Register name tid') | tid' <- shrinkVar ctx tid]+ shrinkAction _ _ (Unregister name) =+ Some (WhereIs name) : [Some (Unregister name') | name' <- shrinkName name]+ shrinkAction _ _ (WhereIs name) =+ [Some (WhereIs name') | name' <- shrinkName name]+ shrinkAction _ _ Spawn = []+ shrinkAction ctx _ (KillThread tid) =+ [Some (KillThread tid') | tid' <- shrinkVar ctx tid]++ initialState = RegState mempty []++ nextState s Spawn _ = s+ nextState s (Register name tid) _step = s{regs = Map.insert name tid (regs s)}+ nextState s (Unregister name) _step =+ s{regs = Map.delete name (regs s)}+ nextState s (KillThread tid) _ =+ s+ { dead = tid : dead s+ , regs = Map.filter (/= tid) (regs s)+ }+ nextState s WhereIs{} _ = s++type RegM = ReaderT Registry IO++instance RunModel RegState RegM where+ perform _ Spawn _ = do+ lift $ forkIO (threadDelay 10000000)+ perform _ (Register name tid) env = do+ reg <- ask+ lift $ try $ register reg name (env tid)+ perform _ (Unregister name) _ = do+ reg <- ask+ lift $ try $ unregister reg name+ perform _ (WhereIs name) _ = do+ reg <- ask+ lift $ whereis reg name+ perform _ (KillThread tid) env = do+ lift $ killThread (env tid)+ lift $ threadDelay 100++ postcondition (s, _) (WhereIs name) env mtid = do+ pure $ (env <$> Map.lookup name (regs s)) == mtid+ postcondition _ Register{} _ res = do+ pure $ isRight res+ postcondition _ _ _ _ = pure True++ postconditionOnFailure (s, _) act@Register{} _ res = do+ monitorPost $+ tabulate+ "Reason for -Register"+ [ why s act+ | Left{} <- [res]+ ]+ pure $ isLeft res+ postconditionOnFailure _s _ _ _ = pure True++ monitoring (_s, s') act@(showDictAction -> ShowDict) _ res =+ counterexample (show res ++ " <- " ++ show act ++ "\n -- State: " ++ show s')+ . tabulate "Registry size" [show $ Map.size (regs s')]++data ShowDict a where+ ShowDict :: Show (Realized RegM a) => ShowDict a++showDictAction :: forall a. Action RegState a -> ShowDict a+showDictAction Spawn{} = ShowDict+showDictAction WhereIs{} = ShowDict+showDictAction Register{} = ShowDict+showDictAction Unregister{} = ShowDict+showDictAction KillThread{} = ShowDict++instance DynLogicModel RegState where+ restricted _ = False++why :: RegState -> Action RegState a -> String+why s (Register name tid) =+ unwords $+ ["name already registered" | name `Map.member` regs s]+ ++ ["tid already registered" | tid `elem` Map.elems (regs s)]+ ++ ["dead thread" | tid `elem` dead s]+why _ _ = "(impossible)"++arbitraryName :: Gen String+arbitraryName = elements allNames++probablyRegistered :: RegState -> Gen String+probablyRegistered s = oneof $ map pure (Map.keys $ regs s) ++ [arbitraryName]++probablyUnregistered :: RegState -> Gen String+probablyUnregistered s = elements $ allNames ++ (allNames \\ Map.keys (regs s))++shrinkName :: String -> [String]+shrinkName name = [n | n <- allNames, n < name]++allNames :: [String]+allNames = ["a", "b", "c", "d", "e"]++prop_Registry :: Actions RegState -> Property+prop_Registry s =+ monadicIO $ do+ monitor $ counterexample "\nExecution\n"+ reg <- lift setupRegistry+ runPropertyReaderT (runActions s) reg+ QC.assert True++propDL :: DL RegState () -> Property+propDL d = forAllDL d prop_Registry++-- DL helpers++unregisterNameAndTid :: String -> Var ThreadId -> DL RegState ()+unregisterNameAndTid name tid = do+ s <- getModelStateDL+ sequence_+ [ action $ Unregister name'+ | (name', tid') <- Map.toList $ regs s+ , name' == name || tid' == tid+ ]++unregisterTid :: Var ThreadId -> DL RegState ()+unregisterTid tid = do+ s <- getModelStateDL+ sequence_+ [ action $ Unregister name+ | (name, tid') <- Map.toList $ regs s+ , tid' == tid+ ]++getAlive :: DL RegState [Var ThreadId]+getAlive = do+ s <- getModelStateDL+ ctx <- getVarContextDL+ pure $ ctxAtType @ThreadId ctx \\ dead s++pickThread :: DL RegState (Var ThreadId)+pickThread = do+ tids <- ctxAtType @ThreadId <$> getVarContextDL+ forAllQ $ elementsQ tids++pickAlive :: DL RegState (Var ThreadId)+pickAlive = do+ alive <- getAlive+ forAllQ $ elementsQ alive++pickFreshName :: DL RegState String+pickFreshName = do+ used <- Map.keys . regs <$> getModelStateDL+ forAllQ $ elementsQ (allNames \\ used)++-- test that the registry never contains more than k processes++regLimit :: Int -> DL RegState ()+regLimit k = do+ anyActions_+ assertModel "Too many processes" $ \s -> Map.size (regs s) <= k++-- test that we can register a pid that is not dead, if we unregister the name first.++canRegisterAlive :: String -> DL RegState ()+canRegisterAlive name = do+ tid <- pickAlive+ unregisterNameAndTid name tid+ action $ Register name tid+ pure ()++canRegister :: DL RegState ()+canRegister = do+ anyActions_+ name <- pickFreshName+ canRegisterAlive name++canRegisterNoUnregister :: DL RegState ()+canRegisterNoUnregister = do+ anyActions_+ name <- pickFreshName+ tid <- pickAlive+ action $ Register name tid+ pure ()++tests :: TestTree+tests =+ testGroup+ "registry model example"+ [ testProperty "prop_Registry" $ prop_Registry+ , testProperty "canRegister" $ propDL canRegister+ , testProperty "canRegisterNoUnregister" $ expectFailure $ propDL canRegisterNoUnregister+ ]