0 | module Hedgehog.Internal.Property
  1 |
  2 | import Control.Monad.Either
  3 | import Control.Monad.Identity
  4 | import Control.Monad.Trans
  5 | import Control.Monad.Writer
  6 | import Data.Lazy
  7 | import Data.List.Quantifiers
  8 | import public Data.Double.Bounded
  9 | import Data.DPair
 10 | import Derive.Prelude
 11 | import Hedgehog.Internal.Gen
 12 | import Hedgehog.Internal.Range
 13 | import Hedgehog.Internal.Util
 14 | import Statistics.Confidence
 15 | import Text.Show.Diff
 16 | import Text.Show.Pretty
 17 |
 18 | %language ElabReflection
 19 |
 20 | %default total
 21 |
 22 | --------------------------------------------------------------------------------
 23 | --          Tagged Primitives
 24 | --------------------------------------------------------------------------------
 25 |
 26 | public export
 27 | data Tag =
 28 |     ConfidenceTag
 29 |   | CoverCountTag
 30 |   | GroupNameTag
 31 |   | LabelNameTag
 32 |   | PropertyCountTag
 33 |   | PropertyNameTag
 34 |   | ShrinkCountTag
 35 |   | ShrinkLimitTag
 36 |   | TestCountTag
 37 |   | TestLimitTag
 38 |   | EarlyTermLowerBoundTag
 39 |
 40 | %runElab derive "Tag" [Show,Eq,Ord]
 41 |
 42 | public export
 43 | record Tagged (tag : Tag) (t : Type) where
 44 |   constructor MkTagged
 45 |   unTag : t
 46 |
 47 | %runElab derivePattern "Tagged" [I,P]
 48 |   [Show,Eq,Ord,Num,FromString,Semigroup,Monoid]
 49 |
 50 | public export %inline
 51 | Semigroup (Tagged t Nat) where (<+>) = (+)
 52 |
 53 | public export %inline
 54 | Monoid (Tagged t Nat) where neutral = 0
 55 |
 56 | public export %inline
 57 | Show a => Interpolation (Tagged t a) where
 58 |   interpolate (MkTagged x) = show x
 59 |
 60 | ||| The total number of tests which are covered by a classifier.
 61 | |||
 62 | ||| Can be constructed using numeric literals.
 63 | public export
 64 | 0 CoverCount : Type
 65 | CoverCount = Tagged CoverCountTag Nat
 66 |
 67 | ||| The name of a group of properties.
 68 | public export
 69 | 0 GroupName : Type
 70 | GroupName = Tagged GroupNameTag String
 71 |
 72 | ||| The number of properties in a group.
 73 | public export
 74 | 0 PropertyCount : Type
 75 | PropertyCount = Tagged PropertyCountTag Nat
 76 |
 77 | ||| The numbers of times a property was able to shrink after a failing test.
 78 | public export
 79 | 0 ShrinkCount : Type
 80 | ShrinkCount = Tagged ShrinkCountTag Nat
 81 |
 82 | ||| The number of shrinks to try before giving up on shrinking.
 83 | |||
 84 | ||| Can be constructed using numeric literals:
 85 | public export
 86 | 0 ShrinkLimit : Type
 87 | ShrinkLimit = Tagged ShrinkLimitTag Nat
 88 |
 89 | ||| The number of tests a property ran successfully.
 90 | public export
 91 | 0 TestCount : Type
 92 | TestCount = Tagged TestCountTag Nat
 93 |
 94 | ||| The number of successful tests that need to be run before a property test
 95 | ||| is considered successful.
 96 | |||
 97 | ||| Can be constructed using numeric literals.
 98 | public export
 99 | 0 TestLimit : Type
