quickcheck-dynamic-3.4.0: src/Test/QuickCheck/DynamicLogic/Internal.hs
module Test.QuickCheck.DynamicLogic.Internal where
import Control.Applicative
import Control.Arrow (second)
import Control.Monad
import Data.Typeable
import Test.QuickCheck hiding (generate)
import Test.QuickCheck.DynamicLogic.CanGenerate
import Test.QuickCheck.DynamicLogic.Quantify
import Test.QuickCheck.DynamicLogic.SmartShrinking
import Test.QuickCheck.DynamicLogic.Utils qualified as QC
import Test.QuickCheck.StateModel
-- | A `DynFormula` may depend on the QuickCheck size parameter
newtype DynFormula s = DynFormula {unDynFormula :: Int -> DynLogic s}
-- | Base Dynamic logic formulae language.
-- Formulae are parameterised
-- over the type of state `s` to which they apply. A `DynLogic` value
-- cannot be constructed directly, one has to use the various "smart
-- constructors" provided, see the /Building formulae/ section.
data DynLogic s
= -- | False
EmptySpec
| -- | True
Stop
| -- | After any action the predicate should hold
AfterAny (DynPred s)
| -- | Choice (angelic or demonic)
Alt ChoiceType (DynLogic s) (DynLogic s)
| -- | Prefer this branch if trying to stop.
Stopping (DynLogic s)
| -- | After a specific action the predicate should hold
forall a.
(Eq (Action s a), Show (Action s a), Typeable a) =>
After (ActionWithPolarity s a) (Var a -> DynPred s)
| Error String (DynPred s)
| -- | Adjust the probability of picking a branch
Weight Double (DynLogic s)
| -- | Generating a random value
forall a.
QuantifyConstraints a =>
ForAll (Quantification a) (a -> DynLogic s)
| -- | Apply a QuickCheck property modifier (like `tabulate` or `collect`)
Monitor (Property -> Property) (DynLogic s)
data ChoiceType = Angelic | Demonic
deriving (Eq, Show)
type DynPred s = Annotated s -> DynLogic s
-- * Building formulae
-- | Ignore this formula, i.e. backtrack and try something else. @forAllScripts ignore (const True)@
-- will discard all test cases (equivalent to @False ==> True@).
ignore :: DynFormula s
ignore = DynFormula . const $ EmptySpec
-- | `True` for DL formulae.
passTest :: DynFormula s
passTest = DynFormula . const $ Stop
-- | Given `f` must be `True` given /any/ state.
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 = 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
-- `Stuck`.
(|||) :: DynFormula s -> DynFormula s -> DynFormula s
-- In formulae, we use only angelic choice. But it becomes demonic
-- after one step (that is, the choice has been made).
DynFormula f ||| DynFormula g = DynFormula $ \n -> Alt Angelic (f n) (g n)
-- | First-order quantification of variables.
-- Formula @f@ is `True` iff. it is `True` /for all/ possible values of `q`. The
-- underlying framework will generate values of `q` and check the formula holds
-- 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 q f
| isEmptyQ q' = ignore
| otherwise = DynFormula $ \n -> ForAll q' $ ($ n) . unDynFormula . f
where
q' = quantify q
-- | Adjust weight for selecting formula.
-- This is mostly useful in relation with `(|||)` combinator, in order to tweak the
-- priority for generating the next step(s) of the test that matches the formula.
weight :: Double -> DynFormula s -> DynFormula s
weight w f = DynFormula $ Weight w . unDynFormula f
-- | Get the current QuickCheck size parameter.
withSize :: (Int -> DynFormula s) -> DynFormula s
withSize f = DynFormula $ \n -> unDynFormula (f n) n
-- | Prioritise doing this if we are
-- trying to stop generation.
toStop :: DynFormula s -> DynFormula s
toStop (DynFormula f) = DynFormula $ Stopping . f
-- | Successfully ends the test.
done :: Annotated s -> DynFormula s
done _ = passTest
-- | Ends test with given error message.
errorDL :: String -> DynFormula s
errorDL s = DynFormula . const $ Error s (const EmptySpec)
-- | Embed QuickCheck's monitoring functions (eg. `label`, `tabulate`) in
-- a formula.
-- This is useful to improve the reporting from test execution, esp. in the
-- case of failures.
monitorDL :: (Property -> Property) -> DynFormula s -> DynFormula s
monitorDL m (DynFormula f) = DynFormula $ Monitor m . f
-- | Formula should hold at any state.
-- In effect this leads to exploring alternatives from a given state `s` and ensuring
-- formula holds in all those states.
always :: (Annotated s -> DynFormula s) -> (Annotated s -> DynFormula s)
always p s = withSize $ \n -> toStop (p s) ||| p s ||| weight (fromIntegral n) (afterAny (always p))
data FailingAction s
= ErrorFail String
| forall a. (Typeable a, Eq (Action s a)) => ActionFail (ActionWithPolarity s a)
instance StateModel s => HasVariables (FailingAction s) where
getAllVariables ErrorFail{} = mempty
getAllVariables (ActionFail a) = getAllVariables a
instance StateModel s => Eq (FailingAction s) where
ErrorFail s == ErrorFail s' = s == s'
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 (ActionWithPolarity a pol)) = show pol ++ " : " ++ show a
data DynLogicTest s
= BadPrecondition (TestSequence s) (FailingAction s) (Annotated s)
| Looping (TestSequence s)
| Stuck (TestSequence s) (Annotated s)
| DLScript (TestSequence s)
data Witnesses r where
Do :: r -> Witnesses r
Witness :: QuantifyConstraints a => a -> Witnesses r -> Witnesses r
discardWitnesses :: Witnesses r -> r
discardWitnesses (Do r) = r
discardWitnesses (Witness _ k) = discardWitnesses k
pattern Witnesses :: Witnesses () -> r -> Witnesses r
pattern Witnesses w r <- ((\wit -> (void wit, discardWitnesses wit)) -> (w, r))
where
Witnesses w r = r <$ w
{-# COMPLETE Witnesses #-}
deriving instance Functor Witnesses
deriving instance Foldable Witnesses
deriving instance Traversable Witnesses
instance Eq r => Eq (Witnesses r) where
Do r == Do r' = r == r'
Witness (a :: a) k == Witness (a' :: a') k' =
case eqT @a @a' of
Just Refl -> a == a' && k == k'
Nothing -> False
_ == _ = False
instance Show r => Show (Witnesses r) where
show (Do r) = "Do $ " ++ show r
show (Witness a k) = "Witness (" ++ show a ++ " :: " ++ show (typeOf a) ++ ")\n" ++ show k -- TODO
type TestStep s = Witnesses (Step s)
newtype TestSequence s = TestSeq (Witnesses (TestContinuation s))
deriving instance StateModel s => Show (TestSequence s)
deriving instance StateModel s => Eq (TestSequence s)
data TestContinuation s
= ContStep (Step s) (TestSequence s)
| ContStop
pattern TestSeqStop :: TestSequence s
pattern TestSeqStop = TestSeq (Do ContStop)
pattern TestSeqStep :: Step s -> TestSequence s -> TestSequence s
pattern TestSeqStep s ss = TestSeq (Do (ContStep s ss))
-- The `()` are the constraints required to use the pattern, and the `(Typeable a, ...)` are the
-- ones provided when you do (including a fresh type variable `a`).
-- See https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/pattern_synonyms.html#typing-of-pattern-synonyms
pattern TestSeqWitness :: () => forall a. QuantifyConstraints a => a -> TestSequence s -> TestSequence s
pattern TestSeqWitness a ss <- TestSeq (Witness a (TestSeq -> ss))
where
TestSeqWitness a (TestSeq ss) = TestSeq (Witness a ss)
{-# COMPLETE TestSeqWitness, TestSeqStep, TestSeqStop #-}
deriving instance StateModel s => Show (TestContinuation s)
deriving instance StateModel s => Eq (TestContinuation s)
consSeq :: TestStep s -> TestSequence s -> TestSequence s
consSeq step ss = TestSeq $ flip ContStep ss <$> step
unconsSeq :: TestSequence s -> Maybe (TestStep s, TestSequence s)
unconsSeq (TestSeq ss) =
case discardWitnesses ss of
ContStop -> Nothing
ContStep s rest -> Just (s <$ ss, rest)
unstopSeq :: TestSequence s -> Maybe (Witnesses ())
unstopSeq (TestSeq ss) =
case discardWitnesses ss of
ContStop -> Just $ () <$ ss
ContStep{} -> Nothing
pattern TestSeqStopW :: Witnesses () -> TestSequence s
pattern TestSeqStopW w <- (unstopSeq -> Just w)
where
TestSeqStopW w = TestSeq (ContStop <$ w)
pattern (:>) :: TestStep s -> TestSequence s -> TestSequence s
pattern step :> ss <- (unconsSeq -> Just (step, ss))
where
step :> ss = consSeq step ss
{-# COMPLETE TestSeqStopW, (:>) #-}
nullSeq :: TestSequence s -> Bool
nullSeq TestSeqStop = True
nullSeq _ = False
dropSeq :: Int -> TestSequence s -> TestSequence s
dropSeq n _ | n < 0 = error "dropSeq: negative number"
dropSeq 0 ss = ss
dropSeq _ TestSeqStop = TestSeqStop
dropSeq n (TestSeqWitness _ ss) = dropSeq (n - 1) ss
dropSeq n (TestSeqStep _ ss) = dropSeq (n - 1) ss
getContinuation :: TestSequence s -> TestSequence s
getContinuation (TestSeq w) = case discardWitnesses w of
ContStop -> TestSeqStop
ContStep _ s -> s
unlines' :: [String] -> String
unlines' [] = ""
unlines' xs = init $ unlines xs
prettyTestSequence :: VarContext -> TestSequence s -> String
prettyTestSequence ctx ss = unlines' $ zipWith (++) ("do " : repeat " ") $ prettySeq ss
where
prettySeq (TestSeqStopW w) = prettyWitnesses w
prettySeq (Witnesses w step :> ss') = prettyWitnesses w ++ show (WithUsedVars ctx step) : prettySeq ss'
prettyWitnesses :: Witnesses () -> [String]
prettyWitnesses (Witness a w) = ("_ <- forAllQ $ exactlyQ $ " ++ show a) : prettyWitnesses w
prettyWitnesses Do{} = []
instance StateModel s => Show (DynLogicTest s) where
show (BadPrecondition ss bad s) =
prettyTestSequence (usedVariables ss <> allVariables bad) ss
++ "\n -- In state: "
++ show s
++ "\n "
++ prettyBad bad
where
prettyBad :: FailingAction s -> String
prettyBad (ErrorFail e) = "assert " ++ show e ++ " False"
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"
usedVariables :: forall s. StateModel s => TestSequence s -> VarContext
usedVariables = go initialAnnotatedState
where
go :: Annotated s -> TestSequence s -> VarContext
go aState TestSeqStop = allVariables (underlyingState aState)
go aState (TestSeqWitness a ss) = allVariables a <> go aState ss
go aState (TestSeqStep step@(_ := act) ss) =
allVariables act
<> allVariables (underlyingState aState)
<> go (nextStateStep step aState) ss
-- | Restricted calls are not generated by "AfterAny"; they are included
-- in tests explicitly using "After" in order to check specific
-- properties at controlled times, so they are likely to fail if
-- invoked at other times.
class StateModel s => DynLogicModel s where
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 = forAllMappedScripts id id
-- | `Property` function suitable for formulae without choice.
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
n = unsafeNextVarIndex $ vars s
in case generate chooseUniqueNextStep d n s 500 of
Nothing -> counterexample "Generating Non-unique script in forAllUniqueScripts" False
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 to from f k =
QC.withSize $ \n ->
let d = unDynFormula f n
in forAllShrinkBlind
(Smart 0 <$> sized ((from <$>) . generateDLTest d))
(shrinkSmart ((from <$>) . shrinkDLTest d . to))
$ \(Smart _ script) ->
withDLScript d k (to script)
withDLScript :: (DynLogicModel s, Testable a) => DynLogic s -> (Actions s -> a) -> DynLogicTest s -> Property
withDLScript d k test =
validDLTest d test . applyMonitoring d test . property $ k (scriptFromDL test)
withDLScriptPrefix :: (DynLogicModel s, Testable a) => DynFormula s -> (Actions s -> a) -> DynLogicTest s -> Property
withDLScriptPrefix f k test =
QC.withSize $ \n ->
let d = unDynFormula f n
test' = unfailDLTest d test
in validDLTest d test' . applyMonitoring d test' . property $ k (scriptFromDL test')
generateDLTest :: DynLogicModel s => DynLogic s -> Int -> Gen (DynLogicTest s)
generateDLTest d size = generate chooseNextStep d 0 (initialStateFor d) size
onDLTestSeq :: (TestSequence s -> TestSequence s) -> DynLogicTest s -> DynLogicTest s
onDLTestSeq f (BadPrecondition ss bad s) = BadPrecondition (f ss) bad s
onDLTestSeq f (Looping ss) = Looping (f ss)
onDLTestSeq f (Stuck ss s) = Stuck (f ss) s
onDLTestSeq f (DLScript ss) = DLScript (f ss)
consDLTest :: TestStep s -> DynLogicTest s -> DynLogicTest s
consDLTest step = onDLTestSeq (step :>)
consDLTestW :: Witnesses () -> DynLogicTest s -> DynLogicTest s
consDLTestW w = onDLTestSeq (addW w)
where
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 chooseNextStepFun d n s size =
if n > sizeLimit size
then return $ Looping TestSeqStop
else do
let preferred = if n > size then stopping d else noStopping d
useStep (BadAction (Witnesses ws bad)) _ = return $ BadPrecondition (TestSeqStopW ws) bad s
useStep StoppingStep _ = return $ DLScript TestSeqStop
useStep (Stepping step d') _ =
case discardWitnesses step of
var := act ->
consDLTest step
<$> generate
chooseNextStepFun
d'
(n + 1)
(computeNextState s act var)
size
useStep NoStep alt = alt
foldr
(\step k -> do try <- chooseNextStepFun s n step; useStep try k)
(return $ Stuck TestSeqStop s)
[preferred, noAny preferred, d, noAny d]
sizeLimit :: Int -> Int
sizeLimit size = 2 * size + 20
initialStateFor :: StateModel s => DynLogic s -> Annotated s
initialStateFor _ = initialAnnotatedState
stopping :: DynLogic s -> DynLogic s
stopping EmptySpec = EmptySpec
stopping Stop = Stop
stopping (After act k) = After act k
stopping (Error m k) = Error m k
stopping (AfterAny _) = EmptySpec
stopping (Alt b d d') = Alt b (stopping d) (stopping d')
stopping (Stopping d) = d
stopping (Weight w d) = Weight w (stopping d)
stopping (ForAll _ _) = EmptySpec -- ???
stopping (Monitor f d) = Monitor f (stopping d)
noStopping :: DynLogic s -> DynLogic s
noStopping EmptySpec = EmptySpec
noStopping Stop = EmptySpec
noStopping (After act k) = After act k
noStopping (Error m k) = Error m k
noStopping (AfterAny k) = AfterAny k
noStopping (Alt b d d') = Alt b (noStopping d) (noStopping d')
noStopping (Stopping _) = EmptySpec
noStopping (Weight w d) = Weight w (noStopping d)
noStopping (ForAll q f) = ForAll q f
noStopping (Monitor f d) = Monitor f (noStopping d)
noAny :: DynLogic s -> DynLogic s
noAny EmptySpec = EmptySpec
noAny Stop = Stop
noAny (After act k) = After act k
noAny (Error m k) = Error m k
noAny (AfterAny _) = EmptySpec
noAny (Alt b d d') = Alt b (noAny d) (noAny d')
noAny (Stopping d) = Stopping (noAny d)
noAny (Weight w d) = Weight w (noAny d)
noAny (ForAll q f) = ForAll q f
noAny (Monitor f d) = Monitor f (noAny d)
nextSteps :: DynLogic s -> Gen [(Double, Witnesses (DynLogic s))]
nextSteps = nextSteps' generateQ
nextSteps' :: Monad m => (forall a. Quantification a -> m a) -> DynLogic s -> m [(Double, Witnesses (DynLogic s))]
nextSteps' _ EmptySpec = pure []
nextSteps' _ Stop = pure [(1, Do $ Stop)]
nextSteps' _ (After act k) = pure [(1, Do $ After act k)]
nextSteps' _ (Error m k) = pure [(1, Do $ Error m k)]
nextSteps' _ (AfterAny k) = pure [(1, Do $ AfterAny k)]
nextSteps' gen (Alt _ d d') = (++) <$> nextSteps' gen d <*> nextSteps' gen d'
nextSteps' gen (Stopping d) = nextSteps' gen d
nextSteps' gen (Weight w d) = do
steps <- nextSteps' gen d
pure [(w * w', s) | (w', s) <- steps, w * w' > never]
nextSteps' gen (ForAll q f) = do
x <- gen q
map (second $ Witness x) <$> nextSteps' gen (f x)
nextSteps' gen (Monitor _f d) = nextSteps' gen d
chooseOneOf :: [(Double, a)] -> Gen a
chooseOneOf steps = frequency [(round (w / never), return s) | (w, s) <- steps]
never :: Double
never = 1.0e-9
data NextStep s
= StoppingStep
| Stepping (Witnesses (Step s)) (DynLogic s)
| NoStep
| BadAction (Witnesses (FailingAction s))
chooseNextStep :: DynLogicModel s => Annotated s -> Int -> DynLogic s -> Gen (NextStep s)
chooseNextStep s n d = do
nextSteps d >>= \case
[] -> return NoStep
steps -> do
let bads = concatMap (findBad . snd) steps
findBad = traverse $ flip badActions s
case bads of
[] -> do
chosen <- chooseOneOf steps
let takeStep = \case
Stop -> return StoppingStep
After a k ->
return $ Stepping (Do $ mkVar n := a) (k (mkVar n) (computeNextState s a (mkVar n)))
AfterAny k -> do
m <- keepTryingUntil 100 (computeArbitraryAction s) $
\case
Some act -> computePrecondition s act && not (restrictedPolar act)
case m of
Nothing -> return NoStep
Just (Some a@ActionWithPolarity{}) ->
return $
Stepping
(Do $ mkVar n := a)
(k (computeNextState s a (mkVar n)))
EmptySpec -> error "chooseNextStep: EmptySpec"
ForAll{} -> error "chooseNextStep: ForAll"
Error{} -> error "chooseNextStep: Error"
Alt{} -> error "chooseNextStep: Alt"
Stopping{} -> error "chooseNextStep: Stopping"
Weight{} -> error "chooseNextStep: Weight"
Monitor{} -> error "chooseNextStep: Monitor"
go (Do d') = takeStep d'
go (Witness a step) =
go step
>>= pure . \case
StoppingStep -> StoppingStep -- TODO: This is a bit fishy
Stepping step' dl -> Stepping (Witness a step') dl
NoStep -> NoStep
BadAction bad -> BadAction (Witness a bad)
go chosen
b : _ -> return $ BadAction b
chooseUniqueNextStep :: (MonadFail m, DynLogicModel s) => Annotated s -> Int -> DynLogic s -> m (NextStep s)
chooseUniqueNextStep s n d = do
steps <- map snd <$> nextSteps' (const bad) d
case steps of
[] -> return NoStep
[Do EmptySpec] -> return NoStep
[Do Stop] -> return StoppingStep
[Do (After a k)] -> return $ Stepping (Do $ mkVar n := a) (k (mkVar n) (computeNextState s a (mkVar n)))
_ -> bad
where
bad = fail "chooseUniqueNextStep: non-unique action in DynLogic"
keepTryingUntil :: Int -> Gen a -> (a -> Bool) -> Gen (Maybe a)
keepTryingUntil 0 _ _ = return Nothing
keepTryingUntil n g p = do
x <- g
if p x then return $ Just x else scale (+ 1) $ keepTryingUntil (n - 1) g p
shrinkDLTest :: DynLogicModel s => DynLogic s -> DynLogicTest s -> [DynLogicTest s]
shrinkDLTest _ (Looping _) = []
shrinkDLTest d tc =
[ test | as' <- shrinkScript d (getScript tc), let pruned = pruneDLTest d as'
test = makeTestFromPruned d pruned,
-- Don't shrink a non-executable test case to an executable one.
case (tc, test) of
(DLScript _, _) -> True
(_, DLScript _) -> False
_ -> True
]
nextStateStep :: StateModel s => Step s -> Annotated s -> Annotated s
nextStateStep (var := act) s = computeNextState s act var
shrinkScript :: forall s. DynLogicModel s => DynLogic s -> TestSequence s -> [TestSequence s]
shrinkScript = shrink' initialAnnotatedState
where
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 @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'@ActionWithPolarity{} <- computeShrinkAction s act]
++ [ TestSeqStep step ss'
| ss' <-
shrink'
(nextStateStep step s)
(stepDLStep d s step)
ss
]
nonstructural _ _ TestSeqStop = []
structural _ _ TestSeqStopW{} = []
structural s d (step :> rest) =
TestSeqStop
: reverse (takeWhile (not . nullSeq) [dropSeq (n - 1) rest | n <- iterate (* 2) 1])
++ map (step :>) (shrink' (nextStateStep (discardWitnesses step) s) (stepDLSeq d s (step :> TestSeqStop)) rest)
shrinkWitness :: (StateModel s, Typeable a) => DynLogic s -> a -> [a]
shrinkWitness (ForAll (q :: Quantification a) _) (a :: a') =
case eqT @a @a' of
Just Refl | isaQ q a -> shrinkQ q a
_ -> []
shrinkWitness (Alt _ d d') a = shrinkWitness d a ++ shrinkWitness d' a
shrinkWitness (Stopping d) a = shrinkWitness d a
shrinkWitness (Weight _ d) a = shrinkWitness d a
shrinkWitness (Monitor _ d) a = shrinkWitness d a
shrinkWitness EmptySpec{} _ = []
shrinkWitness Stop{} _ = []
shrinkWitness Error{} _ = []
shrinkWitness After{} _ = []
shrinkWitness AfterAny{} _ = []
-- 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 :: 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 @s d a] of
[] -> prune ds s ss
ds' -> TestSeqWitness a $ prune ds' s ss
prune ds s (TestSeqStep step@(_ := act) ss)
| computePrecondition s act =
case [d' | d <- ds, d' <- stepDL d s (Do step)] of
[] -> prune ds s ss
ds' -> TestSeqStep step $ prune ds' (nextStateStep step s) ss
| otherwise = prune ds s ss
stepDL :: DynLogicModel s => DynLogic s -> Annotated s -> TestStep s -> [DynLogic s]
stepDL (After a k) s (Do (var := act))
-- TOOD: make this nicer when we migrate to 9.2 where we can just bind
-- 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 (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
stepDL (ForAll (q :: Quantification a) f) s (Witness (a :: a') step) =
case eqT @a @a' of
Just Refl -> [d | isaQ q a, d <- stepDL (f a) s step]
Nothing -> []
stepDL (Monitor _f d) s step = stepDL d s step
stepDL _ _ _ = []
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]
Nothing -> []
stepDLW (Alt _ d d') a = stepDLW d a ++ stepDLW d' a
stepDLW (Monitor _ d) a = stepDLW d a
stepDLW (Weight _ d) a = stepDLW d a
stepDLW (Stopping d) a = stepDLW d a
stepDLW _ _ = []
stepDLSeq :: DynLogicModel s => DynLogic s -> Annotated s -> TestSequence s -> DynLogic s
stepDLSeq d _ (TestSeqStopW Do{}) = d
stepDLSeq d s (TestSeqStopW (Witness a w)) = stepDLSeq (stepDLWitness d a) s (TestSeqStopW w)
stepDLSeq d s (step@(Witnesses _ (var := act)) :> ss) =
stepDLSeq (demonicAlt $ stepDL d s step) (computeNextState s act var) ss
stepDLWitness :: forall a s. (DynLogicModel s, Typeable a) => DynLogic s -> a -> DynLogic s
stepDLWitness d a = demonicAlt $ stepDLW d a
stepDLStep :: DynLogicModel s => DynLogic s -> Annotated s -> Step s -> DynLogic s
stepDLStep d s step = stepDLSeq d s (TestSeqStep step TestSeqStop)
demonicAlt :: [DynLogic s] -> DynLogic s
demonicAlt [] = EmptySpec
demonicAlt ds = foldr1 (Alt Demonic) ds
propPruningGeneratedScriptIsNoop :: DynLogicModel s => DynLogic s -> Property
propPruningGeneratedScriptIsNoop d =
forAll (sized $ \n -> choose (1, max 1 n) >>= generateDLTest d) $ \test ->
let script = case test of
BadPrecondition s _ _ -> s
Looping s -> s
Stuck s _ -> s
DLScript s -> s
in script == pruneDLTest d script
getScript :: DynLogicTest s -> TestSequence s
getScript (BadPrecondition s _ _) = s
getScript (Looping s) = s
getScript (Stuck s _) = s
getScript (DLScript s) = s
makeTestFromPruned :: forall s. DynLogicModel s => DynLogic s -> TestSequence s -> DynLogicTest s
makeTestFromPruned dl = make dl initialAnnotatedState
where
make d s TestSeqStop
| b : _ <- badActions @s d s = BadPrecondition TestSeqStop b s
| stuck d s = Stuck TestSeqStop s
| otherwise = DLScript TestSeqStop
make d s (TestSeqWitness a ss) =
onDLTestSeq (TestSeqWitness a) $
make
(stepDLWitness d a)
s
ss
make d s (TestSeqStep step ss) =
onDLTestSeq (TestSeqStep step) $
make
(stepDLStep d s step)
(nextStateStep step s)
ss
-- | If failed, return the prefix up to the failure. Also prunes the test in case the model has
-- changed.
unfailDLTest :: DynLogicModel s => DynLogic s -> DynLogicTest s -> DynLogicTest s
unfailDLTest d test = makeTestFromPruned d $ pruneDLTest d steps
where
steps = case test of
BadPrecondition as _ _ -> as
Stuck as _ -> as
DLScript as -> as
Looping as -> as
stuck :: DynLogicModel s => DynLogic s -> Annotated s -> Bool
stuck EmptySpec _ = True
stuck Stop _ = False
stuck (After _ _) _ = False
stuck (Error _ _) _ = False
stuck (AfterAny _) s =
not $
canGenerate
0.01
(computeArbitraryAction s)
( \case
Some act ->
computePrecondition s 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
stuck (Stopping d) s = stuck d s
stuck (Weight w d) s = w < never || stuck d s
stuck (ForAll _ _) _ = False
stuck (Monitor _ d) s = stuck d s
validDLTest :: StateModel s => DynLogic s -> DynLogicTest s -> Property -> Property
validDLTest _ Stuck{} _ = False ==> False
validDLTest _ test@DLScript{} p = counterexample (show test) p
validDLTest _ test _ = counterexample (show test) False
scriptFromDL :: DynLogicTest s -> Actions s
scriptFromDL (DLScript s) = Actions $ sequenceSteps s
scriptFromDL _ = Actions []
sequenceSteps :: TestSequence s -> [Step s]
sequenceSteps (TestSeq ss) =
case discardWitnesses ss of
ContStop -> []
ContStep s ss' -> s : sequenceSteps ss'
badActionsGiven :: StateModel s => DynLogic s -> Annotated s -> Witnesses a -> [Witnesses (FailingAction s)]
badActionsGiven Stop _ _ = []
badActionsGiven EmptySpec _ _ = []
badActionsGiven AfterAny{} _ _ = []
badActionsGiven (ForAll _ k) s (Witness a step) =
case cast a of
Just a' -> Witness a' <$> badActionsGiven (k a') s step
_ -> []
badActionsGiven (Alt _ d d') s w = badActionsGiven d s w ++ badActionsGiven d' s w
badActionsGiven (Stopping d) s w = badActionsGiven d s w
badActionsGiven (Weight k d) s w = if k < never then [] else badActionsGiven d s w
badActionsGiven (Monitor _ d) s w = badActionsGiven d s w
badActionsGiven d s (Do _) = Do <$> badActions d s
badActionsGiven Error{} _ _ = []
badActionsGiven After{} _ _ = []
badActions :: StateModel s => DynLogic s -> Annotated s -> [FailingAction s]
badActions EmptySpec _ = []
badActions Stop _ = []
badActions (After a _) s
| computePrecondition s a = []
| otherwise = [ActionFail a]
badActions (Error m _) _s = [ErrorFail m]
badActions (AfterAny _) _ = []
badActions (Alt _ d d') s = badActions d s ++ badActions d' s
badActions (Stopping d) s = badActions d s
badActions (Weight w d) s = if w < never then [] else badActions d s
badActions (ForAll _ _) _ = []
badActions (Monitor _ d) s = badActions d s
applyMonitoring :: DynLogicModel s => DynLogic s -> DynLogicTest s -> Property -> Property
applyMonitoring d (DLScript s) p =
case findMonitoring d initialAnnotatedState s of
Just f -> f p
Nothing -> p
applyMonitoring _ Stuck{} p = p
applyMonitoring _ Looping{} p = p
applyMonitoring _ BadPrecondition{} p = p
findMonitoring :: DynLogicModel s => DynLogic s -> Annotated s -> TestSequence s -> Maybe (Property -> Property)
findMonitoring Stop _s TestSeqStop = Just id
findMonitoring (After a k) s (TestSeqStep (var := a') as)
-- TODO: do nicely with eqT instead (avoids `unsafeCoerceVar`)
| Some a == Some a' = findMonitoring (k (unsafeCoerceVar var) s') s' as
where
s' = computeNextState s a' (unsafeCoerceVar var)
findMonitoring (AfterAny k) s as@(TestSeqStep (_var := a) _)
| 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.
findMonitoring d s as <|> findMonitoring d' s as
findMonitoring (Stopping d) s as = findMonitoring d s as
findMonitoring (Weight _ d) s as = findMonitoring d s as
findMonitoring (ForAll (_q :: Quantification a) k) s (TestSeq (Witness (a :: a') as)) =
case eqT @a @a' of
Just Refl -> findMonitoring (k a) s (TestSeq as)
Nothing -> Nothing
findMonitoring (Monitor m d) s as =
(m .) <$> findMonitoring d s as
findMonitoring _ _ _ = Nothing