Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flow execution context across fixture methods when using timeout #2843

Merged
merged 6 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/Adapter/MSTest.TestAdapter/Execution/TestAssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
23 changes: 15 additions & 8 deletions src/Adapter/MSTest.TestAdapter/Execution/TestClassInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<MethodInfo>(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;
Expand Down Expand Up @@ -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<MethodInfo>(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;
Expand Down Expand Up @@ -535,19 +541,20 @@ 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))
{
timeout = localTimeout;
}

return MethodRunner.RunWithTimeoutAndCancellation(
return FixtureMethodRunner.RunWithTimeoutAndCancellation(
() => methodInfo.InvokeAsSynchronousTask(null),
new CancellationTokenSource(),
timeout,
methodInfo,
new ClassExecutionContextScope(ClassType, remainingCleanupCount),
Resource.ClassCleanupWasCancelled,
Resource.ClassCleanupTimedOut);
}
Expand Down
31 changes: 24 additions & 7 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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<MethodInfo>(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
Expand Down Expand Up @@ -635,28 +650,30 @@ 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))
{
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -42,7 +45,7 @@ internal static class MethodRunner
{
try
{
action();
ExecutionContextService.RunActionOnContext(action, executionContextScope);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading