Skip to content

Commit d0f21f8

Browse files
committed
feat: add exceptions rethrow tests
1 parent 961bd4d commit d0f21f8

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

test/test-async.hs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP,ScopedTypeVariables,DeriveDataTypeable #-}
2+
{-# LANGUAGE DeriveAnyClass #-}
23
module Main where
34

45
import Test.Framework (defaultMain, testGroup)
@@ -19,6 +20,10 @@ import Data.Foldable (foldMap)
1920
import Data.Maybe
2021

2122
import Prelude hiding (catch)
23+
import Control.Exception.Annotation (ExceptionAnnotation(..))
24+
import Control.Exception.Context (displayExceptionContext, getExceptionAnnotations)
25+
import GHC.Stack (HasCallStack)
26+
import Control.Exception.Backtrace (Backtraces, displayBacktraces)
2227

2328
main = defaultMain tests
2429

@@ -65,7 +70,11 @@ tests = [
6570
, testCase "concurrentlyE_Monoid" concurrentlyE_Monoid
6671
, testCase "concurrentlyE_Monoid_fail" concurrentlyE_Monoid_fail
6772
#endif
73+
74+
#if MIN_VERSION_base(4,9,0)
75+
, testGroup "exception rethrow" exception_rethrow
6876
]
77+
#endif
6978
]
7079

7180
value = 42 :: Int
@@ -459,3 +468,80 @@ concurrentlyE_Monoid_fail = do
459468
r :: Either Char [Char] <- runConcurrentlyE $ foldMap ConcurrentlyE $ current
460469
assertEqual "The earliest failure" (Left 'u') r
461470
#endif
471+
472+
473+
#if MIN_VERSION_base(4,9,0)
474+
-- The following regroups tests of exception context propagation to ensure that
475+
-- exception rethrown by async keep the initial backtrace.
476+
477+
-- | This is a dummy exception that we can throw
478+
data Exc = Exc
479+
deriving (Show, Exception)
480+
481+
action_wrapper :: HasCallStack => (IO x -> IO y) -> IO y
482+
action_wrapper op = op action
483+
484+
action :: HasCallStack => IO x
485+
action = throwIO Exc
486+
487+
488+
-- | From an exception, extract two lines of context, ignoring the header and
489+
-- the remaining lines.
490+
--
491+
-- For example, when calling the above 'action_wrapper (\x -> x)', in GHC 9.12, the current callstack looks like:
492+
--
493+
--
494+
-- HasCallStack backtrace:
495+
-- throwIO, called at test/test-async.hs:485:11 in async-2.2.5-inplace-test-async:Main
496+
-- action, called at test/test-async.hs:482:10 in async-2.2.5-inplace-test-async:Main
497+
-- action_wrapper, called at <interactive>:2:1 in interactive:Ghci1
498+
--
499+
-- We drop the header (e.g. HasCallStack backtrace:) and only keep the two
500+
-- lines showing the callstack inside "action".
501+
--
502+
-- Note that it does not show where action_wrapper was called, but the idea
503+
-- is that action_wrapper will do the call to the async primitive (e.g.
504+
-- 'concurrently') and will hence keep the trace of where 'concurrently' was
505+
-- called.
506+
extractThrowOrigin :: ExceptionWithContext Exc -> [String]
507+
extractThrowOrigin (ExceptionWithContext ctx e) = do
508+
let backtraces :: [Backtraces] = getExceptionAnnotations ctx
509+
case backtraces of
510+
[backtrace] -> take 2 $ drop 1 (lines (displayBacktraces backtrace))
511+
_ -> error "more than one backtrace"
512+
513+
-- | Run 'action' through a wrapper (using 'action_wrapper') and with a naive
514+
-- wrapper and show that the wrapper returns the same callstack when the
515+
-- exception in 'action' is raised.
516+
compareTwoExceptions op = do
517+
Left direct_exception <- tryWithContext (action_wrapper (\x -> x))
518+
let direct_origin = extractThrowOrigin direct_exception
519+
520+
Left indirect_exception <- tryWithContext (action_wrapper op)
521+
let indirect_origin = extractThrowOrigin indirect_exception
522+
523+
assertEqual "The exception origins" direct_origin indirect_origin
524+
525+
doNothing = pure ()
526+
doForever = doForever
527+
528+
exception_rethrow = [
529+
testCase "concurrentlyL" $ compareTwoExceptions (\action -> concurrently action doNothing),
530+
testCase "concurrentlyR" $ compareTwoExceptions (\action -> concurrently doNothing action),
531+
testCase "concurrently_L" $ compareTwoExceptions (\action -> concurrently_ action doNothing),
532+
testCase "concurrently_R" $ compareTwoExceptions (\action -> concurrently_ doNothing action),
533+
testCase "raceL" $ compareTwoExceptions (\action -> race action doForever),
534+
testCase "raceR" $ compareTwoExceptions (\action -> race doForever action),
535+
testCase "race_L" $ compareTwoExceptions (\action -> race_ action doForever),
536+
testCase "race_R" $ compareTwoExceptions (\action -> race_ doForever action),
537+
testCase "mapConcurrently" $ compareTwoExceptions (\action -> mapConcurrently (\() -> action) [(), (), ()]),
538+
testCase "forConcurrently" $ compareTwoExceptions (\action -> forConcurrently [(), (), ()] (\() -> action)),
539+
testCase "withAsync wait" $ compareTwoExceptions $ \action -> do
540+
withAsync action $ \a -> do
541+
wait a,
542+
testCase "withAsync inside" $ compareTwoExceptions $ \action -> do
543+
withAsync doForever $ \a -> do
544+
action
545+
]
546+
#endif
547+

0 commit comments

Comments
 (0)