Skip to content

Commit df0bd92

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 df0bd92

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

core/Control/Concurrent/Async.hs

Lines changed: 20 additions & 7 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,19 @@ import GHC.Conc (ThreadId(..))
5151
import GHC.Exts
5252
import GHC.IO hiding (onException)
5353

54+
#if !MIN_VERSION_base(4,21,0)
55+
type ExceptionWithContext x = x
56+
57+
catchNoPropagate :: IO a -> (ExceptionWithContext SomeException -> IO a) -> IO a
58+
catchNoPropagate = catchAll
59+
60+
tryWithContext :: IO a -> IO (Either (ExceptionWithContext SomeException) a)
61+
tryWithContext = try
62+
63+
rethrowIO :: ExceptionWithContext SomeException -> IO a
64+
rethrowIO = throwIO
65+
#endif
66+
5467
-- | An asynchronous action spawned by 'async' or 'withAsync'.
5568
-- Asynchronous actions are executed in a separate thread, and
5669
-- operations are provided for waiting for asynchronous actions to
@@ -60,7 +73,7 @@ data Async a = Async
6073
{ asyncThreadId :: {-# UNPACK #-} !ThreadId
6174
-- ^ Returns the t'ThreadId' of the thread running
6275
-- the given t'Async'.
63-
, _asyncWait :: STM (Either SomeException a)
76+
, _asyncWait :: STM (Either (ExceptionWithContext SomeException) a)
6477
}
6578

6679
-- | Spawn an asynchronous action in a separate thread.
@@ -102,11 +115,11 @@ withAsyncUsing :: (IO () -> IO ThreadId)
102115
withAsyncUsing doFork = \action inner -> do
103116
var <- newEmptyTMVarIO
104117
mask $ \restore -> do
105-
t <- doFork $ try (restore action) >>= atomically . putTMVar var
118+
t <- doFork $ tryWithContext (restore action) >>= atomically . putTMVar var
106119
let a = Async t (readTMVar var)
107-
r <- restore (inner a) `catchAll` \e -> do
120+
r <- restore (inner a) `catchNoPropagate` \e -> do
108121
uninterruptibleCancel a
109-
throwIO e
122+
rethrowIO (e :: ExceptionWithContext SomeException)
110123
uninterruptibleCancel a
111124
return r
112125

@@ -130,7 +143,7 @@ wait = tryAgain . atomically . waitSTM
130143
-- > waitCatch = atomically . waitCatchSTM
131144
--
132145
{-# INLINE waitCatch #-}
133-
waitCatch :: Async a -> IO (Either SomeException a)
146+
waitCatch :: Async a -> IO (Either (ExceptionWithContext SomeException) a)
134147
waitCatch = tryAgain . atomically . waitCatchSTM
135148
where
136149
-- See: https://github.com/simonmar/async/issues/14
@@ -146,7 +159,7 @@ waitSTM a = do
146159
-- | A version of 'waitCatch' that can be used inside an STM transaction.
147160
--
148161
{-# INLINE waitCatchSTM #-}
149-
waitCatchSTM :: Async a -> STM (Either SomeException a)
162+
waitCatchSTM :: Async a -> STM (Either (ExceptionWithContext SomeException) a)
150163
waitCatchSTM (Async _ w) = w
151164

152165
-- | 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)