Skip to content

Commit 3064e24

Browse files
committed
feat: add exceptions rethrow tests
1 parent bb69fd4 commit 3064e24

1 file changed

Lines changed: 88 additions & 1 deletion

File tree

test/test-async.hs

Lines changed: 88 additions & 1 deletion
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)
@@ -21,6 +22,12 @@ import Data.Foldable (foldMap)
2122
import Data.Maybe
2223

2324
import 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

2532
main = 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

Comments
 (0)