Skip to content

Commit b56cc42

Browse files
committed
io-sim: exception handling in traceResult
1 parent ef82ae8 commit b56cc42

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

io-sim/src/Control/Monad/IOSim.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ import Data.Typeable (Typeable)
9999

100100
import Data.List.Trace (Trace (..))
101101

102-
import Control.Exception (throw)
102+
import Control.Exception (SomeAsyncException (..), throw)
103103

104104
import Control.Monad.ST.Lazy
105105

@@ -422,7 +422,16 @@ traceResult strict = unsafePerformIO . eval
422422
where
423423
eval :: SimTrace a -> IO (Either Failure a)
424424
eval a = do
425-
r <- try (evaluate a)
425+
-- NOTE: It's fine to let asynchronous exceptions pass through. The only
426+
-- way simulation could raise them is by using `throw` in pure code, while
427+
-- `throwIO` in the simulation will be captured as `FailureException`. So
428+
-- we can safely assume asynchronous exceptions are coming from the
429+
-- environment running the simulation, e.g. `QuickCheck`, as in the case
430+
-- of `within` or `discardedAfter` operators.
431+
r <- tryJust (\e -> case fromException @SomeAsyncException e of
432+
Just _ -> Nothing
433+
Nothing -> Just e)
434+
(evaluate a)
426435
case r of
427436
Left e -> return (Left (FailureEvaluation e))
428437
Right _ -> go a

io-sim/test/Test/Control/Monad/IOSim.hs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{-# LANGUAGE CPP #-}
22

33
{-# OPTIONS_GHC -Wno-orphans #-}
4+
-- `-fno-full-laziness` is needed for `discardAfter` to work correctly, see
5+
-- `unit_discardAfter` below..
6+
{-# OPTIONS_GHC -fno-full-laziness #-}
47

58
module Test.Control.Monad.IOSim
69
( tests
@@ -43,6 +46,7 @@ import Test.Control.Monad.STM
4346
import Test.Control.Monad.Utils
4447

4548
import Test.QuickCheck
49+
import Test.QuickCheck.Property as QC
4650
import Test.Tasty hiding (after)
4751
import Test.Tasty.QuickCheck
4852

@@ -60,6 +64,8 @@ tests =
6064
, testProperty "timeout" prop_timeout
6165
, testProperty "timeouts" prop_timeouts
6266
, testProperty "stacked timeouts" prop_stacked_timeouts
67+
, testProperty "timeout: discardAfter" unit_discardAfter
68+
, testProperty "timeout: within" unit_within
6369
, testProperty "async exceptions 1" unit_timeouts_and_async_exceptions_1
6470
, testProperty "async exceptions 2" unit_timeouts_and_async_exceptions_2
6571
, testProperty "async exceptions 3" unit_timeouts_and_async_exceptions_3
@@ -1073,6 +1079,42 @@ prop_stacked_timeouts timeout0 timeout1 actionDuration =
10731079
= Just Nothing
10741080

10751081

1082+
-- | Check that `discardAfter` works as expected.
1083+
--
1084+
-- NOTE: using `discardAfter` with `IOSim` is more tricky than for `IO`
1085+
-- properties, since `IOSim` is a pure computation. One need to wrap the
1086+
-- simulation in a lambda and use `-fno-full-laziness` to avoid GHC from
1087+
-- moving the thunk outside of the lambda, and evaluating it just once.
1088+
--
1089+
unit_discardAfter :: Property
1090+
unit_discardAfter = mapTotalResult f $ discardAfter 10 $ \() -> runSimOrThrow $ True <$ (forever (threadDelay 10))
1091+
where
1092+
-- if `discard` kills the computation with the `Timeout` exception,
1093+
-- `theException` is `Nothing`, but if `traceResult` wraps it, then it is
1094+
-- a `Just`. We mark each test a success if `theException` is `Nothing`,
1095+
-- otherwise the test would fail with too many discarded cases, but if we re
1096+
-- introduce the bug in `traceResult` then it fails, since then
1097+
-- `theException` is a `Just`.
1098+
f :: QC.Result -> QC.Result
1099+
f r@MkResult { QC.theException = Nothing }
1100+
= r { ok = Just True }
1101+
f r = r
1102+
1103+
1104+
-- | Check that `within` works as expected.
1105+
--
1106+
unit_within :: Property
1107+
unit_within = mapTotalResult f $ within 1 $ runSimOrThrow $ True <$ (forever (threadDelay 10))
1108+
where
1109+
-- if `within` kills the computation with the `Timeout` exception,
1110+
-- `theException` is `Nothing`, but if `traceResult` wraps it, then it is
1111+
-- a `Just`.
1112+
f :: QC.Result -> QC.Result
1113+
f r@MkResult { QC.theException = Nothing }
1114+
= r { expect = False }
1115+
f r = r
1116+
1117+
10761118
unit_timeouts_and_async_exceptions_1 :: Property
10771119
unit_timeouts_and_async_exceptions_1 =
10781120
let trace = runSimTrace experiment in

0 commit comments

Comments
 (0)