Skip to content

Commit fdd0bfc

Browse files
committed
Fix display of HasCallStack backtraces when a test throws an error
There are two parts: * Catch and rethrow exceptions with context in our vendored-in fork of `async`. This is inspired by upstream PR simonmar/async#165. * Bridge over multiple changes to `displayException` in `base` to ensure that at least some stack trace is shown.
1 parent dc40a31 commit fdd0bfc

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

core/Control/Concurrent/Async.hs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
3636
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3737
-}
3838

39-
{-# LANGUAGE MagicHash, UnboxedTuples #-}
39+
{-# LANGUAGE CPP, MagicHash, UnboxedTuples #-}
4040

4141
module Control.Concurrent.Async (
4242
async, withAsync, wait, asyncThreadId, cancel, concurrently
@@ -51,6 +51,12 @@ import GHC.Conc (ThreadId(..))
5151
import GHC.Exts
5252
import GHC.IO hiding (onException)
5353

54+
#if MIN_VERSION_base(4,21,0)
55+
#define ExcWithContext(x) ExceptionWithContext (x)
56+
#else
57+
#define ExcWithContext(x) x
58+
#endif
59+
5460
-- | An asynchronous action spawned by 'async' or 'withAsync'.
5561
-- Asynchronous actions are executed in a separate thread, and
5662
-- operations are provided for waiting for asynchronous actions to
@@ -60,7 +66,7 @@ data Async a = Async
6066
{ asyncThreadId :: {-# UNPACK #-} !ThreadId
6167
-- ^ Returns the t'ThreadId' of the thread running
6268
-- the given t'Async'.
63-
, _asyncWait :: STM (Either SomeException a)
69+
, _asyncWait :: STM (Either (ExcWithContext(SomeException)) a)
6470
}
6571

6672
-- | Spawn an asynchronous action in a separate thread.
@@ -102,11 +108,19 @@ withAsyncUsing :: (IO () -> IO ThreadId)
102108
withAsyncUsing doFork = \action inner -> do
103109
var <- newEmptyTMVarIO
104110
mask $ \restore -> do
111+
#if MIN_VERSION_base(4,21,0)
112+
t <- doFork $ tryWithContext (restore action) >>= atomically . putTMVar var
113+
let a = Async t (readTMVar var)
114+
r <- restore (inner a) `catchNoPropagate` \e -> do
115+
uninterruptibleCancel a
116+
rethrowIO (e :: ExceptionWithContext SomeException)
117+
#else
105118
t <- doFork $ try (restore action) >>= atomically . putTMVar var
106119
let a = Async t (readTMVar var)
107120
r <- restore (inner a) `catchAll` \e -> do
108121
uninterruptibleCancel a
109122
throwIO e
123+
#endif
110124
uninterruptibleCancel a
111125
return r
112126

@@ -130,7 +144,7 @@ wait = tryAgain . atomically . waitSTM
130144
-- > waitCatch = atomically . waitCatchSTM
131145
--
132146
{-# INLINE waitCatch #-}
133-
waitCatch :: Async a -> IO (Either SomeException a)
147+
waitCatch :: Async a -> IO (Either (ExcWithContext(SomeException)) a)
134148
waitCatch = tryAgain . atomically . waitCatchSTM
135149
where
136150
-- See: https://github.com/simonmar/async/issues/14
@@ -146,7 +160,7 @@ waitSTM a = do
146160
-- | A version of 'waitCatch' that can be used inside an STM transaction.
147161
--
148162
{-# INLINE waitCatchSTM #-}
149-
waitCatchSTM :: Async a -> STM (Either SomeException a)
163+
waitCatchSTM :: Async a -> STM (Either (ExcWithContext(SomeException)) a)
150164
waitCatchSTM (Async _ w) = w
151165

152166
-- | Cancel an asynchronous action by throwing the @AsyncCancelled@

core/Test/Tasty/Core.hs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{-# LANGUAGE DeriveGeneric #-}
33
{-# LANGUAGE ExistentialQuantification #-}
44
{-# LANGUAGE FlexibleInstances #-}
5+
{-# LANGUAGE ImplicitParams #-}
56
{-# LANGUAGE LambdaCase #-}
67
{-# LANGUAGE RankNTypes #-}
78
{-# LANGUAGE TypeApplications #-}
@@ -57,6 +58,10 @@ import Test.Tasty.Providers.ConsoleFormat
5758
import Text.Printf
5859
import Text.Read (readMaybe)
5960

61+
#if MIN_VERSION_base(4,21,0) && !MIN_VERSION_base(4,22,0)
62+
import Control.Exception.Context
63+
#endif
64+
6065
-- | If a test failed, 'FailureReason' describes why.
6166
--
6267
-- @since 0.8
@@ -169,12 +174,24 @@ resultSuccessful r =
169174
exceptionResult :: SomeException -> Result
170175
exceptionResult e = Result
171176
{ resultOutcome = Failure $ TestThrewException e
172-
, resultDescription = "Exception: " ++ displayException e
177+
, resultDescription = "Exception: " ++ displayException' e
173178
, resultShortDescription = "FAIL"
174179
, resultTime = 0
175180
, resultDetailsPrinter = noResultDetails
176181
}
177182

183+
displayException' :: SomeException -> String
184+
#if MIN_VERSION_base(4,22,0)
185+
displayException' = displayExceptionWithInfo
186+
#elif MIN_VERSION_base(4,21,0)
187+
displayException' (SomeException e) =
188+
displayException e ++ case displayExceptionContext ?exceptionContext of
189+
"" -> ""
190+
dc -> "\n\n" ++ dc
191+
#else
192+
displayException' = displayException
193+
#endif
194+
178195
-- | Test progress information.
179196
--
180197
-- This may be used by a runner to provide some feedback to the user while

0 commit comments

Comments
 (0)