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

Support '--maximum-failed-tests' to abort test run when failure threshold is reached #4238

Merged
merged 67 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
12520ce
Support '--max-failed-tests' to abort test run when failure threshold…
Youssef1313 Dec 4, 2024
dd480c2
Address review comment
Youssef1313 Dec 4, 2024
e9a4dd8
Remove unused using
Youssef1313 Dec 4, 2024
a9e7d32
Progress
Youssef1313 Dec 4, 2024
f37ea38
Not ideal, and not working even. Keeping in history though
Youssef1313 Dec 4, 2024
ce36d36
Fix build error
Youssef1313 Dec 4, 2024
cb1d50a
Revert
Youssef1313 Dec 5, 2024
0717a76
Use a capability
Youssef1313 Dec 5, 2024
bd63803
Take a different approach
Youssef1313 Dec 5, 2024
46b7577
Progress for policies service
Youssef1313 Dec 5, 2024
be7c6cc
Use GetRequiredService
Youssef1313 Dec 5, 2024
db432ef
Progress
Youssef1313 Dec 5, 2024
c7b1392
Fix warning
Youssef1313 Dec 5, 2024
624f86f
Progress
Youssef1313 Dec 6, 2024
5ef6a17
Progress
Youssef1313 Dec 6, 2024
e9ce6da
Merge branch 'main' into max-failed
Youssef1313 Dec 6, 2024
c53c7d5
Remove done TODO
Youssef1313 Dec 6, 2024
7571531
Cleanup
Youssef1313 Dec 6, 2024
1d245ec
Small fixes
Youssef1313 Dec 6, 2024
993f949
Experimental
Youssef1313 Dec 6, 2024
5496617
Register only when enabled
Youssef1313 Dec 6, 2024
db426ed
Fix build
Youssef1313 Dec 6, 2024
f66be1d
Small progress
Youssef1313 Dec 6, 2024
3240e68
Renames
Youssef1313 Dec 6, 2024
13ea139
Cleanup
Youssef1313 Dec 6, 2024
db0fded
Update tests
Youssef1313 Dec 6, 2024
efa34f1
Bit of progress
Youssef1313 Dec 6, 2024
e3ecc85
Adjust messaging
Youssef1313 Dec 6, 2024
ceae0cc
More progress
Youssef1313 Dec 6, 2024
1634d40
Small fix
Youssef1313 Dec 6, 2024
d2a2c82
Fix test failures
Youssef1313 Dec 6, 2024
60f6e41
Adjust cancellation
Youssef1313 Dec 7, 2024
0a9999a
Adjust for potential race conditions
Youssef1313 Dec 7, 2024
bf81e20
Rename
Youssef1313 Dec 9, 2024
5fb51f4
MSTest side of the feature
Youssef1313 Dec 9, 2024
f928c07
Progress
Youssef1313 Dec 9, 2024
43ba49e
Minor fixes
Youssef1313 Dec 9, 2024
7199f00
Cleanup
Youssef1313 Dec 9, 2024
f57c0a6
TODO:
Youssef1313 Dec 9, 2024
3a4f0ca
Update help tests
Youssef1313 Dec 9, 2024
a249f8f
Fix build
Youssef1313 Dec 9, 2024
6f84743
Fix build errors
Youssef1313 Dec 9, 2024
7b5b396
Fix one more build error
Youssef1313 Dec 9, 2024
8ec5e06
Fix PublicAPI.Unshipped.txt
Youssef1313 Dec 9, 2024
8a23799
Rename capability on MSTest side
Youssef1313 Dec 9, 2024
e0dbaf1
Remove outdated TODO
Youssef1313 Dec 9, 2024
92f3d8d
Rename public method, add test
Youssef1313 Dec 9, 2024
f4732db
Progress
Youssef1313 Dec 9, 2024
ef5bdb9
Doc
Youssef1313 Dec 9, 2024
22d846b
Small progress
Youssef1313 Dec 9, 2024
43d3b21
Fix tests
Youssef1313 Dec 10, 2024
4942254
Remove unused using
Youssef1313 Dec 10, 2024
264eb26
Cleanup
Youssef1313 Dec 10, 2024
f2dfe03
Add comments
Youssef1313 Dec 10, 2024
e7318df
Add test for MSTest
Youssef1313 Dec 10, 2024
f134a13
Address feedback
Youssef1313 Dec 10, 2024
3d35495
Fix build error
Youssef1313 Dec 10, 2024
529c521
Progress
Youssef1313 Dec 10, 2024
7e56af8
Fix test
Youssef1313 Dec 10, 2024
2c1b69a
Fix test
Youssef1313 Dec 10, 2024
612a94c
Fix formatting
Youssef1313 Dec 10, 2024
4f3e7d0
Adjust unit tests per product change
Youssef1313 Dec 10, 2024
17fb777
Try to stabilize test
Youssef1313 Dec 10, 2024
639ba17
Merge branch 'main' into max-failed
Youssef1313 Dec 10, 2024
ffad719
Small fix
Youssef1313 Dec 10, 2024
9e49602
Fix test
Youssef1313 Dec 11, 2024
93edab9
Merge branch 'main' into max-failed
Youssef1313 Dec 11, 2024
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
15 changes: 15 additions & 0 deletions src/Adapter/MSTest.TestAdapter/Execution/ClassCleanupManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,19 @@ public void MarkTestComplete(TestMethodInfo testMethodInfo, TestMethod testMetho
ShouldRunEndOfAssemblyCleanup = _remainingTestsByClass.IsEmpty;
}
}

