@@ -103,78 +103,33 @@ private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, Ca
103103 // Ensure TestSession hooks run before creating test instances
104104 await _testExecutor . EnsureTestSessionHooksExecutedAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
105105
106- // Execute test with retry logic - each retry gets a fresh instance
107- // Timeout is applied per retry attempt, not across all retries
108- await RetryHelper . ExecuteWithRetry ( test . Context , async ( ) =>
106+ // Check if we can use the fast path (no retry, no timeout)
107+ // Note: retryLimit == 0 means "no retries" (run once), not "unlimited retries"
108+ var retryLimit = test . Context . Metadata . TestDetails . RetryLimit ;
109+ var testTimeout = test . Context . Metadata . TestDetails . Timeout ;
110+
111+ if ( retryLimit == 0 && ! testTimeout . HasValue )
109112 {
110- // Get timeout configuration for this attempt
111- var testTimeout = test . Context . Metadata . TestDetails . Timeout ;
112- var timeoutMessage = testTimeout . HasValue
113- ? $ "Test '{ test . Context . Metadata . TestDetails . TestName } ' timed out after { testTimeout . Value } "
114- : null ;
115-
116- // Wrap entire lifecycle (instance creation, initialization, execution) with timeout
117- await TimeoutHelper . ExecuteWithTimeoutAsync (
118- async ct =>
119- {
120- test . Context . Metadata . TestDetails . ClassInstance = await test . CreateInstanceAsync ( ) . ConfigureAwait ( false ) ;
121-
122- // Invalidate cached eligible event objects since ClassInstance changed
123- test . Context . CachedEligibleEventObjects = null ;
124-
125- // Check if this test should be skipped (after creating instance)
126- if ( test . Context . Metadata . TestDetails . ClassInstance is SkippedTestInstance ||
127- ! string . IsNullOrEmpty ( test . Context . SkipReason ) )
128- {
129- await _stateManager . MarkSkippedAsync ( test , test . Context . SkipReason ?? "Test was skipped" ) . ConfigureAwait ( false ) ;
130-
131- await _eventReceiverOrchestrator . InvokeTestSkippedEventReceiversAsync ( test . Context , ct ) . ConfigureAwait ( false ) ;
132-
133- await _eventReceiverOrchestrator . InvokeTestEndEventReceiversAsync ( test . Context , ct ) . ConfigureAwait ( false ) ;
134-
135- return ;
136- }
137-
138- try
139- {
140- _testInitializer . PrepareTest ( test , ct ) ;
141- test . Context . RestoreExecutionContext ( ) ;
142- await _testExecutor . ExecuteAsync ( test , _testInitializer , ct ) . ConfigureAwait ( false ) ;
143- }
144- finally
145- {
146- // Dispose test instance and fire OnDispose after each attempt
147- // This ensures each retry gets a fresh instance
148- var onDispose = test . Context . InternalEvents . OnDispose ;
149- if ( onDispose ? . InvocationList != null )
150- {
151- foreach ( var invocation in onDispose . InvocationList )
152- {
153- try
154- {
155- await invocation . InvokeAsync ( test . Context , test . Context ) . ConfigureAwait ( false ) ;
156- }
157- catch ( Exception disposeEx )
158- {
159- await _logger . LogErrorAsync ( $ "Error during OnDispose for { test . TestId } : { disposeEx } ") . ConfigureAwait ( false ) ;
160- }
161- }
162- }
163-
164- try
165- {
166- await TestExecutor . DisposeTestInstance ( test ) . ConfigureAwait ( false ) ;
167- }
168- catch ( Exception disposeEx )
169- {
170- await _logger . LogErrorAsync ( $ "Error disposing test instance for { test . TestId } : { disposeEx } ") . ConfigureAwait ( false ) ;
171- }
172- }
173- } ,
174- testTimeout ,
175- cancellationToken ,
176- timeoutMessage ) . ConfigureAwait ( false ) ;
177- } ) . ConfigureAwait ( false ) ;
113+ // Fast path: direct execution without wrapper overhead
114+ test . Context . CurrentRetryAttempt = 0 ;
115+ await ExecuteTestLifecycleAsync ( test , cancellationToken ) . ConfigureAwait ( false ) ;
116+ }
117+ else
118+ {
119+ // Slow path: use retry and timeout wrappers
120+ await RetryHelper . ExecuteWithRetry ( test . Context , async ( ) =>
121+ {
122+ var timeoutMessage = testTimeout . HasValue
123+ ? $ "Test '{ test . Context . Metadata . TestDetails . TestName } ' timed out after { testTimeout . Value } "
124+ : null ;
125+
126+ await TimeoutHelper . ExecuteWithTimeoutAsync (
127+ ct => ExecuteTestLifecycleAsync ( test , ct ) ,
128+ testTimeout ,
129+ cancellationToken ,
130+ timeoutMessage ) . ConfigureAwait ( false ) ;
131+ } ) . ConfigureAwait ( false ) ;
132+ }
178133
179134 await _stateManager . MarkCompletedAsync ( test ) . ConfigureAwait ( false ) ;
180135
@@ -306,4 +261,65 @@ private void CollectAllDependencies(AbstractExecutableTest test, HashSet<TestDet
306261 }
307262 }
308263 }
264+
265+ /// <summary>
266+ /// Core test lifecycle execution: instance creation, initialization, execution, and disposal.
267+ /// Extracted to allow bypassing retry/timeout wrappers when not needed.
268+ /// </summary>
269+ private async Task ExecuteTestLifecycleAsync ( AbstractExecutableTest test , CancellationToken cancellationToken )
270+ {
271+ test . Context . Metadata . TestDetails . ClassInstance = await test . CreateInstanceAsync ( ) . ConfigureAwait ( false ) ;
272+
273+ // Invalidate cached eligible event objects since ClassInstance changed
274+ test . Context . CachedEligibleEventObjects = null ;
275+
276+ // Check if this test should be skipped (after creating instance)
277+ if ( test . Context . Metadata . TestDetails . ClassInstance is SkippedTestInstance ||
278+ ! string . IsNullOrEmpty ( test . Context . SkipReason ) )
279+ {
280+ await _stateManager . MarkSkippedAsync ( test , test . Context . SkipReason ?? "Test was skipped" ) . ConfigureAwait ( false ) ;
281+
282+ await _eventReceiverOrchestrator . InvokeTestSkippedEventReceiversAsync ( test . Context , cancellationToken ) . ConfigureAwait ( false ) ;
283+
284+ await _eventReceiverOrchestrator . InvokeTestEndEventReceiversAsync ( test . Context , cancellationToken ) . ConfigureAwait ( false ) ;
285+
286+ return ;
287+ }
288+
289+ try
290+ {
291+ _testInitializer . PrepareTest ( test , cancellationToken ) ;
292+ test . Context . RestoreExecutionContext ( ) ;
293+ await _testExecutor . ExecuteAsync ( test , _testInitializer , cancellationToken ) . ConfigureAwait ( false ) ;
294+ }
295+ finally
296+ {
297+ // Dispose test instance and fire OnDispose after each attempt
298+ // This ensures each retry gets a fresh instance
299+ var onDispose = test . Context . InternalEvents . OnDispose ;
300+ if ( onDispose ? . InvocationList != null )
301+ {
302+ foreach ( var invocation in onDispose . InvocationList )
303+ {
304+ try
305+ {
306+ await invocation . InvokeAsync ( test . Context , test . Context ) . ConfigureAwait ( false ) ;
307+ }
308+ catch ( Exception disposeEx )
309+ {
310+ await _logger . LogErrorAsync ( $ "Error during OnDispose for { test . TestId } : { disposeEx } ") . ConfigureAwait ( false ) ;
311+ }
312+ }
313+ }
314+
315+ try
316+ {
317+ await TestExecutor . DisposeTestInstance ( test ) . ConfigureAwait ( false ) ;
318+ }
319+ catch ( Exception disposeEx )
320+ {
321+ await _logger . LogErrorAsync ( $ "Error disposing test instance for { test . TestId } : { disposeEx } ") . ConfigureAwait ( false ) ;
322+ }
323+ }
324+ }
309325}
0 commit comments