Skip to content

Commit

Permalink
Avoid running earlier successes with --quickcheck-replay
Browse files Browse the repository at this point in the history
On failure, tasty-quickcheck now suggests string seeds (no integer seeds anymore).
Integer seeds are still accepted as input though.
  • Loading branch information
facundominguez committed Mar 19, 2024
1 parent 21ed34d commit 363da1a
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 25 deletions.
12 changes: 12 additions & 0 deletions quickcheck/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Changes
=======

Next Version
------------

* Produce seeds that run a single failing tests instead of reproducing
all the earlier successes ([#410](https://github.com/UnkindPartition/tasty/pull/410)).

Seeds are now strings, instead of single integers. e.g.
`--quickcheck-replay="(SMGen 2909028190965759779 12330386376379709109,0)"`

Single integer seeds are still accepted as input, but they do run through
earlier successes.

Version 0.10.3
--------------

Expand Down
65 changes: 40 additions & 25 deletions quickcheck/Test/Tasty/QuickCheck.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ import Test.QuickCheck hiding -- for re-export
, verboseCheckAll
)

import Control.Applicative
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 Test.QuickCheck.Random (QCGen, mkQCGen)
import Options.Applicative (metavar)
import System.Random (getStdRandom, randomR)
#if !MIN_VERSION_base(4,9,0)
import Control.Applicative
import Data.Monoid
#endif

Expand All @@ -82,7 +82,10 @@ testProperties name = testGroup name . map (uncurry testProperty)
newtype QuickCheckTests = QuickCheckTests Int
deriving (Num, Ord, Eq, Real, Enum, Integral, Typeable)

newtype QuickCheckReplay = QuickCheckReplay (Maybe Int)
-- | Replay seed. The @Left int@ form is kept for legacy purposes.
-- The form @Right (qcgen, intSize)@ holds both the seed and the size
-- to run QuickCheck tests.
newtype QuickCheckReplay = QuickCheckReplay (Maybe (Either Int (QCGen, Int)))
deriving (Typeable)

-- | If a test case fails unexpectedly, show the replay token
Expand Down Expand Up @@ -119,10 +122,12 @@ instance IsOption QuickCheckTests where

instance IsOption QuickCheckReplay where
defaultValue = QuickCheckReplay Nothing
-- Reads a replay int seed
parseValue v = QuickCheckReplay . Just <$> safeRead v
-- Reads either a replay Int seed or a (QCGen, Int) seed
parseValue v = do
QuickCheckReplay . Just <$>
((Left <$> safeRead v) <|> (Right <$> safeRead v))
optionName = return "quickcheck-replay"
optionHelp = return "Random seed to use for replaying a previous test run (use same --quickcheck-max-size)"
optionHelp = return "Random seed to use for replaying a previous test run"
optionCLParser = mkOptionCLParser $ metavar "SEED"

instance IsOption QuickCheckShowReplay where
Expand Down Expand Up @@ -168,30 +173,37 @@ instance IsOption QuickCheckMaxShrinks where
-- This is a low-level function that was originally added for tasty-hspec
-- but may be used by others.
--
-- The returned Int is kept only for backward compatibility purposes. It
-- has no use in tast-quickcheck.
--
-- @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
(intSeed, replaySeed) <- case mReplay of
Nothing -> do
intSeed <- getStdRandom (randomR (1,999999))
return (intSeed, (mkQCGen intSeed, 0))
Just (Left intSeed) -> return (intSeed, (mkQCGen intSeed, 0))
-- The intSeed is not used when the new form of replay seed is used.
Just (Right replaySeed) -> return (0, replaySeed)

let args = QC.stdArgs
{ QC.chatty = False
, QC.maxSuccess = nTests
, QC.maxSize = maxSize
, QC.replay = Just (mkQCGen replaySeed, 0)
, QC.replay = Just replaySeed
, QC.maxDiscardRatio = maxRatio
, QC.maxShrinks = maxShrinks
}

return (replaySeed, args)
return (intSeed, args)

where
QuickCheckTests nTests = lookupOption opts
QuickCheckReplay mReplay = lookupOption opts
QuickCheckMaxSize maxSize = lookupOption opts
QuickCheckMaxRatio maxRatio = lookupOption opts
QuickCheckMaxShrinks maxShrinks = lookupOption opts
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
Expand All @@ -205,12 +217,10 @@ instance IsTest QC where
]