100 | TestLimit = Tagged TestLimitTag Nat
101 |
102 | ||| The number of successful tests that need to be run before the first check
103 | ||| of a confidence interval at the early termination mode.
104 | |||
105 | ||| Can be constructed using numeric literals.
106 | public export
107 | 0 EarlyTermLowerBound : Type
108 | EarlyTermLowerBound = Tagged EarlyTermLowerBoundTag Nat
109 |
110 | ||| The name of a property.
111 | public export
112 | 0 PropertyName : Type
113 | PropertyName = Tagged PropertyNameTag String
114 |
115 | ||| The acceptable occurrence of false positives
116 | |||
117 | ||| Example, `the Confidence 1000000000` would mean that
118 | ||| you'd accept a false positive
119 | ||| for 1 in 10^9 tests.
120 | public export
121 | record Confidence where
122 |   constructor MkConfidence
123 |   confidence : Bits64
124 |   0 inBound    : confidence >= 2 = True
125 |
126 | %runElab derive "Confidence" [Show,Eq,Ord]
127 |
128 | namespace Confidence
129 |   public export
130 |   fromInteger :
131 |        (n : Integer)
132 |     -> {auto 0 prf : the Bits64 (fromInteger n) >= 2 = True}
133 |     -> Confidence
134 |   fromInteger n = MkConfidence (fromInteger n) prf
135 |
136 |   export
137 |   toProbability : Confidence -> Probability
138 |   toProbability $ MkConfidence c _ =
139 |     -- we can do `ratio` because `c` is `>= 2` due to `prf`
140 |     ratio 1 (cast c) @{believe_me Oh} @{believe_me Oh}
141 |
142 | ||| The relative number of tests which are covered by a classifier.
143 | public export
144 | 0 CoverPercentage : Type
145 | CoverPercentage = DoubleBetween 0 100
146 |
147 | ||| The name of a classifier.
148 | public export
149 | 0 LabelName : Type
150 | LabelName = Tagged LabelNameTag String
151 |
152 | --------------------------------------------------------------------------------
153 | --          Journal
154 | --------------------------------------------------------------------------------
155 |
156 | ||| The difference between some expected and actual value.
157 | public export
158 | record Diff where
159 |   constructor MkDiff
160 |   diffPrefix  : String
161 |   diffRemoved : String
162 |   diffInfix   : String
163 |   diffAdded   : String
164 |   diffSuffix  : String
165 |   diffValue   : ValueDiff
166 |
167 | %runElab derive "Hedgehog.Internal.Property.Diff" [Show,Eq]
168 |
169 | ||| Whether a test is covered by a classifier, and therefore belongs to a
170 | ||| 'Class'.
171 | public export
172 | data Cover = NotCovered | Covered
173 |
174 | %runElab derive "Cover" [Show,Eq,Ord]
175 |
176 | public export
177 | Semigroup Cover where
178 |   NotCovered <+> NotCovered = NotCovered
179 |   _          <+> _          = Covered
180 |
181 | public export
182 | Monoid Cover where
183 |   neutral = NotCovered
184 |
185 | public export
186 | toCoverCount : Cover -> CoverCount
187 | toCoverCount NotCovered = 0
188 | toCoverCount Covered    = 1
189 |
190 | ||| The extent to which a test is covered by a classifier.
191 | |||
192 | ||| When a classifier's coverage does not exceed the required minimum, the
193 | ||| test will be failed.
194 | public export
195 | record Label a where
196 |   constructor MkLabel
197 |   labelName       : LabelName
198 |   labelMinimum    : CoverPercentage
199 |   labelAnnotation : a
200 |
201 | %runElab derive "Label" [Show,Eq]
202 |
203 | public export
204 | Functor Label where
205 |   map f = {labelAnnotation $= f}
206 |
207 | public export
208 | Foldable Label where
209 |   foldl f a l = f a l.labelAnnotation
210 |   foldr f a l = f l.labelAnnotation a
211 |   null _ = False
212 |
213 | public export
214 | Traversable Label where
215 |   traverse f l =
216 |     (\v => {labelAnnotation := v} l) <$> f l.labelAnnotation
217 |
218 | ||| This semigroup is right biased. The name, location and percentage from the
219 | ||| rightmost `Label` will be kept. This shouldn't be a problem since the
220 | ||| library doesn't allow setting multiple classes with the same 'ClassifierName'.
221 | export
222 | Semigroup a => Semigroup (Label a) where
223 |   ll <+> lr = { labelAnnotation $= (ll.labelAnnotation <+>) } lr
224 |
225 | ||| Log messages which are recorded during a test run.
226 | public export
227 | data Log : Type where
228 |   Annotation : Lazy String -> Log
229 |   Footnote   : Lazy String -> Log
230 |   LogLabel   : Label Cover -> Log
231 |
232 | %runElab derive "Log" [Show,Eq]
233 |
234 | ||| A record containing the details of a test run.
235 | public export
236 | record Journal where
237 |   constructor MkJournal
238 |   journalLogs : List (Lazy Log)
239 |
240 | %runElab derive "Journal" [Show,Eq,Semigroup,Monoid]
241 |
242 | ||| Details on where and why a test failed.
243 | public export
244 | record Failure where
245 |   constructor MkFailure
246 |   message : String
247 |   diff    : Maybe Diff
248 |
249 | %runElab derive "Failure" [Show,Eq]
250 |
251 | ||| The extent to which all classifiers cover a test.
252 | |||
253 | ||| When a given classification's coverage does not exceed the required/
254 | ||| minimum, the test will be failed.
255 | export
256 | record Coverage a where
257 |   constructor MkCoverage
258 |   coverageLabels : List (LabelName, Label a)
259 |
260 | %runElab derive "Coverage" [Show,Eq]
261 |
262 | export %inline
263 | names : Coverage a -> List LabelName
264 | names = map fst . coverageLabels
265 |
266 | export %inline
267 | labels : Coverage a -> List (Label a)
268 | labels = map snd . coverageLabels
269 |
270 | export %inline
271 | annotations : Coverage a -> List a
272 | annotations = map (labelAnnotation . snd) . coverageLabels
273 |
274 | mergeWith :
275 |      {auto _ : Ord k}
276 |   -> SnocList (k,v)
277 |   -> (v -> v -> v)
278 |   -> List (k,v)
279 |   -> List (k,v)
280 |   -> List (k,v)
281 | mergeWith sp _ [] ys               = sp <>> ys
282 | mergeWith sp _ xs []               = sp <>> xs
283 | mergeWith sp f (x :: xs) (y :: ys) = case compare (fst x) (fst y) of
284 |   LT => mergeWith (sp :< x) f xs (y :: ys)
285 |   EQ => mergeWith (sp :< (fst x, f (snd x) (snd y))) f xs ys
286 |   GT => mergeWith (sp :< y) f (x::xs) ys
287 |
288 | export %inline
289 | Semigroup a => Semigroup (Coverage a) where
290 |   MkCoverage x <+> MkCoverage y =
291 |     MkCoverage $ mergeWith [<] (<+>) x y
292 |
293 | export %inline
294 | Semigroup a => Monoid (Coverage a) where
295 |   neutral = MkCoverage []
296 |
297 | export
298 | Functor Coverage where
299 |   map f = {coverageLabels $= map (mapSnd $ map f) }
300 |
301 | export
302 | Foldable Coverage where
303 |   foldl f acc = foldl f acc . annotations
304 |   foldr f acc = foldr f acc . annotations
305 |   null = null . coverageLabels
306 |
307 | export
308 | Traversable Coverage where
309 |   traverse f (MkCoverage sm) =
310 |     MkCoverage <$> traverse ((traverse . traverse) f) sm
311 |
312 | --------------------------------------------------------------------------------
313 | --          Config
314 | --------------------------------------------------------------------------------
315 |
316 | public export
317 | data TerminationCriteria : Type where
318 |   EarlyTermination        : Confidence -> TestLimit -> EarlyTermLowerBound -> TerminationCriteria
319 |   NoEarlyTermination      : Confidence -> TestLimit -> TerminationCriteria
320 |   NoConfidenceTermination : TestLimit -> TerminationCriteria
321 |
322 | %runElab derive "TerminationCriteria" [Show,Eq]
323 |
324 | ||| Returns main paramters of the termination criteria
325 | |||
326 | ||| Returned size is the minimal starting size according to the criteria.
327 | ||| For confidence-checking criteria it is important to start with maximal size
328 | ||| to achieve correct distribution.
329 | public export
330 | unCriteria : TerminationCriteria -> (Maybe Confidence, TestLimit, Size)
331 | unCriteria (EarlyTermination c t _)    = (Just c, t, maxSize)
332 | unCriteria (NoEarlyTermination c t)    = (Just c, t, maxSize)
333 | unCriteria (NoConfidenceTermination t) = (Nothing, t, minSize)
334 |
335 | ||| Configuration for a property test.
336 | public export
337 | record PropertyConfig where
338 |   constructor MkPropertyConfig
339 |   shrinkLimit         : ShrinkLimit
340 |   terminationCriteria : TerminationCriteria
341 |
342 | %runElab derive "PropertyConfig" [Show,Eq]
343 |
344 | ||| The minimum amount of tests to run for a 'Property'
345 | public export
346 | defaultMinTests : TestLimit
347 | defaultMinTests = 100
348 |
349 | public export
350 | defaultLowerBound : EarlyTermLowerBound
351 | defaultLowerBound = MkTagged $ unTag defaultMinTests
352 |
353 | ||| The default confidence allows one false positive in 10^9 tests
354 | public export
355 | defaultConfidence : Confidence
356 | defaultConfidence = Confidence.fromInteger 1000000000
357 |
358 | ||| The default configuration for a property test.
359 | public export
360 | defaultConfig : PropertyConfig
361 | defaultConfig =
362 |   MkPropertyConfig
363 |     { shrinkLimit = 1000
364 |     , terminationCriteria = NoConfidenceTermination defaultMinTests
365 |     }
366 |
367 | --------------------------------------------------------------------------------
368 | --          Test
369 | --------------------------------------------------------------------------------
370 |
371 | ||| A test monad transformer allows the assertion of expectations.
372 | public export
373 | 0 TestT : (Type -> Type) -> Type -> Type
374 | TestT m = EitherT Failure (WriterT Journal m)
375 |
376 | public export
377 | 0 Test : Type -> Type
378 | Test = TestT Identity
379 |
380 | export
381 | mkTestT : Functor m => m (Either Failure a, Journal) -> TestT m a
382 | mkTestT = MkEitherT . writerT
383 |
384 | export
385 | mkTest : (Either Failure a, Journal) -> Test a
386 | mkTest = mkTestT . Id
387 |
388 | export
389 | runTestT : TestT m a -> m (Either Failure a, Journal)
390 | runTestT = runWriterT . runEitherT
391 |
392 | export
393 | runTest : Test a -> (Either Failure a, Journal)
394 | runTest = runIdentity . runTestT
395 |
396 | ||| Log some information which might be relevant to a potential test failure.
397 | export
398 | writeLog : Applicative m => Lazy Log -> TestT m ()
399 | writeLog x = mkTestT $ pure (Right (), MkJournal [x])
400 |
401 | ||| Fail the test with an error message, useful for building other failure
402 | ||| combinators.
403 | export
404 | failWith : Applicative m => Maybe Diff -> String -> TestT m a
405 | failWith diff msg = mkTestT $ pure (Left $ MkFailure msg diff, neutral)
406 |
407 | ||| Annotates the source code with a message that might be useful for
408 | ||| debugging a test failure.
409 | export %inline
410 | annotate : Applicative m => Lazy String -> TestT m ()
411 | annotate v = writeLog $ Annotation v
412 |
413 | ||| Annotates the source code with a value that might be useful for
414 | ||| debugging a test failure.
415 | export %inline
416 | annotateShow : Show a => Applicative m => a -> TestT m ()
417 | annotateShow v = annotate $ ppShow v
418 |
419 | ||| Annotates the source code with all values separately.
420 | export
421 | annotateAllShow : All Show ts => Monad m => HList ts -> TestT m ()
422 | annotateAllShow         []      = pure ()
423 | annotateAllShow @{_::_} [x]     = annotateShow x
424 | annotateAllShow @{_::_} (x::xs) = annotateShow x >> annotateAllShow xs
425 |
426 | ||| Logs a message to be displayed as additional information in the footer of
427 | ||| the failure report.
428 | export %inline
429 | footnote : Applicative m => Lazy String -> TestT m ()
430 | footnote v = writeLog $ Footnote v
431 |
432 | ||| Logs a value to be displayed as additional information in the footer of
433 | ||| the failure report.
434 | export %inline
435 | footnoteShow : Show a => Applicative m => a -> TestT m ()
436 | footnoteShow v = writeLog (Footnote $ ppShow v)
437 |
438 | ||| Fails with an error that shows the difference between two values.
439 | export %inline
440 | failDiff : Show a => Show b => Applicative m => a -> b -> TestT m ()
441 | failDiff x y =
442 |   case valueDiff <$> reify x <*> reify y of
443 |     Nothing =>
444 |       failWith Nothing $
445 |         unlines
446 |           [ "Failed"
447 |           , "━━ lhs ━━"
448 |           , ppShow x
449 |           , "━━ rhs ━━"
450 |           , ppShow y
451 |           ]
452 |
453 |     Just vdiff@(Same _) =>
454 |       failWith
455 |         (Just $ MkDiff "━━━ Failed ("  "" "no differences" "" ") ━━━" vdiff)
456 |         ""
457 |
458 |     Just vdiff =>
459 |       failWith
460 |         (Just $ MkDiff "━━━ Failed (" "- lhs" ") (" "+ rhs" ") ━━━" vdiff)
461 |         ""
462 |
463 | ||| Causes a test to fail.
464 | export %inline
465 | failure : Applicative m => TestT m a
466 | failure = failWith Nothing ""
467 |
468 | ||| Another name for `pure ()`.
469 | export %inline
470 | success : Monad m => TestT m ()
471 | success = pure ()
472 |
473 | ||| Fails the test if the condition provided is 'False'.
474 | export %inline
475 | assert : Monad m => Bool -> TestT m ()
476 | assert ok = if ok then success else failure
477 |
478 | ||| Fails the test and shows a git-like diff if the comparison operation
479 | ||| evaluates to 'False' when applied to its arguments.
480 | |||
481 | ||| The comparison function is the second argument, which may be
482 | ||| counter-intuitive to Haskell programmers. However, it allows operators to
483 | ||| be written infix for easy reading:
484 | |||
485 | ||| This function behaves like the unix @diff@ tool, which gives a 0 exit
486 | ||| code if the compared files are identical, or a 1 exit code code
487 | ||| otherwise. Like unix @diff@, if the arguments fail the comparison, a
488 | ||| /diff is shown.
489 | |||
490 | export %inline
491 | diff :
492 |      {auto _ : Show a}
493 |   -> {auto _ : Show b}
494 |   -> {auto _ : Monad m}
495 |   -> a -> (a -> b -> Bool) -> b -> TestT m ()
496 | diff x op y = if x `op` y then success else failDiff x y
497 |
498 | export infix 6 ===
499 |
500 | ||| Fails the test if the two arguments provided are not equal.
501 | export %inline
502 | (===) : Eq a => Show a => Monad m => a -> a -> TestT m ()
503 | (===) x y = diff x (==) y
504 |
505 | export infix 4 /==
506 |
507 | ||| Fails the test if the two arguments provided are equal.
508 | export %inline
509 | (/==) : Eq a => Show a => Monad m => a -> a -> TestT m ()
510 | (/==) x y = diff x (/=) y
511 |
512 |
513 | ||| Fails the test if the 'Either' is 'Left', otherwise returns the value in
514 | ||| the 'Right'.
515 | export
516 | evalEither : Show x => Monad m => Either x a -> TestT m a
517 | evalEither (Left x)  = failWith Nothing (ppShow x)
518 | evalEither (Right x) = pure x
519 |
520 | ||| Fails the test if the 'Maybe' is 'Nothing', otherwise returns the value in
521 | ||| the 'Just'.
522 | export
523 | evalMaybe : Monad m => Maybe a -> TestT m a
524 | evalMaybe Nothing = failWith Nothing "the value was Nothing"
525 | evalMaybe (Just x) = pure x
526 |
527 | --------------------------------------------------------------------------------
528 | --          PropertyT
529 | --------------------------------------------------------------------------------
530 |
531 | ||| The property monad allows both the generation of test inputs
532 | |||  and the assertion of expectations.
533 | public export
534 | 0 PropertyT : Type -> Type
535 | PropertyT = TestT Gen
536 |
537 | ||| Generates a random input for the test by running the provided generator.
538 | |||
539 | ||| This is a the same as 'forAll' but allows the user to provide a custom
540 | ||| rendering function. This is useful for values which don't have a
541 | ||| 'Show' instance.
542 | export
543 | forAllWith : (a -> String) -> Gen a -> PropertyT a
544 | forAllWith render gen = do
545 |   x <- lift (lift gen)
546 |   annotate (render x)
547 |   pure x
548 |
549 | ||| Generates a random input for the test by running the provided generator.
550 | export %inline
551 | forAll : Show a => Gen a -> PropertyT a
552 | forAll = forAllWith ppShow
553 |
554 | ||| Generates a random input provided a bunch of generators.
555 | |||
556 | ||| This function is an easy way to write several foralls in a row.
557 | ||| `[a, b, c] <- forAlls [x, y, z]` prints error message like
558 | ||| `(a, b, c) <- [| (forAll x, forAll y, forAll z) |]` but shrinks like
559 | ||| `(a, b, c) <- forAll [| (x, y, z) |]`.
560 | export
561 | forAlls : All Show ts => All Gen ts -> PropertyT (HList ts)
562 | forAlls gens = do
563 |   xs <- lift $ lift $ hlist gens
564 |   annotateAllShow xs
565 |   pure xs
566 |
567 | ||| Lift a test in to a property.
568 | export
569 | test : Test a -> PropertyT a
570 | test = mapEitherT $ mapWriterT (pure . runIdentity)
571 |
572 | --------------------------------------------------------------------------------
573 | --          Property
574 | --------------------------------------------------------------------------------
575 |
576 | ||| A property test, along with some configurable limits like how many times
577 | ||| to run the test.
578 | public export
579 | record Property where
580 |   constructor MkProperty
581 |   config : PropertyConfig
582 |   test   : PropertyT ()
583 |
584 | namespace Property
585 |   ||| Map a config modification function over a property.
586 |   export
587 |   mapConfig : (PropertyConfig -> PropertyConfig) -> Property -> Property
588 |   mapConfig f p = { config $= f } p
589 |
590 |   export
591 |   verifiedTermination :
592 |        {default defaultLowerBound min : EarlyTermLowerBound}
593 |     -> Property -> Property
594 |   verifiedTermination = mapConfig { terminationCriteria $= setEarlyTermination }
595 |
596 |     where
597 |       setEarlyTermination : TerminationCriteria -> TerminationCriteria
598 |       setEarlyTermination (NoEarlyTermination c n)    = EarlyTermination c n min
599 |       setEarlyTermination (NoConfidenceTermination n) = EarlyTermination defaultConfidence n min
600 |       setEarlyTermination (EarlyTermination c n lb)   = EarlyTermination c n min
601 |
602 |   export
603 |   noVerifiedTermination : Property -> Property
604 |   noVerifiedTermination = mapConfig { terminationCriteria $= setNoConfidence }
605 |
606 |     where
607 |       setNoConfidence : TerminationCriteria -> TerminationCriteria
608 |       setNoConfidence (NoEarlyTermination _ n)    = NoConfidenceTermination n
609 |       setNoConfidence (NoConfidenceTermination n) = NoConfidenceTermination n
610 |       setNoConfidence (EarlyTermination _ n _)    = NoConfidenceTermination n
611 |
612 |   ||| Adjust the number of times a property should be executed before it is considered
613 |   ||| successful.
614 |   export
615 |   mapTests : (TestLimit -> TestLimit) -> Property -> Property
616 |   mapTests f = mapConfig {terminationCriteria $= setLimit}
617 |
618 |     where
619 |       setLimit : TerminationCriteria -> TerminationCriteria
620 |       setLimit (NoEarlyTermination c n)    = NoEarlyTermination c (f n)
621 |       setLimit (NoConfidenceTermination n) = NoConfidenceTermination (f n)
622 |       setLimit (EarlyTermination c n lb)   = EarlyTermination c (f n) lb
623 |
624 |   ||| Set the number of times a property should be executed before it is considered
625 |   ||| successful.
626 |   |||
627 |   ||| If you have a test that does not involve any generators and thus does not
628 |   ||| need to run repeatedly, you can use @withTests 1@ to define a property that
629 |   ||| will only be checked once.
630 |   export
631 |   withTests : TestLimit -> Property -> Property
632 |   withTests = mapTests . const
633 |
634 |   ||| Set the number of times a property is allowed to shrink before the test
635 |   ||| runner gives up and prints the counterexample.
636 |   export
637 |   withShrinks : ShrinkLimit -> Property -> Property
638 |   withShrinks n = mapConfig { shrinkLimit := n }
639 |
640 |   ||| Make sure that the result is statistically significant in accordance to
641 |   ||| the passed 'Confidence'
642 |   export
643 |   withConfidence : Confidence -> Property -> Property
644 |   withConfidence c = mapConfig { terminationCriteria $= setConfidence }
645 |
646 |     where
647 |       setConfidence : TerminationCriteria -> TerminationCriteria
648 |       setConfidence (NoEarlyTermination _ n)    = NoEarlyTermination c n
649 |       setConfidence (NoConfidenceTermination n) = NoEarlyTermination c n
650 |       setConfidence (EarlyTermination _ n lb)   = EarlyTermination c n lb
651 |
652 | ||| Creates a property with the default configuration.
653 | export
654 | property : PropertyT () -> Property
655 | property = MkProperty defaultConfig
656 |
657 | ||| Creates a property, that is tested exactly once.
658 | |||
659 | ||| Use this for tests that are not based on randomly generated
660 | ||| inputs.
661 | |||
662 | ||| This is an alias for `withTests 1 . property`.
663 | export %inline
664 | property1 : PropertyT () -> Property
665 | property1 = withTests 1 . property
666 |
667 | ||| A named collection of property tests.
668 | public export
669 | record Group where
670 |   constructor MkGroup
671 |   name       : GroupName
672 |   properties : List (PropertyName, Property)
673 |
674 | namespace Group
675 |   export
676 |   mapProperty : (Property -> Property) -> Group -> Group
677 |   mapProperty f = { properties $= map (mapSnd f) }
678 |
679 |   ||| Map a config modification function over all
680 |   ||| properties in a `Group`.
681 |   export
682 |   mapConfig : (PropertyConfig -> PropertyConfig) -> Group -> Group
683 |   mapConfig = mapProperty . mapConfig
684 |
685 |   ||| Set the number of times the properties in a `Group`
686 |   ||| should be executed before they are considered
687 |   ||| successful.
688 |   export
689 |   withTests : TestLimit -> Group -> Group
690 |   withTests = mapProperty . withTests
691 |
692 |   ||| Set the number of times the properties in a `Group`
693 |   ||| are allowed to shrink before the test
694 |   ||| runner gives up and prints the counterexample.
695 |   export
696 |   withShrinks : ShrinkLimit -> Group -> Group
697 |   withShrinks = mapProperty . withShrinks
698 |
699 |   ||| Make sure that the results of a `Group` are statistically
700 |   ||| significant in accordance to the passed 'Confidence'
701 |   export
702 |   withConfidence : Confidence -> Group -> Group
703 |   withConfidence = mapProperty . withConfidence
704 |
705 | --------------------------------------------------------------------------------
706 | --          Coverage
707 | --------------------------------------------------------------------------------
708 |
709 | export
710 | coverPercentage : TestCount -> CoverCount -> CoverPercentage
711 | coverPercentage (MkTagged tests) (MkTagged count) =
712 |   roughlyFit $ (cast count / cast tests) * 100
713 |
714 | export
715 | labelCovered : TestCount -> Label CoverCount -> Bool
716 | labelCovered tests (MkLabel _ min population) =
717 |   coverPercentage tests population >= min
718 |
719 | export
720 | coverageFailures : TestCount -> Coverage CoverCount -> List $ Label CoverCount
721 | coverageFailures tests kvs =
722 |   filter (not . labelCovered tests) (labels kvs)
723 |
724 | ||| All labels are covered
725 | export
726 | coverageSuccess : TestCount -> Coverage CoverCount -> Bool
727 | coverageSuccess tests c = null $ coverageFailures tests c
728 |
729 | ||| Require a certain percentage of the tests to be covered by the
730 | ||| classifier.
731 | |||
732 | ||| ```idris
733 | |||    prop_with_coverage : Property
734 | |||    prop_with_coverage =
735 | |||      property $ do
736 | |||        match <- forAll Gen.bool
737 | |||        cover 30 "True" $ match
738 | |||        cover 30 "False" $ not match
739 | ||| ```
740 | |||
741 | ||| The example above requires a minimum of 30% coverage for both
742 | ||| classifiers. If these requirements are not met, it will fail the test.
743 | export
744 | cover : Monad m => CoverPercentage -> LabelName -> Bool -> TestT m ()
745 | cover min name covered =
746 |   let cover := if covered then Covered else NotCovered
747 |    in writeLog $ LogLabel (MkLabel name min cover)
748 |
749 | ||| Records the proportion of tests which satisfy a given condition.
750 | |||
751 | ||| ```idris example
752 | |||    prop_with_classifier : Property
753 | |||    prop_with_classifier =
754 | |||      property $ do
755 | |||        xs <- forAll $ Gen.list (Range.linear 0 100) Gen.alpha
756 | |||        for_ xs $ \\x -> do
757 | |||          classify "newborns" $ x == 0
758 | |||          classify "children" $ x > 0 && x < 13
759 | |||          classify "teens" $ x > 12 && x < 20
760 | ||| ```
761 | export
762 | classify : Monad m => LabelName -> Bool -> TestT m ()
763 | classify name covered = cover 0 name covered
764 |
765 | ||| Add a label for each test run. It produces a table showing the percentage
766 | ||| of test runs that produced each label.
767 | export
768 | label : Monad m => LabelName -> TestT m ()
769 | label name = cover 0 name True
770 |
771 | ||| Like 'label', but uses 'Show' to render its argument for display.
772 | export
773 | collect : Show a => Monad m => a -> TestT m ()
774 | collect x = cover 0 (MkTagged $ show x) True
775 |
776 |
777 | fromLabel : Label a -> Coverage a
778 | fromLabel x = MkCoverage $ [(labelName x, x)]
779 |
780 | unionsCoverage : Semigroup a => List (Coverage a) -> Coverage a
781 | unionsCoverage = MkCoverage . concatMap coverageLabels
782 |
783 | export
784 | journalCoverage : Journal -> Coverage CoverCount
785 | journalCoverage =
786 |     map toCoverCount
787 |   . unionsCoverage
788 |   . (>>= fromLog)
789 |   . journalLogs
790 |
791 |   where
792 |     fromLog : Lazy Log -> List (Coverage Cover)
793 |     fromLog (LogLabel x)   = [fromLabel x]
794 |     fromLog (Footnote _)   = []
795 |     fromLog (Annotation _) = []
796 |
797 | --------------------------------------------------------------------------------
798 | --          Confidence
799 | --------------------------------------------------------------------------------
800 |
801 | boundsForLabel :
802 |      TestCount
803 |   -> Confidence
804 |   -> Label CoverCount
805 |   -> (Probability, Probability)
806 | boundsForLabel (MkTagged tests) c lbl = do
807 |   let (tests ** _: (k ** IsSucc k= case tests of
808 |                                          Z       => (1 ** ItIsSucc)
809 |                                          k@(S n) => (k ** ItIsSucc)
810 |   let succ = P $ roughlyFit $ cast (unTag lbl.labelAnnotation) / cast tests
811 |   wilsonBounds (toProbability c) tests succ
812 |
813 | ||| Is true when the test coverage satisfies the specified 'Confidence'
814 | ||| contstraint for all 'Coverage CoverCount's
815 | export
816 | confidenceSuccess : TestCount -> Confidence -> Coverage CoverCount -> Bool
817 | confidenceSuccess tests confidence =
818 |   all assertLow . labels
819 |
820 |   where
821 |     assertLow : Label CoverCount -> Bool
822 |     assertLow cc =
823 |       fst (boundsForLabel tests confidence cc) >= cc.labelMinimum.percent
824 |
825 | ||| Is true when there exists a label that is sure to have failed according to
826 | ||| the 'Confidence' constraint
827 | export
828 | confidenceFailure : TestCount -> Confidence -> Coverage CoverCount -> Bool
829 | confidenceFailure tests confidence =
830 |   any assertHigh . labels
831 |
832 |   where
833 |     assertHigh : Label CoverCount -> Bool
834 |     assertHigh cc =
835 |       snd (boundsForLabel tests confidence cc) < cc.labelMinimum.percent
836 |
837 | export
838 | multOf100 : TestCount -> Bool
839 | multOf100 (MkTagged n) = natToInteger n `mod` 100 == 0
840 |
841 | export
842 | failureVerified : TestCount -> Coverage CoverCount -> Maybe Confidence -> Bool
843 | failureVerified count cover conf =
844 |   multOf100 count &&
845 |   maybe False (\c => confidenceFailure count c cover) conf
846 |
847 | export
848 | successVerified : TestCount -> Coverage CoverCount -> Maybe Confidence -> Bool
849 | successVerified count cover conf =
850 |   multOf100 count &&
851 |   maybe False (\c => confidenceSuccess count c cover) conf
852 |
853 | export
854 | abortEarly :
855 |      TerminationCriteria
856 |   -> TestCount
857 |   -> Coverage CoverCount
858 |   -> Maybe Confidence
859 |   -> Bool
860 | abortEarly (EarlyTermination _ _ etMinTests) tests cover conf =
861 |   let coverageReached     := successVerified tests cover conf
862 |       coverageUnreachable := failureVerified tests cover conf
863 |    in unTag tests >= unTag etMinTests &&
864 |       (coverageReached || coverageUnreachable)
865 |
866 | abortEarly _ _ _ _ = False
867 |