Skip to content

Commit f7ad9ae

Browse files
committed
Implement keepGoing and saveCounterexamples
1 parent fd0d595 commit f7ad9ae

6 files changed

Lines changed: 126 additions & 43 deletions

File tree

src/Test/Mutagen/Config.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ data Config
9292
, maxTraceLength :: Maybe Int
9393
-- ^ The maximim trace length to consider. Useful in conjunction with the
9494
-- `Tree` `traceMethod` when testing lengthy properties.
95+
, keepGoing :: Bool
96+
-- ^ Whether to keep searching for more counterexamples after finding the
97+
-- first one. If set, Mutagen will stop only when reaching the maximum number
98+
-- of successful tests or the timeout, without giving up in the presence of
99+
-- too many discards. Reports will always be a 'Success' or a timeout.
100+
, saveCounterexamples :: Maybe FilePath
101+
-- ^ If set to a 'FilePath', save found counterexamples to the given file.
102+
-- Accepts templated file paths, e.g., "counterexample_@.hs", where "@" will
103+
-- be replaced by a counter. This is useful in combination with 'keepGoing'
104+
-- to save multiple counterexamples.
105+
--
106+
-- NOTE: if 'keepGoing' is enabled and the counterexample path does not
107+
-- contain "@", then the counter is appended to its end.
95108
, chatty :: Bool
96109
-- ^ Print extra info.
97110
, debug :: DebugMode
@@ -120,6 +133,8 @@ defaultConfig =
120133
, examples = []
121134
, traceBackend = Bitmap
122135
, maxTraceLength = Nothing
136+
, keepGoing = False
137+
, saveCounterexamples = Nothing
123138
, chatty = False
124139
, debug = NoDebug
125140
, tui = False

src/Test/Mutagen/Report.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ data Report
2020
-- ^ Number of passed tests
2121
, numDiscarded :: Int
2222
-- ^ Number of discarded tests
23+
, numFailed :: Int
24+
-- ^ Number of failed tests
2325
}
2426
| -- | The property failed for the given arguments
2527
Counterexample

src/Test/Mutagen/Test/Loop.hs

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ import Test.Mutagen.Test.State
5050
, incMutantKindCounter
5151
, incNumBoring
5252
, incNumDiscarded
53+
, incNumFailed
5354
, incNumGenerated
5455
, incNumInteresting
5556
, incNumMutatedFromDiscarded
5657
, incNumMutatedFromPassed
5758
, incNumPassed
5859
, incNumTestsSinceLastInteresting
59-
, incNumTraceLogResets
60+
, incNumTraceStoreResets
61+
, nextCounterexamplePath
6062
, resetNumTestsSinceLastInteresting
6163
, setAutoResetAfter
6264
, setCurrentGenSize
@@ -102,7 +104,8 @@ loop st
102104
-- We discarded too many tests
103105
-- ==> give up
104106
| stNumDiscarded st
105-
>= stMaxDiscardRatio st * max (stNumPassed st) (stMaxSuccess st) =
107+
>= stMaxDiscardRatio st * max (stNumPassed st) (stMaxSuccess st)
108+
&& not (stKeepGoing st) =
106109
giveUp st "too many discarded tests"
107110
-- The time bugdet is over, we check this every so often
108111
-- ==> give up
@@ -127,7 +130,7 @@ loop st
127130
st
128131
& setAutoResetAfter ((* 2) <$> (stAutoResetAfter st))
129132
& setRandomMutations (stRandomMutations st * 2)
130-
& incNumTraceLogResets
133+
& incNumTraceStoreResets
131134
& resetNumTestsSinceLastInteresting
132135
newTest st'
133136
-- Nothing new under the sun
@@ -148,11 +151,9 @@ newTest st = do
148151
stopOnDebugMode (stDebug st'') result
149152
-- check the test result and report a counterexample or continue
150153
case result of
151-
-- Test failed and it was not supposed to, report counterexample
152-
Failed | stExpect st'' -> counterexample st'' args result
153-
-- Test failed but it was expected to, stop testing
154-
Failed -> success st''
155-
-- Test passed or discarded, continue the loop
154+
-- Test failed, report counterexample
155+
Failed -> counterexample st'' args result
156+
-- Test passed or discarded, continue the loop directly
156157
_ -> loop st''
157158
where
158159
-- Stop execution if in debug mode
@@ -165,6 +166,47 @@ newTest st = do
165166
message "Press enter to continue ..."
166167
void (liftIO getLine)
167168

