### NUnit Cancellation Handler Usage Example Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Example of how to instantiate and call the NUnit cancellation handler. This should be invoked when a cancellation request is detected within the NUnit framework. ```csharp // Usage in your NUnit framework: public async Task OnCancellationRequested() { var handler = new NUnitCancellationHandler(); await handler.HandleNUnitCancellation(); } ``` -------------------------------- ### TestFX Process Factory Configuration Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Demonstrates how to configure and start a new process with custom environment variables and an exit event handler. It includes logic for clearing default environment variables if custom ones are provided. ```csharp public static IProcessHandle Start(ProcessConfiguration config, bool cleanDefaultEnvironmentVariableIfCustomAreProvided = false) { ProcessStartInfo processStartInfo = new() { FileName = fullPath, Arguments = config.Arguments, WorkingDirectory = workingDirectory, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, }; // Environment variable management if (config.EnvironmentVariables is not null) { if (cleanDefaultEnvironmentVariableIfCustomAreProvided) { processStartInfo.Environment.Clear(); processStartInfo.EnvironmentVariables.Clear(); } foreach (KeyValuePair kvp in config.EnvironmentVariables) { if (kvp.Value is null) { continue; } processStartInfo.EnvironmentVariables[kvp.Key] = kvp.Value; } } Process process = new() { StartInfo = processStartInfo, EnableRaisingEvents = true, // Important for exit event handling }; // Event handler setup before starting if (config.OnExit != null) { process.Exited += (_, _) => config.OnExit.Invoke(processHandle, process.ExitCode); } } ``` -------------------------------- ### Handle Test Cancellation by Forcing Session End and Process Kill Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md A complete example demonstrating how to handle cancellation requests by immediately forcing the test session to end, delaying for propagation, and then terminating the process tree. This is a critical part of preventing hangs during cancellation. ```csharp public async Task HandleCancellation() { _logger.LogWarning("Test cancellation requested - forcing session end"); // Step 1: Force session end immediately ForceTestSessionEnd(); // Step 2: Brief delay for message propagation await Task.Delay(150, CancellationToken.None); // Step 3: Kill process tree #if NET5_0_OR_GREATER _process.Kill(entireProcessTree: true); #else _process.Kill(); #endif // Step 4: Wait briefly, then force exit if needed var exitTask = Task.Run(async () => { await Task.Delay(2000); if (!_process.HasExited) { Environment.Exit(-1); } }); } ``` -------------------------------- ### Platform Session Event Handling in NUnit Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/HowToCancelWithNUnit.md Manages sending session start and end events to the .NET test connection. These events are crucial for the platform to track the test session's status. ```csharp public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) { TestSessionEvent sessionStartEvent = new( SessionEventTypes.TestSessionStart, testSessionContext.SessionUid.Value, ExecutionId); await _dotnetTestConnection.SendMessageAsync(sessionStartEvent).ConfigureAwait(false); } public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) { TestSessionEvent sessionEndEvent = new( SessionEventTypes.TestSessionEnd, testSessionContext.SessionUid.Value, ExecutionId); await _dotnetTestConnection.SendMessageAsync(sessionEndEvent).ConfigureAwait(false); } ``` -------------------------------- ### Correct Way to Report Test Results via Message Bus Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/HowToCancelWithNUnit.md This snippet demonstrates the intended pattern for running tests and reporting their status (started, completed, cancelled) using the message bus. It includes checks for cancellation requests from various sources before executing and reporting test results. Use this approach for NUnit bridge communication. ```csharp private void RunTestsCore(IEnumerable tests, IRunContext runContext, IFrameworkHandle frameworkHandle) { foreach (var testCase in tests) { // Check cancellation BEFORE starting any test work if (_cancellationRequested || runContext.CancellationToken.IsCancellationRequested || _internalCts.Token.IsCancellationRequested) { // For remaining tests, send cancelled results via message bus var cancelledTestNode = testCase.ToTestNode(); var cancelledResult = TestNodeUpdateMessage.TestNodeSkippedMessage(cancelledTestNode, "Test run was cancelled"); try { CurrentMessageBus?.PublishAsync(this, new TestNodeUpdateMessage(CurrentSessionUid, cancelledResult)); } catch (Exception ex) { // If message bus fails, log but continue - session cleanup will handle it Console.WriteLine($"Failed to send cancelled test result: {ex.Message}"); } continue; } try { // Send test started message var testNode = testCase.ToTestNode(); CurrentMessageBus?.PublishAsync(this, new TestNodeUpdateMessage(CurrentSessionUid, testNode.WithInProgressState())); var result = ExecuteTest(testCase); // Check cancellation before sending result if (_cancellationRequested || runContext.CancellationToken.IsCancellationRequested || _internalCts.Token.IsCancellationRequested) { // Send cancelled result and exit var cancelledResult = TestNodeUpdateMessage.TestNodeSkippedMessage(testNode, "Test execution was cancelled"); CurrentMessageBus?.PublishAsync(this, new TestNodeUpdateMessage(CurrentSessionUid, cancelledResult)); return; } // Send completed result via message bus var completedResult = TestNodeUpdateMessage.TestNodeResultMessage.From(result); CurrentMessageBus?.PublishAsync(this, new TestNodeUpdateMessage(CurrentSessionUid, completedResult)); } catch (OperationCanceledException) { // Just return cleanly - don't send more messages return; } } } ``` -------------------------------- ### Create a Package Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/README.md Execute this command to create a package for the adapter. ```bash .\build -t package ``` -------------------------------- ### Run Acceptance Tests via Command Line Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/src/NUnit.TestAdapter.Tests.Acceptance/README.md Execute this command to build and package the test adapter, then run the acceptance tests. ```cmd build -t acceptance ``` -------------------------------- ### Get NUnit IDataProducer via Static Instance Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md A simple method to retrieve the NUnit IDataProducer by accessing the static CurrentInstance property of NUnitBridgedTestFramework. ```csharp private IDataProducer? GetNUnitDataProducer() { return NUnitBridgedTestFramework.CurrentInstance; } ``` -------------------------------- ### Build the Solution Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/README.md Run this command in the project directory to compile the solution. ```bash .\build ``` -------------------------------- ### Get NUnit IDataProducer via Service Provider Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This method attempts to retrieve the NUnitBridgedTestFramework instance from a service provider, assuming it has been registered with dependency injection. The retrieved framework instance is then used as the IDataProducer. ```csharp private IDataProducer? GetNUnitDataProducer() { try { // Try to get the NUnit framework from service provider var nunitFramework = _serviceProvider?.GetService(); return nunitFramework; // NUnitBridgedTestFramework IS an IDataProducer } catch (Exception ex) { Console.Error.WriteLine($"Failed to get NUnit framework from service provider: {ex.Message}"); return null; } } ``` -------------------------------- ### Build and Run Unit Tests Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/README.md Use this command to build the solution and execute unit tests. ```bash .\build -t test ``` -------------------------------- ### MTP Session Manager Cleanup in NUnit Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Provides examples of explicit cleanup for MTP session managers and related properties. Use this to ensure that session-related resources and states are properly reset in your MTP implementation. ```csharp // Look for something like this in your MTP implementation _testSessionManager?.EndSession(_sessionUid, TestSessionResult.Cancelled); _testExecutionContext?.Complete(); _platformServices?.Shutdown(); // Or check for session state properties that need manual reset if (_mtpSession != null) { _mtpSession.State = TestSessionState.Cancelled; _mtpSession.EndTime = DateTime.UtcNow; _mtpSession = null; } ``` -------------------------------- ### Clean RunTests Methods in NUnit3TestExecutor Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/PlanCancel.md Modify both RunTests methods to check the cancellation flag before starting execution and return cleanly if cancellation is detected. This ensures that test execution stops promptly upon cancellation. ```csharp public void RunTests(IEnumerable sources, IRunContext runContext, IFrameworkHandle frameworkHandle) { try { InitializeForExecution(runContext, frameworkHandle); // Check cancellation before starting if (_cancelled) { TestLog.Debug("Cancellation detected before test execution"); return; // Clean return - let bridge handle session end } RunAssemblies(sources, filter); TestLog.Debug("Test execution completed normally"); } catch (OperationCanceledException) { TestLog.Debug("Test execution cancelled - propagating to bridge"); throw; // Let it propagate to bridge } finally { // Simple cleanup only - no session management Unload(); } } ``` -------------------------------- ### Build and Run Acceptance Tests Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/README.md This command builds the solution and runs acceptance tests. ```bash .\build -t acceptance ``` -------------------------------- ### Complete Tests Before Handle Cancellation in C# Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Demonstrates how to complete all running tests immediately when cancellation is detected, using the still-valid FrameworkHandle to record results. ```csharp public class NUnitBridge : ITestApplication { private readonly IFrameworkHandle _frameworkHandle; private readonly IMessageLogger _messageLogger; private readonly List _runningTests = new(); private volatile bool _cancellationRequested = false; public async Task OnCancellationRequested(CancellationToken cancellationToken) { if (_cancellationRequested) return; _cancellationRequested = true; _messageLogger.SendMessage(TestMessageLevel.Warning, "Cancellation requested - completing all running tests immediately"); // CRITICAL: Complete tests NOW, while FrameworkHandle is still valid await CompleteAllRunningTestsImmediately(); // THEN send session end await SendSessionEndEvent(); // FINALLY terminate process await TerminateProcess(); } private async Task CompleteAllRunningTestsImmediately() { var completionTasks = new List(); foreach (var runningTest in _runningTests.ToList()) { completionTasks.Add(Task.Run(() => { try { // Use the STILL-VALID FrameworkHandle to complete the test var testResult = new TestResult(runningTest.TestCase) { Outcome = TestOutcome.Skipped, ErrorMessage = "Test cancelled due to parallel execution timeout" }; _frameworkHandle.RecordResult(testResult); _frameworkHandle.RecordEnd(runningTest.TestCase, TestOutcome.Skipped); _messageLogger.SendMessage(TestMessageLevel.Informational, $"Completed cancelled test: {runningTest.TestCase.DisplayName}"); } catch (Exception ex) { _messageLogger.SendMessage(TestMessageLevel.Error, $"Failed to complete test {runningTest.TestCase.DisplayName}: {ex.Message}"); } })); } // Wait for all completions with timeout try { await Task.WhenAll(completionTasks).WaitAsync(TimeSpan.FromSeconds(5)); _messageLogger.SendMessage(TestMessageLevel.Informational, $"Successfully completed {completionTasks.Count} running tests"); } catch (TimeoutException) { _messageLogger.SendMessage(TestMessageLevel.Warning, "Timeout while completing running tests - some may remain incomplete"); } _runningTests.Clear(); } } ``` -------------------------------- ### Unified Handle Cancellation Entry Point Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md A unified entry point that dispatches to the appropriate framework-specific cancellation handling method based on the target .NET version. ```csharp public async Task HandleCancellationForWorkspace() { #if NET9_0 await HandleCancellationNet9(); #elif NET8_0 await HandleCancellationNet8(); #elif NET11_0 await HandleCancellationNet11(); #elif NET5_0 await HandleCancellationNet5(); #elif NET48 HandleCancellationNet48(); #elif NET472 HandleCancellationNet472(); #elif NET462 HandleCancellationNet462(); #elif NETSTANDARD2_0 await HandleCancellationNetStandard(); #else // Fallback for any other framework _process?.Kill(); Environment.Exit(-1); #endif } ``` -------------------------------- ### NUnit Cancellation Handler Implementation Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This C# class demonstrates a robust strategy for handling NUnit test session cancellations. It ensures all running tests are completed with correct messages and then exits the process with a success code (0) to prevent the 'InvalidOperationException: A test session start event was received without a corresponding test session end' error. It includes logic for framework-specific delays and test outcome reporting. ```csharp public class NUnitCancellationHandler { private readonly IMessageBus _messageBus; private readonly SessionUid _sessionUid; private readonly List _runningTests; private bool _sessionEnded = false; public async Task HandleNUnitCancellation() { if (_sessionEnded) return; Console.WriteLine($"[NUNIT-CANCEL] Starting cancellation for {_runningTests.Count} running tests"); // Get NUnit framework as IDataProducer IDataProducer? dataProducer = NUnitBridgedTestFramework.CurrentInstance; if (dataProducer == null) { Console.Error.WriteLine("[ERROR] Cannot get NUnit IDataProducer - using fallback"); Environment.Exit(-1); return; } try { // Step 1: Complete all running tests (THIS IS WORKING - 28/28 successful!) await CompleteAllRunningTestsWithCorrectMessages(dataProducer); Console.WriteLine("[SUCCESS] All tests completed and reported to MTP"); // Step 2: Framework-specific delay for message propagation await Task.Delay(GetFrameworkSpecificDelay(), CancellationToken.None); Console.WriteLine("[SUCCESS] NUnit cancellation completed successfully"); } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] NUnit cancellation failed: {ex.Message}"); Environment.Exit(-1); return; } // Step 3: SUCCESS EXIT - prevents the InvalidOperationException Console.WriteLine("[EXIT] Clean exit - all tests completed successfully"); Environment.Exit(0); // Success exit code tells MTP everything worked properly } private TimeSpan GetFrameworkSpecificDelay() { #if NET9_0 || NET8_0 || NET11_0 return TimeSpan.FromMilliseconds(200); // Modern .NET - fast message processing #elif NET5_0 return TimeSpan.FromMilliseconds(250); // .NET 5 - standard delay #elif NET48 || NET472 || NET462 return TimeSpan.FromMilliseconds(300); // .NET Framework needs more time #elif NETSTANDARD2_0 return TimeSpan.FromMilliseconds(250); // .NET Standard - cross-platform #else return TimeSpan.FromMilliseconds(200); // Default fallback #endif } private async Task CompleteAllRunningTestsWithCorrectMessages(IDataProducer dataProducer) { Console.WriteLine($"Completing {_runningTests.Count} running tests..."); foreach (var runningTest in _runningTests.ToList()) { try { // Create proper TestNode var testNode = new TestNode { Uid = new TestNodeUid(runningTest.TestCase.Id.ToString()), DisplayName = runningTest.TestCase.DisplayName, Properties = new PropertyBag() }; // Add framework-specific completion property testNode.Properties.Add(new TestResultProperty( #if NET9_0 || NET8_0 || NET11_0 TestOutcome.Skipped, #elif NET5_0 TestOutcome.Skipped, #elif NET48 || NET472 || NET462 TestOutcome.None, // .NET Framework compatibility #elif NETSTANDARD2_0 TestOutcome.Skipped, #else TestOutcome.None, #endif "Test cancelled due to parallel execution timeout")); // Use proper IData message type var testCompletionMessage = new TestNodeUpdateMessage(_sessionUid, testNode); await _messageBus.PublishAsync(dataProducer, testCompletionMessage); Console.WriteLine($"[COMPLETED] {runningTest.TestCase.DisplayName}"); } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] Failed to complete test {runningTest.TestCase.DisplayName}: {ex.Message}"); } } _runningTests.Clear(); Console.WriteLine($"✅ All tests completed and cleared from tracking"); } } ``` -------------------------------- ### Immediate Process Kill Pattern Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Use this pattern when any cleanup attempt might cause hanging. It skips all cleanup and kills the process immediately. ```csharp public async Task EmergencyStop() { _logger.LogWarning("Emergency stop initiated - skipping all cleanup"); try { // Skip ALL cleanup - just kill the process immediately #if NET5_0_OR_GREATER _process.Kill(entireProcessTree: true); // Wait maximum 3 seconds for process death using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await _process.WaitForExitAsync(cts.Token); #else _process.Kill(); if (!_process.WaitForExit(3000)) { // Process still alive after 3 seconds - force app exit Environment.Exit(-1); } #endif return _process.ExitCode; } catch (Exception ex) { _logger.LogError("Emergency stop failed: {Exception}", ex); // Last resort - terminate entire application Environment.Exit(-1); return -1; // Never reached } } ``` -------------------------------- ### Problem Flow of Failed Test Completions Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Illustrates the sequence of events leading to failed test completions when cancellation is requested and the FrameworkHandle is disposed. ```text ❌ Problem Flow: 1. Cancellation requested 2. FrameworkHandle gets cancelled/disposed 3. Attempts to send test completion events FAIL silently 4. MTP still thinks tests are running (e.g., 28 running tests) 5. Session can't end because "tests are still running" ``` -------------------------------- ### Fix TestNodeUpdateMessage Constructor Issues Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Demonstrates alternative constructors for TestNodeUpdateMessage when the primary one fails. Includes options for a three-parameter constructor, using a TestNodeUpdate wrapper, and falling back to reflection. ```csharp // ❌ This might be failing: var testNodeUpdateMessage = new TestNodeUpdateMessage(_sessionUid, testNode); // ✅ Try these alternatives: try { // Option 1: Three-parameter constructor var testNodeUpdateMessage = new TestNodeUpdateMessage(_sessionUid, testNode, TestNodeUpdate.Empty); await _messageBus.PublishAsync(dataProducer, testNodeUpdateMessage); } catch (Exception ex) { Console.Error.WriteLine($"Three-parameter constructor failed: {ex.Message}"); // Option 2: Use TestNodeUpdate wrapper try { var testNodeUpdate = new TestNodeUpdate { Node = testNode, Properties = testNode.Properties ?? new PropertyBag() }; var testNodeUpdateMessage = new TestNodeUpdateMessage(_sessionUid, testNodeUpdate); await _messageBus.PublishAsync(dataProducer, testNodeUpdateMessage); } catch (Exception ex2) { Console.Error.WriteLine($"TestNodeUpdate wrapper failed: {ex2.Message}"); // Option 3: Use reflection to find correct constructor await CreateMessageViaReflection(dataProducer, testNode); } } ``` -------------------------------- ### Correct IDataProducer for PublishAsync Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This code snippet demonstrates the correct usage of `PublishAsync` by showing the `IDataProducer` parameter. It clarifies that the `VSTestBridgedTestFrameworkBase` instance, which implements `IDataProducer`, should be used instead of `FrameworkHandlerAdapter`. ```csharp _messageBus.PublishAsync(_adapterExtensionBase, testNodeChange).Await(); ``` -------------------------------- ### Configure Bridge Timeouts and Kill Options Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Defines configurable settings for graceful shutdown timeouts, thread cleanup, process tree killing, and logging of forced terminations. ```csharp public class BridgeConfiguration { public TimeSpan GracefulShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30); public TimeSpan ThreadCleanupTimeout { get; set; } = TimeSpan.FromSeconds(10); public bool EnableProcessTreeKill { get; set; } = true; public bool LogForcedTerminations { get; set; } = true; } ``` -------------------------------- ### Implement Background Termination Monitor Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Use a timer to forcefully terminate a process if cleanup exceeds a specified duration. This is a nuclear option for unresponsive cleanup. ```csharp private Timer? _terminationTimer; public async Task StopAsync(CancellationToken cancellationToken = default) { // Start absolute termination timer (nuclear option) _terminationTimer = new Timer(ForceTerminate, null, TimeSpan.FromSeconds(10), Timeout.InfiniteTimeSpan); try { // Try graceful cleanup with very short timeout using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await AttemptGracefulCleanup(cts.Token); // If we get here, cancel the nuclear timer _terminationTimer?.Dispose(); return await WaitForProcessExit(cancellationToken); } catch (Exception) { // Graceful cleanup failed - let nuclear timer handle it return -1; } } ``` ```csharp private void ForceTerminate(object? state) { try { _logger.LogWarning("Triggering nuclear process termination"); #if NET5_0_OR_GREATER _process?.Kill(entireProcessTree: true); #else _process?.Kill(); #endif // Also force exit the entire application if needed if (_process?.HasExited == false) { Task.Run(async () => { await Task.Delay(2000); // Give it 2 seconds if (_process?.HasExited == false) { Environment.Exit(-1); // Nuclear option } }); } } catch (Exception ex) { _logger.LogError("Nuclear termination failed: {Exception}", ex); Environment.Exit(-2); // Ultimate fallback } finally { _terminationTimer?.Dispose(); } } ``` -------------------------------- ### Process Killing Fallback for .NET Framework Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Provides a fallback method for killing a process in .NET Framework, using a simpler approach without process tree killing. Includes error handling. ```csharp #else // .NET Framework fallback private bool TryKillProcessTree(TimeSpan timeout) { try { _process.Kill(); return _process.WaitForExit((int)timeout.TotalMilliseconds); } catch { return false; } } #endif ``` -------------------------------- ### Base Class Session Management in NUnit Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/HowToCancelWithNUnit.md Handles the creation and closing of test sessions. Ensures a session is created only once and waits for pending requests during session closing, respecting cancellation tokens. ```csharp public sealed override Task CreateTestSessionAsync(CreateTestSessionContext context) { if (_sessionUid is not null) { throw new InvalidOperationException("Session already created"); } _sessionUid = context.SessionUid; return Task.FromResult(new CreateTestSessionResult { IsSuccess = true }); } public sealed override async Task CloseTestSessionAsync(CloseTestSessionContext context) { // Clear initial count _incomingRequestCounter.Signal(); // Wait for remaining request processing (handles cancellation) await _incomingRequestCounter.WaitAsync(context.CancellationToken).ConfigureAwait(false); _sessionUid = null; return new CloseTestSessionResult { IsSuccess = true }; } ``` -------------------------------- ### Simplify Test Completion Handling Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/PlanCancel.md Replaces the existing `HandleTestCompletionWithNuclearOption` method with a simplified version that focuses on resource cleanup and marking the session as inactive. It logs completion status and handles exceptions during cleanup. ```csharp private async Task HandleTestCompletion(ITestExecutor executor, bool wasCancelled) { try { // Simple resource cleanup if (executor is IDisposable disposable) { disposable.Dispose(); } // Mark session as inactive (but don't send messages - platform handles it) _testSessionActive = false; var nunitExecutor = executor as NUnit3TestExecutor; nunitExecutor?.LogToDump("TestCompletion", $ ``` ```csharp Test completion handled - wasCancelled: {wasCancelled}"); } catch (Exception ex) { var nunitExecutor = executor as NUnit3TestExecutor; nunitExecutor?.LogToDump("TestCompletion", $"Error in test completion: {ex.Message}"); } finally { // Clean up references - let platform handle session lifecycle CurrentMessageBus = null; CurrentSessionUid = null; _currentMessageBus = null; } // CRITICAL: Return normally - let CloseTestSessionAsync handle session end } ``` -------------------------------- ### NUnit Fire-and-Forget Session Cleanup with Timeouts Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This strategy implements fire-and-forget cleanup with absolute timeouts for unresponsive scenarios. It attempts session cleanup and proceeds with force termination if cleanup times out. ```csharp public async Task StopAsync(CancellationToken cancellationToken = default) { // Step 1: Attempt fire-and-forget session cleanup var cleanupTask = FireAndForgetSessionCleanup(); var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); var completedTask = await Task.WhenAny(cleanupTask, timeoutTask); if (completedTask == timeoutTask) { _logger.LogWarning("Session cleanup timed out - proceeding with force termination"); } // Step 2: Force kill regardless of cleanup status return await ForceTerminateProcess(); } ``` ```csharp private async Task FireAndForgetSessionCleanup() { try { // Use very short timeouts for all operations using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); // Fire session end event but don't wait for confirmation _ = Task.Run(async () => { try { if (_testSessionActive) { await SendTestSessionEndEvent(TestSessionResult.Cancelled); } } catch (Exception ex) { _logger.LogWarning("Session end event failed: {Exception}", ex); } }, cts.Token); // Brief delay to let the event fire await Task.Delay(100, cts.Token); // Force message bus completion with timeout if (_messageBus != null) { var busTask = _messageBus.CompleteAsync(cts.Token); var timeoutTask = Task.Delay(TimeSpan.FromSeconds(1), cts.Token); await Task.WhenAny(busTask, timeoutTask); } } catch (Exception ex) { _logger.LogWarning("Fire-and-forget cleanup failed: {Exception}", ex); } } ``` -------------------------------- ### Create TestNodeUpdate Instance Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This helper method attempts to create an instance of a TestNodeUpdate type and set its 'Node' and 'Properties' properties. It's used when constructing messages that require a specific TestNodeUpdate object. ```csharp private object? CreateTestNodeUpdate(Type testNodeUpdateType, TestNode testNode) { try { // Try to create TestNodeUpdate instance var testNodeUpdate = Activator.CreateInstance(testNodeUpdateType); if (testNodeUpdate == null) return null; // Try to set Node property var nodeProperty = testNodeUpdateType.GetProperty("Node"); nodeProperty?.SetValue(testNodeUpdate, testNode); // Try to set Properties if it exists var propertiesProperty = testNodeUpdateType.GetProperty("Properties"); propertiesProperty?.SetValue(testNodeUpdate, testNode.Properties); return testNodeUpdate; } catch (Exception ex) { Console.Error.WriteLine($"Failed to create TestNodeUpdate: {ex.Message}"); return null; } } ``` -------------------------------- ### Process Tree Killing for .NET 5+ Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Implements a method to kill the process and its entire tree with aggressive timeouts for .NET 5 and later. Includes error handling for cleanup operations. ```csharp #if NET5_0_OR_GREATER private async Task TryKillProcessTree(TimeSpan timeout) { try { _process.Kill(entireProcessTree: true); using var cts = new CancellationTokenSource(timeout); await _process.WaitForExitAsync(cts.Token); return true; } catch { return false; } } #endif ``` -------------------------------- ### Handle Cancellation for .NET Framework 4.7.2 Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Implements process killing and waiting for exit with manual garbage collection for older .NET Framework versions. ```csharp private void HandleCancellationNet472() { try { _process.Kill(); _process.WaitForExit(3000); } catch (InvalidOperationException) { // Process already exited } // Manual cleanup for older framework GC.Collect(); GC.WaitForPendingFinalizers(); } ``` -------------------------------- ### Handle Cancellation for .NET 8 Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Implements cancellation for .NET 8 using modern async patterns. It kills the process tree and waits for exit, with a fallback to Environment.Exit(-1) upon cancellation. ```csharp #elif NET8_0 // .NET 8 - Modern async with compatibility considerations private async Task HandleCancellationNet8() { try { _process.Kill(entireProcessTree: true); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await _process.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { Environment.Exit(-1); } } #endif ``` -------------------------------- ### Framework-Specific Constructor for .NET Framework Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Attempts to use a simpler constructor for `TestNodeUpdateMessage`, which may be applicable to older .NET Framework versions. This is a fallback if other methods fail. ```csharp private async Task TryFrameworkSpecificMessage(IDataProducer dataProducer, TestNode testNode, string testName) { try { // .NET Framework might use simpler constructor var message = new TestNodeUpdateMessage(_sessionUid, testNode); await _messageBus.PublishAsync(dataProducer, message); Console.WriteLine($"✅ Framework-specific constructor worked for: {testName}"); return true; } catch (Exception ex) { Console.Error.WriteLine($"Framework-specific approach failed: {ex.Message}"); return false; } } ``` -------------------------------- ### Handle Cancellation for .NET 11 (Future) Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Placeholder for future .NET 11 cancellation patterns. Assumes the use of the latest available APIs for process killing and waiting for exit. ```csharp #elif NET11_0 // .NET 11 - Future-compatible patterns (if applicable) private async Task HandleCancellationNet11() { // Use latest available APIs await _process.Kill(entireProcessTree: true); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); await _process.WaitForExitAsync(cts.Token); } #endif ``` -------------------------------- ### Conditional Async Patterns for .NET Versions Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Use modern async patterns for .NET 5+ and Task-based async for older frameworks to manage session lifecycle. ```csharp #if NET5_0_OR_GREATER // Use modern async patterns await _messageBus.CompleteAsync(cancellationToken); #else // Use Task-based async for older frameworks await _messageBus.CompleteAsync(cancellationToken).ConfigureAwait(false); #endif ``` -------------------------------- ### Framework-Specific Message Creation with Conditional Compilation Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Selects the appropriate message creation strategy based on the target .NET framework version using conditional compilation. This ensures compatibility across different .NET runtimes. ```csharp private async Task CreateMessageForSpecificFramework(IDataProducer dataProducer, TestNode testNode, string testName) { #if NET9_0 || NET8_0 || NET11_0 // Modern .NET - try newer API patterns first return await CreateTestNodeUpdateMessageWithReflection(dataProducer, testNode, testName) || await TryAlternativeMessageTypes(dataProducer, testNode, testName); #elif NET5_0 // .NET 5 - try standard approach return await CreateTestNodeUpdateMessageWithReflection(dataProducer, testNode, testName); #elif NET48 || NET472 || NET462 // .NET Framework - might need different approach return await TryFrameworkSpecificMessage(dataProducer, testNode, testName) || await CreateTestNodeUpdateMessageWithReflection(dataProducer, testNode, testName); #elif NETSTANDARD2_0 // .NET Standard - cross-platform compatible return await CreateTestNodeUpdateMessageWithReflection(dataProducer, testNode, testName); #else // Fallback return await CreateTestNodeUpdateMessageWithReflection(dataProducer, testNode, testName); #endif } ``` -------------------------------- ### Handle Cancellation for .NET Standard 2.0 Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Uses a cross-platform compatible approach with task-based waiting for .NET Standard environments. ```csharp private async Task HandleCancellationNetStandard() { try { // Use cross-platform compatible approach _process.Kill(); // Task-based waiting for .NET Standard var waitTask = Task.Run(() => { try { _process.WaitForExit(); } catch (InvalidOperationException) { // Process already exited } }); await Task.WhenAny(waitTask, Task.Delay(5000)); } catch (Exception) { Environment.Exit(-1); } } ``` -------------------------------- ### Simplified NUnit Cancellation with Process Exit Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md When tests complete successfully, this simplified approach is recommended. It completes all running tests and then forces a process exit with code 0. ```csharp private async Task HandleNUnitCancellationSimplified() { Console.WriteLine($"[NUNIT-CANCEL] Starting cancellation for {_runningTests.Count} running tests"); // Get NUnit framework as IDataProducer IDataProducer? dataProducer = NUnitBridgedTestFramework.CurrentInstance; if (dataProducer == null) { Environment.Exit(-1); return; } // Complete all tests (THIS IS WORKING!) await CompleteAllRunningTestsWithCorrectMessages(dataProducer); // Brief delay for propagation await Task.Delay(300, CancellationToken.None); Console.WriteLine("[SUCCESS] All tests completed - forcing exit"); Environment.Exit(0); // Success exit since tests completed } ``` -------------------------------- ### NUnit Aggressive Termination Configuration Options Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Defines configuration settings for controlling the aggressiveness of test process termination, including timeouts and fallback mechanisms. ```csharp public class BridgeConfiguration { public TimeSpan GracefulShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan ThreadCleanupTimeout { get; set; } = TimeSpan.FromSeconds(2); public bool EnableProcessTreeKill { get; set; } = true; public bool LogForcedTerminations { get; set; } = true; // New aggressive options public bool EnableNuclearTermination { get; set; } = true; public TimeSpan NuclearTimeoutSeconds { get; set; } = TimeSpan.FromSeconds(10); public bool AllowEnvironmentExit { get; set; } = true; public bool SkipSessionCleanupOnTimeout { get; set; } = true; } ``` -------------------------------- ### Complete Running Tests with Standard and Alternative Messages Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This method iterates through running tests, attempting to complete them using a standard TestNodeUpdateMessage. If the standard approach fails, it falls back to alternative message types. It includes error handling for each step. ```csharp private async Task CompleteAllRunningTestsWithCorrectMessages(IDataProducer dataProducer) { foreach (var runningTest in _runningTests.ToList()) { try { // Create proper TestNode var testNode = new TestNode { Uid = new TestNodeUid(runningTest.TestCase.Id.ToString()), DisplayName = runningTest.TestCase.DisplayName, Properties = new PropertyBag() }; Console.WriteLine($"Created TestNode for: {runningTest.TestCase.DisplayName}"); // Try multiple approaches to complete the test bool completed = false; // Approach 1: Standard TestNodeUpdateMessage try { var testCompletionMessage = new TestNodeUpdateMessage(_sessionUid, testNode); if (testCompletionMessage != null) { await _messageBus.PublishAsync(dataProducer, testCompletionMessage); Console.WriteLine($"✅ [STANDARD] {runningTest.TestCase.DisplayName}"); completed = true; } } catch (Exception ex) { Console.Error.WriteLine($"Standard approach failed: {ex.Message}"); } // Approach 2: Alternative message types if (!completed) { await CompleteTestWithAlternativeMessages(dataProducer, testNode, runningTest.TestCase.DisplayName); completed = true; // Assume it worked if no exception } } catch (Exception ex) { Console.Error.WriteLine($"[ERROR] Failed to complete test {runningTest.TestCase.DisplayName}: {ex.Message}"); } } _runningTests.Clear(); } ``` -------------------------------- ### Integrate Graceful Shutdown with Forced Shutdown Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Implements a stop asynchronous method that first attempts a graceful session end. If graceful end fails, it proceeds with forced session completion and then process termination. ```csharp public async Task StopAsync(CancellationToken cancellationToken = default) { // Step 1: Complete test session gracefully if (await TryGracefulSessionEnd(TimeSpan.FromSeconds(10))) { return await base.StopAsync(cancellationToken); } // Step 2: Force session completion if graceful failed await HandleForcedShutdownAsync(cancellationToken); // Step 3: Continue with process termination return await base.StopAsync(cancellationToken); } ``` -------------------------------- ### Complete Tests via Direct Message Bus Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Accesses the MTP message bus directly to complete tests if the FrameworkHandle is unavailable. Logs an error if direct message bus access is not configured. ```csharp // For Microsoft TestFX codebase - access the underlying message bus private async Task CompleteTestsViaMessageBus() { if (_messageBus == null) { _messageLogger.SendMessage(TestMessageLevel.Error, "No direct message bus access available"); return; } foreach (var runningTest in _runningTests.ToList()) { try { var testNodeUpdate = new TestNodeUpdateMessage( _sessionUid, new TestNodeUpdate { Node = runningTest.TestNode, Property = new TestResultProperty( TestOutcome.Skipped, "Test cancelled - completed via direct message bus") }); await _messageBus.PublishAsync(testNodeUpdate); } catch (Exception ex) { _messageLogger.SendMessage(TestMessageLevel.Error, $"Failed to complete test via message bus: {ex.Message}"); } } } ``` -------------------------------- ### Safely Complete Test with FrameworkHandle Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Completes a running test, marking it as skipped due to cancellation, if the FrameworkHandle is usable. Logs an error if the handle is cancelled or disposed. ```csharp private async Task CompleteTestSafely(RunningTestInfo runningTest) { if (!IsFrameworkHandleUsable()) { _messageLogger.SendMessage(TestMessageLevel.Error, "Cannot complete test - FrameworkHandle is cancelled/disposed"); return; } try { var testResult = new TestResult(runningTest.TestCase) { Outcome = TestOutcome.Skipped, ErrorMessage = "Test cancelled due to timeout", EndTime = DateTime.UtcNow }; _frameworkHandle.RecordResult(testResult); _frameworkHandle.RecordEnd(runningTest.TestCase, TestOutcome.Skipped); } catch (InvalidOperationException ex) when (ex.Message.Contains("cancel")) { _messageLogger.SendMessage(TestMessageLevel.Error, $"FrameworkHandle was cancelled while completing test: {ex.Message}"); } } ``` -------------------------------- ### Log FrameworkHandle State Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Logs the current state of the FrameworkHandle by attempting to send an informational message. Differentiates between a responsive handle and an unusable one based on exceptions. ```csharp private void LogFrameworkHandleState() { try { _frameworkHandle.SendMessage(TestMessageLevel.Informational, "Handle state check"); _messageLogger.SendMessage(TestMessageLevel.Informational, "FrameworkHandle is responsive and usable"); } catch (Exception ex) { _messageLogger.SendMessage(TestMessageLevel.Error, $"FrameworkHandle is not usable: {ex.Message} (Type: {ex.GetType().Name})"); } } ``` -------------------------------- ### Proper RunTests() Method Implementation in NUnit Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/HowToCancelWithNUnit.md Implement RunTests() methods to frequently check for cancellation and return cleanly. This includes checking multiple cancellation sources like a volatile flag, run context, and an internal CancellationTokenSource. OperationCanceledExceptions should propagate cleanly. ```csharp private volatile bool _cancellationRequested; private readonly CancellationTokenSource _internalCts = new(); public void RunTests(IEnumerable sources, IRunContext runContext, IFrameworkHandle frameworkHandle) { try { var tests = DiscoverTests(sources); RunTestsCore(tests, runContext, frameworkHandle); } catch (OperationCanceledException) { // Don't log or handle - just let it propagate cleanly throw; } // CRITICAL: Don't catch other exceptions that might prevent proper session cleanup } public void RunTests(IEnumerable tests, IRunContext runContext, IFrameworkHandle frameworkHandle) { try { RunTestsCore(tests, runContext, frameworkHandle); } catch (OperationCanceledException) { // Let cancellation propagate cleanly to the bridge throw; } } private void RunTestsCore(IEnumerable tests, IRunContext runContext, IFrameworkHandle frameworkHandle) { foreach (var testCase in tests) { // Check multiple cancellation sources if (_cancellationRequested || runContext.CancellationToken.IsCancellationRequested || _internalCts.Token.IsCancellationRequested) { // Report remaining tests as skipped and return cleanly frameworkHandle.RecordResult(new TestResult(testCase) { Outcome = TestOutcome.Skipped, ErrorMessage = "Test run was cancelled" }); continue; } try { var result = ExecuteTest(testCase); frameworkHandle.RecordResult(result); } catch (OperationCanceledException) { frameworkHandle.RecordResult(new TestResult(testCase) { Outcome = TestOutcome.Skipped, ErrorMessage = "Test execution was cancelled" }); throw; // Propagate to exit the loop and method } } // CRITICAL: Return normally when done - don't throw or exit abnormally } ``` -------------------------------- ### Handle Cancellation for .NET Framework 4.6.2 Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Provides a simple kill and wait mechanism for the oldest supported .NET Framework, with broad exception handling and fallback exit codes. ```csharp private void HandleCancellationNet462() { try { // Simple kill for oldest supported framework _process.Kill(); // Use older WaitForExit overload if (!_process.WaitForExit(3000)) { // Fallback for very old framework Environment.Exit(-1); } } catch (Exception ex) { // Broad exception handling for compatibility Console.Error.WriteLine($"Process termination failed: {ex}"); Environment.Exit(-2); } } ``` -------------------------------- ### Handle Cancellation for .NET 9 Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md Implements cancellation for .NET 9 using enhanced process handling. It attempts to kill the entire process tree and wait for exit, falling back to Environment.Exit(-1) if the operation is cancelled. ```csharp #if NET9_0 // .NET 9 - Latest async patterns with enhanced process tree killing private async Task HandleCancellationNet9() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); try { // .NET 9 enhanced process handling await _process.Kill(entireProcessTree: true); await _process.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { // .NET 9 pattern: immediate Environment.Exit fallback Environment.Exit(-1); } } #endif ``` -------------------------------- ### Handle NUnit Cancellation by Passing Framework Instance Source: https://github.com/nunit/nunit3-vs-adapter/blob/main/docs/NUnitCancel.md This approach involves passing the NUnitBridgedTestFramework instance directly to the cancellation handler. The framework instance is then used as the IDataProducer. ```csharp // In your NUnit cancellation handler public async Task HandleCancellation(NUnitBridgedTestFramework framework) { // Use the framework directly as IDataProducer IDataProducer dataProducer = framework; if (dataProducer != null && _messageBus != null) { await CompleteTestsViaMessageBus(dataProducer); } } private async Task CompleteTestsViaMessageBus(IDataProducer dataProducer) { foreach (var runningTest in _runningTests.ToList()) { try { // Create TestNode and message as before... var testNodeUpdateMessage = new TestNodeUpdateMessage(_sessionUid, testNode); // Use the NUnitBridgedTestFramework instance as IDataProducer await _messageBus.PublishAsync(dataProducer, testNodeUpdateMessage); Console.WriteLine($"Completed test via NUnit framework: {runningTest.TestCase.DisplayName}"); } catch (Exception ex) { Console.Error.WriteLine($"Failed to complete test: {ex.Message}"); } } } ```