internal static void ForceCleanup(TypeCache typeCache)
{
IEnumerable<TestClassInfo> classInfoCache = typeCache.ClassInfoListWithExecutableCleanupMethods;
foreach (TestClassInfo classInfo in classInfoCache)
{
classInfo.ExecuteClassCleanup();
}

IEnumerable<TestAssemblyInfo> assemblyInfoCache = typeCache.AssemblyInfoListWithExecutableCleanupMethods;
foreach (TestAssemblyInfo assemblyInfo in assemblyInfoCache)
{
assemblyInfo.ExecuteAssemblyCleanup();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ private void ExecuteTestsInSource(IEnumerable<TestCase> tests, IRunContext? runC
ExecuteTestsWithTestRunner(testsToRun, frameworkHandle, source, sourceLevelParameters, testRunner);
}

if (MSTestGracefulStopTestExecutionCapability.Instance.IsStopRequested)
{
testRunner.ForceCleanup();
}

PlatformServiceProvider.Instance.AdapterTraceLogger.LogInfo("Executed tests belonging to source {0}", source);
}

Expand All @@ -419,6 +424,10 @@ private void ExecuteTestsWithTestRunner(
foreach (TestCase currentTest in orderedTests)
{
_testRunCancellationToken?.ThrowIfCancellationRequested();
if (MSTestGracefulStopTestExecutionCapability.Instance.IsStopRequested)
{
break;
}

// If it is a fixture test, add it to the list of fixture tests and do not execute it.
// It is executed by test itself.
Expand Down
2 changes: 2 additions & 0 deletions src/Adapter/MSTest.TestAdapter/Execution/UnitTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,6 @@ private bool IsTestMethodRunnable(
notRunnableResult = null;
return true;
}

internal void ForceCleanup() => ClassCleanupManager.ForceCleanup(_typeCache);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.Capabilities.TestFramework;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

#pragma warning disable TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
internal sealed class MSTestGracefulStopTestExecutionCapability : IGracefulStopTestExecutionCapability
#pragma warning restore TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
{
private MSTestGracefulStopTestExecutionCapability()
{
}

public static MSTestGracefulStopTestExecutionCapability Instance { get; } = new();

public bool IsStopRequested { get; private set; }

public Task StopTestExecutionAsync(CancellationToken cancellationToken)
{
IsStopRequested = true;
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Testing.Extensions.VSTestBridge.Helpers;
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Services;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -20,12 +21,16 @@ public static void AddMSTest(this ITestApplicationBuilder testApplicationBuilder
testApplicationBuilder.AddRunSettingsService(extension);
testApplicationBuilder.AddTestCaseFilterService(extension);
testApplicationBuilder.AddTestRunParametersService(extension);
#pragma warning disable TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
testApplicationBuilder.AddMaximumFailedTestsService(extension);
#pragma warning restore TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
testApplicationBuilder.AddRunSettingsEnvironmentVariableProvider(extension);
testApplicationBuilder.RegisterTestFramework(
serviceProvider => new TestFrameworkCapabilities(
new VSTestBridgeExtensionBaseCapabilities(),
#pragma warning disable TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
new MSTestBannerCapability(serviceProvider.GetRequiredService<IPlatformInformation>())),
new MSTestBannerCapability(serviceProvider.GetRequiredService<IPlatformInformation>()),
MSTestGracefulStopTestExecutionCapability.Instance),
#pragma warning restore TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
(capabilities, serviceProvider) => new MSTestBridgedTestFramework(extension, getTestAssemblies, serviceProvider, capabilities));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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;

namespace Microsoft.Testing.Platform.Capabilities.TestFramework;

/// <summary>
/// A capability to support stopping test execution gracefully, without cancelling/aborting everything.
/// This is used to support '--maximum-failed-tests'.
/// </summary>
/// <remarks>
/// Test frameworks can choose to run any needed cleanup when cancellation is requested.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IGracefulStopTestExecutionCapability : ITestFrameworkCapability
{
Task StopTestExecutionAsync(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.Globalization;

using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.CommandLine;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Resources;

namespace Microsoft.Testing.Platform.CommandLine;

internal sealed class MaxFailedTestsCommandLineOptionsProvider(IExtension extension) : ICommandLineOptionsProvider
{
internal const string MaxFailedTestsOptionKey = "maximum-failed-tests";

private static readonly IReadOnlyCollection<CommandLineOption> OptionsCache =
[
new(MaxFailedTestsOptionKey, PlatformResources.PlatformCommandLineMaxFailedTestsOptionDescription, ArgumentArity.ExactlyOne, isHidden: false),
];

public string Uid => extension.Uid;

public string Version => extension.Version;

public string DisplayName => extension.DisplayName;

public string Description => extension.Description;

public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions()
=> OptionsCache;

public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
=> ValidationResult.ValidTask;

public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments)
{
if (commandOption.Name == MaxFailedTestsOptionKey)
{
string arg = arguments[0];
// We consider --maximum-failed-tests 0 as valid.
// The idea is that we stop the execution when we *exceed* the max failed tests, not when *reach*.
// So zero means, stop execution on the first failure.
return int.TryParse(arg, out int maxFailedTestsResult) && maxFailedTestsResult >= 0
? ValidationResult.ValidTask
: ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, PlatformResources.MaxFailedTestsMustBePositive, arg));
}

throw ApplicationStateGuard.Unreachable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestHost;
using Microsoft.Testing.Platform.Helpers;
using Microsoft.Testing.Platform.Messages;
using Microsoft.Testing.Platform.Resources;
using Microsoft.Testing.Platform.Services;

namespace Microsoft.Testing.Platform.Extensions;

internal sealed class AbortForMaxFailedTestsExtension : IDataConsumer
{
private readonly int? _maxFailedTests;
private readonly IGracefulStopTestExecutionCapability? _capability;
private readonly IStopPoliciesService _policiesService;
private readonly ITestApplicationCancellationTokenSource _testApplicationCancellationTokenSource;
private int _failCount;

public AbortForMaxFailedTestsExtension(
ICommandLineOptions commandLineOptions,
IGracefulStopTestExecutionCapability? capability,
IStopPoliciesService policiesService,
ITestApplicationCancellationTokenSource testApplicationCancellationTokenSource)
{
if (commandLineOptions.TryGetOptionArgumentList(MaxFailedTestsCommandLineOptionsProvider.MaxFailedTestsOptionKey, out string[]? args) &&
int.TryParse(args[0], out int maxFailedTests) &&
maxFailedTests >= 0)
{
_maxFailedTests = maxFailedTests;
}

_capability = capability;
_policiesService = policiesService;
_testApplicationCancellationTokenSource = testApplicationCancellationTokenSource;
}

public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage)];

/// <inheritdoc />
public string Uid { get; } = nameof(AbortForMaxFailedTestsExtension);

/// <inheritdoc />
public string Version { get; } = AppVersion.DefaultSemVer;

/// <inheritdoc />
public string DisplayName { get; } = nameof(AbortForMaxFailedTestsExtension);

/// <inheritdoc />
public string Description { get; } = PlatformResources.AbortForMaxFailedTestsDescription;

/// <inheritdoc />
public Task<bool> IsEnabledAsync() => Task.FromResult(_maxFailedTests.HasValue && _capability is not null);

public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var node = (TestNodeUpdateMessage)value;

// If we are called, the extension is enabled, which means both _maxFailedTests and capability are not null.
RoslynDebug.Assert(_maxFailedTests is not null);
RoslynDebug.Assert(_capability is not null);

TestNodeStateProperty testNodeStateProperty = node.TestNode.Properties.Single<TestNodeStateProperty>();
if (TestNodePropertiesCategories.WellKnownTestNodeTestRunOutcomeFailedProperties.Any(t => t == testNodeStateProperty.GetType()) &&
++_failCount > _maxFailedTests.Value &&
// If already triggered, don't do it again.
!_policiesService.IsMaxFailedTestsTriggered)
{
await _capability.StopTestExecutionAsync(_testApplicationCancellationTokenSource.CancellationToken);
await _policiesService.ExecuteMaxFailedTestsCallbacksAsync(_maxFailedTests.Value, _testApplicationCancellationTokenSource.CancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ internal static class ExitCodes
public const int TestAdapterTestSessionFailure = 10;
public const int DependentProcessExited = 11;
public const int IncompatibleProtocolVersion = 12;
public const int TestExecutionStoppedForMaxFailedTests = 13;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public static class TestApplicationBuilderExtensions
{
public static void AddTreeNodeFilterService(this ITestApplicationBuilder testApplicationBuilder, IExtension extension)
=> testApplicationBuilder.CommandLine.AddProvider(() => new TreeNodeFilterCommandLineOptionsProvider(extension));

/// <summary>
/// Registers the command-line options provider for '--maximum-failed-tests'.
/// </summary>
/// <param name="builder">The test application builder.</param>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public static void AddMaximumFailedTestsService(this ITestApplicationBuilder builder, IExtension extension)
=> builder.CommandLine.AddProvider(() => new MaxFailedTestsCommandLineOptionsProvider(extension));
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ public async Task<ITestHost> BuildAsync(
// Set the concrete command line options to the proxy.
commandLineOptionsProxy.SetCommandLineOptions(commandLineHandler);

// This is needed by output device.
var policiesService = new StopPoliciesService(testApplicationCancellationTokenSource);
serviceProvider.AddService(policiesService);

bool hasServerFlag = commandLineHandler.TryGetOptionArgumentList(PlatformCommandLineProvider.ServerOptionKey, out string[]? protocolName);
bool isJsonRpcProtocol = protocolName is null || protocolName.Length == 0 || protocolName[0].Equals(PlatformCommandLineProvider.JsonRpcProtocolName, StringComparison.OrdinalIgnoreCase);

Expand Down Expand Up @@ -313,9 +317,9 @@ public async Task<ITestHost> BuildAsync(
// Register the ITestApplicationResult
TestApplicationResult testApplicationResult = new(
proxyOutputDevice,
serviceProvider.GetTestApplicationCancellationTokenSource(),
serviceProvider.GetCommandLineOptions(),
serviceProvider.GetEnvironment());
serviceProvider.GetEnvironment(),
policiesService);
serviceProvider.AddService(testApplicationResult);

// ============= SETUP COMMON SERVICE USED IN ALL MODES END ===============//
Expand Down Expand Up @@ -376,6 +380,8 @@ await LogTestHostCreatedAsync(
TestHostOrchestratorConfiguration testHostOrchestratorConfiguration = await TestHostOrchestratorManager.BuildAsync(serviceProvider);
if (testHostOrchestratorConfiguration.TestHostOrchestrators.Length > 0 && !commandLineHandler.IsOptionSet(PlatformCommandLineProvider.DiscoverTestsOptionKey))
{
policiesService.ProcessRole = TestProcessRole.TestHostOrchestrator;
await proxyOutputDevice.HandleProcessRoleAsync(TestProcessRole.TestHostOrchestrator);
return new TestHostOrchestratorHost(testHostOrchestratorConfiguration, serviceProvider);
}

Expand Down Expand Up @@ -411,6 +417,8 @@ await LogTestHostCreatedAsync(
if (testHostControllers.RequireProcessRestart)
{
testHostControllerInfo.IsCurrentProcessTestHostController = true;
policiesService.ProcessRole = TestProcessRole.TestHostController;
await proxyOutputDevice.HandleProcessRoleAsync(TestProcessRole.TestHostController);
TestHostControllersTestHost testHostControllersTestHost = new(testHostControllers, testHostControllersServiceProvider, passiveNode, systemEnvironment, loggerFactory, systemClock);

await LogTestHostCreatedAsync(
Expand All @@ -424,6 +432,8 @@ await LogTestHostCreatedAsync(
}

// ======= TEST HOST MODE ======== //
policiesService.ProcessRole = TestProcessRole.TestHost;
await proxyOutputDevice.HandleProcessRoleAsync(TestProcessRole.TestHost);

// Setup the test host working folder.
// Out of the test host controller extension the current working directory is the test host working directory.
Expand Down Expand Up @@ -724,6 +734,17 @@ private async Task<ITestFramework> BuildTestFrameworkAsync(TestFrameworkBuilderD
dataConsumersBuilder.Add(pushOnlyProtocolDataConsumer);
}

var abortForMaxFailedTestsExtension = new AbortForMaxFailedTestsExtension(
serviceProvider.GetCommandLineOptions(),
serviceProvider.GetTestFrameworkCapabilities().GetCapability<IGracefulStopTestExecutionCapability>(),
serviceProvider.GetRequiredService<IStopPoliciesService>(),
serviceProvider.GetTestApplicationCancellationTokenSource());

if (await abortForMaxFailedTestsExtension.IsEnabledAsync())
{
dataConsumersBuilder.Add(abortForMaxFailedTestsExtension);
}

IDataConsumer[] dataConsumerServices = dataConsumersBuilder.ToArray();

// Build the message bus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ internal interface IPlatformOutputDevice : IExtension
Task DisplayAfterSessionEndRunAsync();

Task DisplayAsync(IOutputDeviceDataProducer producer, IOutputDeviceData data);

Task HandleProcessRoleAsync(TestProcessRole processRole);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.ServerMode;
using Microsoft.Testing.Platform.Services;

Expand Down Expand Up @@ -33,7 +34,9 @@ internal async Task<ProxyOutputDevice> BuildAsync(ServiceProvider serviceProvide
return new ProxyOutputDevice(
nonServerOutputDevice,
useServerModeOutputDevice
? new ServerModePerCallOutputDevice(serviceProvider)
? new ServerModePerCallOutputDevice(
serviceProvider.GetService<FileLoggerProvider>(),
serviceProvider.GetRequiredService<IStopPoliciesService>())
: null);
}

Expand All @@ -51,5 +54,6 @@ public static TerminalOutputDevice GetDefaultTerminalOutputDevice(ServiceProvide
serviceProvider.GetCommandLineOptions(),
serviceProvider.GetFileLoggerInformation(),
serviceProvider.GetLoggerFactory(),
serviceProvider.GetClock());
serviceProvider.GetClock(),
serviceProvider.GetRequiredService<IStopPoliciesService>());
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ internal async Task InitializeAsync(ServerTestHost serverTestHost)
await _serverModeOutputDevice.InitializeAsync(serverTestHost);
}
}

internal async Task HandleProcessRoleAsync(TestProcessRole processRole)
{
await OriginalOutputDevice.HandleProcessRoleAsync(processRole);
if (_serverModeOutputDevice is not null)
{
await _serverModeOutputDevice.HandleProcessRoleAsync(processRole);
}
}
}
Loading
Loading