From 4ebda26746d1fcfd7dd14d30f184c0c66778ef19 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:30:39 +0000 Subject: [PATCH 1/4] fix: catch unhandled exceptions in ExecuteRequestAsync to prevent IDE RPC crashes Stop re-throwing exceptions from ExecuteRequestAsync so they don't propagate to the MTP host and crash Rider's JSON-RPC channel. The error is still logged and reported via ReportUnhandledException, and the session now closes with IsSuccess = false. Unified BeforeSessionHooksFailed/AfterSessionHooksFailed into a single SessionFailed flag since both are only checked together. Closes #5263 --- TUnit.Engine/Framework/TUnitServiceProvider.cs | 2 +- TUnit.Engine/Framework/TUnitTestFramework.cs | 11 +++++++---- TUnit.Engine/TestSessionCoordinator.cs | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 2799b12d17..92e6a5ac82 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -57,7 +57,7 @@ public ITestExecutionFilter? Filter public CancellationTokenSource FailFastCancellationSource { get; } public ParallelLimitLockProvider ParallelLimitLockProvider { get; } public ObjectLifecycleService ObjectLifecycleService { get; } - public bool AfterSessionHooksFailed { get; set; } + public bool SessionFailed { get; set; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT/trimmed scenarios")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Reflection mode is not used in AOT scenarios")] diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index f2a334056a..5a14afe948 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -84,9 +84,13 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) } catch (Exception e) { - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync(e); + var serviceProvider = GetOrCreateServiceProvider(context); + await serviceProvider.Logger.LogErrorAsync(e); await ReportUnhandledException(context, e); - throw; + + // Do NOT re-throw — propagating the exception crashes Rider's JSON-RPC + // channel and hides the actual error from the user (see #5263). + serviceProvider.SessionFailed = true; } finally { @@ -100,8 +104,7 @@ public async Task CloseTestSessionAsync(CloseTestSession if (_serviceProvidersPerSession.TryRemove(context.SessionUid.Value, out var serviceProvider)) { - // Check if After(TestSession) hooks failed - if (serviceProvider.AfterSessionHooksFailed) + if (serviceProvider.SessionFailed) { isSuccess = false; } diff --git a/TUnit.Engine/TestSessionCoordinator.cs b/TUnit.Engine/TestSessionCoordinator.cs index bcecb5e828..398378b947 100644 --- a/TUnit.Engine/TestSessionCoordinator.cs +++ b/TUnit.Engine/TestSessionCoordinator.cs @@ -99,10 +99,9 @@ private async Task ExecuteTestsCore(List testList, Cance // Schedule and execute tests (batch approach to preserve ExecutionContext) var success = await _testScheduler.ScheduleAndExecuteAsync(testList, linkedCts.Token); - // Track whether After(TestSession) hooks failed if (!success) { - _serviceProvider.AfterSessionHooksFailed = true; + _serviceProvider.SessionFailed = true; } } From 9724e5dcac117e869ee20e598442d5e196872d8f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:40:40 +0000 Subject: [PATCH 2/4] refactor: deduplicate GetOrCreateServiceProvider call in cancellation path --- TUnit.Engine/Framework/TUnitTestFramework.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index 5a14afe948..d05173d0d3 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -69,16 +69,10 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) } catch (Exception e) when (IsCancellationException(e)) { - // Check if this is a normal cancellation or fail-fast cancellation - if (context.CancellationToken.IsCancellationRequested) - { - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync("The test run was cancelled."); - } - else - { - // This is likely a fail-fast cancellation - await GetOrCreateServiceProvider(context).Logger.LogErrorAsync("Test execution stopped due to fail-fast."); - } + var message = context.CancellationToken.IsCancellationRequested + ? "The test run was cancelled." + : "Test execution stopped due to fail-fast."; + await GetOrCreateServiceProvider(context).Logger.LogErrorAsync(message); throw; } From 7b23ba9f66cdf16d5bda3ba136e0711447714d61 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:43:42 +0000 Subject: [PATCH 3/4] docs: clarify why cancellation path re-throws safely --- TUnit.Engine/Framework/TUnitTestFramework.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index d05173d0d3..f84628cacd 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -74,6 +74,7 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) : "Test execution stopped due to fail-fast."; await GetOrCreateServiceProvider(context).Logger.LogErrorAsync(message); + // Re-throw is safe here — MTP handles OperationCanceledException specially. throw; } catch (Exception e) From 287c0fc52e4ba35c36edf23a56a51d68b6b700c7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:45:46 +0000 Subject: [PATCH 4/4] docs: frame error-swallowing comment around MTP contract, not Rider --- TUnit.Engine/Framework/TUnitTestFramework.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index f84628cacd..c8f8acdd07 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -83,8 +83,8 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) await serviceProvider.Logger.LogErrorAsync(e); await ReportUnhandledException(context, e); - // Do NOT re-throw — propagating the exception crashes Rider's JSON-RPC - // channel and hides the actual error from the user (see #5263). + // Do NOT re-throw — MTP hosts expect errors via CloseTestSessionResult, + // not propagated exceptions. Re-throwing breaks JSON-RPC transports (#5263). serviceProvider.SessionFailed = true; } finally