tasty-quickcheck-0.10.3: Test/Tasty/QuickCheck.hs
-- | This module allows to use QuickCheck properties in tasty.
{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, NamedFieldPuns #-}
module Test.Tasty.QuickCheck
( testProperty
, testProperties
, QuickCheckTests(..)
, QuickCheckReplay(..)
, QuickCheckShowReplay(..)
, QuickCheckMaxSize(..)
, QuickCheckMaxRatio(..)
, QuickCheckVerbose(..)
, QuickCheckMaxShrinks(..)
-- * Re-export of Test.QuickCheck
, module Test.QuickCheck
-- * Internal
-- | If you are building a test suite, you don't need these functions.
--
-- They may be used by other tasty add-on packages (such as tasty-hspec).
, QC(..)
, optionSetToArgs
) where
import Test.Tasty ( testGroup )
import Test.Tasty.Providers
import Test.Tasty.Options
import qualified Test.QuickCheck as QC
import qualified Test.QuickCheck.Test as QC
import qualified Test.QuickCheck.State as QC
import qualified Test.QuickCheck.Text as QC
import Test.Tasty.Runners (formatMessage, emptyProgress)
import Test.QuickCheck hiding -- for re-export
( quickCheck
, Args(..)
, Result
, stdArgs
, quickCheckWith
, quickCheckWithResult
, quickCheckResult
, verboseCheck
, verboseCheckWith
, verboseCheckWithResult
, verboseCheckResult
, verbose
-- Template Haskell functions
#if MIN_VERSION_QuickCheck(2,11,0)
, allProperties
#endif
, forAllProperties
, quickCheckAll
, verboseCheckAll
)
import qualified Data.Char as Char
import Data.Typeable
import Data.List
import Text.Printf
import Text.Read (readMaybe)
import Test.QuickCheck.Random (mkQCGen)
import Options.Applicative (metavar)
import System.Random (getStdRandom, randomR)
#if !MIN_VERSION_base(4,9,0)
import Control.Applicative
import Data.Monoid
#endif
newtype QC = QC QC.Property
deriving Typeable
-- | Create a 'TestTree' for a QuickCheck 'QC.Testable' property
testProperty :: QC.Testable a => TestName -> a -> TestTree
testProperty name prop = singleTest name $ QC $ QC.property prop
-- | Create a test from a list of QuickCheck properties. To be used
-- with 'Test.QuickCheck.allProperties'. E.g.
--
-- >tests :: TestTree
-- >tests = testProperties "Foo" $allProperties
testProperties :: TestName -> [(String, Property)] -> TestTree
testProperties name = testGroup name . map (uncurry testProperty)
-- | Number of test cases for QuickCheck to generate
newtype QuickCheckTests = QuickCheckTests Int
deriving (Num, Ord, Eq, Real, Enum, Integral, Typeable)
newtype QuickCheckReplay = QuickCheckReplay (Maybe Int)
deriving (Typeable)
-- | If a test case fails unexpectedly, show the replay token
newtype QuickCheckShowReplay = QuickCheckShowReplay Bool
deriving (Typeable)
-- | Size of the biggest test cases
newtype QuickCheckMaxSize = QuickCheckMaxSize Int
deriving (Num, Ord, Eq, Real, Enum, Integral, Typeable)
-- | Maximum number of of discarded tests per successful test before giving up.
newtype QuickCheckMaxRatio = QuickCheckMaxRatio Int
deriving (Num, Ord, Eq, Real, Enum, Integral, Typeable)
-- | Show the test cases that QuickCheck generates
newtype QuickCheckVerbose = QuickCheckVerbose Bool
deriving (Typeable)
-- | Number of shrinks allowed before QuickCheck will fail a test.
--
-- @since 0.10.2
newtype QuickCheckMaxShrinks = QuickCheckMaxShrinks Int
deriving (Num, Ord, Eq, Real, Enum, Integral, Typeable)
instance IsOption QuickCheckTests where
defaultValue = 100
parseValue =
-- We allow numeric underscores for readability; see
-- https://github.com/UnkindPartition/tasty/issues/263
fmap QuickCheckTests . safeRead . filter (/= '_')
optionName = return "quickcheck-tests"
optionHelp = return "Number of test cases for QuickCheck to generate. Underscores accepted: e.g. 10_000_000"
optionCLParser = mkOptionCLParser $ metavar "NUMBER"
instance IsOption QuickCheckReplay where
defaultValue = QuickCheckReplay Nothing
-- Reads a replay int seed
parseValue v = QuickCheckReplay . Just <$> safeRead v
optionName = return "quickcheck-replay"
optionHelp = return "Random seed to use for replaying a previous test run (use same --quickcheck-max-size)"
optionCLParser = mkOptionCLParser $ metavar "SEED"
instance IsOption QuickCheckShowReplay where
defaultValue = QuickCheckShowReplay False
parseValue = fmap QuickCheckShowReplay . safeReadBool
optionName = return "quickcheck-show-replay"
optionHelp = return "Show a replay token for replaying tests"
optionCLParser = flagCLParser Nothing (QuickCheckShowReplay True)
defaultMaxSize :: Int
defaultMaxSize = QC.maxSize QC.stdArgs
instance IsOption QuickCheckMaxSize where
defaultValue = fromIntegral defaultMaxSize
parseValue = fmap QuickCheckMaxSize . safeRead
optionName = return "quickcheck-max-size"
optionHelp = return "Size of the biggest test cases quickcheck generates"
optionCLParser = mkOptionCLParser $ metavar "NUMBER"
instance IsOption QuickCheckMaxRatio where
defaultValue = fromIntegral $ QC.maxDiscardRatio QC.stdArgs
parseValue = fmap QuickCheckMaxRatio . safeRead
optionName = return "quickcheck-max-ratio"
optionHelp = return "Maximum number of discared tests per successful test before giving up"
optionCLParser = mkOptionCLParser $ metavar "NUMBER"
instance IsOption QuickCheckVerbose where
defaultValue = QuickCheckVerbose False
parseValue = fmap QuickCheckVerbose . safeReadBool
optionName = return "quickcheck-verbose"
optionHelp = return "Show the generated test cases"
optionCLParser = mkFlagCLParser mempty (QuickCheckVerbose True)
instance IsOption QuickCheckMaxShrinks where
defaultValue = QuickCheckMaxShrinks (QC.maxShrinks QC.stdArgs)
parseValue = fmap QuickCheckMaxShrinks . safeRead
optionName = return "quickcheck-shrinks"
optionHelp = return "Number of shrinks allowed before QuickCheck will fail a test"
optionCLParser = mkOptionCLParser $ metavar "NUMBER"
-- | Convert tasty options into QuickCheck options.
--
-- This is a low-level function that was originally added for tasty-hspec
-- but may be used by others.
--
-- @since 0.9.1
optionSetToArgs :: OptionSet -> IO (Int, QC.Args)
optionSetToArgs opts = do
replaySeed <- case mReplay of
Nothing -> getStdRandom (randomR (1,999999))
Just seed -> return seed
let args = QC.stdArgs
{ QC.chatty = False
, QC.maxSuccess = nTests
, QC.maxSize = maxSize
, QC.replay = Just (mkQCGen replaySeed, 0)
, QC.maxDiscardRatio = maxRatio
, QC.maxShrinks = maxShrinks
}
return (replaySeed, args)
where
QuickCheckTests nTests = lookupOption opts
QuickCheckReplay mReplay = lookupOption opts
QuickCheckMaxSize maxSize = lookupOption opts
QuickCheckMaxRatio maxRatio = lookupOption opts
QuickCheckMaxShrinks maxShrinks = lookupOption opts
instance IsTest QC where
testOptions = return
[ Option (Proxy :: Proxy QuickCheckTests)
, Option (Proxy :: Proxy QuickCheckReplay)
, Option (Proxy :: Proxy QuickCheckShowReplay)
, Option (Proxy :: Proxy QuickCheckMaxSize)
, Option (Proxy :: Proxy QuickCheckMaxRatio)
, Option (Proxy :: Proxy QuickCheckVerbose)
, Option (Proxy :: Proxy QuickCheckMaxShrinks)
]
run opts (QC prop) yieldProgress = do
(replaySeed, args) <- optionSetToArgs opts
let
QuickCheckShowReplay showReplay = lookupOption opts
QuickCheckVerbose verbose = lookupOption opts
maxSize = QC.maxSize args
replayMsg = makeReplayMsg replaySeed maxSize
-- Quickcheck already catches exceptions, no need to do it here.
r <- quickCheck yieldProgress
args
(if verbose then QC.verbose prop else prop)
qcOutput <- formatMessage $ QC.output r
let qcOutputNl =
if "\n" `isSuffixOf` qcOutput
then qcOutput
else qcOutput ++ "\n"
testSuccessful = successful r
putReplayInDesc = (not testSuccessful) || showReplay
return $
(if testSuccessful then testPassed else testFailed)
(qcOutputNl ++
(if putReplayInDesc then replayMsg else ""))
-- | Like the original 'QC.quickCheck' but is reporting progress using tasty
-- callback.
--
quickCheck :: (Progress -> IO ())
-> QC.Args
-> QC.Property
-> IO QC.Result
quickCheck yieldProgress args prop = do
-- Here we rely on the fact that QuickCheck currently prints its progress to
-- stderr and the overall status (which we don't need) to stdout
tm <- QC.newTerminal
(const $ pure ())
(\progressText -> yieldProgress emptyProgress { progressPercent = parseProgress progressText })
QC.withState args $ \ s ->
QC.test s { QC.terminal = tm } prop
where
-- QuickCheck outputs something like "(15461 tests)\b\b\b\b\b\b\b\b\b\b\b\b\b"
parseProgress :: String -> Float
parseProgress = maybe 0 (\n -> fromIntegral (n :: Int) / fromIntegral (QC.maxSuccess args))
. readMaybe
. takeWhile Char.isDigit
. drop 1
successful :: QC.Result -> Bool
successful r =
case r of
QC.Success {} -> True
_ -> False
makeReplayMsg :: Int -> Int -> String
makeReplayMsg seed size = let
sizeStr = if (size /= defaultMaxSize)
then printf " --quickcheck-max-size=%d" size
else ""
in printf "Use --quickcheck-replay=%d%s to reproduce." seed sizeStr