11{-# LANGUAGE CPP,ScopedTypeVariables,DeriveDataTypeable #-}
2+ {-# LANGUAGE DeriveAnyClass #-}
23module Main where
34
45import Test.Framework (defaultMain , testGroup )
@@ -21,6 +22,12 @@ import Data.Foldable (foldMap)
2122import Data.Maybe
2223
2324import Prelude hiding (catch )
25+ #if MIN_VERSION_base(4,21,0)
26+ import Control.Exception.Annotation (ExceptionAnnotation (.. ))
27+ import Control.Exception.Context (displayExceptionContext , getExceptionAnnotations )
28+ import Control.Exception.Backtrace (Backtraces , displayBacktraces )
29+ #endif
30+ import GHC.Stack (HasCallStack )
2431
2532main = defaultMain tests
2633
@@ -78,6 +85,10 @@ tests = [
7885 case_mapConcurrentlyBounded_exception
7986 , testCase " Warden" case_Warden
8087 , testCase " Warden_spawn_after_shutdown" case_Warden_spawn_after_shutdown
88+
89+ #if MIN_VERSION_base(4,21,0)
90+ , testGroup " exception rethrow" exception_rethrow
91+ #endif
8192 ]
8293 ]
8394
@@ -547,4 +558,80 @@ case_Warden_spawn_after_shutdown = do
547558 r <- try $ spawn warden $ return ()
548559 case r of
549560 Left (WardenException {}) -> return () -- expected
550- Right _ -> assertFailure " Expected WardenException"
561+ Right _ -> assertFailure " Expected WardenException"
562+
563+ #if MIN_VERSION_base(4,21,0)
564+ -- The following regroups tests of exception context propagation to ensure that
565+ -- exception rethrown by async keep the initial backtrace.
566+
567+ -- | This is a dummy exception that we can throw
568+ data Exc = Exc
569+ deriving (Show , Exception )
570+
571+ action_wrapper :: HasCallStack => (IO x -> IO y ) -> IO y
572+ action_wrapper op = op action
573+
574+ action :: HasCallStack => IO x
575+ action = throwIO Exc
576+
577+
578+ -- | From an exception, extract two lines of context, ignoring the header and
579+ -- the remaining lines.
580+ --
581+ -- For example, when calling the above 'action_wrapper (\x -> x)', in GHC 9.12, the current callstack looks like:
582+ --
583+ --
584+ -- HasCallStack backtrace:
585+ -- throwIO, called at test/test-async.hs:485:11 in async-2.2.5-inplace-test-async:Main
586+ -- action, called at test/test-async.hs:482:10 in async-2.2.5-inplace-test-async:Main
587+ -- action_wrapper, called at <interactive>:2:1 in interactive:Ghci1
588+ --
589+ -- We drop the header (e.g. HasCallStack backtrace:) and only keep the two
590+ -- lines showing the callstack inside "action".
591+ --
592+ -- Note that it does not show where action_wrapper was called, but the idea
593+ -- is that action_wrapper will do the call to the async primitive (e.g.
594+ -- 'concurrently') and will hence keep the trace of where 'concurrently' was
595+ -- called.
596+ extractThrowOrigin :: ExceptionWithContext Exc -> [String ]
597+ extractThrowOrigin (ExceptionWithContext ctx e) = do
598+ let backtraces :: [Backtraces ] = getExceptionAnnotations ctx
599+ case backtraces of
600+ [backtrace] -> take 2 $ drop 1 (lines (displayBacktraces backtrace))
601+ _ -> error " more than one backtrace"
602+
603+ -- | Run 'action' through a wrapper (using 'action_wrapper') and with a naive
604+ -- wrapper and show that the wrapper returns the same callstack when the
605+ -- exception in 'action' is raised.
606+ compareTwoExceptions op = do
607+ Left direct_exception <- tryWithContext (action_wrapper (\ x -> x))
608+ let direct_origin = extractThrowOrigin direct_exception
609+
610+ Left indirect_exception <- tryWithContext (action_wrapper op)
611+ let indirect_origin = extractThrowOrigin indirect_exception
612+
613+ assertEqual " The exception origins" direct_origin indirect_origin
614+
615+ doNothing = pure ()
616+ doForever = doForever
617+
618+ exception_rethrow = [
619+ testCase " concurrentlyL" $ compareTwoExceptions (\ action -> concurrently action doNothing),
620+ testCase " concurrentlyR" $ compareTwoExceptions (\ action -> concurrently doNothing action),
621+ testCase " concurrently_L" $ compareTwoExceptions (\ action -> concurrently_ action doNothing),
622+ testCase " concurrently_R" $ compareTwoExceptions (\ action -> concurrently_ doNothing action),
623+ testCase " raceL" $ compareTwoExceptions (\ action -> race action doForever),
624+ testCase " raceR" $ compareTwoExceptions (\ action -> race doForever action),
625+ testCase " race_L" $ compareTwoExceptions (\ action -> race_ action doForever),
626+ testCase " race_R" $ compareTwoExceptions (\ action -> race_ doForever action),
627+ testCase " mapConcurrently" $ compareTwoExceptions (\ action -> mapConcurrently (\ () -> action) [() , () , () ]),
628+ testCase " forConcurrently" $ compareTwoExceptions (\ action -> forConcurrently [() , () , () ] (\ () -> action)),
629+ testCase " withAsync wait" $ compareTwoExceptions $ \ action -> do
630+ withAsync action $ \ a -> do
631+ wait a,
632+ testCase " withAsync inside" $ compareTwoExceptions $ \ action -> do
633+ withAsync doForever $ \ a -> do
634+ action
635+ ]
636+ #endif
637+
0 commit comments