11using System . Diagnostics . CodeAnalysis ;
22using System . Reflection ;
3+ using System . Runtime . ExceptionServices ;
34using TUnit . Core ;
45using TUnit . Core . Exceptions ;
56using TUnit . Core . Interfaces ;
@@ -57,6 +58,9 @@ public async Task ExecuteAsync(AbstractExecutableTest executableTest, Cancellati
5758 var testClass = executableTest . Metadata . TestClassType ;
5859 var testAssembly = testClass . Assembly ;
5960
61+ Exception ? capturedException = null ;
62+ Exception ? hookException = null ;
63+
6064 try
6165 {
6266 await EnsureTestSessionHooksExecutedAsync ( ) . ConfigureAwait ( false ) ;
@@ -72,7 +76,6 @@ await _eventReceiverOrchestrator.InvokeFirstTestInSessionEventReceiversAsync(
7276 await _beforeHookTaskCache . GetOrCreateBeforeAssemblyTask ( testAssembly , assembly => _hookExecutor . ExecuteBeforeAssemblyHooksAsync ( assembly , CancellationToken . None ) )
7377 . ConfigureAwait ( false ) ;
7478
75- // Event receivers for first test in assembly
7679 await _eventReceiverOrchestrator . InvokeFirstTestInAssemblyEventReceiversAsync (
7780 executableTest . Context ,
7881 executableTest . Context . ClassContext . AssemblyContext ,
@@ -83,7 +86,6 @@ await _eventReceiverOrchestrator.InvokeFirstTestInAssemblyEventReceiversAsync(
8386 await _beforeHookTaskCache . GetOrCreateBeforeClassTask ( testClass , _ => _hookExecutor . ExecuteBeforeClassHooksAsync ( testClass , CancellationToken . None ) )
8487 . ConfigureAwait ( false ) ;
8588
86- // Event receivers for first test in class
8789 await _eventReceiverOrchestrator . InvokeFirstTestInClassEventReceiversAsync (
8890 executableTest . Context ,
8991 executableTest . Context . ClassContext ,
@@ -97,7 +99,6 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(
9799
98100 executableTest . Context . RestoreExecutionContext ( ) ;
99101
100- // Only wrap the actual test execution with timeout, not the hooks
101102 var testTimeout = executableTest . Context . Metadata . TestDetails . Timeout ;
102103 var timeoutMessage = testTimeout . HasValue
103104 ? $ "Test '{ executableTest . Context . Metadata . TestDetails . TestName } ' execution timed out after { testTimeout . Value } "
@@ -111,51 +112,47 @@ await TimeoutHelper.ExecuteWithTimeoutAsync(
111112
112113 executableTest . SetResult ( TestState . Passed ) ;
113114 }
114- catch ( SkipTestException )
115+ catch ( SkipTestException ex )
115116 {
116117 executableTest . SetResult ( TestState . Skipped ) ;
117- throw ;
118+ capturedException = ex ;
118119 }
119120 catch ( Exception ex )
120121 {
121122 executableTest . SetResult ( TestState . Failed , ex ) ;
122-
123- // Run per-retry cleanup hooks before re-throwing
123+ capturedException = ex ;
124+ }
125+ finally
126+ {
124127 try
125128 {
126- // Run After(Test) hooks
127129 await _hookExecutor . ExecuteAfterTestHooksAsync ( executableTest , cancellationToken ) . ConfigureAwait ( false ) ;
128-
129130 await _eventReceiverOrchestrator . InvokeTestEndEventReceiversAsync ( executableTest . Context , cancellationToken ) . ConfigureAwait ( false ) ;
130131 }
131- catch
132+ catch ( Exception ex )
132133 {
133- // Swallow any exceptions from hooks when we already have a test failure
134+ hookException = ex ;
134135 }
136+ }
135137
136- // Check if the result was overridden - if so, respect the override and don't re-throw
137- if ( executableTest . Context . Execution . Result ? . IsOverridden == true )
138- {
139- // Result was overridden, respect the override state (Passed, Failed, Skipped, etc.)
140- // Don't re-throw the exception - the override has final say
141- }
142- else
143- {
144- throw ;
145- }
138+ // If test passed but hooks failed, throw hook exception to fail the test
139+ if ( capturedException == null && hookException != null )
140+ {
141+ ExceptionDispatchInfo . Capture ( hookException ) . Throw ( ) ;
146142 }
147- finally
143+
144+ // If test failed or was skipped, handle the test exception
145+ if ( capturedException != null )
148146 {
149- // Per-retry cleanup - runs for success path only
150- if ( executableTest . State != TestState . Failed )
147+ if ( capturedException is SkipTestException )
151148 {
152- // Run After(Test) hooks
153- await _hookExecutor . ExecuteAfterTestHooksAsync ( executableTest , cancellationToken ) . ConfigureAwait ( false ) ;
149+ ExceptionDispatchInfo . Capture ( capturedException ) . Throw ( ) ;
150+ }
154151
155- await _eventReceiverOrchestrator . InvokeTestEndEventReceiversAsync ( executableTest . Context , cancellationToken ) . ConfigureAwait ( false ) ;
152+ if ( executableTest . Context . Execution . Result ? . IsOverridden != true )
153+ {
154+ ExceptionDispatchInfo . Capture ( capturedException ) . Throw ( ) ;
156155 }
157- // Note: Test instance disposal and After(Class/Assembly/Session) hooks
158- // are now handled in TestCoordinator after the retry loop completes
159156 }
160157 }
161158
0 commit comments