diff --git a/samples/Playground/Program.cs b/samples/Playground/Program.cs index 35fee852f9..b64cce2d2c 100644 --- a/samples/Playground/Program.cs +++ b/samples/Playground/Program.cs @@ -26,7 +26,7 @@ public static async Task Main(string[] args) { #if NETCOREAPP // To attach to the children - Microsoft.Testing.TestInfrastructure.DebuggerUtility.AttachCurrentProcessToParentVSProcess(); + // Microsoft.Testing.TestInfrastructure.DebuggerUtility.AttachCurrentProcessToParentVSProcess(); #endif ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs index f44eb1e82b..6299b37188 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs @@ -58,7 +58,9 @@ public async Task ExecuteAsync(ITestFramework testFramework, ClientInfo client, IMessageBus messageBus = ServiceProvider.GetMessageBus(); // Execute the test request - await ExecuteRequestAsync(testFramework, request, messageBus, cancellationToken); + var taskCompletionSource = new TaskCompletionSource(); + using CancellationTokenRegistration r = cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()); + await Task.WhenAny(ExecuteRequestAsync(testFramework, request, messageBus, cancellationToken), taskCompletionSource.Task); CloseTestSessionResult closeTestSessionResult = await testFramework.CloseTestSessionAsync(new(sessionId, client, cancellationToken)); await HandleTestSessionResultAsync(closeTestSessionResult.IsSuccess, closeTestSessionResult.WarningMessage, closeTestSessionResult.ErrorMessage); @@ -68,6 +70,8 @@ public async Task ExecuteAsync(ITestFramework testFramework, ClientInfo client, public virtual async Task ExecuteRequestAsync(ITestFramework testFramework, TestExecutionRequest request, IMessageBus messageBus, CancellationToken cancellationToken) { + await Task.Yield(); + using SemaphoreSlim requestSemaphore = new(0, 1); await testFramework.ExecuteRequestAsync(new(request, messageBus, new SemaphoreSlimRequestCompleteNotifier(requestSemaphore), cancellationToken)); await requestSemaphore.WaitAsync(cancellationToken); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs index d2fd2c24e5..11cc9b011c 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/AbortionTests.cs @@ -14,7 +14,15 @@ public sealed class AbortionTests : AcceptanceTestBase await AbortWithCTRLPlusC_CancellingTests(tfm, parallelize: true); + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + public async Task AbortWithCTRLPlusC_CancellingNonParallelTests(string tfm) + => await AbortWithCTRLPlusC_CancellingTests(tfm, parallelize: false); + + internal async Task AbortWithCTRLPlusC_CancellingTests(string tfm, bool parallelize) { // We expect the same semantic for Linux, the test setup is not cross and we're using specific // Windows API because this gesture is not easy xplat. @@ -25,17 +33,45 @@ public async Task AbortWithCTRLPlusC_CancellingTests(string tfm) var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, tfm); + string? parameters = null; + if (parallelize) + { + // Providing runSettings even with Parallelize Workers = 1, will "enable" parallelization and will run via different path. + // So providing the settings only to the parallel run. + string runSettingsPath = Path.Combine(testHost.DirectoryName, $"{(parallelize ? "parallel" : "serial")}.runsettings"); + File.WriteAllText(runSettingsPath, $""" + + + + {(parallelize ? 0 : 1)} + MethodLevel + + + + """); + parameters = $"--settings {runSettingsPath}"; + } + string fileCreationPath = Path.Combine(testHost.DirectoryName, "fileCreation"); - File.WriteAllText(fileCreationPath, string.Empty); - TestHostResult testHostResult = await testHost.ExecuteAsync(environmentVariables: new() + TestHostResult testHostResult = await testHost.ExecuteAsync(parameters, environmentVariables: new() { ["FILE_DIRECTORY"] = fileCreationPath, }); - testHostResult.AssertExitCodeIs(ExitCodes.TestSessionAborted); + // To ensure we don't cancel right away, so tests have chance to run, and block our + // cancellation if we do it wrong. + testHostResult.AssertOutputContains("Waiting for file creation."); + if (parallelize) + { + testHostResult.AssertOutputContains("Test Parallelization enabled for"); + } + else + { + testHostResult.AssertOutputDoesNotContain("Test Parallelization enabled for"); + } - testHostResult.AssertOutputMatchesRegex("Canceling the test session.*"); + testHostResult.AssertExitCodeIs(ExitCodes.TestSessionAborted); } public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.NuGetGlobalPackagesFolder) @@ -92,6 +128,7 @@ public static async Task Main(string[] args) } else { + Console.WriteLine("Waiting for file creation."); Thread.Sleep(1000); } } @@ -131,12 +168,18 @@ public async Task TestA() var fireCtrlCTask = Task.Run(() => { // Delay for a short period before firing CTRL+C to simulate some processing time - Task.Delay(1000).Wait(); File.WriteAllText(Environment.GetEnvironmentVariable("FILE_DIRECTORY")!, string.Empty); }); - // Start a task that represents the infinite delay, which should be canceled - await Task.Delay(Timeout.Infinite, TestContext.CancellationTokenSource.Token); + // Wait for 10s, and after that kill the process. + // When we cancel by CRTL+C we do non-graceful teardown so the Environment.Exit should never be reached, + // because the test process already terminated. + // + // If we do reach it, we will see 11111 exit code, and it will fail the test assertion, because we did not cancel. + // (If we don't exit here, the process will happily run to completion after 10 seconds, but will still report + // cancelled exit code, so that is why we are more aggressive here.) + await Task.Delay(10_000); + Environment.Exit(11111); } } """;