Test/QuickCheck/Test.hs

module Test.QuickCheck.Test where

--------------------------------------------------------------------------
-- imports

import Test.QuickCheck.Gen
import Test.QuickCheck.Property hiding ( Result( reason, interrupted ) )
import qualified Test.QuickCheck.Property as P
import Test.QuickCheck.Text
import Test.QuickCheck.State
import Test.QuickCheck.Exception

import System.Random
 ( RandomGen(..)
 , newStdGen
 , StdGen
 )

import Data.Char
 ( isSpace
 )

import Data.List
 ( sort
 , group
 , groupBy
 , intersperse
 )

--------------------------------------------------------------------------
-- quickCheck

-- * Running tests

-- | Args specifies arguments to the QuickCheck driver
data Args
 = Args
 { replay :: Maybe (StdGen,Int) -- ^ should we replay a previous test?
 , maxSuccess :: Int -- ^ maximum number of successful tests before succeeding
 , maxDiscard :: Int -- ^ maximum number of discarded tests before giving up
 , maxSize :: Int -- ^ size to use for the biggest test cases
 }
 deriving ( Show, Read )

-- | Result represents the test result
data Result
 = Success -- a successful test run
 { labels :: [(String,Int)] -- ^ labels and frequencies found during all tests
 }
 | GaveUp -- given up
 { numTests :: Int -- ^ number of successful tests performed
 , labels :: [(String,Int)] -- ^ labels and frequencies found during all tests
 }
 | Failure -- failed test run
 { usedSeed :: StdGen -- ^ what seed was used
 , usedSize :: Int -- ^ what was the test size
 , reason :: String -- ^ what was the reason
 , labels :: [(String,Int)] -- ^ labels and frequencies found during all successful tests
 }
 | NoExpectedFailure -- the expected failure did not happen
 { labels :: [(String,Int)] -- ^ labels and frequencies found during all successful tests
 }
 deriving ( Show, Read )

-- | isSuccess checks if the test run result was a success
isSuccess :: Result -> Bool
isSuccess Success{} = True
isSuccess _ = False

-- | stdArgs are the default test arguments used
stdArgs :: Args
stdArgs = Args
 { replay = Nothing
 , maxSuccess = 100
 , maxDiscard = 500
 , maxSize = 100
-- noShrinking flag?
 }

-- | Tests a property and prints the results to 'stdout'.
quickCheck :: Testable prop => prop -> IO ()
quickCheck p = quickCheckWith stdArgs p

-- | Tests a property, using test arguments, and prints the results to 'stdout'.
quickCheckWith :: Testable prop => Args -> prop -> IO ()
quickCheckWith args p = quickCheckWithResult args p >> return ()

-- | Tests a property, produces a test result, and prints the results to 'stdout'.
quickCheckResult :: Testable prop => prop -> IO Result
quickCheckResult p = quickCheckWithResult stdArgs p

-- | Tests a property, using test arguments, produces a test result, and prints the results to 'stdout'.
quickCheckWithResult :: Testable prop => Args -> prop -> IO Result
quickCheckWithResult args p =
 do tm <- newTerminal
 rnd <- case replay args of
 Nothing -> newStdGen
 Just (rnd,_) -> return rnd
 test MkState{ terminal = tm
 , maxSuccessTests = maxSuccess args
 , maxDiscardedTests = maxDiscard args
 , computeSize = case replay args of
 Nothing -> computeSize (maxSuccess args) (maxSize args)
 Just (_,s) -> \_ _ -> s
 , numSuccessTests = 0
 , numDiscardedTests = 0
 , collected = []
 , expectedFailure = False
 , randomSeed = rnd
 , isShrinking = False
 , numSuccessShrinks = 0
 , numTryShrinks = 0
 } (unGen (property p))
 where computeSize maxSuccess maxSize n d
 -- e.g. with maxSuccess = 250, maxSize = 100, goes like this:
 -- 0, 1, 2, ..., 99, 0, 1, 2, ..., 99, 0, 2, 4, ..., 98.
 | n `roundTo` maxSize + maxSize <= maxSuccess ||
 n >= maxSuccess ||
 maxSuccess `mod` maxSize == 0 = n `mod` maxSize + d `div` 10
 | otherwise =
 (n `mod` maxSize) * maxSize `div` (maxSuccess `mod` maxSize) + d `div` 10
 n `roundTo` m = (n `div` m) * m

--------------------------------------------------------------------------
-- main test loop

test :: State -> (StdGen -> Int -> Prop) -> IO Result
test st f
 | numSuccessTests st >= maxSuccessTests st = doneTesting st f
 | numDiscardedTests st >= maxDiscardedTests st = giveUp st f
 | otherwise = runATest st f

doneTesting :: State -> (StdGen -> Int -> Prop) -> IO Result
doneTesting st f =
 do -- CALLBACK done_testing?
 if expectedFailure st then
 putPart (terminal st)
 ( "+++ OK, passed "
 ++ show (numSuccessTests st)
 ++ " tests"
 )
 else
 putPart (terminal st)
 ( bold ("*** Failed!")
 ++ " Passed "
 ++ show (numSuccessTests st)
 ++ " tests (expected failure)"
 )
 success st
 if expectedFailure st then
 return Success{ labels = summary st }
 else
 return NoExpectedFailure{ labels = summary st }
 
giveUp :: State -> (StdGen -> Int -> Prop) -> IO Result
giveUp st f =
 do -- CALLBACK gave_up?
 putPart (terminal st)
 ( bold ("*** Gave up!")
 ++ " Passed only "
 ++ show (numSuccessTests st)
 ++ " tests"
 )
 success st
 return GaveUp{ numTests = numSuccessTests st
 , labels = summary st
 }

