11{-# LANGUAGE AllowAmbiguousTypes #-}
2+ {-# LANGUAGE FunctionalDependencies #-}
23{-# LANGUAGE QuantifiedConstraints #-}
4+ {-# LANGUAGE RecordWildCards #-}
35{-# LANGUAGE UndecidableInstances #-}
46
57-- | Model-Based Testing library for use with Haskell QuickCheck.
@@ -25,6 +27,7 @@ module Test.QuickCheck.StateModel (
2527 Env ,
2628 Generic ,
2729 IsPerformResult ,
30+ Options (.. ),
2831 monitorPost ,
2932 counterexamplePost ,
3033 stateAfter ,
@@ -37,6 +40,10 @@ module Test.QuickCheck.StateModel (
3740 computePrecondition ,
3841 computeArbitraryAction ,
3942 computeShrinkAction ,
43+ generateActionsWithOptions ,
44+ shrinkActionsWithOptions ,
45+ defaultOptions ,
46+ moreActions ,
4047) where
4148
4249import Control.Monad
@@ -358,56 +365,100 @@ usedVariables (Actions as) = go initialAnnotatedState as
358365 <> go (computeNextState aState act var) steps
359366
360367instance forall state . StateModel state => Arbitrary (Actions state ) where
361- arbitrary = do
362- (as, rejected) <- arbActions initialAnnotatedState 1
363- return $ Actions_ rejected (Smart 0 as)
364- where
365- arbActions :: Annotated state -> Int -> Gen ([Step state ], [String ])
366- arbActions s step = sized $ \ n ->
367- let w = n `div` 2 + 1
368- in frequency
369- [ (1 , return ([] , [] ))
370- ,
371- ( w
372- , do
373- (mact, rej) <- satisfyPrecondition
374- case mact of
375- Just (Some act@ ActionWithPolarity {}) -> do
376- let var = mkVar step
377- (as, rejected) <- arbActions (computeNextState s act var) (step + 1 )
378- return ((var := act) : as, rej ++ rejected)
379- Nothing ->
380- return ([] , [] )
381- )
382- ]
383- where
384- satisfyPrecondition = sized $ \ n -> go n (2 * n) [] -- idea copied from suchThatMaybe
385- go m n rej
386- | m > n = return (Nothing , rej)
387- | otherwise = do
388- a <- resize m $ computeArbitraryAction s
389- case a of
390- Some act ->
391- if computePrecondition s act
392- then return (Just (Some act), rej)
393- else go (m + 1 ) n (actionName (polarAction act) : rej)
394-
395- shrink (Actions_ rs as) =
396- map (Actions_ rs) (shrinkSmart (map (prune . map fst ) . concatMap customActionsShrinker . shrinkList shrinker . withStates) as)
397- where
398- shrinker :: (Step state , Annotated state ) -> [(Step state , Annotated state )]
399- shrinker (v := act, s) = [(unsafeCoerceVar v := act', s) | Some act'@ ActionWithPolarity {} <- computeShrinkAction s act]
400-
401- customActionsShrinker :: [(Step state , Annotated state )] -> [[(Step state , Annotated state )]]
402- customActionsShrinker acts =
403- let usedVars = mconcat [getAllVariables a <> getAllVariables (underlyingState s) | (_ := a, s) <- acts]
404- binding (v := _, _) = Some v `Set.member` usedVars
405- -- Remove at most one non-binding action
406- go [] = [[] ]
407- go (p : ps)
408- | binding p = map (p : ) (go ps)
409- | otherwise = ps : map (p : ) (go ps)
410- in go acts
368+ arbitrary = generateActionsWithOptions defaultOptions
369+ shrink = shrinkActionsWithOptions defaultOptions
370+
371+ data QCDProperty state = QCDProperty
372+ { runQCDProperty :: Actions state -> Property
373+ , qcdPropertyOptions :: Options state
374+ }
375+
376+ instance StateModel state => Testable (QCDProperty state ) where
377+ property QCDProperty {.. } =
378+ forAllShrink
379+ (generateActionsWithOptions qcdPropertyOptions)
380+ (shrinkActionsWithOptions qcdPropertyOptions)
381+ runQCDProperty
382+
383+ class QCDProp state p | p -> state where
384+ qcdProperty :: p -> QCDProperty state
385+
386+ instance QCDProp state (QCDProperty state ) where
387+ qcdProperty = id
388+
389+ instance Testable p => QCDProp state (Actions state -> p ) where
390+ qcdProperty p = QCDProperty (property . p) defaultOptions
391+
392+ modifyOptions :: QCDProperty state -> (Options state -> Options state ) -> QCDProperty state
393+ modifyOptions p f =
394+ let opts = qcdPropertyOptions p
395+ in p{qcdPropertyOptions = f opts}
396+
397+ moreActions :: QCDProp state p => Rational -> p -> QCDProperty state
398+ moreActions r p =
399+ modifyOptions (qcdProperty p) $ \ opts -> opts{actionLengthMultiplier = actionLengthMultiplier opts * r}
400+
401+ -- NOTE: indexed on state for forwards compatibility, e.g. when we
402+ -- want to give an explicit initial state
403+ data Options state = Options { actionLengthMultiplier :: Rational }
404+
405+ defaultOptions :: Options state
406+ defaultOptions = Options {actionLengthMultiplier = 1 }
407+
408+ -- | Generate arbitrary actions with the `GenActionsOptions`. More flexible than using the type-based
409+ -- modifiers.
410+ generateActionsWithOptions :: forall state . StateModel state => Options state -> Gen (Actions state )
411+ generateActionsWithOptions Options {.. } = do
412+ (as, rejected) <- arbActions [] [] initialAnnotatedState 1
413+ return $ Actions_ rejected (Smart 0 as)
414+ where
415+ arbActions :: [Step state ] -> [String ] -> Annotated state -> Int -> Gen ([Step state ], [String ])
416+ arbActions steps rejected s step = sized $ \ n -> do
417+ let w = round (actionLengthMultiplier * fromIntegral n) `div` 2 + 1
418+ continue <- frequency [(1 , pure False ), (w, pure True )]
419+ if continue
420+ then do
421+ (mact, rej) <- satisfyPrecondition
422+ case mact of
423+ Just (Some act@ ActionWithPolarity {}) -> do
424+ let var = mkVar step
425+ arbActions
426+ ((var := act) : steps)
427+ (rej ++ rejected)
428+ (computeNextState s act var)
429+ (step + 1 )
430+ Nothing ->
431+ return (reverse steps, rejected)
432+ else return (reverse steps, rejected)
433+ where
434+ satisfyPrecondition = sized $ \ n -> go n (2 * n) [] -- idea copied from suchThatMaybe
435+ go m n rej
436+ | m > n = return (Nothing , rej)
437+ | otherwise = do
438+ a <- resize m $ computeArbitraryAction s
439+ case a of
440+ Some act ->
441+ if computePrecondition s act
442+ then return (Just (Some act), rej)
443+ else go (m + 1 ) n (actionName (polarAction act) : rej)
444+
445+ shrinkActionsWithOptions :: forall state . StateModel state => Options state -> Actions state -> [Actions state ]
446+ shrinkActionsWithOptions _ (Actions_ rs as) =
447+ map (Actions_ rs) (shrinkSmart (map (prune . map fst ) . concatMap customActionsShrinker . shrinkList shrinker . withStates) as)
448+ where
449+ shrinker :: (Step state , Annotated state ) -> [(Step state , Annotated state )]
450+ shrinker (v := act, s) = [(unsafeCoerceVar v := act', s) | Some act'@ ActionWithPolarity {} <- computeShrinkAction s act]
451+
452+ customActionsShrinker :: [(Step state , Annotated state )] -> [[(Step state , Annotated state )]]
453+ customActionsShrinker acts =
454+ let usedVars = mconcat [getAllVariables a <> getAllVariables (underlyingState s) | (_ := a, s) <- acts]
455+ binding (v := _, _) = Some v `Set.member` usedVars
456+ -- Remove at most one non-binding action
457+ go [] = [[] ]
458+ go (p : ps)
459+ | binding p = map (p : ) (go ps)
460+ | otherwise = ps : map (p : ) (go ps)
461+ in go acts
411462
412463-- Running state models
413464
@@ -498,14 +549,25 @@ runActions
498549 => Actions state
499550 -> PropertyM m (Annotated state , Env )
500551runActions (Actions_ rejected (Smart _ actions)) = do
501- (finalState, env) <- runSteps initialAnnotatedState [] actions
552+ let bucket = \ n -> let (a, b) = go n in show a ++ " - " ++ show b
553+ where
554+ go n
555+ | n < 100 = (d * 10 , d * 10 + 9 )
556+ | otherwise = let (a, b) = go d in (a * 10 , b * 10 + 9 )
557+ where
558+ d = div n 10
559+ monitor $ tabulate " # of actions" [show $ bucket $ length actions]
560+ (finalState, env, names, polars) <- runSteps initialAnnotatedState [] actions
561+ monitor $ tabulate " Actions" names
562+ monitor $ tabulate " Action polarity" $ map show polars
502563 unless (null rejected) $
503564 monitor $
504565 tabulate " Actions rejected by precondition" rejected
505566 return (finalState, env)
506567
507- -- | Core function to execute a sequence of `Step` given some initial `Env`ironment
508- -- and `Annotated` state.
568+ -- | Core function to execute a sequence of `Step` given some initial `Env`ironment and `Annotated`
569+ -- state. Return the list of action names and polarities to work around
570+ -- https://github.com/nick8325/quickcheck/issues/416 causing repeated calls to tabulate being slow.
509571runSteps
510572 :: forall state m e
511573 . ( StateModel state
@@ -516,23 +578,23 @@ runSteps
516578 => Annotated state
517579 -> Env
518580 -> [Step state ]
519- -> PropertyM m (Annotated state , Env )
520- runSteps s env [] = return (s, reverse env)
581+ -> PropertyM m (Annotated state , Env , [ String ], [ Polarity ] )
582+ runSteps s env [] = return (s, reverse env, [] , [] )
521583runSteps s env ((v := act) : as) = do
522584 pre $ computePrecondition s act
523585 ret <- run $ performResultToEither <$> perform (underlyingState s) action (lookUpVar env)
524586 let name = show polar ++ actionName action
525- monitor $ tabulate " Actions" [name]
526- monitor $ tabulate " Action polarity" [show polar]
527587 case (polar, ret) of
528588 (PosPolarity , Left err) ->
529589 positiveActionFailed err
530590 (PosPolarity , Right val) -> do
531591 (s', env') <- positiveActionSucceeded ret val
532- runSteps s' env' as
592+ (s'', env'', names, polars) <- runSteps s' env' as
593+ pure (s'', env'', name : names, polar : polars)
533594 (NegPolarity , _) -> do
534595 (s', env') <- negativeActionResult ret
535- runSteps s' env' as
596+ (s'', env'', names, polars) <- runSteps s' env' as
597+ pure (s'', env'', name : names, polar : polars)
536598 where
537599 polar = polarity act
538600
0 commit comments