Skip to content

Commit c74f125

Browse files
committed
Add shrink timeout
Add withShrinkTimeoutMicros to allow configuring shrink behavior in terms of a timeout.
1 parent ad75bea commit c74f125

6 files changed

Lines changed: 258 additions & 8 deletions

File tree

hedgehog/hedgehog.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ test-suite test
137137
Test.Hedgehog.Filter
138138
Test.Hedgehog.Maybe
139139
Test.Hedgehog.Seed
140+
Test.Hedgehog.Shrink
140141
Test.Hedgehog.Skip
141142
Test.Hedgehog.Text
142143
Test.Hedgehog.Zip

hedgehog/src/Hedgehog.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ module Hedgehog (
8080
, withShrinks
8181
, ShrinkLimit
8282

83+
, withShrinkTimeoutMicros
84+
, ShrinkTimeoutMicros
85+
8386
, withRetries
8487
, ShrinkRetries
8588

@@ -188,6 +191,7 @@ import Hedgehog.Internal.Property (Property, PropertyT, PropertyName)
188191
import Hedgehog.Internal.Property (Group(..), GroupName)
189192
import Hedgehog.Internal.Property (Confidence, verifiedTermination, withConfidence)
190193
import Hedgehog.Internal.Property (ShrinkLimit, withShrinks)
194+
import Hedgehog.Internal.Property (ShrinkTimeoutMicros, withShrinkTimeoutMicros)
191195
import Hedgehog.Internal.Property (ShrinkRetries, withRetries)
192196
import Hedgehog.Internal.Property (Skip, withSkip)
193197
import Hedgehog.Internal.Property (Test, TestT, property, test)

hedgehog/src/Hedgehog/Internal/Property.hs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ module Hedgehog.Internal.Property (
3333
, DiscardLimit(..)
3434
, DiscardCount(..)
3535
, ShrinkLimit(..)
36+
, ShrinkTimeoutMicros (..)
3637
, ShrinkCount(..)
3738
, Skip(..)
3839
, ShrinkPath(..)
3940
, ShrinkRetries(..)
4041
, withTests
4142
, withDiscards
4243
, withShrinks
44+
, withShrinkTimeoutMicros
4345
, withRetries
4446
, withSkip
4547
, property
@@ -281,6 +283,7 @@ data PropertyConfig =
281283
PropertyConfig {
282284
propertyDiscardLimit :: !DiscardLimit
283285
, propertyShrinkLimit :: !ShrinkLimit
286+
, propertyShrinkTimeoutMicros :: !(Maybe ShrinkTimeoutMicros)
284287
, propertyShrinkRetries :: !ShrinkRetries
285288
, propertyTerminationCriteria :: !TerminationCriteria
286289

@@ -339,6 +342,19 @@ newtype ShrinkLimit =
339342
ShrinkLimit Int
340343
deriving (Eq, Ord, Show, Num, Enum, Real, Integral, Lift)
341344

345+
-- | The time limit before giving up on shrinking, in microseconds.
346+
--
347+
-- Can be constructed using numeric literals:
348+
--
349+
-- @
350+
-- -- 1_000_000 microseconds == 1 second
351+
-- 1_000_000 :: ShrinkTimeoutMicros
352+
-- @
353+
--
354+
newtype ShrinkTimeoutMicros =
355+
ShrinkTimeoutMicros Int
356+
deriving (Eq, Ord, Show, Num, Enum, Real, Integral, Lift)
357+
342358
-- | The numbers of times a property was able to shrink after a failing test.
343359
--
344360
newtype ShrinkCount =
@@ -1164,6 +1180,8 @@ defaultConfig =
11641180
100
11651181
, propertyShrinkLimit =
11661182
1000
1183+
, propertyShrinkTimeoutMicros =
1184+
Nothing
11671185
, propertyShrinkRetries =
11681186
0
11691187
, propertyTerminationCriteria =
@@ -1248,6 +1266,15 @@ withShrinks :: ShrinkLimit -> Property -> Property
12481266
withShrinks n =
12491267
mapConfig $ \config -> config { propertyShrinkLimit = n }
12501268

1269+
-- | Set the timeout -- in microseconds -- after which the test runner gives
1270+
-- up on shrinking and prints the best counterexample. Note that shrinking
1271+
-- can be cancelled before the timeout if the 'ShrinkLimit' is reached.
1272+
-- See 'withShrinks'.
1273+
--
1274+
withShrinkTimeoutMicros :: ShrinkTimeoutMicros -> Property -> Property
1275+
withShrinkTimeoutMicros n =
1276+
mapConfig $ \config -> config { propertyShrinkTimeoutMicros = Just n }
1277+
12511278
-- | Set the number of times a property will be executed for each shrink before
12521279
-- the test runner gives up and tries a different shrink. See 'ShrinkRetries'
12531280
-- for more information.
@@ -1491,4 +1518,4 @@ collect x =
14911518
--
14921519
-- These functions are exported in case you need them in a pinch, but are not
14931520
-- part of the public API and may change at any time, even as part of a minor
1494-
-- update.
1521+
-- update.

hedgehog/src/Hedgehog/Internal/Runner.hs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{-# LANGUAGE CPP #-}
44
{-# LANGUAGE DeriveLift #-}
55
{-# LANGUAGE DoAndIfThenElse #-}
6+
{-# LANGUAGE FlexibleContexts #-}
67
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
78
{-# LANGUAGE LambdaCase #-}
89
{-# LANGUAGE NoImplicitPrelude #-}
@@ -31,7 +32,10 @@ import Control.Concurrent.STM (TVar, atomically)
3132
import qualified Control.Concurrent.STM.TVar as TVar
3233
import Control.Exception.Safe (MonadCatch, catchAny)
3334
import Control.Monad.IO.Class (MonadIO(..))
35+
import Control.Monad.Trans.Control (MonadBaseControl (..))
36+
import Data.IORef (newIORef, readIORef, writeIORef)
3437
import Data.Maybe (isJust)
38+
import qualified System.Timeout as T
3539

3640
import Hedgehog.Internal.Config
3741
import Hedgehog.Internal.Gen (evalGenT)
@@ -42,6 +46,7 @@ import Hedgehog.Internal.Property (Journal(..), Coverage(..), CoverCou
4246
import Hedgehog.Internal.Property (Property(..), PropertyConfig(..), PropertyName(..))
4347
import Hedgehog.Internal.Property (PropertyT(..), Failure(..), runTestT)
4448
import Hedgehog.Internal.Property (ShrinkLimit, ShrinkRetries, withTests, withSkip)
49+
import Hedgehog.Internal.Property (ShrinkTimeoutMicros (..))
4550
import Hedgehog.Internal.Property (TerminationCriteria(..))
4651
import Hedgehog.Internal.Property (TestCount(..), PropertyCount(..))
4752
import Hedgehog.Internal.Property (confidenceSuccess, confidenceFailure)
@@ -118,17 +123,27 @@ runTreeN n m = do
118123
pure o
119124

120125
takeSmallest ::
121-
MonadIO m
126+
forall m.
127+
( MonadBaseControl IO m
128+
, MonadIO m
129+
)
122130
=> ShrinkCount
123131
-> ShrinkPath
124132
-> ShrinkLimit
133+
-> Maybe ShrinkTimeoutMicros
125134
-> ShrinkRetries
126135
-> (Progress -> m ())
127136
-> NodeT m (Maybe (Either Failure (), Journal))
128137
-> m Result
129-
takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit retries updateUI =
138+
takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit mstimeLimit retries updateUI =
130139
let
131-
loop shrinks revShrinkPath = \case
140+
loop ::
141+
ShrinkCount
142+
-> [Int]
143+
-> (Result -> m ())
144+
-> NodeT m (Maybe (Either Failure (), Journal))
145+
-> m Result
146+
loop shrinks revShrinkPath updateResultSoFar = \case
132147
NodeT Nothing _ ->
133148
pure GaveUp
134149

@@ -141,6 +156,7 @@ takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit retries updateUI =
141156
failure =
142157
mkFailure shrinks shrinkPath Nothing loc err mdiff (reverse logs)
143158

159+
updateResultSoFar (Failed failure)
144160
updateUI $ Shrinking failure
145161

146162
if shrinks >= fromIntegral slimit then
@@ -150,14 +166,27 @@ takeSmallest shrinks0 (ShrinkPath shrinkPath0) slimit retries updateUI =
150166
findM (zip [0..] xs) (Failed failure) $ \(n, m) -> do
151167
o <- runTreeN retries m
152168
if isFailure o then
153-
Just <$> loop (shrinks + 1) (n : revShrinkPath) o
169+
Just <$> loop (shrinks + 1) (n : revShrinkPath) updateResultSoFar o
154170
else
155171
return Nothing
156172

157173
Right () ->
158174
return OK
159-
in
160-
loop shrinks0 (reverse shrinkPath0)
175+
runLoop = loop shrinks0 (reverse shrinkPath0)
176+
in case mstimeLimit of
177+
-- no time limit, shrink normally
178+
Nothing -> runLoop (const (pure ()))
179+
-- run the loop in the timeout
180+
Just (ShrinkTimeoutMicros timeLimit) -> \nodeT -> do
181+
resultSoFar <- liftIO $ newIORef Nothing
182+
let updateResultSoFar = liftIO . writeIORef resultSoFar . Just
183+
timeout timeLimit (runLoop updateResultSoFar nodeT) >>= \case
184+
-- timed out, return preliminary result if it exists
185+
Nothing -> liftIO (readIORef resultSoFar) <&> \case
186+
Nothing -> GaveUp
187+
Just r -> r
188+
-- did not time out, return result
189+
Just r -> pure r
161190

162191
-- | Follow a given shrink path, instead of searching exhaustively. Assume that
163192
-- the end of the path is minimal, and don't try to shrink any further than
@@ -204,7 +233,9 @@ skipToShrink (ShrinkPath shrinkPath) updateUI =
204233

205234
checkReport ::
206235
forall m.
207-
MonadIO m
236+
( MonadBaseControl IO m
237+
, MonadIO m
238+
)
208239
=> MonadCatch m
209240
=> PropertyConfig
210241
-> Size
@@ -361,6 +392,7 @@ checkReport cfg size0 seed0 test0 updateUI = do
361392
0
362393
(ShrinkPath [])
363394
(propertyShrinkLimit cfg)
395+
(propertyShrinkTimeoutMicros cfg)
364396
(propertyShrinkRetries cfg)
365397
(updateUI . mkReport)
366398
node
@@ -594,3 +626,16 @@ checkParallel =
594626
, runnerVerbosity =
595627
Nothing
596628
}
629+
630+
-- vendored from lifted-base
631+
timeout :: MonadBaseControl IO m => Int -> m a -> m (Maybe a)
632+
timeout t m =
633+
liftBaseWith (\runInIO -> T.timeout t (runInIO m)) >>=
634+
maybe (pure Nothing) (fmap Just . restoreM)
635+
636+
-- vendored from base's Data.Functor until base < 4.11.0.0 is dropped
637+
-- (ghc 8.4.1)
638+
(<&>) :: Functor f => f a -> (a -> b) -> f b
639+
as <&> f = f <$> as
640+
641+
infixl 1 <&>

0 commit comments

Comments
 (0)