Skip to content

Commit

Permalink
More improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Evangelink committed Dec 26, 2024
1 parent 3da2c4f commit b0e0475
Show file tree
Hide file tree
Showing 9 changed files with 966 additions and 798 deletions.
59 changes: 40 additions & 19 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
[Obsolete(Constants.PublicTypeObsoleteMessage)]
#endif
#endif
public class TestMethodInfo : ITestMethod
public class TestMethodInfo : ITestMethod, IDisposable
{
/// <summary>
/// Specifies the timeout when it is not set in a test case.
Expand All @@ -32,6 +32,7 @@ public class TestMethodInfo : ITestMethod
private object? _classInstance;
private bool _isTestContextSet;
private bool _isTestCleanupInvoked;
private CancellationTokenSource? _timeoutTokenSource;

internal TestMethodInfo(
MethodInfo testMethod,
Expand Down Expand Up @@ -482,6 +483,15 @@ private void RunTestCleanupMethod(TestResult result)
{
try
{
// Reset the cancellation token source to avoid cancellation of cleanup methods because of the init or test method cancellation.
TestMethodOptions.TestContext!.Context.CancellationTokenSource = new CancellationTokenSource();

// 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(TestMethodOptions.TestContext.Context.CancellationTokenSource.Cancel);
}

// Test cleanups are called in the order of discovery
// Current TestClass -> Parent -> Grandparent
testCleanupException = testCleanupMethod is not null
Expand Down Expand Up @@ -782,18 +792,27 @@ private bool SetTestContext(object classInstance, TestResult result)
// In most cases, exception will be TargetInvocationException with real exception wrapped
// in the InnerException; or user code throws an exception.
// It also seems that in rare cases the ex can be null.
Exception actualException = ex.InnerException ?? ex;
string exceptionMessage = actualException.GetFormattedExceptionMessage();
StackTraceInformation? stackTraceInfo = actualException.GetStackTraceInformation();
Exception realException = ex.GetRealException();

string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_InstanceCreationError,
TestClassName,
exceptionMessage);
if (realException.IsOperationCanceledExceptionFromToken(TestMethodOptions.TestContext!.Context.CancellationTokenSource.Token))
{
result.Outcome = UTF.UnitTestOutcome.Timeout;
result.TestFailureException = new TestFailedException(ObjectModelUnitTestOutcome.Timeout, string.Format(CultureInfo.CurrentCulture, Resource.Execution_Test_Timeout, TestMethodName));
}
else
{
string exceptionMessage = realException.GetFormattedExceptionMessage();
StackTraceInformation? stackTraceInfo = realException.GetStackTraceInformation();

result.Outcome = UTF.UnitTestOutcome.Failed;
result.TestFailureException = new TestFailedException(ObjectModelUnitTestOutcome.Failed, errorMessage, stackTraceInfo);
string errorMessage = string.Format(
CultureInfo.CurrentCulture,
Resource.UTA_InstanceCreationError,
TestClassName,
exceptionMessage);

result.Outcome = UTF.UnitTestOutcome.Failed;
result.TestFailureException = new TestFailedException(ObjectModelUnitTestOutcome.Failed, errorMessage, stackTraceInfo);
}
}

return classInstance;
Expand All @@ -811,12 +830,11 @@ private TestResult ExecuteInternalWithTimeout(object?[]? arguments)

if (TestMethodOptions.TimeoutInfo.CooperativeCancellation)
{
CancellationTokenSource? timeoutTokenSource = null;
try
{
timeoutTokenSource = new(TestMethodOptions.TimeoutInfo.Timeout);
timeoutTokenSource.Token.Register(TestMethodOptions.TestContext!.Context.CancellationTokenSource.Cancel);
if (timeoutTokenSource.Token.IsCancellationRequested)
_timeoutTokenSource = new(TestMethodOptions.TimeoutInfo.Timeout);
_timeoutTokenSource.Token.Register(TestMethodOptions.TestContext!.Context.CancellationTokenSource.Cancel);
if (_timeoutTokenSource.Token.IsCancellationRequested)
{
return new()
{
Expand All @@ -840,15 +858,16 @@ private TestResult ExecuteInternalWithTimeout(object?[]? arguments)
Outcome = UTF.UnitTestOutcome.Timeout,
TestFailureException = new TestFailedException(
ObjectModelUnitTestOutcome.Timeout,
timeoutTokenSource.Token.IsCancellationRequested
_timeoutTokenSource.Token.IsCancellationRequested
? string.Format(CultureInfo.CurrentCulture, Resource.Execution_Test_Timeout, TestMethodName)
: string.Format(CultureInfo.CurrentCulture, Resource.Execution_Test_Cancelled, TestMethodName)),
};
}
}
finally
{
timeoutTokenSource?.Dispose();
_timeoutTokenSource?.Dispose();
_timeoutTokenSource = null;
}
}

Expand All @@ -867,8 +886,7 @@ void ExecuteAsyncAction()
}
}

