Skip to content

Commit c16d82d

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

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

test/test-async.hs

Lines changed: 88 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,12 @@ import Data.Foldable (foldMap)
1920
import Data.Maybe
2021

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

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

Comments
 (0)