1
1
{-# LANGUAGE AllowAmbiguousTypes #-}
2
+ {-# LANGUAGE FunctionalDependencies #-}
2
3
{-# LANGUAGE QuantifiedConstraints #-}
4
+ {-# LANGUAGE RecordWildCards #-}
3
5
{-# LANGUAGE UndecidableInstances #-}
4
6
5
7
-- | Model-Based Testing library for use with Haskell QuickCheck.
@@ -25,6 +27,7 @@ module Test.QuickCheck.StateModel (
25
27
Env ,
26
28
Generic ,
27
29
IsPerformResult ,
30
+ Options (.. ),
28
31
monitorPost ,
29
32
counterexamplePost ,
30
33
stateAfter ,
@@ -37,6 +40,10 @@ module Test.QuickCheck.StateModel (
37
40
computePrecondition ,
38
41
computeArbitraryAction ,
39
42
computeShrinkAction ,
43
+ generateActionsWithOptions ,
44
+ shrinkActionsWithOptions ,
45
+ defaultOptions ,
46
+ moreActions ,
40
47
) where
41
48
42
49
import Control.Monad
@@ -358,56 +365,100 @@ usedVariables (Actions as) = go initialAnnotatedState as
358
365
<> go (computeNextState aState act var) steps
359
366
360
367
instance 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
411
462
412
463
-- Running state models
413
464
@@ -498,14 +549,25 @@ runActions
498
549
=> Actions state
499
550
-> PropertyM m (Annotated state , Env )
500
551
runActions (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
502
563
unless (null rejected) $
503
564
monitor $
504
565
tabulate " Actions rejected by precondition" rejected
505
566
return (finalState, env)
506
567
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.
509
571
runSteps
510
572
:: forall state m e
511
573
. ( StateModel state
@@ -516,23 +578,23 @@ runSteps
516
578
=> Annotated state
517
579
-> Env
518
580
-> [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, [] , [] )
521
583
runSteps s env ((v := act) : as) = do
522
584
pre $ computePrecondition s act
523
585
ret <- run $ performResultToEither <$> perform (underlyingState s) action (lookUpVar env)
524
586
let name = show polar ++ actionName action
525
- monitor $ tabulate " Actions" [name]
526
- monitor $ tabulate " Action polarity" [show polar]
527
587
case (polar, ret) of
528
588
(PosPolarity , Left err) ->
529
589
positiveActionFailed err
530
590
(PosPolarity , Right val) -> do
531
591
(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)
533
594
(NegPolarity , _) -> do
534
595
(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)
536
598
where
537
599
polar = polarity act
538
600
0 commit comments