169+
-- | Found a bug!
170+
--
171+
-- NOTE: if 'keepGoing' is disabled, this is also a terminal state.
172+
counterexample :: (MonadMutagen m) => MutagenState -> Args -> Result -> m Report
173+
counterexample st args result = do
174+
message "Found counterexample!"
175+
pretty args
176+
message "Reason of failure:"
177+
case resultReason result of
178+
Just failureReason -> message failureReason
179+
Nothing -> message "assertion failed"
180+
case resultException result of
181+
Just exc -> do
182+
message "The exception was:"
183+
message (show exc)
184+
Nothing -> return ()
185+
case nextCounterexamplePath st of
186+
Just path -> do
187+
message $ "Saving counterexample to: " <> path
188+
liftIO $ writeFile path (show args)
189+
Nothing -> return ()
190+
let st' = st & incNumFailed
191+
next st'
192+
where
193+
next st'
194+
-- Check if we should keep going
195+
| stKeepGoing st = do
196+
message $ "Failed " <> show (stNumFailed st) <> " times, continuing..."
197+
loop st'
198+
-- Check if this was an expected failure and mask the report as success
199+
| not (stExpect st) =
200+
success st'
201+
-- Otherwise, report the counterexample
202+
| otherwise =
203+
return
204+
Counterexample
205+
{ numPassed = stNumPassed st
206+
, numDiscarded = stNumDiscarded st
207+
, failingArgs = args
208+
}
209+
168210
-- * Terminal states
169211

170212
-- | All tests passed successfully
@@ -175,6 +217,7 @@ success st = do
175217
Success
176218
{ numPassed = stNumPassed st
177219
, numDiscarded = stNumDiscarded st
220+
, numFailed = stNumFailed st
178221
}
179222

180223
-- | Too many discarded tests
@@ -188,27 +231,6 @@ giveUp st gaveUpReason = do
188231
, numDiscarded = stNumDiscarded st
189232
}
190233

191-
-- | Found a bug!
192-
counterexample :: (MonadMutagen m) => MutagenState -> Args -> Result -> m Report
193-
counterexample st args result = do
194-
message "Found counterexample!"
195-
pretty args
196-
message "*** Reason of failure:"
197-
case resultReason result of
198-
Just failureReason -> message failureReason
199-
Nothing -> message "assertion failed"
200-
case resultException result of
201-
Just exc -> do
202-
message "*** The exception was:"
203-
message (show exc)
204-
Nothing -> return ()
205-
return
206-
Counterexample
207-
{ numPassed = stNumPassed st
208-
, numDiscarded = stNumDiscarded st
209-
, failingArgs = args
210-
}
211-
212234
-- | Expected failure did not occur
213235
noExpectedFailure :: (MonadMutagen m) => MutagenState -> m Report
214236
noExpectedFailure st = do

src/Test/Mutagen/Test/State.hs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ module Test.Mutagen.Test.State
2121
, updateDiscardedQueue
2222
, setFragmentStore
2323
, updateFragmentStore
24-
, incNumTraceLogResets
24+
, incNumTraceStoreResets
2525
, incNumPassed
2626
, incNumDiscarded
27+
, incNumFailed
2728
, incNumGenerated
2829
, incNumMutatedFromPassed
2930
, incNumMutatedFromDiscarded
@@ -36,10 +37,13 @@ module Test.Mutagen.Test.State
3637
-- * State-related utilities
3738
, timedOut
3839
, computeSize
40+
, nextCounterexamplePath
3941
)
4042
where
4143

44+
import Data.List (elemIndex)
4245
import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
46+
import System.FilePath ((<.>))
4347
import Test.Mutagen.Config (Config (..), DebugMode (..))
4448
import Test.Mutagen.Fragment
4549
( FragmentStore
@@ -97,6 +101,10 @@ data MutagenState
97101
-- ^ Mirrored from 'Config.filterFragments'
98102
, stMaxTraceLength :: !(Maybe Int)
99103
-- ^ Mirrored from 'Config.maxTraceLength'
104+
, stKeepGoing :: !Bool
105+
-- ^ Mirrored from 'Config.keepGoing'
106+
, stSaveCounterexamples :: !(Maybe FilePath)
107+
-- ^ Mirrored from 'Config.saveCounterexamples'
100108
, stChatty :: !Bool
101109
-- ^ Mirrored from 'Config.chatty'
102110
, stDebug :: !DebugMode
@@ -143,14 +151,16 @@ data MutagenState
143151
-- ^ Number of passed test cases
144152
, stNumDiscarded :: !Int
145153
-- ^ Number of discarded test cases
154+
, stNumFailed :: !Int
155+
-- ^ Number of failed test cases (only useful when 'keepGoing' is enabled)
146156
, stNumInteresting :: !Int
147157
-- ^ Number of interesting test cases
148158
, stNumBoring :: !Int
149159
-- ^ Number of boring test cases
150160
, stNumTestsSinceLastInteresting :: !Int
151161
-- ^ Number of test cases since the last interesting one
152-
, stNumTraceLogResets :: !Int
153-
-- ^ Number of times the trace logs have been reset
162+
, stNumTraceStoreResets :: !Int
163+
-- ^ Number of times the trace store have been reset
154164
}
155165

