Add shrink timeout#488
Conversation
58a6512 to
620ca5f
Compare
|
Not a maintainer myself, but I think this is neat! More name bikeshedding: I'd suggest adding "micros" (e.g.
Given a known shrink tree, you could have a test deliberately hang on a given input, e.g. a timeout of 100 microseconds and hang for 200 after five shrinks. |
|
My two cents: I like the naming format |
1393063 to
8419e26
Compare
Thanks for the idea! I added such a test here. I made the timeout longer (1 second) as lower values would often cause at least one ci job to fail (usually MacOS on an older GHC), due to the test timing out before it reached the generated values I specifically wanted it to get stuck on. I also had a "sanity-check" style test that verified the timeout indeed cancels shrinking in the expected wall-clock time. -- Time limit of 2 seconds. Verifies that withShrinkTime indeed cancels
-- shrinking within the time limit we want.
prop_ShrinkTimeLimitClock :: Property
prop_ShrinkTimeLimitClock =
property $ do
startTime <- liftIO $ Clock.getMonotonicTime
annotateShow startTime
_ <- checkModPropGen delay30s (withShrinkTime 2000000)
endTime <- liftIO $ Clock.getMonotonicTime
annotateShow endTime
let timeElapsed = endTime - startTime
annotateShow timeElapsed
-- should be around 2
diff timeElapsed (>=) 1.5
diff timeElapsed (<=) 2.5
where
delay30s x = when (x == 13) (liftIO $ CC.threadDelay 30000000)Unfortunately this relies on As far as the name goes, I agree adding the units is a good idea. Yet |
|
Also it is probably a good idea to mention -- | Set the timeout -- in microseconds -- after which the test runner gives up
-- on shrinking and prints the best counterexample. Note that shrinking can be
-- cancelled before the timeout if the 'ShrinkLimit' is reached
-- (defaults to 1,000). See 'withShrinks'.
--
withShrinkTime :: ShrinkTimeLimit -> Property -> Property |
c74f125 to
c247881
Compare
|
I went ahead and made those changes:
|
29b2b1a to
ab42332
Compare
|
@tbidne, can I re-review this? |
|
@moodmosaic Please do, thanks! |
|
I'm not a maintainer on the Haskell package, but it looks ok to me apart from a few small things. The style guide is pretty clear on its preference for I'm not sure the behaviour with small timeouts is correct. It should report the failure case which started the search instead of Timing based tests are always a bit finicky – maybe add something to the examples instead? Having thought about this a bit more; I think that the current type for The "tell" is There's also weirdness around its returns. It can't return Thus: I think the shrink should be: takeSmallest ::
MonadIO m
=> ShrinkCount
-> ShrinkPath
-> ShrinkLimit
-> ShrinkRetries
-> (FailureReport -> m ())
-> Failure
-> Journal
-> [TreeT m (Maybe (Either Failure (), Journal))]
-> m FailureReport
takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit retries updateUI =
let
loop shrinks revShrinkPath (Failure loc err mdiff) (Journal logs) xs = do
let
shrinkPath =
ShrinkPath $ reverse revShrinkPath
failure =
mkFailure shrinks shrinkPath Nothing loc err mdiff (reverse logs)
updateUI failure
if shrinks >= fromIntegral slimit then
-- if we've hit the shrink limit, don't shrink any further
pure failure
else
findM (zip [0..] xs) (failure) $ \(n, m) -> do
o <- runTreeN retries m
case o of
NodeT (Just (Left smallerFailure, smallerLogs)) children ->
Just <$>
loop (shrinks + 1) (n : revShrinkPath) smallerFailure smallerLogs children
_ ->
return Nothing
in
loop shrinks0 (reverse shrinkPath0)Ok. Back to the PR at hand, if you use that type, then things get a bit simpler, as you have "on hand" the starting position and won't return There's also the question of multiple return paths, the |
|
Thanks @HuwCampbell 👍 Agreed! @tbidne could you address the above? It LGTM once the comments are addressed. Thank you! 🙏 |
7a7fff1 to
ca59c7b
Compare
takeSmallest should return FailureReport, not Result, as the latter throws away information. This improves clarity and later extensions.
Add withShrinkTimeoutMicros to allow configuring shrink behavior in terms of a timeout.
|
Apologies for the delay. I have rebased the PR via @HuwCampbell's excellent suggestion. The first commit is purely the suggested refactor, which passes the test suite locally. The second commit is my changes, and indeed the refactor makes this significantly simpler. I also added the other requested changes e.g. I am curious what others think! It occurs to me that I could also add |
Resolves #476.
There are some design decisions to make, so this is intended more to jumpstart the conversation than it is to present a finished implementation. Some notes:
ShrinkTimeLimit :: Intindependent of the currentShrinkLimit :: Int. This way is backwards compatible, and it allows one to specify both a total number of shrinks and a time limit. That said, it is arguable that combining these choices into a single option makes for a friendlier interface as e.g. someone might specify a time limit of30sonly to be unexpectedly thwarted by the defaultShrinkLimitof 1,000.ShrinkTimeLimitcould also be namedShrinkTimeout.Intmicroseconds to make interop withtimeoutsimpler, though I could imagine choosing something less implementation-derived, say, seconds (and maybe a different type e.g.Natural).IORefiffShrinkTimeLimitis set. IfShrinkTimeLimitis not set then the "update" logic isconst (pure ()). I mention this in case it can affect performance, as an alternative would be to have two totally different loops, at the cost of code reuse.Thanks!