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

Better handling of CTS and timeout tokens #4444

Merged
merged 11 commits into from
Dec 28, 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
143 changes: 92 additions & 51 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ internal static UnitTestResult[] ToUnitTestResults(this IReadOnlyCollection<UTF.
new TestFailedException(
outcome,
testFailureException.TryGetMessage(),
testFailureException is TestFailedException testException ? testException.StackTraceInformation : testFailureException.TryGetStackTraceInformation()))
testFailureException is TestFailedException testException
? testException.StackTraceInformation
: testFailureException.TryGetStackTraceInformation()))
: new UnitTestResult { Outcome = outcome };
unitTestResult.StandardOut = testResult.LogOutput;
unitTestResult.StandardError = testResult.LogError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ public static UnitTestOutcome ToUnitTestOutcome(this UTF.UnitTestOutcome framewo
_ => UnitTestOutcome.Error,
};

internal static UTF.UnitTestOutcome ToAdapterOutcome(this UnitTestOutcome outcome)
=> outcome switch
{
UnitTestOutcome.Failed => UTF.UnitTestOutcome.Failed,
UnitTestOutcome.Inconclusive => UTF.UnitTestOutcome.Inconclusive,
UnitTestOutcome.InProgress => UTF.UnitTestOutcome.InProgress,
UnitTestOutcome.Passed => UTF.UnitTestOutcome.Passed,
UnitTestOutcome.Timeout => UTF.UnitTestOutcome.Timeout,
UnitTestOutcome.NotRunnable => UTF.UnitTestOutcome.NotRunnable,
UnitTestOutcome.NotFound => UTF.UnitTestOutcome.NotFound,
_ => UTF.UnitTestOutcome.Error,
};

/// <summary>
/// Returns more important outcome of two.
/// </summary>
Expand Down
45 changes: 29 additions & 16 deletions src/Adapter/MSTest.TestAdapter/Helpers/FixtureMethodRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions;

using UnitTestOutcome = Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestOutcome;

Expand All @@ -14,8 +15,18 @@ internal static class FixtureMethodRunner
{
internal static TestFailedException? RunWithTimeoutAndCancellation(
Action action, CancellationTokenSource cancellationTokenSource, TimeoutInfo? timeoutInfo, MethodInfo methodInfo,
IExecutionContextScope executionContextScope, string methodCanceledMessageFormat, string methodTimedOutMessageFormat)
IExecutionContextScope executionContextScope, string methodCanceledMessageFormat, string methodTimedOutMessageFormat,
// When a test method is marked with [Timeout], this timeout is applied from ctor to destructor, so we need to take
// that into account when processing the OCE of the action.
(CancellationTokenSource TokenSource, int Timeout)? testTimeoutInfo = default)
{
if (cancellationTokenSource.Token.IsCancellationRequested)
Evangelink marked this conversation as resolved.
Show resolved Hide resolved
{
return new(
UnitTestOutcome.Timeout,
string.Format(CultureInfo.InvariantCulture, methodCanceledMessageFormat, methodInfo.DeclaringType!.FullName, methodInfo.Name));
}

// 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 (timeoutInfo is null)
Expand All @@ -27,13 +38,22 @@ 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,
string.Format(CultureInfo.InvariantCulture, methodCanceledMessageFormat, methodInfo.DeclaringType!.FullName, methodInfo.Name));
testTimeoutInfo?.TokenSource.Token.IsCancellationRequested == true
? string.Format(
CultureInfo.InvariantCulture,
methodTimedOutMessageFormat,
methodInfo.DeclaringType!.FullName,
methodInfo.Name,
testTimeoutInfo.Value.Timeout)
: string.Format(
CultureInfo.InvariantCulture,
methodCanceledMessageFormat,
methodInfo.DeclaringType!.FullName,
methodInfo.Name));
}

throw;
Expand Down Expand Up @@ -75,7 +95,7 @@ internal static class FixtureMethodRunner
action();
return null;
}
catch (OperationCanceledException)
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 @@ -148,9 +168,7 @@ internal static class FixtureMethodRunner
methodInfo.Name,
timeout));
}
catch (Exception ex) when
(ex is OperationCanceledException
|| (ex is AggregateException aggregateEx && aggregateEx.InnerExceptions.OfType<TaskCanceledException>().Any()))
catch (Exception ex) when (ex.IsOperationCanceledExceptionFromToken(cancellationTokenSource.Token))
{
return new(
UnitTestOutcome.Timeout,
Expand All @@ -168,7 +186,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 @@ -212,12 +230,7 @@ internal static class FixtureMethodRunner
methodInfo.Name,
timeout));
}
catch (Exception ex) when