CancellationToken cancelToken = TestMethodOptions.TestContext!.Context.CancellationTokenSource.Token;
if (PlatformServiceProvider.Instance.ThreadOperations.Execute(ExecuteAsyncAction, TestMethodOptions.TimeoutInfo.Timeout, cancelToken))
if (PlatformServiceProvider.Instance.ThreadOperations.Execute(ExecuteAsyncAction, TestMethodOptions.TimeoutInfo.Timeout, TestMethodOptions.TestContext!.Context.CancellationTokenSource.Token))
{
if (failure != null)
{
Expand Down Expand Up @@ -902,4 +920,7 @@ void ExecuteAsyncAction()
RunTestCleanupMethod(timeoutResult);
return timeoutResult;
}

void IDisposable.Dispose()
=> _timeoutTokenSource?.Dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ internal static Exception GetRealException(this Exception exception)
return exception;
}

internal static bool IsOperationCanceledExceptionFromToken(this Exception ex, CancellationToken cancellationToken)
=> (ex is OperationCanceledException oce && oce.CancellationToken == cancellationToken)
|| (ex is AggregateException aggregateEx && aggregateEx.InnerExceptions.OfType<OperationCanceledException>().Any(oce => oce.CancellationToken == cancellationToken));

/// <summary>
/// Get the exception message if available, empty otherwise.
/// </summary>
Expand Down
18 changes: 5 additions & 13 deletions src/Adapter/MSTest.TestAdapter/Helpers/FixtureMethodRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ internal static class FixtureMethodRunner
}
catch (Exception ex)
{
Exception realException = ex.GetRealException();

if (realException is OperationCanceledException oce && oce.CancellationToken == cancellationTokenSource.Token)
if (ex.GetRealException().IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
return new(
UnitTestOutcome.Timeout,
Expand Down Expand Up @@ -82,9 +80,7 @@ internal static class FixtureMethodRunner
action();
return null;
}
catch (Exception ex) when
((ex is OperationCanceledException oce && oce.CancellationToken == cancellationTokenSource.Token)
|| (ex is AggregateException aggregateEx && aggregateEx.InnerExceptions.OfType<TaskCanceledException>().Any()))
catch (Exception ex) when (ex.IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
// Ideally we would like to check that the token of the exception matches cancellationTokenSource but TestContext
// instances are not well defined so we have to handle the exception entirely.
Expand Down Expand Up @@ -157,9 +153,7 @@ internal static class FixtureMethodRunner
methodInfo.Name,
timeout));
}
catch (Exception ex) when
((ex is OperationCanceledException oce && oce.CancellationToken == cancellationTokenSource.Token)
|| (ex is AggregateException aggregateEx && aggregateEx.InnerExceptions.OfType<TaskCanceledException>().Any()))
catch (Exception ex) when (ex.IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
return new(
UnitTestOutcome.Timeout,
Expand All @@ -177,7 +171,7 @@ internal static class FixtureMethodRunner
}
}

[System.Runtime.Versioning.SupportedOSPlatform("windows")]
[SupportedOSPlatform("windows")]
private static TestFailedException? RunWithTimeoutAndCancellationWithSTAThread(
Action action, CancellationTokenSource cancellationTokenSource, int timeout, MethodInfo methodInfo,
IExecutionContextScope executionContextScope, string methodCanceledMessageFormat, string methodTimedOutMessageFormat)
Expand Down Expand Up @@ -221,9 +215,7 @@ internal static class FixtureMethodRunner
methodInfo.Name,
timeout));
}
catch (Exception ex) when
((ex is OperationCanceledException oce && oce.CancellationToken == cancellationTokenSource.Token)
|| (ex is AggregateException aggregateEx && aggregateEx.InnerExceptions.OfType<TaskCanceledException>().Any()))
catch (Exception ex) when (ex.IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
return new(
UnitTestOutcome.Timeout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private static void CollectTestContextFieldsAssignedInConstructor(
}

if (operation is ISimpleAssignmentOperation assignmentOperation &&
assignmentOperation.Target is IMemberReferenceOperation { Member: IFieldSymbol { } candidateField } targetMemberReference &&
assignmentOperation.Target is IMemberReferenceOperation { Member: IFieldSymbol { } candidateField } &&
assignmentOperation.Value is IParameterReferenceOperation parameterReference &&
SymbolEqualityComparer.Default.Equals(parameterReference.Parameter, testContextParameter))
{
Expand Down
2 changes: 1 addition & 1 deletion src/TestFramework/TestFramework.Extensions/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public abstract class TestContext
/// <summary>
/// Gets or sets the cancellation token source. This token source is canceled when test times out. Also when explicitly canceled the test will be aborted.
/// </summary>
public virtual CancellationTokenSource CancellationTokenSource { get; protected set; } = new();
public virtual CancellationTokenSource CancellationTokenSource { get; protected internal set; } = new();

public object?[]? TestData { get; protected set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
<Using Include="Polyfills" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter" Key="$(VsPublicKey)" />
</ItemGroup>

<ItemGroup>
<None Update="build\winui\MSTest.TestFramework.targets">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
Loading

0 comments on commit b0e0475

Please sign in to comment.