11{-# LANGUAGE CPP,ScopedTypeVariables,DeriveDataTypeable #-}
2+ {-# LANGUAGE DeriveAnyClass #-}
23module Main where
34
45import Test.Framework (defaultMain , testGroup )
@@ -19,6 +20,10 @@ import Data.Foldable (foldMap)
1920import Data.Maybe
2021
2122import 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
2328main = 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
7180value = 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