// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Platform.Acceptance.IntegrationTests; using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; using Microsoft.Testing.Platform.Helpers; namespace MSTest.Acceptance.IntegrationTests; // Legacy equivalence map for MSTest.VstestConsoleWrapper.IntegrationTests.TimeoutTests.ValidateTimeoutTests: // - TimeoutTest_WhenTimeoutReached_ForcesTestAbort is already equivalent to // MSTest.Acceptance.IntegrationTests.TimeoutTestMethodTests.Timeout_WhenMethodTimeoutAndWaitInTestMethod_TestGetsCanceled. // - TimeoutTest_WhenTimeoutReached_CancelsTestContextToken had no marker/output equivalent and is preserved below. // - TimeoutTest_WhenUserCancelsTestContextToken_AbortTest and // RegularTest_WhenUserCancelsTestContextToken_TestContinues had no acceptance test for their distinction and are preserved below. // - TimeoutTest_WhenUserCallsThreadAbort_AbortTest had no acceptance equivalent and is preserved below for .NET Framework. [TestClass] public sealed class LegacyTimeoutBehaviorTests : AcceptanceTestBase { [TestMethod] [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] public async Task TimeoutExpiration_CancelsTestContextTokenAndAllowsBackgroundOutput(string tfm) { string markerPath = Path.Combine(Path.GetTempPath(), $"mstest-timeout-{Guid.NewGuid():N}.txt"); try { TestHostResult result = await ExecuteAsync( tfm, "TimeoutExpirationCancelsToken", new() { ["TIMEOUT_MARKER"] = markerPath }); result.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); result.AssertOutputContainsSummary(failed: 1, passed: 0, skipped: 0); result.AssertOutputContains("TimeoutExpirationCancelsToken"); Assert.IsTrue(File.Exists(markerPath), $"Timeout cancellation marker was not created: {markerPath}"); Assert.AreEqual("cancellation observed", File.ReadAllText(markerPath)); } finally { File.Delete(markerPath); } } [TestMethod] [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] public async Task UserCancellation_WithAndWithoutTimeoutPreservesExactFailedTests(string tfm) { string timedMarkerPath = Path.Combine(Path.GetTempPath(), $"mstest-timed-cancel-{Guid.NewGuid():N}.txt"); string regularMarkerPath = Path.Combine(Path.GetTempPath(), $"mstest-regular-cancel-{Guid.NewGuid():N}.txt"); try { TestHostResult timedResult = await ExecuteAsync( tfm, "UserCancellationWithTimeout", new() { ["TIMED_CANCEL_MARKER"] = timedMarkerPath }); TestHostResult regularResult = await ExecuteAsync( tfm, "UserCancellationWithoutTimeout", new() { ["REGULAR_CANCEL_MARKER"] = regularMarkerPath }); timedResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); timedResult.AssertOutputContainsSummary(failed: 1, passed: 0, skipped: 0); timedResult.AssertOutputContains("UserCancellationWithTimeout"); timedResult.AssertOutputContains("Test 'UserCancellationWithTimeout' was canceled"); Assert.IsFalse(File.Exists(timedMarkerPath), "Execution should not continue after timeout-aware cancellation."); regularResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); regularResult.AssertOutputContainsSummary(failed: 1, passed: 0, skipped: 0); regularResult.AssertOutputContains("UserCancellationWithoutTimeout"); regularResult.AssertOutputDoesNotContain("Test 'UserCancellationWithoutTimeout' was canceled"); Assert.AreEqual("continued", File.ReadAllText(regularMarkerPath)); } finally { File.Delete(timedMarkerPath); File.Delete(regularMarkerPath); } } [TestMethod] [OSCondition(OperatingSystems.Windows)] public async Task ThreadAbort_OnNetFrameworkAbortsTheTimeoutTest() { string markerPath = Path.Combine(Path.GetTempPath(), $"mstest-thread-abort-{Guid.NewGuid():N}.txt"); try { TestHostResult result = await ExecuteAsync( TargetFrameworks.NetFramework[0], "ThreadAbortAbortsTimeoutTest", new() { ["THREAD_ABORT_MARKER"] = markerPath }); result.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); result.AssertOutputContainsSummary(failed: 1, passed: 0, skipped: 0); result.AssertOutputContains("ThreadAbortAbortsTimeoutTest"); Assert.IsFalse(File.Exists(markerPath), "Execution should not continue after Thread.Abort."); } finally { File.Delete(markerPath); } } private async Task ExecuteAsync( string tfm, string methodName, Dictionary environmentVariables) { var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, TestAssetFixture.ProjectName, tfm); return await testHost.ExecuteAsync( $"--filter Name={methodName} --output Detailed", environmentVariables, cancellationToken: TestContext.CancellationToken); } public sealed class TestAssetFixture : TestAssetFixtureBase { public const string ProjectName = "LegacyTimeoutBehavior"; public string TargetAssetPath => GetAssetPath(ProjectName); public override (string ID, string Name, string Code) GetAssetsToGenerate() => (ProjectName, ProjectName, SourceCode .PatchTargetFrameworks(TargetFrameworks.All) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion)); private const string SourceCode = """ #file LegacyTimeoutBehavior.csproj Exe true preview $TargetFrameworks$ #file TimeoutCases.cs using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace LegacyTimeoutBehavior; #pragma warning disable CS0618 [TestClass] public class TimeoutCases { public TestContext TestContext { get; set; } = null!; [TestMethod] [Timeout(1000)] public void TimeoutExpirationCancelsToken() { var worker = new Thread(() => { try { Task.Delay(100_000).Wait(TestContext.CancellationTokenSource.Token); } catch (OperationCanceledException) { File.WriteAllText(Environment.GetEnvironmentVariable("TIMEOUT_MARKER")!, "cancellation observed"); } }); worker.Start(); worker.Join(); } [TestMethod] [Timeout(int.MaxValue)] public void UserCancellationWithTimeout() { TestContext.CancellationTokenSource.Cancel(); Thread.Sleep(10_000); File.WriteAllText(Environment.GetEnvironmentVariable("TIMED_CANCEL_MARKER")!, "continued unexpectedly"); } [TestMethod] public void UserCancellationWithoutTimeout() { TestContext.CancellationTokenSource.Cancel(); File.WriteAllText(Environment.GetEnvironmentVariable("REGULAR_CANCEL_MARKER")!, "continued"); Assert.Fail("Expected failure proves execution continued after cancellation."); } #if NETFRAMEWORK [TestMethod] [Timeout(int.MaxValue)] public void ThreadAbortAbortsTimeoutTest() { Thread.CurrentThread.Abort(); File.WriteAllText(Environment.GetEnvironmentVariable("THREAD_ABORT_MARKER")!, "continued unexpectedly"); } #endif } """; } public TestContext TestContext { get; set; } = default!; }