From a4b0767ffc7381af2e65fa611c96ed77b2bae781 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:28:50 +0000 Subject: [PATCH 1/2] Initial plan From 68b35b02c2e2ab2444a52e3245f3156d3ab340e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:43:50 +0000 Subject: [PATCH 2/2] Fix OpenTelemetry missing root span by reordering session activity lifecycle The "test session" activity was created before [Before(TestSession)] hooks, but users set up TracerProvider (which adds the ActivityListener) in those hooks. The ActivitySource had no listeners so StartActivity returned null. Similarly, FinishSessionActivity ran after [After(TestSession)] hooks which dispose the TracerProvider, so the root span was never exported. Fix: create the session activity after before-hooks run, and stop it before after-hooks run. Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> Agent-Logs-Url: https://github.com/thomhurst/TUnit/sessions/98881459-ab61-411c-8a31-0902db096e73 --- TUnit.Engine/Services/HookExecutor.cs | 81 ++++++++++++++------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index e2c3589063..7dcab8564c 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -31,9 +31,41 @@ public HookExecutor( public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken cancellationToken) { - var sessionContext = _contextProvider.TestSessionContext; + var hooks = await _hookCollectionService.CollectBeforeTestSessionHooksAsync().ConfigureAwait(false); + + if (hooks.Count > 0) + { + foreach (var hook in hooks) + { + try + { + _contextProvider.TestSessionContext.RestoreExecutionContext(); + await ExecuteHookWithActivityAsync(hook, _contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + if (ex is SkipTestException) + { + throw; + } + + if (ex.InnerException is SkipTestException skipEx) + { + ExceptionDispatchInfo.Capture(skipEx).Throw(); + } + throw new BeforeTestSessionException($"BeforeTestSession hook failed: {ex.Message}", ex); + } + } + } + + // Start the session activity AFTER hooks have run, because user hooks + // typically set up the TracerProvider / ActivityListener. If we started + // the activity before hooks, the ActivitySource would have no listeners + // and StartActivity would return null - producing no root span. #if NET + var sessionContext = _contextProvider.TestSessionContext; + if (TUnitActivitySource.Source.HasListeners()) { sessionContext.Activity = TUnitActivitySource.StartActivity( @@ -46,47 +78,24 @@ public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken canc ]); } #endif - - var hooks = await _hookCollectionService.CollectBeforeTestSessionHooksAsync().ConfigureAwait(false); - - if (hooks.Count == 0) - { - return; - } - - foreach (var hook in hooks) - { - try - { - _contextProvider.TestSessionContext.RestoreExecutionContext(); - await ExecuteHookWithActivityAsync(hook, _contextProvider.TestSessionContext, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - if (ex is SkipTestException) - { - throw; - } - - if (ex.InnerException is SkipTestException skipEx) - { - ExceptionDispatchInfo.Capture(skipEx).Throw(); - } - - throw new BeforeTestSessionException($"BeforeTestSession hook failed: {ex.Message}", ex); - } - } } public async ValueTask> ExecuteAfterTestSessionHooksAsync(CancellationToken cancellationToken) { + // Stop the session activity BEFORE hooks run, because user hooks + // typically dispose the TracerProvider / ActivityListener. If we + // stopped the activity after hooks, the exporter would already be + // gone and the root span would never be exported. +#if NET + var hasTestFailures = _contextProvider.TestSessionContext.AllTests + .Any(t => t.Result is { State: TestState.Failed or TestState.Timeout or TestState.Cancelled }); + FinishSessionActivity(hasErrors: hasTestFailures); +#endif + var hooks = await _hookCollectionService.CollectAfterTestSessionHooksAsync().ConfigureAwait(false); if (hooks.Count == 0) { -#if NET - FinishSessionActivity(hasErrors: false); -#endif return []; } @@ -109,10 +118,6 @@ public async ValueTask> ExecuteAfterTestSessionHooksAsync(Cancel } } -#if NET - FinishSessionActivity(hasErrors: exceptions is { Count: > 0 }); -#endif - return exceptions ?? []; }