From e4ac5674e24884db2ca497fa5c532ef823356eec Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 8 Apr 2025 08:32:58 +0200 Subject: [PATCH 1/6] dotnet test for MTP: Support TestTfmsInParallel --- src/Cli/dotnet/Commands/Test/CliConstants.cs | 2 + .../dotnet/Commands/Test/MSBuildHandler.cs | 58 ++++------ .../dotnet/Commands/Test/MSBuildUtility.cs | 18 +-- src/Cli/dotnet/Commands/Test/Models.cs | 103 ++++++++++++++++++ .../Test/SolutionAndProjectUtility.cs | 48 ++++++-- .../Test/TestApplicationActionQueue.cs | 60 +++++----- .../Commands/Test/TestModulesFilterHandler.cs | 4 +- .../Commands/Test/TestingPlatformCommand.cs | 24 ++-- 8 files changed, 223 insertions(+), 94 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/CliConstants.cs b/src/Cli/dotnet/Commands/Test/CliConstants.cs index 8c217f0c7503..3214a5051f34 100644 --- a/src/Cli/dotnet/Commands/Test/CliConstants.cs +++ b/src/Cli/dotnet/Commands/Test/CliConstants.cs @@ -95,4 +95,6 @@ internal static class ProjectProperties internal const string RunArguments = "RunArguments"; internal const string RunWorkingDirectory = "RunWorkingDirectory"; internal const string AppDesignerFolder = "AppDesignerFolder"; + internal const string TestTfmsInParallel = "TestTfmsInParallel"; + internal const string BuildInParallel = "BuildInParallel"; } diff --git a/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs b/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs index 433e51aaea8c..865807c0cca5 100644 --- a/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs +++ b/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs @@ -9,13 +9,13 @@ namespace Microsoft.DotNet.Cli.Commands.Test; -internal sealed class MSBuildHandler(BuildOptions buildOptions, TestApplicationActionQueue actionQueue, TerminalTestReporter output) : IDisposable +internal sealed class MSBuildHandler(BuildOptions buildOptions, TestApplicationActionQueue actionQueue, TerminalTestReporter output) { private readonly BuildOptions _buildOptions = buildOptions; private readonly TestApplicationActionQueue _actionQueue = actionQueue; private readonly TerminalTestReporter _output = output; - private readonly ConcurrentBag _testApplications = []; + private readonly ConcurrentBag _testApplications = []; private bool _areTestingPlatformApplications = true; public bool RunMSBuild() @@ -64,7 +64,7 @@ private int RunBuild(string directoryPath) return ExitCode.GenericFailure; } - (IEnumerable projects, bool restored) = GetProjectsProperties(projectOrSolutionFilePath, isSolution); + (IEnumerable projects, bool restored) = GetProjectsProperties(projectOrSolutionFilePath, isSolution); InitializeTestApplications(projects); @@ -73,17 +73,17 @@ private int RunBuild(string directoryPath) private int RunBuild(string filePath, bool isSolution) { - (IEnumerable projects, bool restored) = GetProjectsProperties(filePath, isSolution); + (IEnumerable projects, bool restored) = GetProjectsProperties(filePath, isSolution); InitializeTestApplications(projects); return restored ? ExitCode.Success : ExitCode.GenericFailure; } - private void InitializeTestApplications(IEnumerable modules) + private void InitializeTestApplications(IEnumerable moduleGroups) { // If one test app has IsTestingPlatformApplication set to false (VSTest and not MTP), then we will not run any of the test apps - IEnumerable vsTestTestProjects = modules.Where(module => !module.IsTestingPlatformApplication); + IEnumerable vsTestTestProjects = moduleGroups.SelectMany(group => group.GetVSTestAndNotMTPModules()); if (vsTestTestProjects.Any()) { @@ -100,16 +100,9 @@ private void InitializeTestApplications(IEnumerable modules) return; } - foreach (TestModule module in modules) + foreach (NonParallelizedTestModuleGroup moduleGroup in moduleGroups) { - if (!module.IsTestProject && !module.IsTestingPlatformApplication) - { - // This should never happen. We should only ever create TestModule if it's a test project. - throw new UnreachableException($"This program location is thought to be unreachable. Class='{nameof(MSBuildHandler)}' Method='{nameof(InitializeTestApplications)}'"); - } - - var testApp = new TestApplication(module, _buildOptions); - _testApplications.Add(testApp); + _testApplications.Add(moduleGroup); } } @@ -127,9 +120,9 @@ public bool EnqueueTestApplications() return true; } - private (IEnumerable Projects, bool Restored) GetProjectsProperties(string solutionOrProjectFilePath, bool isSolution) + private (IEnumerable Projects, bool Restored) GetProjectsProperties(string solutionOrProjectFilePath, bool isSolution) { - (IEnumerable projects, bool isBuiltOrRestored) = isSolution ? + (IEnumerable projects, bool isBuiltOrRestored) = isSolution ? MSBuildUtility.GetProjectsFromSolution(solutionOrProjectFilePath, _buildOptions) : MSBuildUtility.GetProjectsFromProject(solutionOrProjectFilePath, _buildOptions); @@ -138,7 +131,7 @@ public bool EnqueueTestApplications() return (projects, isBuiltOrRestored); } - private static void LogProjectProperties(IEnumerable modules) + private static void LogProjectProperties(IEnumerable moduleGroups) { if (!Logger.TraceEnabled) { @@ -147,26 +140,21 @@ private static void LogProjectProperties(IEnumerable modules) var logMessageBuilder = new StringBuilder(); - foreach (var module in modules) + foreach (var moduleGroup in moduleGroups) { - logMessageBuilder.AppendLine($"{ProjectProperties.ProjectFullPath}: {module.ProjectFullPath}"); - logMessageBuilder.AppendLine($"{ProjectProperties.IsTestProject}: {module.IsTestProject}"); - logMessageBuilder.AppendLine($"{ProjectProperties.IsTestingPlatformApplication}: {module.IsTestingPlatformApplication}"); - logMessageBuilder.AppendLine($"{ProjectProperties.TargetFramework}: {module.TargetFramework}"); - logMessageBuilder.AppendLine($"{ProjectProperties.RunCommand}: {module.RunProperties.RunCommand}"); - logMessageBuilder.AppendLine($"{ProjectProperties.RunArguments}: {module.RunProperties.RunArguments}"); - logMessageBuilder.AppendLine($"{ProjectProperties.RunWorkingDirectory}: {module.RunProperties.RunWorkingDirectory}"); - logMessageBuilder.AppendLine(); + foreach (var module in moduleGroup) + { + logMessageBuilder.AppendLine($"{ProjectProperties.ProjectFullPath}: {module.ProjectFullPath}"); + logMessageBuilder.AppendLine($"{ProjectProperties.IsTestProject}: {module.IsTestProject}"); + logMessageBuilder.AppendLine($"{ProjectProperties.IsTestingPlatformApplication}: {module.IsTestingPlatformApplication}"); + logMessageBuilder.AppendLine($"{ProjectProperties.TargetFramework}: {module.TargetFramework}"); + logMessageBuilder.AppendLine($"{ProjectProperties.RunCommand}: {module.RunProperties.RunCommand}"); + logMessageBuilder.AppendLine($"{ProjectProperties.RunArguments}: {module.RunProperties.RunArguments}"); + logMessageBuilder.AppendLine($"{ProjectProperties.RunWorkingDirectory}: {module.RunProperties.RunWorkingDirectory}"); + logMessageBuilder.AppendLine(); + } } Logger.LogTrace(() => logMessageBuilder.ToString()); } - - public void Dispose() - { - foreach (var testApplication in _testApplications) - { - testApplication.Dispose(); - } - } } diff --git a/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs index 4dc250193aab..d9d535bfcf70 100644 --- a/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs @@ -17,7 +17,7 @@ internal static class MSBuildUtility { private const string dotnetTestVerb = "dotnet-test"; - public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) { SolutionModel solutionModel = SlnFileFactory.CreateFromFileOrDirectory(solutionFilePath, includeSolutionFilterFiles: true, includeSolutionXmlFiles: true); @@ -25,7 +25,7 @@ public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProj if (!isBuiltOrRestored) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), isBuiltOrRestored); } string rootDirectory = solutionFilePath.HasExtension(".slnf") ? @@ -35,25 +35,25 @@ public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProj FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb); var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), loggers: logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); - ConcurrentBag projects = GetProjectsProperties(collection, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); + ConcurrentBag projects = GetProjectsProperties(collection, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); logger?.ReallyShutdown(); return (projects, isBuiltOrRestored); } - public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) { bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(projectFilePath, buildOptions); if (!isBuiltOrRestored) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), isBuiltOrRestored); } FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb); var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); - IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, buildOptions.NoLaunchProfile); + IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, buildOptions.NoLaunchProfile); logger?.ReallyShutdown(); return (projects, isBuiltOrRestored); @@ -117,16 +117,16 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption return result == (int)BuildResultCode.Success; } - private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, IEnumerable projects, BuildOptions buildOptions) + private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, IEnumerable projects, BuildOptions buildOptions) { - var allProjects = new ConcurrentBag(); + var allProjects = new ConcurrentBag(); Parallel.ForEach( projects, new ParallelOptions { MaxDegreeOfParallelism = buildOptions.DegreeOfParallelism }, (project) => { - IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, buildOptions.NoLaunchProfile); + IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, buildOptions.NoLaunchProfile); foreach (var projectMetadata in projectsMetadata) { allProjects.Add(projectMetadata); diff --git a/src/Cli/dotnet/Commands/Test/Models.cs b/src/Cli/dotnet/Commands/Test/Models.cs index 6815c91c0f29..280737fdcadf 100644 --- a/src/Cli/dotnet/Commands/Test/Models.cs +++ b/src/Cli/dotnet/Commands/Test/Models.cs @@ -1,11 +1,114 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; namespace Microsoft.DotNet.Cli.Commands.Test; +/// +/// Groups test modules that should NOT be run in parallel. +/// Parallelization here refers only to the inner test modules. +/// Note that multiple NonParallelizedTestModuleGroups can be run in parallel. +/// This class serves only the purpose of disabling parallelizing of TFMs when user sets TestTfmsInParallel to false. +/// For a single TFM project, we will use the constructor with a single module. Meaning it will be parallelized. +/// For a multi TFM project: +/// - If parallelization is enabled, we will create multiple NonParallelizedTestModuleGroups, each with a single module. +/// - If parallelization is not enabled, we will create a single NonParallelizedTestModuleGroup with all modules. +/// +internal sealed class NonParallelizedTestModuleGroup : IEnumerable +{ + public NonParallelizedTestModuleGroup(List modules) + { + Modules = modules; + } + + public NonParallelizedTestModuleGroup(TestModule module) + { + // This constructor is used when there is only one module. + Module = module; + } + + public List? Modules { get; } + + public TestModule? Module { get; } + + public TestModule[] GetVSTestAndNotMTPModules() + { + if (Modules is not null) + { + return Modules.Where(module => !module.IsTestingPlatformApplication).ToArray(); + } + + if (!Module.IsTestingPlatformApplication) + { + return [Module]; + } + + return Array.Empty(); + } + + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal struct Enumerator : IEnumerator + { + private readonly NonParallelizedTestModuleGroup _group; + private int _index = -1; + + public Enumerator(NonParallelizedTestModuleGroup group) + { + _group = group; + } + + public TestModule Current + { + get + { + if (_index < 0) + { + throw new InvalidOperationException(); + } + + if (_group.Modules is not null) + { + return _group.Modules[_index]; + } + + if (_index != 0) + { + throw new InvalidOperationException(); + } + + return _group.Module; + } + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + _index++; + + if (_group.Modules is not null) + { + return _index < _group.Modules.Count; + } + + return _index == 0; + } + + public void Reset() => _index = -1; + } +} + internal sealed record TestModule(RunProperties RunProperties, string? ProjectFullPath, string? TargetFramework, bool IsTestingPlatformApplication, bool IsTestProject, ProjectLaunchSettingsModel? LaunchSettings); internal sealed record Handshake(Dictionary? Properties); diff --git a/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs index 6e7f6d98757e..d2f8521cf090 100644 --- a/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs @@ -119,33 +119,65 @@ public static string GetRootDirectory(string solutionOrProjectFilePath) return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory; } - public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, bool noLaunchProfile) + public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, bool noLaunchProfile) { - var projects = new List(); + var projects = new List(); ProjectInstance projectInstance = EvaluateProject(projectCollection, projectFilePath, null); var targetFramework = projectInstance.GetPropertyValue(ProjectProperties.TargetFramework); var targetFrameworks = projectInstance.GetPropertyValue(ProjectProperties.TargetFrameworks); + Logger.LogTrace(() => $"Loaded project '{Path.GetFileName(projectFilePath)}' with TargetFramework '{targetFramework}', TargetFrameworks '{targetFrameworks}', IsTestProject '{projectInstance.GetPropertyValue(ProjectProperties.IsTestProject)}', and '{ProjectProperties.IsTestingPlatformApplication}' is '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}'."); if (!string.IsNullOrEmpty(targetFramework) || string.IsNullOrEmpty(targetFrameworks)) { if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) { - projects.Add(module); + projects.Add(new NonParallelizedTestModuleGroup(module)); } } else { + if (!bool.TryParse(projectInstance.GetPropertyValue(ProjectProperties.TestTfmsInParallel), out bool testTfmsInParallel) && + !bool.TryParse(projectInstance.GetPropertyValue(ProjectProperties.BuildInParallel), out testTfmsInParallel)) + { + // TestTfmsInParallel takes precedence over BuildInParallel. + // If, for some reason, we cannot parse either property as bool, we default to true. + testTfmsInParallel = true; + } + var frameworks = targetFrameworks.Split(CliConstants.SemiColon, StringSplitOptions.RemoveEmptyEntries); - foreach (var framework in frameworks) + if (testTfmsInParallel) { - projectInstance = EvaluateProject(projectCollection, projectFilePath, framework); - Logger.LogTrace(() => $"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}')."); + foreach (var framework in frameworks) + { + projectInstance = EvaluateProject(projectCollection, projectFilePath, framework); + Logger.LogTrace(() => $"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}')."); + + if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) + { + projects.Add(new NonParallelizedTestModuleGroup(module)); + } + } + } + else + { + List? innerModules = null; + foreach (var framework in frameworks) + { + projectInstance = EvaluateProject(projectCollection, projectFilePath, framework); + Logger.LogTrace(() => $"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}')."); + + if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) + { + innerModules ??= new List(frameworks.Length); + innerModules.Add(module); + } + } - if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) + if (innerModules is not null) { - projects.Add(module); + projects.Add(new NonParallelizedTestModuleGroup(innerModules)); } } } diff --git a/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs b/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs index 9cf74504f4f8..2a982937f2ed 100644 --- a/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs +++ b/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs @@ -7,25 +7,25 @@ namespace Microsoft.DotNet.Cli.Commands.Test; internal class TestApplicationActionQueue { - private readonly Channel _channel; + private readonly Channel _channel; private readonly List _readers; private int? _aggregateExitCode; private static readonly Lock _lock = new(); - public TestApplicationActionQueue(int degreeOfParallelism, Func> action) + public TestApplicationActionQueue(int degreeOfParallelism, BuildOptions buildOptions, Func> action) { - _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); + _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); _readers = []; for (int i = 0; i < degreeOfParallelism; i++) { - _readers.Add(Task.Run(async () => await Read(action))); + _readers.Add(Task.Run(async () => await Read(action, buildOptions))); } } - public void Enqueue(TestApplication testApplication) + public void Enqueue(NonParallelizedTestModuleGroup testApplication) { if (!_channel.Writer.TryWrite(testApplication)) { @@ -48,37 +48,41 @@ public void EnqueueCompleted() _channel.Writer.Complete(); } - private async Task Read(Func> action) + private async Task Read(Func> action, BuildOptions buildOptions) { - await foreach (var testApp in _channel.Reader.ReadAllAsync()) + await foreach (var nonParallelizedGroup in _channel.Reader.ReadAllAsync()) { - int result = await action(testApp); - - lock (_lock) + foreach (var module in nonParallelizedGroup) { - 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) + var testApp = new TestApplication(module, buildOptions); + int result = await action(testApp); + testApp.Dispose(); + lock (_lock) { - if (_aggregateExitCode == ExitCode.Success) + if (_aggregateExitCode is null) { - // The current result we are dealing with is the first failure after previous Success. - // So we assign the current failure. + // 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 (result != ExitCode.Success) - { - // If we get a new failure result, which is different from a previous failure, we use GenericFailure. - _aggregateExitCode = ExitCode.GenericFailure; - } - else + else if (_aggregateExitCode.Value != result) { - // The current result is a success, but we already have a failure. - // So, we keep the failure exit code. + 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. + } } } } diff --git a/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs b/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs index 7d5bf4bd3b9d..86c241da51a1 100644 --- a/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs +++ b/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs @@ -15,7 +15,7 @@ internal sealed class TestModulesFilterHandler(TestApplicationActionQueue action private readonly TestApplicationActionQueue _actionQueue = actionQueue; private readonly TerminalTestReporter _output = output; - public bool RunWithTestModulesFilter(ParseResult parseResult, BuildOptions buildOptions) + public bool RunWithTestModulesFilter(ParseResult parseResult) { // If the module path pattern(s) was provided, we will use that to filter the test modules string testModules = parseResult.GetValue(TestingPlatformOptions.TestModulesFilterOption); @@ -48,7 +48,7 @@ public bool RunWithTestModulesFilter(ParseResult parseResult, BuildOptions build foreach (string testModule in testModulePaths) { - var testApp = new TestApplication(new TestModule(new RunProperties(testModule, null, null), null, null, true, true, null), buildOptions); + var testApp = new NonParallelizedTestModuleGroup(new TestModule(new RunProperties(testModule, null, null), null, null, true, true, null)); // Write the test application to the channel _actionQueue.Enqueue(testApp); } diff --git a/src/Cli/dotnet/Commands/Test/TestingPlatformCommand.cs b/src/Cli/dotnet/Commands/Test/TestingPlatformCommand.cs index b1eaf0a39b4e..f3d79147714f 100644 --- a/src/Cli/dotnet/Commands/Test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/Commands/Test/TestingPlatformCommand.cs @@ -49,9 +49,10 @@ private int RunInternal(ParseResult parseResult) InitializeOutput(degreeOfParallelism, parseResult, testOptions.IsHelp); - InitializeActionQueue(degreeOfParallelism, testOptions, testOptions.IsHelp); - BuildOptions buildOptions = MSBuildUtility.GetBuildOptions(parseResult, degreeOfParallelism); + + InitializeActionQueue(degreeOfParallelism, testOptions, buildOptions); + _msBuildHandler = new(buildOptions, _actionQueue, _output); TestModulesFilterHandler testModulesFilterHandler = new(_actionQueue, _output); @@ -59,7 +60,7 @@ private int RunInternal(ParseResult parseResult) if (testOptions.HasFilterMode) { - if (!testModulesFilterHandler.RunWithTestModulesFilter(parseResult, buildOptions)) + if (!testModulesFilterHandler.RunWithTestModulesFilter(parseResult)) { return ExitCode.GenericFailure; } @@ -98,15 +99,15 @@ private void PrepareEnvironment(ParseResult parseResult, out TestOptions testOpt _isRetry = arguments.Contains("--retry-failed-tests"); } - private void InitializeActionQueue(int degreeOfParallelism, TestOptions testOptions, bool isHelp) + private void InitializeActionQueue(int degreeOfParallelism, TestOptions testOptions, BuildOptions buildOptions) { - if (isHelp) + if (testOptions.IsHelp) { - InitializeHelpActionQueue(degreeOfParallelism, testOptions); + InitializeHelpActionQueue(degreeOfParallelism, testOptions, buildOptions); } else { - InitializeTestExecutionActionQueue(degreeOfParallelism, testOptions); + InitializeTestExecutionActionQueue(degreeOfParallelism, testOptions, buildOptions); } } @@ -138,9 +139,9 @@ private void InitializeOutput(int degreeOfParallelism, ParseResult parseResult, _output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, _isDiscovery, isHelp, _isRetry); } - private void InitializeHelpActionQueue(int degreeOfParallelism, TestOptions testOptions) + private void InitializeHelpActionQueue(int degreeOfParallelism, TestOptions testOptions, BuildOptions buildOptions) { - _actionQueue = new(degreeOfParallelism, async (TestApplication testApp) => + _actionQueue = new(degreeOfParallelism, buildOptions, async (TestApplication testApp) => { testApp.HelpRequested += OnHelpRequested; testApp.ErrorReceived += _eventHandlers.OnErrorReceived; @@ -150,9 +151,9 @@ private void InitializeHelpActionQueue(int degreeOfParallelism, TestOptions test }); } - private void InitializeTestExecutionActionQueue(int degreeOfParallelism, TestOptions testOptions) + private void InitializeTestExecutionActionQueue(int degreeOfParallelism, TestOptions testOptions, BuildOptions buildOptions) { - _actionQueue = new(degreeOfParallelism, async (TestApplication testApp) => + _actionQueue = new(degreeOfParallelism, buildOptions, async (TestApplication testApp) => { testApp.HandshakeReceived += _eventHandlers.OnHandshakeReceived; testApp.DiscoveredTestsReceived += _eventHandlers.OnDiscoveredTestsReceived; @@ -198,7 +199,6 @@ private void CompleteRun(int? exitCode) private void CleanUp() { - _msBuildHandler?.Dispose(); _eventHandlers?.Dispose(); } } From 60cdfcc14ab63ae62fb1fdf2723d4286ad6d081f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 8 Apr 2025 09:31:43 +0200 Subject: [PATCH 2/6] Add test --- .../TestProject/Program.cs | 103 ++++++++++++++++++ .../TestProject/TestProject.csproj | 19 ++++ ...ProjectWithMultipleTFMsParallelization.sln | 25 +++++ .../dotnet.config | 2 + ...etTestBuildsAndRunsTestsForMultipleTFMs.cs | 39 +++++++ 5 files changed, 188 insertions(+) create mode 100644 test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs create mode 100644 test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/TestProject.csproj create mode 100644 test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProjectWithMultipleTFMsParallelization.sln create mode 100644 test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/dotnet.config diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs new file mode 100644 index 000000000000..619fb5dce08f --- /dev/null +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestFramework; + +namespace TestProjectWithNetFM +{ + internal class Program + { + public static async Task Main(string[] args) + { + // To attach to the children + ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); + testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new DummyTestAdapter()); + + ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); + return await testApplication.RunAsync(); + } + } + + public class DummyTestAdapter : ITestFramework, IDataProducer + { + public string Uid => nameof(DummyTestAdapter); + + public string Version => "2.0.0"; + + public string DisplayName => nameof(DummyTestAdapter); + + public string Description => nameof(DummyTestAdapter); + + public Task IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] + { + typeof(TestNodeUpdateMessage) + }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task CloseTestSessionAsync(CloseTestSessionContext context) + => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); + + public async Task ExecuteRequestAsync(ExecuteRequestContext context) + { + var currentProcess = Process.GetCurrentProcess(); + var processPath = currentProcess.MainModule?.FileName; + if (processPath is null) + { + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("Process path is null"), "")), + })); + + context.Complete(); + return; + } + + if (!processPath.Contains("TestProjectWithMultipleTFMsParallelization")) + { + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception($"Process path is unexpected. Found: '{processPath}'."), "")), + })); + + context.Complete(); + return; + } + + await Task.Delay(5000); + + var processes = Process.GetProcessesByName(currentProcess.ProcessName); + if (processes.Where(p => p.Id != Process.GetCurrentProcess().Id && p.MainModule is not null && p.MainModule.FileName.Contains("TestProjectWithMultipleTFMsParallelization")).Any()) + { + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")), + })); + } + else + { + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("This is run in parallel!"), "")), + })); + } + + await Task.Delay(5000); + + + context.Complete(); + } + } +} diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/TestProject.csproj b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/TestProject.csproj new file mode 100644 index 000000000000..4b9f2619bbf2 --- /dev/null +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/TestProject.csproj @@ -0,0 +1,19 @@ + + + + + Exe + $(CurrentTargetFramework);net9.0 + enable + enable + false + latest + false + true + + + + + + + \ No newline at end of file diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProjectWithMultipleTFMsParallelization.sln b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProjectWithMultipleTFMsParallelization.sln new file mode 100644 index 000000000000..62931ef6461b --- /dev/null +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProjectWithMultipleTFMsParallelization.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35505.181 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "TestProject\TestProject.csproj", "{D2E321F2-3513-99DE-C37E-6D48D15F404D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D2E321F2-3513-99DE-C37E-6D48D15F404D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2E321F2-3513-99DE-C37E-6D48D15F404D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2E321F2-3513-99DE-C37E-6D48D15F404D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2E321F2-3513-99DE-C37E-6D48D15F404D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3D7914D1-7D03-4FFB-8D0F-7FFA6B6BA6A0} + EndGlobalSection +EndGlobal diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/dotnet.config b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/dotnet.config new file mode 100644 index 000000000000..28daa0a28213 --- /dev/null +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/dotnet.config @@ -0,0 +1,2 @@ +[dotnet.test.runner] +name= "Microsoft.Testing.Platform" \ No newline at end of file diff --git a/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs b/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs index 6e83faaa8dae..4e626ba6b2f8 100644 --- a/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs +++ b/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs @@ -98,6 +98,45 @@ public void RunProjectWithMultipleTFMs_ShouldReturnExitCodeGenericFailure(string result.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RunProjectWithMultipleTFMs_ParallelizationTest_RunInParallelShouldFail(bool testTfmsInParallel) + { + TestAsset testInstance = _testAssetsManager.CopyTestAsset("TestProjectWithMultipleTFMsParallelization", Guid.NewGuid().ToString()) + .WithSource(); + testInstance.WithTargetFrameworks($"{DotnetVersionHelper.GetPreviousDotnetVersion()};{ToolsetInfo.CurrentTargetFramework}", "TestProject"); + + CommandResult result = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute(CommonOptions.PropertiesOption.Name, $"TestTfmsInParallel={testTfmsInParallel}"); + + if (testTfmsInParallel) + { + if (!TestContext.IsLocalized()) + { + result.StdOut + .Should().Contain("Test run summary: Failed!") + .And.Contain("total: 1") + .And.Contain("succeeded: 0") + .And.Contain("failed: 1") + .And.Contain("skipped: 0") + .And.Contain("This is run in parallel!"); + } + else + { + result.StdOut + .Should().Contain("This is run in parallel!"); + } + + result.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); + } + else + { + result.ExitCode.Should().Be(ExitCodes.Success); + } + } + [InlineData(TestingConstants.Debug)] [InlineData(TestingConstants.Release)] [Theory] From e9a70e239c96954ea6e3f42087cdb5b768bb3672 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 8 Apr 2025 10:12:12 +0200 Subject: [PATCH 3/6] Fix test --- .../TestProject/Program.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs index 619fb5dce08f..302bc9d4a5e9 100644 --- a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs @@ -59,23 +59,11 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) return; } - if (!processPath.Contains("TestProjectWithMultipleTFMsParallelization")) - { - await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() - { - Uid = "Test0", - DisplayName = "Test0", - Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception($"Process path is unexpected. Found: '{processPath}'."), "")), - })); - - context.Complete(); - return; - } - await Task.Delay(5000); var processes = Process.GetProcessesByName(currentProcess.ProcessName); - if (processes.Where(p => p.Id != Process.GetCurrentProcess().Id && p.MainModule is not null && p.MainModule.FileName.Contains("TestProjectWithMultipleTFMsParallelization")).Any()) + var pathPrefix = Path.GetDirectoryName(Path.GetDirectoryName(processPath)); + if (processes.Where(p => p.Id != Process.GetCurrentProcess().Id && p.MainModule is not null && p.MainModule.FileName.StartsWith(pathPrefix!)).Any()) { await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() { From 1aafda3a5f5921663482c226a8cbbc128db718dd Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 8 Apr 2025 13:30:07 +0200 Subject: [PATCH 4/6] Fix reversed condition --- .../TestProject/Program.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs index 302bc9d4a5e9..62af94acd3de 100644 --- a/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs +++ b/test/TestAssets/TestProjects/TestProjectWithMultipleTFMsParallelization/TestProject/Program.cs @@ -65,26 +65,25 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) var pathPrefix = Path.GetDirectoryName(Path.GetDirectoryName(processPath)); if (processes.Where(p => p.Id != Process.GetCurrentProcess().Id && p.MainModule is not null && p.MainModule.FileName.StartsWith(pathPrefix!)).Any()) { - await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() - { - Uid = "Test0", - DisplayName = "Test0", - Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")), - })); + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("This is run in parallel!"), "")), + })); } else { - await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() - { - Uid = "Test0", - DisplayName = "Test0", - Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("This is run in parallel!"), "")), - })); - } + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() + { + Uid = "Test0", + DisplayName = "Test0", + Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")), + })); + } await Task.Delay(5000); - context.Complete(); } } From 4c74d85e2c70558a5812d82e7beec3006a072952 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 8 Apr 2025 15:01:25 +0200 Subject: [PATCH 5/6] Fix test --- .../GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs b/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs index 4e626ba6b2f8..432293277ba4 100644 --- a/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs +++ b/test/dotnet-test.Tests/GivenDotnetTestBuildsAndRunsTestsForMultipleTFMs.cs @@ -117,9 +117,9 @@ public void RunProjectWithMultipleTFMs_ParallelizationTest_RunInParallelShouldFa { result.StdOut .Should().Contain("Test run summary: Failed!") - .And.Contain("total: 1") + .And.Contain("total: 2") .And.Contain("succeeded: 0") - .And.Contain("failed: 1") + .And.Contain("failed: 2") .And.Contain("skipped: 0") .And.Contain("This is run in parallel!"); } From 519ab97261178688c655841e0cfc173bf487ae54 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 9 Apr 2025 10:03:20 +0200 Subject: [PATCH 6/6] Rename --- src/Cli/dotnet/Commands/Test/MSBuildHandler.cs | 17 ++++++++--------- src/Cli/dotnet/Commands/Test/MSBuildUtility.cs | 18 +++++++++--------- src/Cli/dotnet/Commands/Test/Models.cs | 17 ++++++++--------- .../Commands/Test/SolutionAndProjectUtility.cs | 10 +++++----- .../Test/TestApplicationActionQueue.cs | 6 +++--- .../Commands/Test/TestModulesFilterHandler.cs | 2 +- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs b/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs index 865807c0cca5..c7b1520916fd 100644 --- a/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs +++ b/src/Cli/dotnet/Commands/Test/MSBuildHandler.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.Diagnostics; using Microsoft.DotNet.Cli.Commands.Test.Terminal; using Microsoft.DotNet.Cli.Utils; using Microsoft.Testing.Platform.OutputDevice.Terminal; @@ -15,7 +14,7 @@ internal sealed class MSBuildHandler(BuildOptions buildOptions, TestApplicationA private readonly TestApplicationActionQueue _actionQueue = actionQueue; private readonly TerminalTestReporter _output = output; - private readonly ConcurrentBag _testApplications = []; + private readonly ConcurrentBag _testApplications = []; private bool _areTestingPlatformApplications = true; public bool RunMSBuild() @@ -64,7 +63,7 @@ private int RunBuild(string directoryPath) return ExitCode.GenericFailure; } - (IEnumerable projects, bool restored) = GetProjectsProperties(projectOrSolutionFilePath, isSolution); + (IEnumerable projects, bool restored) = GetProjectsProperties(projectOrSolutionFilePath, isSolution); InitializeTestApplications(projects); @@ -73,14 +72,14 @@ private int RunBuild(string directoryPath) private int RunBuild(string filePath, bool isSolution) { - (IEnumerable projects, bool restored) = GetProjectsProperties(filePath, isSolution); + (IEnumerable projects, bool restored) = GetProjectsProperties(filePath, isSolution); InitializeTestApplications(projects); return restored ? ExitCode.Success : ExitCode.GenericFailure; } - private void InitializeTestApplications(IEnumerable moduleGroups) + private void InitializeTestApplications(IEnumerable moduleGroups) { // If one test app has IsTestingPlatformApplication set to false (VSTest and not MTP), then we will not run any of the test apps IEnumerable vsTestTestProjects = moduleGroups.SelectMany(group => group.GetVSTestAndNotMTPModules()); @@ -100,7 +99,7 @@ private void InitializeTestApplications(IEnumerable Projects, bool Restored) GetProjectsProperties(string solutionOrProjectFilePath, bool isSolution) + private (IEnumerable Projects, bool Restored) GetProjectsProperties(string solutionOrProjectFilePath, bool isSolution) { - (IEnumerable projects, bool isBuiltOrRestored) = isSolution ? + (IEnumerable projects, bool isBuiltOrRestored) = isSolution ? MSBuildUtility.GetProjectsFromSolution(solutionOrProjectFilePath, _buildOptions) : MSBuildUtility.GetProjectsFromProject(solutionOrProjectFilePath, _buildOptions); @@ -131,7 +130,7 @@ public bool EnqueueTestApplications() return (projects, isBuiltOrRestored); } - private static void LogProjectProperties(IEnumerable moduleGroups) + private static void LogProjectProperties(IEnumerable moduleGroups) { if (!Logger.TraceEnabled) { diff --git a/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs index d9d535bfcf70..836b3f69356e 100644 --- a/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MSBuildUtility.cs @@ -17,7 +17,7 @@ internal static class MSBuildUtility { private const string dotnetTestVerb = "dotnet-test"; - public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) { SolutionModel solutionModel = SlnFileFactory.CreateFromFileOrDirectory(solutionFilePath, includeSolutionFilterFiles: true, includeSolutionXmlFiles: true); @@ -25,7 +25,7 @@ public static (IEnumerable Projects, bool IsBuil if (!isBuiltOrRestored) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), isBuiltOrRestored); } string rootDirectory = solutionFilePath.HasExtension(".slnf") ? @@ -35,25 +35,25 @@ public static (IEnumerable Projects, bool IsBuil FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb); var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), loggers: logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); - ConcurrentBag projects = GetProjectsProperties(collection, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); + ConcurrentBag projects = GetProjectsProperties(collection, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); logger?.ReallyShutdown(); return (projects, isBuiltOrRestored); } - public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) { bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(projectFilePath, buildOptions); if (!isBuiltOrRestored) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), isBuiltOrRestored); } FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb); var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); - IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, buildOptions.NoLaunchProfile); + IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, buildOptions.NoLaunchProfile); logger?.ReallyShutdown(); return (projects, isBuiltOrRestored); @@ -117,16 +117,16 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption return result == (int)BuildResultCode.Success; } - private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, IEnumerable projects, BuildOptions buildOptions) + private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, IEnumerable projects, BuildOptions buildOptions) { - var allProjects = new ConcurrentBag(); + var allProjects = new ConcurrentBag(); Parallel.ForEach( projects, new ParallelOptions { MaxDegreeOfParallelism = buildOptions.DegreeOfParallelism }, (project) => { - IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, buildOptions.NoLaunchProfile); + IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, buildOptions.NoLaunchProfile); foreach (var projectMetadata in projectsMetadata) { allProjects.Add(projectMetadata); diff --git a/src/Cli/dotnet/Commands/Test/Models.cs b/src/Cli/dotnet/Commands/Test/Models.cs index 280737fdcadf..e734abdce3b4 100644 --- a/src/Cli/dotnet/Commands/Test/Models.cs +++ b/src/Cli/dotnet/Commands/Test/Models.cs @@ -9,22 +9,21 @@ namespace Microsoft.DotNet.Cli.Commands.Test; /// /// Groups test modules that should NOT be run in parallel. -/// Parallelization here refers only to the inner test modules. -/// Note that multiple NonParallelizedTestModuleGroups can be run in parallel. +/// The whole group is still parallelizable with other groups, just that the inner modules are run sequentially. /// This class serves only the purpose of disabling parallelizing of TFMs when user sets TestTfmsInParallel to false. /// For a single TFM project, we will use the constructor with a single module. Meaning it will be parallelized. /// For a multi TFM project: -/// - If parallelization is enabled, we will create multiple NonParallelizedTestModuleGroups, each with a single module. -/// - If parallelization is not enabled, we will create a single NonParallelizedTestModuleGroup with all modules. +/// - If parallelization is enabled, we will create multiple ParallelizableTestModuleGroupWithSequentialInnerModuless, each with a single module. +/// - If parallelization is not enabled, we will create a single ParallelizableTestModuleGroupWithSequentialInnerModules with all modules. /// -internal sealed class NonParallelizedTestModuleGroup : IEnumerable +internal sealed class ParallelizableTestModuleGroupWithSequentialInnerModules : IEnumerable { - public NonParallelizedTestModuleGroup(List modules) + public ParallelizableTestModuleGroupWithSequentialInnerModules(List modules) { Modules = modules; } - public NonParallelizedTestModuleGroup(TestModule module) + public ParallelizableTestModuleGroupWithSequentialInnerModules(TestModule module) { // This constructor is used when there is only one module. Module = module; @@ -58,10 +57,10 @@ public Enumerator GetEnumerator() internal struct Enumerator : IEnumerator { - private readonly NonParallelizedTestModuleGroup _group; + private readonly ParallelizableTestModuleGroupWithSequentialInnerModules _group; private int _index = -1; - public Enumerator(NonParallelizedTestModuleGroup group) + public Enumerator(ParallelizableTestModuleGroupWithSequentialInnerModules group) { _group = group; } diff --git a/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs index d2f8521cf090..166d687bed90 100644 --- a/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs @@ -119,9 +119,9 @@ public static string GetRootDirectory(string solutionOrProjectFilePath) return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory; } - public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, bool noLaunchProfile) + public static IEnumerable GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, bool noLaunchProfile) { - var projects = new List(); + var projects = new List(); ProjectInstance projectInstance = EvaluateProject(projectCollection, projectFilePath, null); var targetFramework = projectInstance.GetPropertyValue(ProjectProperties.TargetFramework); @@ -133,7 +133,7 @@ public static IEnumerable GetProjectProperties(s { if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) { - projects.Add(new NonParallelizedTestModuleGroup(module)); + projects.Add(new ParallelizableTestModuleGroupWithSequentialInnerModules(module)); } } else @@ -156,7 +156,7 @@ public static IEnumerable GetProjectProperties(s if (GetModuleFromProject(projectInstance, projectCollection.Loggers, noLaunchProfile) is { } module) { - projects.Add(new NonParallelizedTestModuleGroup(module)); + projects.Add(new ParallelizableTestModuleGroupWithSequentialInnerModules(module)); } } } @@ -177,7 +177,7 @@ public static IEnumerable GetProjectProperties(s if (innerModules is not null) { - projects.Add(new NonParallelizedTestModuleGroup(innerModules)); + projects.Add(new ParallelizableTestModuleGroupWithSequentialInnerModules(innerModules)); } } } diff --git a/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs b/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs index 2a982937f2ed..69bf25e4727b 100644 --- a/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs +++ b/src/Cli/dotnet/Commands/Test/TestApplicationActionQueue.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test; internal class TestApplicationActionQueue { - private readonly Channel _channel; + private readonly Channel _channel; private readonly List _readers; private int? _aggregateExitCode; @@ -16,7 +16,7 @@ internal class TestApplicationActionQueue public TestApplicationActionQueue(int degreeOfParallelism, BuildOptions buildOptions, Func> action) { - _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); + _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false }); _readers = []; for (int i = 0; i < degreeOfParallelism; i++) @@ -25,7 +25,7 @@ public TestApplicationActionQueue(int degreeOfParallelism, BuildOptions buildOpt } } - public void Enqueue(NonParallelizedTestModuleGroup testApplication) + public void Enqueue(ParallelizableTestModuleGroupWithSequentialInnerModules testApplication) { if (!_channel.Writer.TryWrite(testApplication)) { diff --git a/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs b/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs index 86c241da51a1..3ed760aebc1b 100644 --- a/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs +++ b/src/Cli/dotnet/Commands/Test/TestModulesFilterHandler.cs @@ -48,7 +48,7 @@ public bool RunWithTestModulesFilter(ParseResult parseResult) foreach (string testModule in testModulePaths) { - var testApp = new NonParallelizedTestModuleGroup(new TestModule(new RunProperties(testModule, null, null), null, null, true, true, null)); + var testApp = new ParallelizableTestModuleGroupWithSequentialInnerModules(new TestModule(new RunProperties(testModule, null, null), null, null, true, true, null)); // Write the test application to the channel _actionQueue.Enqueue(testApp); }