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 _ | P.interrupted res = localMinFound st res localMin st 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.