156166
-- | Initialize the internal state
@@ -189,6 +199,8 @@ initMutagenState cfg (Property gen runner) = do
189199
, stUseFragments = useFragments cfg
190200
, stFilterFragments = filterFragments cfg
191201
, stMaxTraceLength = maxTraceLength cfg
202+
, stKeepGoing = keepGoing cfg
203+
, stSaveCounterexamples = saveCounterexamples cfg
192204
, stChatty = chatty cfg || debug cfg /= NoDebug
193205
, stDebug = debug cfg
194206
, stTui = tui cfg
@@ -212,11 +224,12 @@ initMutagenState cfg (Property gen runner) = do
212224
, stNumRandMutants = 0
213225
, stNumFragMutants = 0
214226
, stNumPassed = 0
227+
, stNumFailed = 0
215228
, stNumDiscarded = 0
216229
, stNumInteresting = 0
217230
, stNumBoring = 0
218231
, stNumTestsSinceLastInteresting = 0
219-
, stNumTraceLogResets = 0
232+
, stNumTraceStoreResets = 0
220233
}
221234
where
222235
fragmentStoreFromExamples =
@@ -276,9 +289,9 @@ updateFragmentStore f st =
276289

277290
-- ** Incrementers
278291

279-
incNumTraceLogResets :: MutagenState -> MutagenState
280-
incNumTraceLogResets st =
281-
st{stNumTraceLogResets = stNumTraceLogResets st + 1}
292+
incNumTraceStoreResets :: MutagenState -> MutagenState
293+
incNumTraceStoreResets st =
294+
st{stNumTraceStoreResets = stNumTraceStoreResets st + 1}
282295

283296
incNumPassed :: MutagenState -> MutagenState
284297
incNumPassed st =
@@ -288,6 +301,10 @@ incNumDiscarded :: MutagenState -> MutagenState
288301
incNumDiscarded st =
289302
st{stNumDiscarded = stNumDiscarded st + 1}
290303

304+
incNumFailed :: MutagenState -> MutagenState
305+
incNumFailed st =
306+
st{stNumFailed = stNumFailed st + 1}
307+
291308
incNumGenerated :: MutagenState -> MutagenState
292309
incNumGenerated st =
293310
st{stNumGenerated = stNumGenerated st + 1}
@@ -359,3 +376,19 @@ computeSize st
359376
`min` stMaxGenSize st
360377
where
361378
roundTo n m = (n `div` m) * m
379+
380+
-- | Compute the path for the next counterexample to be saved.
381+
--
382+
-- If no counterexamples are to be saved, returns 'Nothing'. If a path template
383+
-- is provided (containing '@'), replaces '@' with the current failure counter.
384+
-- Otherwise, appends the counter to the given path.
385+
nextCounterexamplePath :: MutagenState -> Maybe FilePath
386+
nextCounterexamplePath st
387+
| Just path <- stSaveCounterexamples st =
388+
case elemIndex '@' path of
389+
Just n -> Just (replace n (show (stNumFailed st + 1)) path)
390+
Nothing -> Just (path <.> show (stNumFailed st + 1))
391+
| otherwise = Nothing
392+
where
393+
replace n s str =
394+
let (before, after) = splitAt n str in before <> s <> drop 1 after

src/Test/Mutagen/Test/Terminal.hs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ printGlobalStats st = do
219219
<> show (stNumPassed st)
220220
<> " tests ("
221221
<> show (stNumDiscarded st)
222-
<> " discarded)"
222+
<> " discarded, "
223+
<> show (stNumFailed st)
224+
<> " failed)"
223225
message
224226
$ "* Tests origin: "
225227
<> show (stNumGenerated st)
@@ -248,8 +250,8 @@ printGlobalStats st = do
248250
<> ", using "
249251
<> show (stRandomMutations st)
250252
<> " random mutations (after "
251-
<> show (stNumTraceLogResets st)
252-
<> " trace log resets)"
253+
<> show (stNumTraceStoreResets st)
254+
<> " trace store resets)"
253255
message
254256
$ "* Current generation size: "
255257
<> show (stCurrentGenSize st)
@@ -268,10 +270,10 @@ printGlobalStats st = do
268270
-- | Print short statistics about the testing session
269271
printShortStats :: (MonadTerminal m) => MutagenState -> m ()
270272
printShortStats st = do
271-
let total = stNumPassed st + stNumDiscarded st
273+
let total = stNumPassed st + stNumDiscarded st + stNumFailed st
272274
let mutated = stNumMutatedFromPassed st + stNumMutatedFromDiscarded st
273275
message
274-
$ "Ran "
276+
$ "Executed "
275277
<> show total
276278
<> " tests ("
277279
<> show (percentage (stNumPassed st) total)

test/re/Main.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ main = do
4343
config :: Config
4444
config =
4545
defaultConfig
46-
{ maxGenSize =
46+
{ maxSuccess =
47+
-- Number of successful tests to find before stopping
48+
10000
49+
, maxGenSize =
4750
-- Max generation size
4851
5
4952
, useLazyPrunning =
@@ -61,7 +64,13 @@ config =
6164
-- Only store fragments of the following types
6265
allow @ASCII
6366
<> allow @(RE ASCII)
64-
, chatty =
67+
, -- , keepGoing =
68+
-- -- Keep going after finding the first counterexample
69+
-- True
70+
-- , saveCounterexamples =
71+
-- -- Save found counterexamples to a file
72+
-- Just "tmp/prop_optimize_@.txt"
73+
chatty =
6574
-- Print extra info
6675
False
6776
, debug =

0 commit comments

Comments
 (0)