// This exception occurs when the cancellation happens before the task is actually started.
((ex is TaskCanceledException tce && tce.CancellationToken == cancellationTokenSource.Token)
|| (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
4 changes: 2 additions & 2 deletions src/Adapter/MSTest.TestAdapter/Resources/Resource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Adapter/MSTest.TestAdapter/Resources/Resource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Execution_Test_Timeout" xml:space="preserve">
<value>Test '{0}' exceeded execution timeout period.</value>
<value>Test '{0}' timed out after {1}ms</value>
</data>
<data name="SourcesNotSupported" xml:space="preserve">
<value>Running tests in any of the provided sources is not supported for the selected platform</value>
Expand Down Expand Up @@ -308,7 +308,7 @@ Error: {1}</value>
<value>Cannot run test method '{0}.{1}': Method has parameters, but does not define any test source. Use '[DataRow]', '[DynamicData]', or a custom 'ITestDataSource' data source to provide test data.</value>
</data>
<data name="Execution_Test_Cancelled" xml:space="preserve">
<value>Test '{0}' execution has been aborted.</value>
<value>Test '{0}' was canceled</value>
</data>
<data name="CannotEnumerateIDataSourceAttribute" xml:space="preserve">
<value>Exception occurred while enumerating IDataSource attribute on "{0}.{1}": {2}</value>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ byl však přijat tento počet argumentů: {4} s typy {5}.</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">Test {0} překročil časový limit spuštění.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Chyba: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">Spouštění testu {0} se přerušilo.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">Spouštění testu {0} se přerušilo.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ aber empfing {4} Argument(e) mit den Typen „{5}“.</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">Der Test "{0}" hat das Ausführungstimeout überschritten.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Fehler: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">Die Ausführung des Tests "{0}" wurde abgebrochen.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">Die Ausführung des Tests "{0}" wurde abgebrochen.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ pero recibió {4} argumento(s), con los tipos "{5}".</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">La prueba '{0}' superó el tiempo de espera de ejecución.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Error: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">Se anuló la ejecución de la prueba "{0}".</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">Se anuló la ejecución de la prueba "{0}".</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ mais a reçu {4} argument(s), avec les types « {5} ».</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">Le test '{0}' a dépassé le délai d'attente de l'exécution.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Erreur : {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">L'exécution du test '{0}' a été abandonnée.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">L'exécution du test '{0}' a été abandonnée.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.it.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ ma ha ricevuto {4} argomenti, con tipi "{5}".</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">È stato superato il periodo di timeout per l'esecuzione del test '{0}'.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Errore: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">L'esecuzione del test '{0}' è stata interrotta.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">L'esecuzione del test '{0}' è stata interrotta.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.ja.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ but received {4} argument(s), with types '{5}'.</source>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">テスト '{0}' は実行タイムアウトを超えました。</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -419,9 +419,9 @@ Error: {1}</source>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">テスト '{0}' の実行が中止されました。</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">テスト '{0}' の実行が中止されました。</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.ko.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ but received {4} argument(s), with types '{5}'.</source>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">'{0}' 테스트가 실행 시간 제한을 초과했습니다.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Error: {1}</source>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">테스트 '{0}' 실행이 중단되었습니다.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">테스트 '{0}' 실행이 중단되었습니다.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
12 changes: 6 additions & 6 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.pl.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ ale liczba odebranych argumentów to {4} z typami „{5}”.</target>
<note />
</trans-unit>
<trans-unit id="Execution_Test_Timeout">
<source>Test '{0}' exceeded execution timeout period.</source>
<target state="translated">Test {0}” przekroczył okres limitu czasu na wykonanie.</target>
<note></note>
<source>Test '{0}' timed out after {1}ms</source>
<target state="new">Test '{0}' timed out after {1}ms</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
Expand Down Expand Up @@ -418,9 +418,9 @@ Błąd: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="Execution_Test_Cancelled">
<source>Test '{0}' execution has been aborted.</source>
<target state="translated">Wykonanie testu „{0}” zostało przerwane.</target>
<note></note>
<source>Test '{0}' was canceled</source>
<target state="needs-review-translation">Wykonanie testu „{0}” zostało przerwane.</target>
<note />
</trans-unit>
<trans-unit id="InvalidClassCleanupLifecycleValue">
<source>Invalid value '{0}' specified for 'ClassCleanupLifecycle'. Supported scopes are {1}.</source>
Expand Down
Loading