// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if NETFRAMEWORK using System.Runtime.Remoting.Messaging; #endif using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Execution; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; #pragma warning disable CA1852 // Seal internal types - This class is inherited in tests. internal partial class TestMethodInfo { /// /// Runs TestCleanup methods of parent TestClass and base classes. /// /// Instance of TestResult. /// The timeout token source. /// The activation and cleanup state for this invocation. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] private async SynchronizationContextPreservingTask RunTestCleanupMethodAsync( TestResult result, CancellationTokenSource? timeoutTokenSource, TestInvocationState invocationState) { _ = result ?? throw new ArgumentNullException(nameof(result)); if (!invocationState.TryGetCleanupSnapshot(out TestInvocationCleanupSnapshot snapshot)) { return; } bool hasCleanupsToInvoke = HasCleanupsToInvoke(snapshot.ClassInstanceLease); bool shouldResetCancellationTokenSource = snapshot.IsTestContextSet && TestContext.Context.CancellationTokenSource.IsCancellationRequested; if ((!hasCleanupsToInvoke && !shouldResetCancellationTokenSource) || !invocationState.TryBeginCleanup()) { return; } if (shouldResetCancellationTokenSource) { TestContext.Context.CancellationTokenSource = new CancellationTokenSource(); } if (!hasCleanupsToInvoke) { return; } if (!snapshot.IsTestContextSet) { try { if (snapshot.ClassInstanceLease is { RequiresCleanup: true } classInstanceLease) { await classInstanceLease.DisposeAsync().ConfigureAwait(false); } } catch (Exception exception) { result.Outcome = result.Outcome.GetMoreImportantOutcome(UnitTestOutcome.Failed); result.TestFailureException = exception; } return; } MethodInfo? testCleanupMethod = Parent.TestCleanupMethod; Exception? testCleanupException; TestFailedException? globalTestCleanupException = null; MethodInfo? globalTestCleanupFailingMethod = null; MethodInfo? currentGlobalTestCleanupMethod = null; try { try { // If we are running with a method timeout, we need to cancel the cleanup when the overall timeout expires. If it already expired, nothing to do. if (timeoutTokenSource is { IsCancellationRequested: false }) { timeoutTokenSource?.Token.Register(TestContext.Context.CancellationTokenSource.Cancel); } // Test cleanups are called in the order of discovery // Current TestClass -> Parent -> Grandparent testCleanupException = testCleanupMethod is not null ? await InvokeCleanupMethodAsync(testCleanupMethod, snapshot.ClassInstance, timeoutTokenSource).ConfigureAwait(false) : null; if (testCleanupException is null) { foreach (MethodInfo baseCleanupMethod in Parent.BaseTestCleanupMethodsQueue) { testCleanupMethod = baseCleanupMethod; testCleanupException = await InvokeCleanupMethodAsync(baseCleanupMethod, snapshot.ClassInstance, timeoutTokenSource).ConfigureAwait(false); if (testCleanupException is not null) { break; } } } } finally { if (snapshot.ClassInstanceLease is { RequiresCleanup: true } classInstanceLease) { await classInstanceLease.DisposeAsync().ConfigureAwait(false); } foreach ((MethodInfo method, TimeoutInfo? timeoutInfo) in Parent.Parent.GlobalTestCleanups) { currentGlobalTestCleanupMethod = method; globalTestCleanupException = await InvokeGlobalCleanupMethodAsync(method, timeoutInfo, timeoutTokenSource).ConfigureAwait(false); if (globalTestCleanupException is not null) { // Stop on first global cleanup failure, mirroring the local test cleanup loop above. globalTestCleanupFailingMethod = method; break; } currentGlobalTestCleanupMethod = null; } } } catch (Exception ex) { testCleanupException = ex; if (globalTestCleanupFailingMethod is null && currentGlobalTestCleanupMethod is not null) { globalTestCleanupFailingMethod = currentGlobalTestCleanupMethod; } if (globalTestCleanupFailingMethod is not null) { testCleanupMethod = globalTestCleanupFailingMethod; } } // If no earlier cleanup failure was recorded but a global test cleanup failed, surface it // so the wrapping logic below produces a TestFailedException with the per-method message. if (testCleanupException is null && globalTestCleanupException is not null) { testCleanupException = globalTestCleanupException; testCleanupMethod = globalTestCleanupFailingMethod; } // If testCleanup was successful, then don't do anything if (testCleanupException == null) { return; } Exception realException = testCleanupException.GetRealException(); // Check `realException` (not `testCleanupException`) so that a `TestFailedException` wrapped in // a `TargetInvocationException` / `TypeInitializationException` still preserves its Outcome // (for example a Timeout outcome must not be silently downgraded to Failed). UnitTestOutcome outcomeFromRealException = realException is TestFailedException testFailedException ? testFailedException.Outcome : realException is AssertInconclusiveException ? UnitTestOutcome.Inconclusive : UnitTestOutcome.Failed; result.Outcome = result.Outcome.GetMoreImportantOutcome(outcomeFromRealException); realException = testCleanupMethod != null ? new TestFailedException( outcomeFromRealException, string.Format(CultureInfo.CurrentCulture, Resource.UTA_CleanupMethodThrows, TestClassName, testCleanupMethod.Name, realException.GetFormattedExceptionMessage()), realException.TryGetStackTraceInformation(), realException) : new TestFailedException( outcomeFromRealException, string.Format(CultureInfo.CurrentCulture, Resource.UTA_CleanupMethodThrowsGeneralError, TestClassName, realException.GetFormattedExceptionMessage()), realException.TryGetStackTraceInformation(), realException); result.TestFailureException = realException; } private bool HasCleanupsToInvoke(ITestClassInstanceLease? classInstanceLease) => Parent.TestCleanupMethod is not null || Parent.BaseTestCleanupMethodsQueue is { Count: > 0 } || classInstanceLease is { RequiresCleanup: true } || Parent.Parent.GlobalTestCleanups is { Count: > 0 }; /// /// Runs TestInitialize methods of parent TestClass and the base classes. /// /// Instance of TestClass. /// Instance of TestResult. /// The timeout token source. /// True if the TestInitialize method(s) did not throw an exception. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] private async SynchronizationContextPreservingTask RunTestInitializeMethodAsync(object? classInstance, TestResult result, CancellationTokenSource? timeoutTokenSource) { DebugEx.Assert(classInstance is not null || Parent.IsFSharpModule, "classInstance is null for a non-module test"); DebugEx.Assert(result != null, "result != null"); MethodInfo? testInitializeMethod = null; Exception? testInitializeException = null; try { // TestInitialize methods for base classes are called in reverse order of discovery // Grandparent -> Parent -> Child TestClass List baseTestInitializeList = Parent.BaseTestInitializeMethodsQueue; for (int i = baseTestInitializeList.Count - 1; i >= 0; i--) { testInitializeMethod = baseTestInitializeList[i]; testInitializeException = testInitializeMethod is not null ? await InvokeInitializeMethodAsync(testInitializeMethod, classInstance, timeoutTokenSource).ConfigureAwait(false) : null; if (testInitializeException is not null) { break; } } if (testInitializeException == null) { testInitializeMethod = Parent.TestInitializeMethod; testInitializeException = testInitializeMethod is not null ? await InvokeInitializeMethodAsync(testInitializeMethod, classInstance, timeoutTokenSource).ConfigureAwait(false) : null; } } catch (Exception ex) { testInitializeException = ex; } // If testInitialization was successful, then don't do anything if (testInitializeException == null) { return true; } // If the exception is already a `TestFailedException` we throw it as-is if (testInitializeException is TestFailedException tfe) { result.Outcome = tfe.Outcome; result.TestFailureException = testInitializeException; return false; } Exception realException = testInitializeException.GetRealException(); // Prefix the exception message with the exception type name as prefix when exception is not assert exception. string exceptionMessage = realException is UnitTestAssertException ? realException.TryGetMessage() : realException.GetFormattedExceptionMessage(); string errorMessage = string.Format( CultureInfo.CurrentCulture, Resource.UTA_InitMethodThrows, TestClassName, testInitializeMethod?.Name, exceptionMessage); StackTraceInformation? stackTrace = realException.GetStackTraceInformation(); result.Outcome = realException is AssertInconclusiveException ? UnitTestOutcome.Inconclusive : UnitTestOutcome.Failed; result.TestFailureException = new TestFailedException( result.Outcome, errorMessage, stackTrace, realException); return false; } private async SynchronizationContextPreservingTask InvokeInitializeMethodAsync(MethodInfo methodInfo, object? classInstance, CancellationTokenSource? timeoutTokenSource) { TimeoutInfo? timeout = null; if (Parent.TestInitializeMethodTimeoutMilliseconds.TryGetValue(methodInfo, out TimeoutInfo localTimeout)) { timeout = localTimeout; } using IMSTestActivity? activity = MSTestInstrumentation.IsEnabled ? MSTestInstrumentation.StartFixtureActivity( MSTestInstrumentation.ActivityNames.TestInitialize, "test_initialize", methodInfo.DeclaringType?.FullName, methodInfo.DeclaringType?.Assembly.GetName().Name) : null; TestFailedException? result = await InvokeFixtureMethodAsync( methodInfo, classInstance, arguments: null, timeout, timeoutTokenSource, Resource.TestInitializeWasCancelled, Resource.TestInitializeTimedOut).ConfigureAwait(false); if (result is not null) { activity?.RecordException(result); } return result; } private async SynchronizationContextPreservingTask InvokeGlobalInitializeMethodAsync(MethodInfo methodInfo, TimeoutInfo? timeoutInfo, CancellationTokenSource? timeoutTokenSource) => await InvokeFixtureMethodAsync( methodInfo, classInstance: null, arguments: [TestContext], timeoutInfo, timeoutTokenSource, Resource.GlobalTestInitializeWasCancelled, Resource.GlobalTestInitializeTimedOut).ConfigureAwait(false); private async SynchronizationContextPreservingTask InvokeCleanupMethodAsync(MethodInfo methodInfo, object? classInstance, CancellationTokenSource? timeoutTokenSource) { TimeoutInfo? timeout = null; if (Parent.TestCleanupMethodTimeoutMilliseconds.TryGetValue(methodInfo, out TimeoutInfo localTimeout)) { timeout = localTimeout; } using IMSTestActivity? activity = MSTestInstrumentation.IsEnabled ? MSTestInstrumentation.StartFixtureActivity( MSTestInstrumentation.ActivityNames.TestCleanup, "test_cleanup", methodInfo.DeclaringType?.FullName, methodInfo.DeclaringType?.Assembly.GetName().Name) : null; TestFailedException? result = await InvokeFixtureMethodAsync( methodInfo, classInstance, arguments: null, timeout, timeoutTokenSource, Resource.TestCleanupWasCancelled, Resource.TestCleanupTimedOut).ConfigureAwait(false); if (result is not null) { activity?.RecordException(result); } return result; } private async SynchronizationContextPreservingTask InvokeGlobalCleanupMethodAsync(MethodInfo methodInfo, TimeoutInfo? timeoutInfo, CancellationTokenSource? timeoutTokenSource) => await InvokeFixtureMethodAsync( methodInfo, classInstance: null, arguments: [TestContext], timeoutInfo, timeoutTokenSource, Resource.GlobalTestCleanupWasCancelled, Resource.GlobalTestCleanupTimedOut).ConfigureAwait(false); private async SynchronizationContextPreservingTask InvokeFixtureMethodAsync( MethodInfo methodInfo, object? classInstance, object?[]? arguments, TimeoutInfo? timeout, CancellationTokenSource? timeoutTokenSource, string cancelledMessage, string timedOutMessage) { TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync( async () => { #if NETFRAMEWORK CallContext.HostContext = _hostContext; #endif Task? task = methodInfo.GetInvokeResultAsync(classInstance, arguments); if (task is not null) { await task.ConfigureAwait(false); } CaptureExecutionContextAfterFixtureIfNeeded(timeout); #if NETFRAMEWORK _hostContext = CallContext.HostContext; #endif }, TestContext.Context.CancellationTokenSource, timeout, methodInfo, _executionContext, cancelledMessage, timedOutMessage, timeoutTokenSource is null ? null : (timeoutTokenSource, TimeoutInfo.Timeout)).ConfigureAwait(false); return result; } private void CaptureExecutionContextAfterFixtureIfNeeded(TimeoutInfo? timeoutInfo) { // After we execute a global test initialize, test initialize, test cleanup, or global test cleanup, we // might need to capture the execution context. // Generally, we do so only if the method has a timeout and that timeout is non-cooperative. // For all other cases, we already use a custom task that preserves synchronization and execution contexts. // NOTE: it seems that in .NET Framework, the synchronization context is part of the execution context. // However, this doesn't appear to be the case in .NET (Core) where the synchronization context is strictly tied to current thread. // In addition, if execution context was captured before (due to use of non-cooperative timeout in a previously run fixture), we still capture here again. if (timeoutInfo?.CooperativeCancellation == false || _executionContext is not null) { _executionContext = ExecutionContext.Capture() ?? _executionContext; } } }