11{-# LANGUAGE CPP,ScopedTypeVariables,DeriveDataTypeable #-}
2+ {-# LANGUAGE DeriveAnyClass #-}
23module Main where
34
45import Test.Framework (defaultMain , testGroup )
@@ -19,6 +20,12 @@ import Data.Foldable (foldMap)
1920import Data.Maybe
2021
2122import Prelude hiding (catch )
23+ #if MIN_VERSION_base(4,21,0)
24+ import Control.Exception.Annotation (ExceptionAnnotation (.. ))
25+ import Control.Exception.Context (displayExceptionContext , getExceptionAnnotations )
26+ import Control.Exception.Backtrace (Backtraces , displayBacktraces )
27+ #endif
28+ import GHC.Stack (HasCallStack )
2229
2330main = defaultMain tests
2431
@@ -65,6 +72,10 @@ tests = [
6572 , testCase " concurrentlyE_Monoid" concurrentlyE_Monoid
6673 , testCase " concurrentlyE_Monoid_fail" concurrentlyE_Monoid_fail
6774#endif
75+
76+ #if MIN_VERSION_base(4,21,0)
77+ , testGroup " exception rethrow" exception_rethrow
78+ #endif
6879 ]
6980 ]
7081
@@ -459,3 +470,80 @@ concurrentlyE_Monoid_fail = do
459470 r :: Either Char [Char ] <- runConcurrentlyE $ foldMap ConcurrentlyE $ current
460471 assertEqual " The earliest failure" (Left ' u' ) r
461472#endif
473+
474+
475+ #if MIN_VERSION_base(4,21,0)
476+ -- The following regroups tests of exception context propagation to ensure that
477+ -- exception rethrown by async keep the initial backtrace.
478+
479+ -- | This is a dummy exception that we can throw
480+ data Exc = Exc
481+ deriving (Show , Exception )
482+
483+ action_wrapper :: HasCallStack => (IO x -> IO y ) -> IO y
484+ action_wrapper op = op action
485+
486+ action :: HasCallStack => IO x
487+ action = throwIO Exc
488+
489+
490+ -- | From an exception, extract two lines of context, ignoring the header and
491+ -- the remaining lines.
492+ --
493+ -- For example, when calling the above 'action_wrapper (\x -> x)', in GHC 9.12, the current callstack looks like:
494+ --
495+ --
496+ -- HasCallStack backtrace:
497+ -- throwIO, called at test/test-async.hs:485:11 in async-2.2.5-inplace-test-async:Main
498+ -- action, called at test/test-async.hs:482:10 in async-2.2.5-inplace-test-async:Main
499+ -- action_wrapper, called at <interactive>:2:1 in interactive:Ghci1
500+ --
501+ -- We drop the header (e.g. HasCallStack backtrace:) and only keep the two
502+ -- lines showing the callstack inside "action".
503+ --
504+ -- Note that it does not show where action_wrapper was called, but the idea
505+ -- is that action_wrapper will do the call to the async primitive (e.g.
506+ -- 'concurrently') and will hence keep the trace of where 'concurrently' was
507+ -- called.
508+ extractThrowOrigin :: ExceptionWithContext Exc -> [String ]
509+ extractThrowOrigin (ExceptionWithContext ctx e) = do
510+ let backtraces :: [Backtraces ] = getExceptionAnnotations ctx
511+ case backtraces of
512+ [backtrace] -> take 2 $ drop 1 (lines (displayBacktraces backtrace))
513+ _ -> error " more than one backtrace"
514+
515+ -- | Run 'action' through a wrapper (using 'action_wrapper') and with a naive
516+ -- wrapper and show that the wrapper returns the same callstack when the
517+ -- exception in 'action' is raised.
518+ compareTwoExceptions op = do
519+ Left direct_exception <- tryWithContext (action_wrapper (\ x -> x))
520+ let direct_origin = extractThrowOrigin direct_exception
521+
522+ Left indirect_exception <- tryWithContext (action_wrapper op)
523+ let indirect_origin = extractThrowOrigin indirect_exception
524+
525+ assertEqual " The exception origins" direct_origin indirect_origin
526+
527+ doNothing = pure ()
528+ doForever = doForever
529+
530+ exception_rethrow = [
531+ testCase " concurrentlyL" $ compareTwoExceptions (\ action -> concurrently action doNothing),
532+ testCase " concurrentlyR" $ compareTwoExceptions (\ action -> concurrently doNothing action),
533+ testCase " concurrently_L" $ compareTwoExceptions (\ action -> concurrently_ action doNothing),
534+ testCase " concurrently_R" $ compareTwoExceptions (\ action -> concurrently_ doNothing action),
535+ testCase " raceL" $ compareTwoExceptions (\ action -> race action doForever),
536+ testCase " raceR" $ compareTwoExceptions (\ action -> race doForever action),
537+ testCase " race_L" $ compareTwoExceptions (\ action -> race_ action doForever),
538+ testCase " race_R" $ compareTwoExceptions (\ action -> race_ doForever action),
539+ testCase " mapConcurrently" $ compareTwoExceptions (\ action -> mapConcurrently (\ () -> action) [() , () , () ]),
540+ testCase " forConcurrently" $ compareTwoExceptions (\ action -> forConcurrently [() , () , () ] (\ () -> action)),
541+ testCase " withAsync wait" $ compareTwoExceptions $ \ action -> do
542+ withAsync action $ \ a -> do
543+ wait a,
544+ testCase " withAsync inside" $ compareTwoExceptions $ \ action -> do
545+ withAsync doForever $ \ a -> do
546+ action
547+ ]
548+ #endif
549+
0 commit comments