| File: Commands\Test\MTP\TestApplicationActionQueue.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Channels; using Microsoft.DotNet.Cli.Commands.Test.IPC.Models; using Microsoft.DotNet.Cli.Commands.Test.Terminal; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Commands.Test; internal class TestApplicationActionQueue { private readonly Channel<ParallelizableTestModuleGroupWithSequentialInnerModules> _channel; private readonly Task[] _readers; private readonly CancellationToken _cancellationToken; private int? _aggregateExitCode; private readonly Lock _lock = new(); public TestApplicationActionQueue( int degreeOfParallelism, BuildOptions buildOptions, TestOptions testOptions, TestResultsDirectoryResolver resultsDirectoryResolver, TerminalTestReporter output, Action<CommandLineOptionMessages> onHelpRequested, CtrlCCancellationManager ctrlC, ArtifactPostProcessingManager artifactPostProcessingManager, TestRunPolicy testRunPolicy, CancellationToken cancellationToken) { _channel = Channel.CreateUnbounded<ParallelizableTestModuleGroupWithSequentialInnerModules>(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); _readers = new Task[degreeOfParallelism]; _cancellationToken = cancellationToken; for (int i = 0; i < degreeOfParallelism; i++) { _readers[i] = Task.Run(async () => await Read( buildOptions, testOptions, resultsDirectoryResolver, output, onHelpRequested, ctrlC, artifactPostProcessingManager, testRunPolicy)); } } public void Enqueue(ParallelizableTestModuleGroupWithSequentialInnerModules testApplication) { if (_cancellationToken.IsCancellationRequested) { return; } if (!_channel.Writer.TryWrite(testApplication)) { throw new InvalidOperationException($"Failed to write to channel for test application: {testApplication}"); } } public int CompleteEnqueueAndWait() { // Notify readers that no more data will be written _channel.Writer.Complete(); Task.WaitAll(_readers); // If _aggregateExitCode is null, that means we didn't get any results. // So, we exit with "zero tests". return _aggregateExitCode ?? ExitCode.ZeroTests; } private async Task Read( BuildOptions buildOptions, TestOptions testOptions, TestResultsDirectoryResolver resultsDirectoryResolver, TerminalTestReporter output, Action<CommandLineOptionMessages> onHelpRequested, CtrlCCancellationManager ctrlC, ArtifactPostProcessingManager artifactPostProcessingManager, TestRunPolicy testRunPolicy) { try { await foreach (var nonParallelizedGroup in _channel.Reader.ReadAllAsync(_cancellationToken)) { foreach (var module in nonParallelizedGroup) { _cancellationToken.ThrowIfCancellationRequested(); int result = ExitCode.GenericFailure; var testApp = new TestApplication( module, buildOptions, testOptions, resultsDirectoryResolver, output, onHelpRequested, artifactPostProcessingManager, testRunPolicy: testRunPolicy); try { using (testApp) { result = await testApp.RunAsync(ctrlC); } } catch (Exception ex) { var exAsString = ex.ToString(); Logger.LogTrace($"Exception running test module {module.RunProperties?.Command} {module.RunProperties?.Arguments}: {exAsString}"); Reporter.Error.WriteLine(string.Format(CliCommandStrings.ErrorRunningTestModule, module.RunProperties?.Command, module.RunProperties?.Arguments, exAsString)); result = ExitCode.GenericFailure; } // A module that ran zero tests (exit code 8) is not, by itself, a whole-run failure. // With --test-modules or a global --filter, some modules may legitimately match no tests. // Normalize it to success here; the aggregate "zero tests ran" verdict is decided once at // the whole-run level in MicrosoftTestingPlatformTestCommand from the total test count. A // stricter per-module minimum requested via -- --minimum-expected-tests N still fails that // module with ExitCode.MinimumExpectedTestsPolicyViolation (9) and is preserved. // See https://github.com/microsoft/testfx/issues/7457. result = NormalizeExitCode(result, testApp.HasFailureDuringDispose); lock (_lock) { if (_aggregateExitCode is null) { // This is the first result we are getting. // So we assign the exit code, regardless of whether it's failure or success. _aggregateExitCode = result; } else if (_aggregateExitCode.Value != result) { if (_aggregateExitCode == ExitCode.Success) { // The current result we are dealing with is the first failure after previous Success. // So we assign the current failure. _aggregateExitCode = result; } else if (result != ExitCode.Success) { // If we get a new failure result, which is different from a previous failure, we use GenericFailure. _aggregateExitCode = ExitCode.GenericFailure; } else { // The current result is a success, but we already have a failure. // So, we keep the failure exit code. } } } } } } catch (OperationCanceledException) when (_cancellationToken.IsCancellationRequested) { // Stop scheduling new test apps once cancellation is requested. // Already-running test apps are left alone so they can gracefully cancel themselves // and report final session state via IPC. } } internal static int NormalizeExitCode(int result, bool hasFailureDuringDispose) { if (result == ExitCode.ZeroTests) { result = ExitCode.Success; } return result == ExitCode.Success && hasFailureDuringDispose ? ExitCode.GenericFailure : result; } }