run opts (QC prop) yieldProgress = do
(replaySeed, args) <- optionSetToArgs opts
(_, 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
Expand All @@ -224,6 +234,8 @@ instance IsTest QC where
else qcOutput ++ "\n"
testSuccessful = successful r
putReplayInDesc = (not testSuccessful) || showReplay
Just seedSz <- return $ replayFromResult r <|> QC.replay args
let replayMsg = makeReplayMsg seedSz
return $
(if testSuccessful then testPassed else testFailed)
(qcOutputNl ++
Expand Down Expand Up @@ -259,9 +271,12 @@ successful r =
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
makeReplayMsg :: (QCGen, Int) -> String
makeReplayMsg seedSz =
printf "Use --quickcheck-replay=\"%s\" to reproduce." (show seedSz)

replayFromResult :: QC.Result -> Maybe (QCGen, Int)
replayFromResult r =
case r of
Failure{} -> Just (QC.usedSeed r, QC.usedSize r)
_ -> Nothing
1 change: 1 addition & 0 deletions quickcheck/tasty-quickcheck.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ test-suite test
, tasty-quickcheck
, tasty-hunit
, pcre-light
, QuickCheck
ghc-options: -Wall
if (!impl(ghc >= 8.0) || os(windows))
buildable: False
45 changes: 45 additions & 0 deletions quickcheck/tests/test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Test.Tasty.Runners as Tasty
import Test.Tasty.QuickCheck
import Test.Tasty.HUnit
import Data.Maybe
import Test.QuickCheck.Random (QCGen)
import Text.Regex.PCRE.Light.Char8
import Text.Printf

Expand Down Expand Up @@ -67,6 +68,29 @@ main =
resultDescription =~ "Failed"
resultDescription =~ "Use .* to reproduce"

, testCase "Replay unexpected failure" $ do
Result{..} <- runMaxSized 3 $ \x -> x /= (2 :: Int)
case resultOutcome of
Tasty.Failure {} -> return ()
_ -> assertFailure $ show resultOutcome
resultDescription =~ "Failed"
resultDescription =~ "Use --quickcheck-replay=.* to reproduce."
let firstResultDescription = resultDescription
Just seedSz <- return (parseSeed resultDescription)

Result{..} <- runReplayWithSeed seedSz $ \x -> x /= (2 :: Int)
case resultOutcome of
Tasty.Failure {} -> return ()
_ -> assertFailure $ show resultOutcome

resultDescription =~ "Failed"
-- Compare the last lines reporting the replay seed.
let lastLine = concat . take 1 . reverse . lines
lastLine resultDescription =~ "Use --quickcheck-replay=.* to reproduce."
lastLine resultDescription @?= lastLine firstResultDescription
-- Exactly one test is executed
resultDescription =~ "Falsified \\(after 1 test\\)"

, testCase "Gave up" $ do
Result{..} <- run' $ \x -> x > x ==> x > (x :: Int)
case resultOutcome of
Expand Down Expand Up @@ -98,3 +122,24 @@ runReplay p =
(singleOption $ QuickCheckShowReplay True)
(QC $ property p)
(const $ return ())

runMaxSized :: Testable p => Int -> p -> IO Result
runMaxSized sz p =
run
(singleOption $ QuickCheckMaxSize sz)
(QC $ property p)
(const $ return ())

runReplayWithSeed :: Testable p => (QCGen, Int) -> p -> IO Result
runReplayWithSeed seedSz p =
run
(singleOption $ QuickCheckReplay $ Just (Right seedSz))
(QC $ property p)
(const $ return ())

-- | Reads a seed from a message like
--
-- > "Use --quickcheck-single-replay=\"(SMGen 2909028190965759779 12330386376379709109,0)\" to reproduce."
--
parseSeed :: String -> Maybe (QCGen, Int)
parseSeed = safeRead . takeWhile (/= '\"') . drop 1 . dropWhile (/='\"')

0 comments on commit 363da1a

Please sign in to comment.