From a0515c5c6f630f1c6063a53a67d3eccf841faa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 16 May 2024 17:44:38 +0200 Subject: [PATCH] Flow execution context across fixture methods when using timeout (#2843) --- .../Execution/TestAssemblyInfo.cs | 12 +- .../Execution/TestClassInfo.cs | 23 +- .../Execution/TestMethodInfo.cs | 31 +- ...MethodRunner.cs => FixtureMethodRunner.cs} | 19 +- .../Services/AssemblyExecutionContextScope.cs | 14 + .../Services/ClassExecutionContextScope.cs | 31 + .../Services/ExecutionContextService.cs | 192 +++++ .../Services/IExecutionContextScope.cs | 9 + .../Services/InstanceExecutionContextScope.cs | 35 + .../ThreadContextTests.cs | 676 +++++++++++++++++- .../testsbaseline.txt | 36 +- 11 files changed, 1036 insertions(+), 42 deletions(-) rename src/Adapter/MSTest.TestAdapter/Helpers/{MethodRunner.cs => FixtureMethodRunner.cs} (83%) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/AssemblyExecutionContextScope.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/ClassExecutionContextScope.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/ExecutionContextService.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/IExecutionContextScope.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/InstanceExecutionContextScope.cs diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs b/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs index 6b40113939..826a41fc9b 100644 --- a/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs +++ b/src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -8,6 +8,7 @@ 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; using Microsoft.VisualStudio.TestTools.UnitTesting; using UnitTestOutcome = Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestOutcome; @@ -140,11 +141,12 @@ public void RunAssemblyInitialize(TestContext testContext) { try { - AssemblyInitializationException = MethodRunner.RunWithTimeoutAndCancellation( + AssemblyInitializationException = FixtureMethodRunner.RunWithTimeoutAndCancellation( () => AssemblyInitializeMethod.InvokeAsSynchronousTask(null, testContext), testContext.CancellationTokenSource, AssemblyInitializeMethodTimeoutMilliseconds, AssemblyInitializeMethod, + new AssemblyExecutionContextScope(isCleanup: false), Resource.AssemblyInitializeWasCancelled, Resource.AssemblyInitializeTimedOut); } @@ -213,11 +215,12 @@ public void RunAssemblyInitialize(TestContext testContext) { try { - assemblyCleanupException = MethodRunner.RunWithTimeoutAndCancellation( + assemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation( () => AssemblyCleanupMethod.InvokeAsSynchronousTask(null), new CancellationTokenSource(), AssemblyCleanupMethodTimeoutMilliseconds, AssemblyCleanupMethod, + new AssemblyExecutionContextScope(isCleanup: true), Resource.AssemblyCleanupWasCancelled, Resource.AssemblyCleanupTimedOut); } @@ -269,11 +272,12 @@ internal void ExecuteAssemblyCleanup() { try { - assemblyCleanupException = MethodRunner.RunWithTimeoutAndCancellation( + assemblyCleanupException = FixtureMethodRunner.RunWithTimeoutAndCancellation( () => AssemblyCleanupMethod.InvokeAsSynchronousTask(null), new CancellationTokenSource(), AssemblyCleanupMethodTimeoutMilliseconds, AssemblyCleanupMethod, + new AssemblyExecutionContextScope(isCleanup: true), Resource.AssemblyCleanupWasCancelled, Resource.AssemblyCleanupTimedOut); } diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs b/src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs index fd33c38a19..68cdc5ad19 100644 --- a/src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs +++ b/src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; @@ -8,6 +8,7 @@ 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; using Microsoft.VisualStudio.TestTools.UnitTesting; using ObjectModelUnitTestOutcome = Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestOutcome; @@ -363,11 +364,12 @@ public void RunClassInitialize(TestContext testContext) timeout = localTimeout; } - return MethodRunner.RunWithTimeoutAndCancellation( + return FixtureMethodRunner.RunWithTimeoutAndCancellation( () => methodInfo.InvokeAsSynchronousTask(null, testContext), testContext.CancellationTokenSource, timeout, methodInfo, + new ClassExecutionContextScope(ClassType), Resource.ClassInitializeWasCancelled, Resource.ClassInitializeTimedOut); } @@ -404,12 +406,12 @@ public void RunClassInitialize(TestContext testContext) try { classCleanupMethod = ClassCleanupMethod; - ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod) : null; + ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod, BaseClassCleanupMethodsStack.Count) : null; var baseClassCleanupQueue = new Queue(BaseClassCleanupMethodsStack); while (baseClassCleanupQueue.Count > 0 && ClassCleanupException is null) { classCleanupMethod = baseClassCleanupQueue.Dequeue(); - ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod) : null; + ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod, baseClassCleanupQueue.Count) : null; } IsClassCleanupExecuted = ClassCleanupException is null; @@ -482,12 +484,16 @@ internal void ExecuteClassCleanup() try { classCleanupMethod = ClassCleanupMethod; - ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod) : null; + ClassCleanupException = classCleanupMethod is not null + ? InvokeCleanupMethod(classCleanupMethod, BaseClassCleanupMethodsStack.Count) + : null; var baseClassCleanupQueue = new Queue(BaseClassCleanupMethodsStack); while (baseClassCleanupQueue.Count > 0 && ClassCleanupException is null) { classCleanupMethod = baseClassCleanupQueue.Dequeue(); - ClassCleanupException = classCleanupMethod is not null ? InvokeCleanupMethod(classCleanupMethod) : null; + ClassCleanupException = classCleanupMethod is not null + ? InvokeCleanupMethod(classCleanupMethod, baseClassCleanupQueue.Count) + : null; } IsClassCleanupExecuted = ClassCleanupException is null; @@ -535,7 +541,7 @@ internal void ExecuteClassCleanup() throw testFailedException; } - private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo) + private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo, int remainingCleanupCount) { int? timeout = null; if (ClassCleanupMethodTimeoutMilliseconds.TryGetValue(methodInfo, out int localTimeout)) @@ -543,11 +549,12 @@ internal void ExecuteClassCleanup() timeout = localTimeout; } - return MethodRunner.RunWithTimeoutAndCancellation( + return FixtureMethodRunner.RunWithTimeoutAndCancellation( () => methodInfo.InvokeAsSynchronousTask(null), new CancellationTokenSource(), timeout, methodInfo, + new ClassExecutionContextScope(ClassType, remainingCleanupCount), Resource.ClassCleanupWasCancelled, Resource.ClassCleanupTimedOut); } diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs b/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs index 1be63d983e..146037032f 100644 --- a/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; @@ -10,6 +10,7 @@ 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; using Microsoft.VisualStudio.TestTools.UnitTesting; using ObjectModelUnitTestOutcome = Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestOutcome; @@ -239,7 +240,17 @@ private TestResult ExecuteInternal(object?[]? arguments) if (RunTestInitializeMethod(classInstance, result)) { hasTestInitializePassed = true; - TestMethod.InvokeAsSynchronousTask(classInstance, arguments); + if (IsTimeoutSet) + { + ExecutionContextService.RunActionOnContext( + () => TestMethod.InvokeAsSynchronousTask(classInstance, arguments), + new InstanceExecutionContextScope(classInstance, Parent.ClassType)); + } + else + { + TestMethod.InvokeAsSynchronousTask(classInstance, arguments); + } + result.Outcome = UTF.UnitTestOutcome.Passed; } } @@ -448,12 +459,16 @@ private void RunTestCleanupMethod(object classInstance, TestResult result) { // Test cleanups are called in the order of discovery // Current TestClass -> Parent -> Grandparent - testCleanupException = testCleanupMethod is not null ? InvokeCleanupMethod(testCleanupMethod, classInstance) : null; + testCleanupException = testCleanupMethod is not null + ? InvokeCleanupMethod(testCleanupMethod, classInstance, Parent.BaseTestCleanupMethodsQueue.Count) + : null; var baseTestCleanupQueue = new Queue(Parent.BaseTestCleanupMethodsQueue); while (baseTestCleanupQueue.Count > 0 && testCleanupException is null) { testCleanupMethod = baseTestCleanupQueue.Dequeue(); - testCleanupException = testCleanupMethod is not null ? InvokeCleanupMethod(testCleanupMethod, classInstance) : null; + testCleanupException = testCleanupMethod is not null + ? InvokeCleanupMethod(testCleanupMethod, classInstance, baseTestCleanupQueue.Count) + : null; } } finally @@ -635,16 +650,17 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result) timeout = localTimeout; } - return MethodRunner.RunWithTimeoutAndCancellation( + return FixtureMethodRunner.RunWithTimeoutAndCancellation( () => methodInfo.InvokeAsSynchronousTask(classInstance, null), new CancellationTokenSource(), timeout, methodInfo, + new InstanceExecutionContextScope(classInstance, Parent.ClassType), Resource.TestInitializeWasCancelled, Resource.TestInitializeTimedOut); } - private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo, object classInstance) + private TestFailedException? InvokeCleanupMethod(MethodInfo methodInfo, object classInstance, int remainingCleanupCount) { int? timeout = null; if (Parent.TestCleanupMethodTimeoutMilliseconds.TryGetValue(methodInfo, out int localTimeout)) @@ -652,11 +668,12 @@ private bool RunTestInitializeMethod(object classInstance, TestResult result) timeout = localTimeout; } - return MethodRunner.RunWithTimeoutAndCancellation( + return FixtureMethodRunner.RunWithTimeoutAndCancellation( () => methodInfo.InvokeAsSynchronousTask(classInstance, null), new CancellationTokenSource(), timeout, methodInfo, + new InstanceExecutionContextScope(classInstance, Parent.ClassType, remainingCleanupCount), Resource.TestCleanupWasCancelled, Resource.TestCleanupTimedOut); } diff --git a/src/Adapter/MSTest.TestAdapter/Helpers/MethodRunner.cs b/src/Adapter/MSTest.TestAdapter/Helpers/FixtureMethodRunner.cs similarity index 83% rename from src/Adapter/MSTest.TestAdapter/Helpers/MethodRunner.cs rename to src/Adapter/MSTest.TestAdapter/Helpers/FixtureMethodRunner.cs index 60476b5054..7cbf83eede 100644 --- a/src/Adapter/MSTest.TestAdapter/Helpers/MethodRunner.cs +++ b/src/Adapter/MSTest.TestAdapter/Helpers/FixtureMethodRunner.cs @@ -6,17 +6,20 @@ using System.Runtime.InteropServices; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using UnitTestOutcome = Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestOutcome; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; -internal static class MethodRunner +internal static class FixtureMethodRunner { internal static TestFailedException? RunWithTimeoutAndCancellation( Action action, CancellationTokenSource cancellationTokenSource, int? timeout, MethodInfo methodInfo, - string methodCancelledMessageFormat, string methodTimedOutMessageFormat) + IExecutionContextScope executionContextScope, string methodCancelledMessageFormat, string methodTimedOutMessageFormat) { + // If no timeout is specified, we can run the action directly. This avoids any overhead of creating a task/thread and + // ensures that the execution context is preserved (as we run the action on the current thread). if (timeout is null) { action(); @@ -25,13 +28,13 @@ internal static class MethodRunner // We need to start a thread to handle "cancellation" and "timeout" scenarios. return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Thread.CurrentThread.GetApartmentState() == ApartmentState.STA - ? RunWithTimeoutAndCancellationWithSTAThread(action, cancellationTokenSource, timeout, methodInfo, methodCancelledMessageFormat, methodTimedOutMessageFormat) - : RunWithTimeoutAndCancellationWithThreadPool(action, cancellationTokenSource, timeout, methodInfo, methodCancelledMessageFormat, methodTimedOutMessageFormat); + ? RunWithTimeoutAndCancellationWithSTAThread(action, cancellationTokenSource, timeout, methodInfo, executionContextScope, methodCancelledMessageFormat, methodTimedOutMessageFormat) + : RunWithTimeoutAndCancellationWithThreadPool(action, cancellationTokenSource, timeout, methodInfo, executionContextScope, methodCancelledMessageFormat, methodTimedOutMessageFormat); } private static TestFailedException? RunWithTimeoutAndCancellationWithThreadPool( Action action, CancellationTokenSource cancellationTokenSource, int? timeout, MethodInfo methodInfo, - string methodCancelledMessageFormat, string methodTimedOutMessageFormat) + IExecutionContextScope executionContextScope, string methodCancelledMessageFormat, string methodTimedOutMessageFormat) { Exception? realException = null; Task? executionTask = null; @@ -42,7 +45,7 @@ internal static class MethodRunner { try { - action(); + ExecutionContextService.RunActionOnContext(action, executionContextScope); } catch (Exception ex) { @@ -109,14 +112,14 @@ internal static class MethodRunner #endif private static TestFailedException? RunWithTimeoutAndCancellationWithSTAThread( Action action, CancellationTokenSource cancellationTokenSource, int? timeout, MethodInfo methodInfo, - string methodCancelledMessageFormat, string methodTimedOutMessageFormat) + IExecutionContextScope executionContextScope, string methodCancelledMessageFormat, string methodTimedOutMessageFormat) { Exception? realException = null; Thread executionThread = new(new ThreadStart(() => { try { - action(); + ExecutionContextService.RunActionOnContext(action, executionContextScope); } catch (Exception ex) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/AssemblyExecutionContextScope.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/AssemblyExecutionContextScope.cs new file mode 100644 index 0000000000..910206ccba --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/AssemblyExecutionContextScope.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +internal readonly struct AssemblyExecutionContextScope : IExecutionContextScope +{ + public AssemblyExecutionContextScope(bool isCleanup) + { + IsCleanup = isCleanup; + } + + public bool IsCleanup { get; } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/ClassExecutionContextScope.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/ClassExecutionContextScope.cs new file mode 100644 index 0000000000..b393e935b6 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/ClassExecutionContextScope.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +internal readonly struct ClassExecutionContextScope : IExecutionContextScope +{ + public ClassExecutionContextScope(Type type) + { + Type = type; + IsCleanup = false; + RemainingCleanupCount = 0; + } + + public ClassExecutionContextScope(Type type, int remainingCleanupCount) + { + Type = type; + IsCleanup = true; + RemainingCleanupCount = remainingCleanupCount; + } + + public Type Type { get; } + + public bool IsCleanup { get; } + + public int RemainingCleanupCount { get; } + + public override readonly int GetHashCode() => Type.GetHashCode(); + + public override readonly bool Equals(object? obj) => Type.Equals(obj); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/ExecutionContextService.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/ExecutionContextService.cs new file mode 100644 index 0000000000..c38800d1c0 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/ExecutionContextService.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +internal static class ExecutionContextService +{ + /// + /// The execution context to use by class fixtures (ClassInitialize and ClassCleanup). + /// + /// The type used as key is the type of the test class and not the type of the method info, this is ensuring that mutations + /// done in parent classes are correctly impacting only the current context class. + /// + /// The logic for the context associated to the key is as follows: + /// - Copy and reuse the assembly level context, + /// - If not available, capture the current context and use it. + /// After each ClassInitialize or ClassCleanup, mutate (recapture) the context for the class. + /// + private static readonly ConcurrentDictionary ClassesExecutionContexts = new(); + + /// + /// The execution context to use for instance methods fixtures and tests (TestInitialize, TestMethod, TestCleanup). + /// + /// The key is the instance of the test class. + /// + /// The logic for the context associated to the key is as follows: + /// - Copy and reuse the class level context, + /// - If not available, reuse the assembly level context, + /// - If not available, capture the current context and use it. + /// After each TestInitialize or TestCleanup, mutate (recapture) the context for the instance. + /// + private static readonly ConcurrentDictionary InstancesExecutionContexts = new(); + + /// + /// As we only support one assembly level context, we store it here. + /// + private static ExecutionContext? s_assemblyExecutionContext; + + /// + /// When we execute the action, we need to ensure we are restoring the execution context that was captured in the logical flow of execution. + /// After the action is executed we capture the current execution context and save it for the next action to use based on the current execution context scope. + /// + /// The logical flow of execution is: + /// - AssemblyInitialize execution context is saved at "assembly level" and a copy is flown to each ClassInitialize. + /// - ClassInitialize execution context is saved at "class level" and a copy is flown to each TestInitialize/TestMethod/TestCleanup. + /// - TestInitialize/TestMethod/TestCleanup execution context is mutating the "instance level" + /// - ClassCleanup reuses the "class level" execution context. + /// - AssemblyCleanup reuses the "assembly level" execution context. + /// + internal static void RunActionOnContext(Action action, IExecutionContextScope executionContextScope) + { + // TODO: Log (trace/debug) the execution context scope and the current execution context. + // This would be particularly useful if we have a strange context issue to understand what is being set or not, + // What we manage to capture and what we don't, etc. + if (GetScopedExecutionContext(executionContextScope) is not { } executionContext) + { + // We don't have any execution context (that's usually the case when it is being suppressed), so we can run the action directly. + action(); + return; + } + + // We have an execution context, so we need to run the action in that context to ensure the flow of execution is preserved. + ExecutionContext.Run( + executionContext, + _ => + { + action(); + + if (ShouldCleanup(executionContextScope)) + { + CleanupExecutionContext(executionContextScope); + } + else + { + // The execution context and synchronization contexts of the calling thread are returned to their previous + // states when the method completes. That's why we need to capture the state and mutate the state before exiting. + SaveExecutionContext(executionContextScope); + } + }, + null); + } + + /// + /// Capture the new state of the execution context and mutate the right variable/dictionary based on the execution context scope. + /// + private static void SaveExecutionContext(IExecutionContextScope executionContextScope) + { + var capturedContext = ExecutionContext.Capture(); + switch (executionContextScope) + { + case AssemblyExecutionContextScope: + s_assemblyExecutionContext = capturedContext; + break; + + case ClassExecutionContextScope classExecutionContextScope: + ClassesExecutionContexts.AddOrUpdate( + classExecutionContextScope.Type, + _ => capturedContext, + (_, _) => capturedContext); + break; + + case InstanceExecutionContextScope instanceExecutionContextScope: + InstancesExecutionContexts.AddOrUpdate( + instanceExecutionContextScope.Instance, + _ => capturedContext, + (_, _) => capturedContext); + break; + } + } + + /// + /// Clears up the backed up execution state based on the execution context scope. + /// + private static void CleanupExecutionContext(IExecutionContextScope executionContextScope) + { + Debug.Assert(executionContextScope.IsCleanup, "CleanupExecutionContext should be called only in a cleanup scope."); + + switch (executionContextScope) + { + case AssemblyExecutionContextScope: + // When calling the assembly cleanup, we can clear up all the contexts that would not have been cleaned up. + foreach (ExecutionContext? context in InstancesExecutionContexts.Values) + { + context?.Dispose(); + } + + foreach (ExecutionContext? context in ClassesExecutionContexts.Values) + { + context?.Dispose(); + } + + InstancesExecutionContexts.Clear(); + ClassesExecutionContexts.Clear(); + s_assemblyExecutionContext?.Dispose(); + s_assemblyExecutionContext = null; + break; + + case ClassExecutionContextScope classExecutionContextScope: + _ = ClassesExecutionContexts.TryRemove(classExecutionContextScope.Type, out ExecutionContext? classContext); + classContext?.Dispose(); + break; + + case InstanceExecutionContextScope instanceExecutionContextScope: + _ = InstancesExecutionContexts.TryRemove(instanceExecutionContextScope.Instance, out ExecutionContext? instanceContext); + instanceContext?.Dispose(); + break; + } + } + + private static ExecutionContext? GetScopedExecutionContext(IExecutionContextScope executionContextScope) + { + ExecutionContext? executionContext = executionContextScope switch + { + // Return the assembly level context or capture and save it if it doesn't exist. + AssemblyExecutionContextScope => s_assemblyExecutionContext ??= ExecutionContext.Capture(), + + // Return the class level context or if it doesn't exist do the following steps: + // - use the assembly level context if it exists + // - or capture and save current context + ClassExecutionContextScope classExecutionContextScope => ClassesExecutionContexts.GetOrAdd( + classExecutionContextScope.Type, + _ => s_assemblyExecutionContext ?? ExecutionContext.Capture()), + + // Return the instance level context or if it doesn't exist do the following steps: + // - use the class level context if it exists + // - or use the assembly level context if it exists + // - or capture and save current context + InstanceExecutionContextScope instanceExecutionContextScope => InstancesExecutionContexts.GetOrAdd( + instanceExecutionContextScope.Instance, + _ => ClassesExecutionContexts.TryGetValue(instanceExecutionContextScope.Type, out ExecutionContext? classExecutionContext) + ? classExecutionContext + : s_assemblyExecutionContext ?? ExecutionContext.Capture()), + _ => throw new NotSupportedException($"Unsupported execution context scope: {executionContextScope.GetType()}"), + }; + + // Always create a copy of the context because running twice on the same context results in an error. + return executionContext?.CreateCopy(); + } + + private static bool ShouldCleanup(this IExecutionContextScope executionContextScope) + => executionContextScope.IsCleanup + && executionContextScope switch + { + AssemblyExecutionContextScope => true, + ClassExecutionContextScope classExecutionContextScope => classExecutionContextScope.RemainingCleanupCount == 0, + InstanceExecutionContextScope instanceExecutionContext => instanceExecutionContext.RemainingCleanupCount == 0, + _ => throw new NotSupportedException($"Unsupported execution context scope: {executionContextScope.GetType()}"), + }; +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/IExecutionContextScope.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/IExecutionContextScope.cs new file mode 100644 index 0000000000..7119d94a5c --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/IExecutionContextScope.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +internal interface IExecutionContextScope +{ + public bool IsCleanup { get; } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/InstanceExecutionContextScope.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/InstanceExecutionContextScope.cs new file mode 100644 index 0000000000..4ba72a5b76 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/InstanceExecutionContextScope.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +internal readonly struct InstanceExecutionContextScope : IExecutionContextScope +{ + public InstanceExecutionContextScope(object instance, Type type) + { + Instance = instance; + Type = type; + IsCleanup = false; + RemainingCleanupCount = 0; + } + + public InstanceExecutionContextScope(object instance, Type type, int remainingCleanupCount) + { + Instance = instance; + Type = type; + IsCleanup = true; + RemainingCleanupCount = remainingCleanupCount; + } + + public object Instance { get; } + + public Type Type { get; } + + public bool IsCleanup { get; } + + public int RemainingCleanupCount { get; } + + public override readonly int GetHashCode() => Instance.GetHashCode(); + + public override readonly bool Equals(object? obj) => Instance.Equals(obj); +} diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ThreadContextTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ThreadContextTests.cs index ec63a5fe99..1dafee57de 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ThreadContextTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/ThreadContextTests.cs @@ -20,7 +20,7 @@ public ThreadContextTests(ITestExecutionContext testExecutionContext, TestAssetF [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] public async Task ThreadingContext_WhenCultureIsNotSet_TestMethodFails(string tfm) { - var testHost = TestHost.LocateFrom(_testAssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm); + var testHost = TestHost.LocateFrom(_testAssetFixture.InitToTestProjectPath, TestAssetFixture.InitToTestProjectName, tfm); TestHostResult testHostResult = await testHost.ExecuteAsync(); testHostResult.AssertOutputContains("Failed: 1, Passed: 0, Skipped: 0, Total: 1"); } @@ -39,19 +39,111 @@ public async Task ThreadingContext_WhenChangedInTestInitialize_IsPassedToTestMet private async Task SetCultureInFixtureMethodAndRunTests(string tfm, string envVarKey) { - var testHost = TestHost.LocateFrom(_testAssetFixture.ProjectPath, TestAssetFixture.ProjectName, tfm); - TestHostResult testHostResult = await testHost.ExecuteAsync(environmentVariables: new() { [envVarKey] = "1" }); + var testHost = TestHost.LocateFrom(_testAssetFixture.InitToTestProjectPath, TestAssetFixture.InitToTestProjectName, tfm); + TestHostResult testHostResult = await testHost.ExecuteAsync(environmentVariables: new() { [envVarKey] = "true" }); testHostResult.AssertExitCodeIs(0); testHostResult.AssertOutputContains("Failed: 0, Passed: 1, Skipped: 0, Total: 1"); } + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsProjectPath, TestAssetFixture.CultureFlowsProjectName, tfm); + TestHostResult testHostResult = await testHost.ExecuteAsync(); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 1, Skipped: 0, Total: 1"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsProjectPath, TestAssetFixture.CultureFlowsProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "sta.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}"); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 1, Skipped: 0, Total: 1"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsProjectPath, TestAssetFixture.CultureFlowsProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "sta-timeout.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}", environmentVariables: new() + { + ["MSTEST_TEST_FLOW_CONTEXT"] = "true", + }); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 1, Skipped: 0, Total: 1"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsProjectPath, TestAssetFixture.CultureFlowsProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "timeout.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}", environmentVariables: new() + { + ["MSTEST_TEST_FLOW_CONTEXT"] = "true", + }); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 1, Skipped: 0, Total: 1"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_Inheritance_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsInheritanceProjectPath, TestAssetFixture.CultureFlowsInheritanceProjectName, tfm); + TestHostResult testHostResult = await testHost.ExecuteAsync(); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 8, Skipped: 0, Total: 8"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_Inheritance_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsInheritanceProjectPath, TestAssetFixture.CultureFlowsInheritanceProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "sta.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}"); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 8, Skipped: 0, Total: 8"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_Inheritance_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsInheritanceProjectPath, TestAssetFixture.CultureFlowsInheritanceProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "sta-timeout.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}", environmentVariables: new() + { + ["MSTEST_TEST_FLOW_CONTEXT"] = "true", + }); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 8, Skipped: 0, Total: 8"); + } + + [ArgumentsProvider(nameof(TargetFrameworks.All), typeof(TargetFrameworks))] + public async Task ThreadingContext_Inheritance_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string tfm) + { + var testHost = TestHost.LocateFrom(_testAssetFixture.CultureFlowsInheritanceProjectPath, TestAssetFixture.CultureFlowsInheritanceProjectName, tfm); + string runSettingsFilePath = Path.Combine(testHost.DirectoryName, "timeout.runsettings"); + TestHostResult testHostResult = await testHost.ExecuteAsync($"--settings {runSettingsFilePath}", environmentVariables: new() + { + ["MSTEST_TEST_FLOW_CONTEXT"] = "true", + }); + testHostResult.AssertExitCodeIs(0); + testHostResult.AssertOutputContains("Failed: 0, Passed: 8, Skipped: 0, Total: 8"); + } + [TestFixture(TestFixtureSharingStrategy.PerTestGroup)] public sealed class TestAssetFixture(AcceptanceFixture acceptanceFixture) : TestAssetFixtureBase(acceptanceFixture.NuGetGlobalPackagesFolder) { - public const string ProjectName = "ThreadContextProject"; - private const string SourceCode = """ -#file ThreadContextProject.csproj + public const string InitToTestProjectName = "InitToTestThreadContextProject"; + public const string CultureFlowsProjectName = "CultureFlowsThreadContextProject"; + public const string CultureFlowsInheritanceProjectName = "CultureFlowsInheritanceThreadContextProject"; + private const string InitToTestSourceCode = """ +#file InitToTestThreadContextProject.csproj @@ -70,7 +162,7 @@ public sealed class TestAssetFixture(AcceptanceFixture acceptanceFixture) #file UnitTest1.cs -namespace ThreadContextProject; +namespace InitToTestThreadContextProject; using System; using System.Globalization; @@ -85,7 +177,7 @@ public class UnitTest1 [AssemblyInitialize] public static void AssemblyInitialize(TestContext context) { - if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_ASSEMBLY_INIT") == "1") + if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_ASSEMBLY_INIT") == "true") { CultureInfo.CurrentCulture = new CultureInfo(CultureCodeName); } @@ -94,7 +186,7 @@ public static void AssemblyInitialize(TestContext context) [ClassInitialize] public static void ClassInitialize(TestContext context) { - if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_CLASS_INIT") == "1") + if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_CLASS_INIT") == "true") { CultureInfo.CurrentCulture = new CultureInfo(CultureCodeName); } @@ -103,7 +195,7 @@ public static void ClassInitialize(TestContext context) [TestInitialize] public void TestInitialize() { - if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_TEST_INIT") == "1") + if (Environment.GetEnvironmentVariable("MSTEST_TEST_SET_CULTURE_TEST_INIT") == "true") { CultureInfo.CurrentCulture = new CultureInfo(CultureCodeName); } @@ -117,12 +209,570 @@ public void TestMethod1() } """; - public string ProjectPath => GetAssetPath(ProjectName); + private const string CultureFlowsSourceCode = """ +#file sta.runsettings + + + + STA + + + +#file sta-timeout.runsettings + + + + STA + + + 10001 + 10002 + 10003 + 30004 + 10005 + 10006 + 10007 + + + +#file timeout.runsettings + + + + 10001 + 10002 + 10003 + 30004 + 10005 + 10006 + 10007 + + + +#file CultureFlowsThreadContextProject.csproj + + + + Exe + true + $TargetFrameworks$ + latest + + + + + + + + + + + PreserveNewest + + + + + +#file UnitTest1.cs +namespace CultureFlowsThreadContextProject; + +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UnitTest1 +{ + private const string AssemblyInitCultureCodeName = "th-TH"; + private const string ClassInitCultureCodeName = "tr-TR"; + private const string TestInitCultureCodeName = "sv-SE"; + private const string TestMethodCultureCodeName = "ak-GH"; + private const string TestCleanupCultureCodeName = "pt-BR"; + private const string ClassCleanupCultureCodeName = "hu-HU"; + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext context) + { + CultureInfo.CurrentCulture = new CultureInfo(AssemblyInitCultureCodeName); + } + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + Assert.AreEqual(AssemblyInitCultureCodeName, CultureInfo.CurrentCulture.Name, + "ClassInitialize culture should have been the one set by AssemblyInitialize"); + CultureInfo.CurrentCulture = new CultureInfo(ClassInitCultureCodeName); + } + + [TestInitialize] + public void TestInitialize() + { + Assert.AreEqual(ClassInitCultureCodeName, CultureInfo.CurrentCulture.Name, + "TestInitialize culture should have been the one set by ClassInitialize"); + CultureInfo.CurrentCulture = new CultureInfo(TestInitCultureCodeName); + } + + [TestMethod] + public void TestMethod1() + { + Assert.AreEqual(TestInitCultureCodeName, CultureInfo.CurrentCulture.Name, + "TestMethod culture should have been the one set by TestInitialize"); + CultureInfo.CurrentCulture = new CultureInfo(TestMethodCultureCodeName); + } + + [TestCleanup] + public void TestCleanup() + { + Assert.AreEqual(TestMethodCultureCodeName, CultureInfo.CurrentCulture.Name, + "TestCleanup culture should have been the one set by TestMethod"); + CultureInfo.CurrentCulture = new CultureInfo(TestCleanupCultureCodeName); + } + + [ClassCleanup] + public static void ClassCleanup() + { + if (Environment.GetEnvironmentVariable("MSTEST_TEST_FLOW_CONTEXT") == "true") + { + Assert.AreEqual(ClassInitCultureCodeName, CultureInfo.CurrentCulture.Name, + "ClassCleanup culture should have been the one set by ClassInitialize"); + } + else + { + Assert.AreEqual(TestCleanupCultureCodeName, CultureInfo.CurrentCulture.Name, + "ClassCleanup culture should have been the one set by TestCleanup"); + CultureInfo.CurrentCulture = new CultureInfo(ClassCleanupCultureCodeName); + } + } + + [AssemblyCleanup] + public static void AssemblyCleanup() + { + if (Environment.GetEnvironmentVariable("MSTEST_TEST_FLOW_CONTEXT") == "true") + { + Assert.AreEqual(AssemblyInitCultureCodeName, CultureInfo.CurrentCulture.Name, + "AssemblyCleanup culture should have been the one set by AssemblyInitialize"); + } + else + { + Assert.AreEqual(ClassCleanupCultureCodeName, CultureInfo.CurrentCulture.Name, + "AssemblyCleanup culture should have been the one set by ClassCleanup"); + } + } +} +"""; + + private const string CultureFlowsInheritanceSourceCode = """ +#file sta.runsettings + + + + STA + + + +#file sta-timeout.runsettings + + + + STA + + + 10001 + 10002 + 10003 + 30004 + 10005 + 10006 + 10007 + + + +#file timeout.runsettings + + + + 10001 + 10002 + 10003 + 30004 + 10005 + 10006 + 10007 + + + +#file CultureFlowsInheritanceThreadContextProject.csproj + + + + Exe + true + $TargetFrameworks$ + latest + + + + + + + + + + + PreserveNewest + + + + + +#file UnitTest1.cs +namespace CultureFlowsExtraCasesThreadContextProject; + +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +public class ExpectedCultures +{ + public const string BaseClassInitCulture = "fr-FR"; + public const string BaseTestInitCulture = "hr-BA"; + public const string IntermediateClassInitCulture = "es-ES"; + public const string IntermediateTestInitCulture = "et-EE"; + public const string IntermediateTestCleanupCulture = "hu-HU"; + public const string IntermediateClassCleanupCulture = "vi-VN"; + public const string TestMethodCulture = "it-IT"; +} + +public class BaseClassWithInheritance +{ + private static TestContext _testContext; + + [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] + public static void BaseClassInitialize(TestContext testContext) + { + if (_testContext is not null) + { + throw new InvalidOperationException($"Was expected to be running tests sequentially but '{_testContext.ManagedMethod}' is still running when we received '{testContext.ManagedMethod}'"); + } + + _testContext = testContext; + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.BaseClassInitCulture); + } + + [ClassCleanup(InheritanceBehavior.BeforeEachDerivedClass, ClassCleanupBehavior.EndOfClass)] + public static void BaseClassCleanup() + { + switch (_testContext.ManagedMethod) + { + case "DerivedClassIntermediateClassWithoutInheritanceBaseClassWithInheritanceTestMethod": + if (Environment.GetEnvironmentVariable("MSTEST_TEST_FLOW_CONTEXT") == "true") + { + Assert.AreEqual(ExpectedCultures.BaseClassInitCulture, CultureInfo.CurrentCulture.Name); + } + else + { + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + } + break; + + case "DerivedClassIntermediateClassWithInheritanceBaseClassWithInheritanceTestMethod": + Assert.AreEqual(ExpectedCultures.IntermediateClassCleanupCulture, CultureInfo.CurrentCulture.Name); + break; + + default: + throw new NotSupportedException($"Unsupported method name '{_testContext.ManagedMethod}'"); + } + + _testContext = null; + } +} + +public class BaseClassWithoutInheritance +{ + [ClassInitialize(InheritanceBehavior.None)] + public static void BaseClassInitialize(TestContext testContext) + { + Assert.Fail("BaseClassWithoutInheritance.BaseClassInitialize should not have been called"); + } + + [ClassCleanup(InheritanceBehavior.None)] + public static void BaseClassCleanup() + { + Assert.Fail("BaseClassWithoutInheritance.BaseClassCleanup should not have been called"); + } +} + +public class IntermediateClassWithInheritanceBaseClassWithInheritance : BaseClassWithInheritance +{ + [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] + public static void IntermediateClassInitialize(TestContext testContext) + { + Assert.AreEqual(ExpectedCultures.BaseClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateClassInitCulture); + } + + [ClassCleanup(InheritanceBehavior.BeforeEachDerivedClass, ClassCleanupBehavior.EndOfClass)] + public static void IntermediateClassCleanup() + { + if (Environment.GetEnvironmentVariable("MSTEST_TEST_FLOW_CONTEXT") == "true") + { + Assert.AreEqual(ExpectedCultures.IntermediateClassInitCulture, CultureInfo.CurrentCulture.Name); + } + else + { + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + } + + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateClassCleanupCulture); + } +} + +public class IntermediateClassWithInheritanceBaseClassWithoutInheritance : BaseClassWithoutInheritance +{ + [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] + public static void IntermediateClassInitialize(TestContext testContext) + { + Assert.AreNotEqual(ExpectedCultures.BaseClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateClassInitCulture); + } + + [ClassCleanup(InheritanceBehavior.BeforeEachDerivedClass, ClassCleanupBehavior.EndOfClass)] + public static void IntermediateClassCleanup() + { + if (Environment.GetEnvironmentVariable("MSTEST_TEST_FLOW_CONTEXT") == "true") + { + Assert.AreEqual(ExpectedCultures.IntermediateClassInitCulture, CultureInfo.CurrentCulture.Name); + } + else + { + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + } + + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateClassCleanupCulture); + } +} + +public class IntermediateClassWithoutInheritanceBaseClassWithInheritance : BaseClassWithInheritance +{ + [ClassInitialize(InheritanceBehavior.None)] + public static void IntermediateClassInitialize(TestContext testContext) + { + Assert.Fail("IntermediateClassWithoutInheritanceBaseClassWithInheritance.IntermediateClassInitialize should not have been called"); + } + + [ClassCleanup(InheritanceBehavior.None)] + public static void IntermediateClassCleanup() + { + Assert.Fail("IntermediateClassWithoutInheritanceBaseClassWithInheritance.IntermediateClassCleanup should not have been called"); + } +} + +public class IntermediateClassWithoutInheritanceBaseClassWithoutInheritance : BaseClassWithoutInheritance +{ + [ClassInitialize(InheritanceBehavior.None)] + public static void IntermediateClassInitialize(TestContext testContext) + { + Assert.Fail("IntermediateClassWithoutInheritanceBaseClassWithoutInheritance.IntermediateClassInitialize should not have been called"); + } + + [ClassCleanup(InheritanceBehavior.None)] + public static void IntermediateClassCleanup() + { + Assert.Fail("IntermediateClassWithoutInheritanceBaseClassWithoutInheritance.IntermediateClassCleanup should not have been called"); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithInheritanceBaseClassWithInheritance : IntermediateClassWithInheritanceBaseClassWithInheritance +{ + [TestMethod] + public void DerivedClassIntermediateClassWithInheritanceBaseClassWithInheritanceTestMethod() + { + Assert.AreEqual(ExpectedCultures.IntermediateClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithInheritanceBaseClassWithoutInheritance : IntermediateClassWithInheritanceBaseClassWithoutInheritance +{ + [TestMethod] + public void DerivedClassIntermediateClassWithInheritanceBaseClassWithoutInheritanceTestMethod() + { + Assert.AreEqual(ExpectedCultures.IntermediateClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithoutInheritanceBaseClassWithInheritance : IntermediateClassWithoutInheritanceBaseClassWithInheritance +{ + [TestMethod] + public void DerivedClassIntermediateClassWithoutInheritanceBaseClassWithInheritanceTestMethod() + { + Assert.AreEqual(ExpectedCultures.BaseClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithoutInheritanceBaseClassWithoutInheritance : IntermediateClassWithoutInheritanceBaseClassWithoutInheritance +{ + [TestMethod] + public void DerivedClassIntermediateClassWithoutInheritanceBaseClassWithoutInheritanceTestMethod() + { + Assert.AreNotEqual(ExpectedCultures.IntermediateClassInitCulture, CultureInfo.CurrentCulture.Name); + Assert.AreNotEqual(ExpectedCultures.BaseClassInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +public class BaseClassWithTestInitCleanup +{ + public TestContext TestContext { get; set; } + + [TestInitialize] + public void BaseTestInitialize() + { + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.BaseTestInitCulture); + } + + [TestCleanup] + public void BaseTestCleanup() + { + switch (TestContext.ManagedMethod) + { + case "DerivedClassIntermediateClassWithTestInitCleanupBaseClassWithTestInitCleanupTestMethod": + Assert.AreEqual(ExpectedCultures.IntermediateTestCleanupCulture, CultureInfo.CurrentCulture.Name); + break; + + case "DerivedClassIntermediateClassWithoutTestInitCleanupBaseClassWithTestInitCleanupTestMethod": + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + break; + + default: + throw new NotSupportedException($"Unsupported method name '{TestContext.ManagedMethod}'"); + } + } +} + +public class BaseClassWithoutTestInitCleanup +{ +} + +public class IntermediateClassWithTestInitCleanupBaseClassWithTestInitCleanup : BaseClassWithTestInitCleanup +{ + [TestInitialize] + public void IntermediateTestInitialize() + { + Assert.AreEqual(ExpectedCultures.BaseTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateTestInitCulture); + } + + [TestCleanup] + public void IntermediateTestCleanup() + { + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateTestCleanupCulture); + } +} + +public class IntermediateClassWithoutTestInitCleanupBaseClassWithTestInitCleanup : BaseClassWithTestInitCleanup +{ +} + +public class IntermediateClassWithTestInitCleanupBaseClassWithoutTestInitCleanup : BaseClassWithoutTestInitCleanup +{ + [TestInitialize] + public void IntermediateTestInitialize() + { + Assert.AreNotEqual(ExpectedCultures.BaseTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateTestInitCulture); + } + + [TestCleanup] + public void IntermediateTestCleanup() + { + Assert.AreEqual(ExpectedCultures.TestMethodCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.IntermediateTestCleanupCulture); + } +} + +public class IntermediateClassWithoutTestInitCleanupBaseClassWithoutTestInitCleanup : BaseClassWithoutTestInitCleanup +{ +} + +[TestClass] +public class DerivedClassIntermediateClassWithTestInitCleanupBaseClassWithTestInitCleanup : IntermediateClassWithTestInitCleanupBaseClassWithTestInitCleanup +{ + [TestMethod] + public void DerivedClassIntermediateClassWithTestInitCleanupBaseClassWithTestInitCleanupTestMethod() + { + Assert.AreEqual(ExpectedCultures.IntermediateTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + + +[TestClass] +public class DerivedClassIntermediateClassWithTestInitCleanupBaseClassWithoutTestInitCleanup : IntermediateClassWithTestInitCleanupBaseClassWithoutTestInitCleanup +{ + [TestMethod] + public void DerivedClassIntermediateClassWithTestInitCleanupBaseClassWithoutTestInitCleanupTestMethod() + { + Assert.AreEqual(ExpectedCultures.IntermediateTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithoutTestInitCleanupBaseClassWithTestInitCleanup : IntermediateClassWithoutTestInitCleanupBaseClassWithTestInitCleanup +{ + [TestMethod] + public void DerivedClassIntermediateClassWithoutTestInitCleanupBaseClassWithTestInitCleanupTestMethod() + { + Assert.AreEqual(ExpectedCultures.BaseTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} + +[TestClass] +public class DerivedClassIntermediateClassWithoutTestInitCleanupBaseClassWithoutTestInitCleanup : IntermediateClassWithoutTestInitCleanupBaseClassWithoutTestInitCleanup +{ + [TestMethod] + public void DerivedClassIntermediateClassWithoutTestInitCleanupBaseClassWithoutTestInitCleanupTestMethod() + { + Assert.AreNotEqual(ExpectedCultures.IntermediateTestInitCulture, CultureInfo.CurrentCulture.Name); + Assert.AreNotEqual(ExpectedCultures.BaseTestInitCulture, CultureInfo.CurrentCulture.Name); + CultureInfo.CurrentCulture = new CultureInfo(ExpectedCultures.TestMethodCulture); + } +} +"""; + + public string InitToTestProjectPath => GetAssetPath(InitToTestProjectName); + + public string CultureFlowsProjectPath => GetAssetPath(CultureFlowsProjectName); + + public string CultureFlowsInheritanceProjectPath => GetAssetPath(CultureFlowsInheritanceProjectName); public override IEnumerable<(string ID, string Name, string Code)> GetAssetsToGenerate() { - yield return (ProjectName, ProjectName, - SourceCode + yield return (InitToTestProjectName, InitToTestProjectName, + InitToTestSourceCode + .PatchTargetFrameworks(TargetFrameworks.All) + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion)); + + yield return (CultureFlowsProjectName, CultureFlowsProjectName, + CultureFlowsSourceCode + .PatchTargetFrameworks(TargetFrameworks.All) + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion)); + + yield return (CultureFlowsInheritanceProjectName, CultureFlowsInheritanceProjectName, + CultureFlowsInheritanceSourceCode .PatchTargetFrameworks(TargetFrameworks.All) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion)); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/testsbaseline.txt b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/testsbaseline.txt index 9aefa86b38..a7ea71d8ac 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/testsbaseline.txt +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/testsbaseline.txt @@ -162,8 +162,6 @@ MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.R MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_CentralPackageManagement_Standalone(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Release) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_DotnetTest(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Debug) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_DotnetTest(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Release) -MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Debug) -MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Release) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Enable_Default_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, bool) (disabled,Debug,CodeCoverage) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Enable_Default_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, bool) (disabled,Release,CodeCoverage) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Enable_Default_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, bool) (enabled,Debug,CodeCoverage) @@ -180,6 +178,8 @@ MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.R MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Selectively_Enabled_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, string, string, string) (multitfm,Release,HangDump) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Selectively_Enabled_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, string, string, string) (multitfm,Release,Retry) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone_Selectively_Enabled_Extensions(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration, string, string, string) (multitfm,Release,TrxReport) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Debug) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_MSTestRunner_Standalone(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Release) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_VSTest(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Debug) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.SdkTests.RunTests_With_VSTest(string, Microsoft.Testing.TestInfrastructure.BuildConfiguration) (multitfm,Release) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.TestDiscoveryTests.DiscoverTests_FindsAllTests(string) (net462) @@ -190,6 +190,26 @@ MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.TestDiscov MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.TestDiscoveryTests.DiscoverTests_WithFilter_FindsOnlyFilteredOnes(string) (net6.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.TestDiscoveryTests.DiscoverTests_WithFilter_FindsOnlyFilteredOnes(string) (net7.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.TestDiscoveryTests.DiscoverTests_WithFilter_FindsOnlyFilteredOnes(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_Inheritance_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net8.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenChangedInAssemblyInitialize_IsPassedToTestMethod(string) (net462) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenChangedInAssemblyInitialize_IsPassedToTestMethod(string) (net6.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenChangedInAssemblyInitialize_IsPassedToTestMethod(string) (net7.0) @@ -206,6 +226,18 @@ MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadCont MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenCultureIsNotSet_TestMethodFails(string) (net6.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenCultureIsNotSet_TestMethodFails(string) (net7.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenCultureIsNotSet_TestMethodFails(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThread_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingSTAThreadAndTimeout_CurrentCultureFlowsBetweenMethods(string) (net8.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net462) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net6.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net7.0) +MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadContextTests.ThreadingContext_WhenUsingTimeout_CurrentCultureFlowsBetweenMethods(string) (net8.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadingTests.LifecycleAttributesTaskThreading_WhenMainIsNotSTA_RunsettingsAsksForSTA_OnWindows_ThreadIsSTA(string) (net462) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadingTests.LifecycleAttributesTaskThreading_WhenMainIsNotSTA_RunsettingsAsksForSTA_OnWindows_ThreadIsSTA(string) (net6.0) MSTest.Acceptance.IntegrationTests.MSTest.Acceptance.IntegrationTests.ThreadingTests.LifecycleAttributesTaskThreading_WhenMainIsNotSTA_RunsettingsAsksForSTA_OnWindows_ThreadIsSTA(string) (net7.0)