runATest :: State -> (StdGen -> Int -> Prop) -> IO Result
runATest st f =
 do -- CALLBACK before_test
 putTemp (terminal st)
 ( "("
 ++ number (numSuccessTests st) "test"
 ++ concat [ "; " ++ show (numDiscardedTests st) ++ " discarded"
 | numDiscardedTests st > 0
 ]
 ++ ")"
 )
 let size = computeSize st (numSuccessTests st) (numDiscardedTests st)
 MkRose mres ts <- protectRose (unProp (f rnd1 size))
 res <- mres
 callbackPostTest st res
 
 case ok res of
 Just True -> -- successful test
 do test st{ numSuccessTests = numSuccessTests st + 1
 , randomSeed = rnd2
 , collected = stamp res : collected st
 , expectedFailure = expect res
 } f
 
 Nothing -> -- discarded test
 do test st{ numDiscardedTests = numDiscardedTests st + 1
 , randomSeed = rnd2
 , expectedFailure = expect res
 } f
 
 Just False -> -- failed test
 do if expect res
 then putPart (terminal st) (bold "*** Failed! ")
 else putPart (terminal st) "+++ OK, failed as expected. "
 putTemp (terminal st)
 ( short 30 (P.reason res)
 ++ " (after "
 ++ number (numSuccessTests st+1) "test"
 ++ ")..."
 )
 foundFailure st res ts
 if not (expect res) then
 return Success{ labels = summary st }
 else
 return Failure{ usedSeed = randomSeed st -- correct! (this will be split first)
 , usedSize = size
 , reason = P.reason res
 , labels = summary st
 }
 where
 (rnd1,rnd2) = split (randomSeed st)

summary :: State -> [(String,Int)]
summary st = reverse
 . sort
 . map (\ss -> (head ss, (length ss * 100) `div` numSuccessTests st))
 . group
 . sort
 $ [ concat (intersperse ", " s')
 | s <- collected st
 , let s' = [ t | (t,_) <- s ]
 , not (null s')
 ]

success :: State -> IO ()
success st =
 case labels ++ covers of
 [] -> do putLine (terminal st) "."
 [pt] -> do putLine (terminal st)
 ( " ("
 ++ dropWhile isSpace pt
 ++ ")."
 )
 cases -> do putLine (terminal st) ":"
 sequence_ [ putLine (terminal st) pt | pt <- cases ]
 where
 labels = reverse
 . sort
 . map (\ss -> (showP ((length ss * 100) `div` numSuccessTests st) ++ head ss))
 . group
 . sort
 $ [ concat (intersperse ", " s')
 | s <- collected st
 , let s' = [ t | (t,0) <- s ]
 , not (null s')
 ]
 
 covers = [ ("only " ++ show occurP ++ "% " ++ fst (head lps) ++ "; not " ++ show reqP ++ "%")
 | lps <- groupBy first
 . sort
 $ [ lp
 | lps <- collected st
 , lp <- maxi lps
 , snd lp > 0
 ]
 , let occurP = (100 * length lps) `div` maxSuccessTests st
 reqP = maximum (map snd lps)
 , occurP < reqP
 ]
 
 (x,_) `first` (y,_) = x == y 

 maxi = map (\lps -> (fst (head lps), maximum (map snd lps)))
 . groupBy first
 . sort

 showP p = (if p < 10 then " " else "") ++ show p ++ "% "

--------------------------------------------------------------------------
-- main shrinking loop

foundFailure :: State -> P.Result -> [Rose (IO P.Result)] -> IO ()
foundFailure st res ts =
 do localMin st{ numTryShrinks = 0, isShrinking = True } res ts

localMin :: State -> P.Result -> [Rose (IO P.Result)] -> IO ()
localMin st res [] = localMinFound st res
localMin st res _ | P.interrupted res = localMinFound st res

localMin st res (t : ts) =
 do -- CALLBACK before_test
 MkRose mres' ts' <- protectRose t
 res' <- mres'
 putTemp (terminal st)
 ( short 35 (P.reason res)
 ++ " (after " ++ number (numSuccessTests st+1) "test"
 ++ concat [ " and "
 ++ show (numSuccessShrinks st)
 ++ concat [ "." ++ show (numTryShrinks st) | numTryShrinks st > 0 ]
 ++ " shrink"
 ++ (if numSuccessShrinks st == 1
 && numTryShrinks st == 0
 then "" else "s")
 | numSuccessShrinks st > 0 || numTryShrinks st > 0
 ]
 ++ ")..."
 )
 callbackPostTest st res'
 if ok res' == Just False
 then foundFailure st{ numSuccessShrinks = numSuccessShrinks st + 1 } res' ts'
 else localMin st{ numTryShrinks = numTryShrinks st + 1 } res ts

localMinFound :: State -> P.Result -> IO ()
localMinFound st res =
 do putLine (terminal st)
 ( P.reason res
 ++ " (after " ++ number (numSuccessTests st+1) "test"
 ++ concat [ " and " ++ number (numSuccessShrinks st) "shrink"
 | numSuccessShrinks st > 0
 ]
 ++ "): "
 )
 callbackPostFinalFailure st res

--------------------------------------------------------------------------
-- callbacks

callbackPostTest :: State -> P.Result -> IO ()
callbackPostTest st res =
 sequence_ [ f st res | PostTest f <- callbacks res ]

callbackPostFinalFailure :: State -> P.Result -> IO ()
callbackPostFinalFailure st res =
 sequence_ [ f st res | PostFinalFailure f <- callbacks res ]

--------------------------------------------------------------------------
-- the end.

AltStyle によって変換されたページ (->オリジナル) /