diff --git a/src/Cli/dotnet/Commands/Test/CliConstants.cs b/src/Cli/dotnet/Commands/Test/CliConstants.cs index d80f70ea62d8..345c2ef5c2d2 100644 --- a/src/Cli/dotnet/Commands/Test/CliConstants.cs +++ b/src/Cli/dotnet/Commands/Test/CliConstants.cs @@ -151,4 +151,8 @@ internal static class ProjectProperties internal const string BuildInParallel = "BuildInParallel"; internal const string IsTraversal = "IsTraversal"; internal const string ProjectReferenceItemName = "ProjectReference"; + internal const string UseArtifactsOutput = "UseArtifactsOutput"; + internal const string ArtifactsPath = "ArtifactsPath"; + internal const string ArtifactsProjectName = "ArtifactsProjectName"; + internal const string ArtifactsPivots = "ArtifactsPivots"; } diff --git a/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs index c2af2da86076..50955fb73019 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs @@ -261,6 +261,15 @@ internal static string GetOutputDirectory(BuildOptions buildOptions, ArtifactPos return Path.GetFullPath(resultsDirectory); } + if (job.Application.Module.UseArtifactsOutput + && TestResultsDirectoryResolver.GetResultsDirectoryRoot( + buildOptions.PathOptions, + job.Application.Module, + Directory.GetCurrentDirectory()) is { } artifactsResultsDirectory) + { + return Path.GetFullPath(artifactsResultsDirectory); + } + ArtifactPostProcessingArtifact[] inputs = [ .. job.Groups diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index e3e128f89b68..06a15bf2a20a 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -261,7 +261,8 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult) ? ResultsDirectoryLayout.PerModule : ResultsDirectoryLayout.Flat, configFile, - diagnosticOutputDirectory); + diagnosticOutputDirectory, + parseResult.HasOption(definition.ResultsDirectoryLayoutOption)); return new BuildOptions( pathOptions, diff --git a/src/Cli/dotnet/Commands/Test/MTP/Models.cs b/src/Cli/dotnet/Commands/Test/MTP/Models.cs index 1a36c192bf12..f76ce45fbc82 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Models.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Models.cs @@ -119,4 +119,8 @@ internal sealed record TestModule( LaunchProfile? LaunchSettings, string TargetPath, string? DotnetRootArchVariableName, - IReadOnlyDictionary EnvironmentVariables); + IReadOnlyDictionary EnvironmentVariables, + bool UseArtifactsOutput = false, + string? ArtifactsPath = null, + string? ArtifactsProjectName = null, + string? ArtifactsPivots = null); diff --git a/src/Cli/dotnet/Commands/Test/MTP/Options.cs b/src/Cli/dotnet/Commands/Test/MTP/Options.cs index 37bb0e548d2b..af1d571c01d0 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Options.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Options.cs @@ -48,7 +48,8 @@ internal record PathOptions( string? ResultsDirectoryPath, ResultsDirectoryLayout ResultsDirectoryLayout, string? ConfigFilePath, - string? DiagnosticOutputDirectoryPath); + string? DiagnosticOutputDirectoryPath, + bool ResultsDirectoryLayoutSpecified = false); internal record BuildOptions( PathOptions PathOptions, diff --git a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs index b84c22f27059..da67e4f9198c 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs @@ -611,7 +611,25 @@ private static (string? device, string? runtimeIdentifier) SelectDeviceForTfm( rootVariableName = null; } - return new TestModule(runProperties, PathUtility.FixFilePath(projectFullPath), targetFramework, isTestingPlatformApplication, launchSettings, project.GetPropertyValue(ProjectProperties.TargetPath), rootVariableName, runtimeEnvironmentVariables); + _ = bool.TryParse(project.GetPropertyValue(ProjectProperties.UseArtifactsOutput), out bool useArtifactsOutput); + string artifactsPath = project.GetPropertyValue(ProjectProperties.ArtifactsPath); + string? fullArtifactsPath = string.IsNullOrEmpty(artifactsPath) + ? null + : Path.GetFullPath(PathUtility.FixFilePath(artifactsPath), project.Directory); + + return new TestModule( + runProperties, + PathUtility.FixFilePath(projectFullPath), + targetFramework, + isTestingPlatformApplication, + launchSettings, + project.GetPropertyValue(ProjectProperties.TargetPath), + rootVariableName, + runtimeEnvironmentVariables, + useArtifactsOutput, + fullArtifactsPath, + project.GetPropertyValue(ProjectProperties.ArtifactsProjectName), + project.GetPropertyValue(ProjectProperties.ArtifactsPivots)); [RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")] [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Temporary unblock for dotnet/msbuild#14064 (MSBuild build APIs are now [RequiresUnreferencedCode]). dotnet CLI runs MSBuild in-proc (not trimmed). Remove when dotnet/sdk#55225 is fixed.")] diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestResultsDirectoryResolver.cs b/src/Cli/dotnet/Commands/Test/MTP/TestResultsDirectoryResolver.cs index 9b0c653a40f1..5059845ef664 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestResultsDirectoryResolver.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestResultsDirectoryResolver.cs @@ -22,6 +22,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test; internal sealed class TestResultsDirectoryResolver { private const string DefaultResultsDirectoryName = "TestResults"; + private const string ArtifactsTestDirectoryName = "test"; private const string UnknownComponent = "unknown"; private const int MaxPathComponentLength = 255; @@ -32,31 +33,44 @@ internal sealed class TestResultsDirectoryResolver private readonly string _workingDirectory; private readonly string _identityRoot; private readonly HashSet _ambiguousProjectNames; - - private TestResultsDirectoryResolver(PathOptions pathOptions, string workingDirectory, string identityRoot, HashSet ambiguousProjectNames) + private readonly bool _shared; + + private TestResultsDirectoryResolver( + PathOptions pathOptions, + string workingDirectory, + string identityRoot, + HashSet ambiguousProjectNames, + bool shared = false) { _pathOptions = pathOptions; _workingDirectory = workingDirectory; _identityRoot = identityRoot; _ambiguousProjectNames = ambiguousProjectNames; + _shared = shared; } public static TestResultsDirectoryResolver Create(PathOptions pathOptions, IEnumerable modules, string workingDirectory) { - if (pathOptions.ResultsDirectoryLayout == ResultsDirectoryLayout.Flat) + List materializedModules = [.. modules]; + List perModuleLayoutModules = + [ + .. materializedModules.Where(module => GetResultsDirectoryLayout(pathOptions, module) == ResultsDirectoryLayout.PerModule) + ]; + + if (perModuleLayoutModules.Count == 0) { return new TestResultsDirectoryResolver(pathOptions, workingDirectory, workingDirectory, []); } + // Anchor identities to the directory shared by every module rather than the current // directory, so the same solution produces the same folder names no matter where // 'dotnet test' was invoked from. - List materializedModules = [.. modules]; - string identityRoot = GetCommonRootDirectory(materializedModules, workingDirectory); + string identityRoot = GetCommonRootDirectory(perModuleLayoutModules, workingDirectory); Dictionary> identitiesByProjectName = new(StringComparer.OrdinalIgnoreCase); - foreach (TestModule module in materializedModules) + foreach (TestModule module in perModuleLayoutModules) { - string projectName = GetProjectName(module); + string projectName = GetProjectName(module, UsesArtifactsOutputDefaults(pathOptions, module)); if (!identitiesByProjectName.TryGetValue(projectName, out HashSet? identities)) { identities = new HashSet(StringComparer.Ordinal); @@ -79,34 +93,62 @@ public static TestResultsDirectoryResolver Create(PathOptions pathOptions, IEnum } /// - /// A resolver that always yields the configured results directory, whatever the requested - /// layout. Used by internal invocations such as artifact post-processing, which merge results - /// across modules and so must not be scoped to a single module's directory. + /// A resolver that always yields the run-level results directory root, whatever the requested + /// layout. The root can come from an explicit results directory, artifacts output, or the + /// default results directory. Used by internal invocations such as artifact post-processing, + /// which merge results across modules and so must not be scoped to a single module's directory. /// public static TestResultsDirectoryResolver CreateShared(PathOptions pathOptions, string workingDirectory) - => new(pathOptions with { ResultsDirectoryLayout = ResultsDirectoryLayout.Flat }, workingDirectory, workingDirectory, []); + => new(pathOptions, workingDirectory, workingDirectory, [], shared: true); public string? Resolve(TestModule module) { - if (_pathOptions.ResultsDirectoryLayout == ResultsDirectoryLayout.Flat) + string? resultsDirectory = GetResultsDirectoryRoot(_pathOptions, module, _workingDirectory); + if (_shared || GetResultsDirectoryLayout(_pathOptions, module) == ResultsDirectoryLayout.Flat) { - return _pathOptions.ResultsDirectoryPath; + return resultsDirectory; } - string resultsDirectory = _pathOptions.ResultsDirectoryPath - ?? Path.Combine(_workingDirectory, DefaultResultsDirectoryName); - + string resultsRoot = resultsDirectory!; string resolved = Path.GetFullPath( - Path.Combine(resultsDirectory, GetProjectDirectoryName(module), GetPivotDirectoryName(module))); + Path.Combine(resultsRoot, GetProjectDirectoryName(module), GetPivotDirectoryName(module))); // Sanitization strips separators and dot-only components, so a module can never steer its // results out of the requested root. Asserted rather than thrown because it is unreachable // by design and only a future change to the component rules could break it. - Debug.Assert(IsUnderRoot(resolved, resultsDirectory), $"'{resolved}' escaped the results directory '{resultsDirectory}'."); + Debug.Assert(IsUnderRoot(resolved, resultsRoot), $"'{resolved}' escaped the results directory '{resultsRoot}'."); return resolved; } + internal static string? GetResultsDirectoryRoot(PathOptions pathOptions, TestModule module, string workingDirectory) + { + if (pathOptions.ResultsDirectoryPath is { } configuredResultsDirectory) + { + return configuredResultsDirectory; + } + + if (module.UseArtifactsOutput && module.ArtifactsPath is { } artifactsPath) + { + return Path.Combine(artifactsPath, ArtifactsTestDirectoryName); + } + + return GetResultsDirectoryLayout(pathOptions, module) == ResultsDirectoryLayout.PerModule + ? Path.Combine(workingDirectory, DefaultResultsDirectoryName) + : null; + } + + private static ResultsDirectoryLayout GetResultsDirectoryLayout(PathOptions pathOptions, TestModule module) + => UsesArtifactsOutputDefaults(pathOptions, module) + ? ResultsDirectoryLayout.PerModule + : pathOptions.ResultsDirectoryLayout; + + private static bool UsesArtifactsOutputDefaults(PathOptions pathOptions, TestModule module) + => !pathOptions.ResultsDirectoryLayoutSpecified + && pathOptions.ResultsDirectoryPath is null + && module.UseArtifactsOutput + && module.ArtifactsPath is not null; + private static bool IsUnderRoot(string candidate, string root) { string normalizedRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(root)); @@ -180,7 +222,7 @@ private static string GetCommonPrefixDirectory(string first, string second) /// private string GetProjectDirectoryName(TestModule module) { - string projectName = GetProjectName(module); + string projectName = GetProjectName(module, UsesArtifactsOutputDefaults(_pathOptions, module)); return LimitComponentLength(_ambiguousProjectNames.Contains(projectName) ? $"{projectName}_{GetShortHash(GetProjectIdentity(module, _identityRoot))}" @@ -188,13 +230,19 @@ private string GetProjectDirectoryName(TestModule module) } /// - /// The pivot folder distinguishing runs of the same project across target frameworks and - /// runtimes. Multiple elements are joined by an underscore, following the artifacts layout. - /// The configuration is deliberately not part of the pivot: a single test run targets one - /// configuration, so it would only ever add a constant level to every path. + /// The pivot folder distinguishing runs of the same project. Artifacts output reuses the + /// evaluated ArtifactsPivots, including configuration and any applicable target framework + /// or runtime identifier. An explicitly requested per-module layout instead uses target + /// framework and runtime or architecture. /// - private static string GetPivotDirectoryName(TestModule module) + private string GetPivotDirectoryName(TestModule module) { + if (UsesArtifactsOutputDefaults(_pathOptions, module) + && !string.IsNullOrEmpty(module.ArtifactsPivots)) + { + return LimitComponentLength(SanitizePathComponent(module.ArtifactsPivots).ToLowerInvariant()); + } + string targetFramework = SanitizePathComponent(module.TargetFramework); string runtime = SanitizePathComponent(GetRuntimeComponent(module)); @@ -216,11 +264,13 @@ private static string GetRuntimeComponent(TestModule module) return GetTargetArchitecture(module).ToString(); } - private static string GetProjectName(TestModule module) + private static string GetProjectName(TestModule module, bool useArtifactsOutputDefaults) { - string? projectName = string.IsNullOrEmpty(module.ProjectFullPath) - ? Path.GetFileNameWithoutExtension(module.TargetPath) - : Path.GetFileNameWithoutExtension(module.ProjectFullPath); + string? projectName = useArtifactsOutputDefaults && !string.IsNullOrEmpty(module.ArtifactsProjectName) + ? module.ArtifactsProjectName + : string.IsNullOrEmpty(module.ProjectFullPath) + ? Path.GetFileNameWithoutExtension(module.TargetPath) + : Path.GetFileNameWithoutExtension(module.ProjectFullPath); return SanitizePathComponent(projectName); } diff --git a/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs index 28e6f5594515..a8118926e70b 100644 --- a/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs @@ -288,6 +288,26 @@ public void GetOutputDirectory_WithResultsDirectory_UsesThatDirectory() outputDirectory.Should().Be(Path.GetFullPath(resultsDirectory)); } + [TestMethod] + public void GetOutputDirectory_WithArtifactsOutput_UsesArtifactsTestDirectory() + { + string artifactsDirectory = Path.Combine(Path.GetTempPath(), "artifacts"); + TestModule module = CreateModule() with + { + UseArtifactsOutput = true, + ArtifactsPath = artifactsDirectory, + }; + ArtifactPostProcessingJob job = CreateJob( + module, + CreateArtifact(Path.Combine(artifactsDirectory, "test", "project", "result.trx"), "microsoft.testing.trx")); + + string outputDirectory = ArtifactPostProcessingManager.GetOutputDirectory( + CreateBuildOptions(), + job); + + outputDirectory.Should().Be(Path.Combine(artifactsDirectory, "test")); + } + [TestMethod] public void GetOutputDirectory_WithoutResultsDirectory_PrefersDirectoryOfElectedApplicationInput() { @@ -344,8 +364,16 @@ public void GetOutputDirectory_WhenElectedApplicationProducedNoInput_UsesFirstIn } private static ArtifactPostProcessingJob CreateJob(params ArtifactPostProcessingArtifact[] artifacts) + => CreateJob(CreateModule(), artifacts); + + private static ArtifactPostProcessingJob CreateJob(TestModule module, params ArtifactPostProcessingArtifact[] artifacts) { - ArtifactPostProcessingApplication application = CreateApplication(); + var application = new ArtifactPostProcessingApplication( + module, + "net10.0", + "x64", + new HashSet(StringComparer.Ordinal) { "microsoft.testing.trx", "microsoft.codecoverage" }, + new HashSet(StringComparer.Ordinal)); return new ArtifactPostProcessingJob( application, [new ArtifactPostProcessingGroup("microsoft.testing.trx", IsKind: true, artifacts, [application])]); diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index a0a9e29f049f..dfbbb588514d 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -363,6 +363,34 @@ public void RunMultipleTestProjectsWritingTheSameReportName_ShouldKeepBothWithPe reports.Select(File.ReadAllText).Should().BeEquivalentTo(["TestProjectA", "TestProjectB"]); } + [TestMethod] + public void RunMultipleTestProjectsWithArtifactsOutput_ShouldKeepReportsUnderArtifacts() + { + TestAsset testInstance = TestAssetsManager.CopyTestAsset("MultiTestProjectSolutionWithSharedReportName", Guid.NewGuid().ToString()) + .WithSource(); + File.WriteAllText( + Path.Combine(testInstance.Path, "Directory.Build.props"), + """ + + + true + + + """); + string resultsDirectory = Path.Combine(testInstance.Path, "artifacts", "test"); + + CommandResult result = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute("-c", TestingConstants.Debug); + + result.ExitCode.Should().Be(ExitCodes.Success); + + string[] reports = Directory.GetFiles(resultsDirectory, "report.txt", SearchOption.AllDirectories); + reports.Should().HaveCount(2, "artifacts output defaults to a collision-safe per-module layout"); + reports.Select(File.ReadAllText).Should().BeEquivalentTo(["TestProjectA", "TestProjectB"]); + Directory.Exists(Path.Combine(testInstance.Path, "TestResults")).Should().BeFalse(); + } + [TestMethod] public void RunTestProjectsWithTheSameNameAndPerModuleLayout_ShouldDisambiguateAndKeepBothReports() { diff --git a/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs b/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs index f4f6dbdf5e08..40b503a3ef5a 100644 --- a/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/TestCommandParserTests.cs @@ -632,10 +632,10 @@ public void MTPCommandRejectsInvalidListTestsFormatValue(string format) } [TestMethod] - [DataRow(null, nameof(ResultsDirectoryLayout.Flat))] - [DataRow("flat", nameof(ResultsDirectoryLayout.Flat))] - [DataRow("per-module", nameof(ResultsDirectoryLayout.PerModule))] - public void MTPCommandParsesResultsDirectoryLayout(string? value, string expected) + [DataRow(null, nameof(ResultsDirectoryLayout.Flat), false)] + [DataRow("flat", nameof(ResultsDirectoryLayout.Flat), true)] + [DataRow("per-module", nameof(ResultsDirectoryLayout.PerModule), true)] + public void MTPCommandParsesResultsDirectoryLayout(string? value, string expected, bool expectedSpecified) { var command = new TestCommandDefinition.MicrosoftTestingPlatform(); var parseResult = value is null @@ -643,7 +643,9 @@ public void MTPCommandParsesResultsDirectoryLayout(string? value, string expecte : command.Parse(["--results-directory-layout", value]); parseResult.Errors.Should().BeEmpty(); - MSBuildUtility.GetBuildOptions(parseResult).PathOptions.ResultsDirectoryLayout.ToString().Should().Be(expected); + PathOptions pathOptions = MSBuildUtility.GetBuildOptions(parseResult).PathOptions; + pathOptions.ResultsDirectoryLayout.ToString().Should().Be(expected); + pathOptions.ResultsDirectoryLayoutSpecified.Should().Be(expectedSpecified); } [TestMethod] diff --git a/test/dotnet.Tests/CommandTests/Test/TestResultsDirectoryResolverTests.cs b/test/dotnet.Tests/CommandTests/Test/TestResultsDirectoryResolverTests.cs index 496e540cd1c6..f542d7e5f576 100644 --- a/test/dotnet.Tests/CommandTests/Test/TestResultsDirectoryResolverTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/TestResultsDirectoryResolverTests.cs @@ -52,6 +52,49 @@ public void ResolveUsesDefaultRootForPerModuleLayout() resolver.Resolve(module).Should().Be(Path.Combine(WorkingDirectory, "TestResults", "ProjectA", "net10.0_x64")); } + [TestMethod] + public void ResolveUsesArtifactsOutputRootAndPerModuleLayoutByDefault() + { + string artifactsPath = Path.Combine(WorkingDirectory, "artifacts"); + TestModule module = CreateModule( + "ProjectA", + useArtifactsOutput: true, + artifactsPath: artifactsPath, + artifactsProjectName: "CustomProject", + artifactsPivots: "debug_net10.0"); + TestResultsDirectoryResolver resolver = CreateResolver(null, ResultsDirectoryLayout.Flat, module); + + resolver.Resolve(module).Should().Be( + Path.Combine(artifactsPath, "test", "CustomProject", "debug_net10.0")); + } + + [TestMethod] + public void ResolveKeepsConfiguredResultsDirectoryFlatInArtifactsOutputMode() + { + string resultsDirectory = Path.Combine(WorkingDirectory, "custom-results"); + TestModule module = CreateModule( + "ProjectA", + useArtifactsOutput: true, + artifactsPath: Path.Combine(WorkingDirectory, "artifacts")); + TestResultsDirectoryResolver resolver = CreateResolver(resultsDirectory, ResultsDirectoryLayout.Flat, module); + + resolver.Resolve(module).Should().Be(resultsDirectory); + } + + [TestMethod] + public void ResolveHonorsExplicitFlatLayoutInArtifactsOutputMode() + { + string artifactsPath = Path.Combine(WorkingDirectory, "artifacts"); + TestModule module = CreateModule("ProjectA", useArtifactsOutput: true, artifactsPath: artifactsPath); + TestResultsDirectoryResolver resolver = CreateResolver( + null, + ResultsDirectoryLayout.Flat, + layoutSpecified: true, + module); + + resolver.Resolve(module).Should().Be(Path.Combine(artifactsPath, "test")); + } + [TestMethod] public void ResolveNestsTargetFrameworksOfTheSameProjectUnderOneProjectDirectory() { @@ -169,10 +212,32 @@ public void ResolveFallsBackToAssemblyNameWhenModuleHasNoProjectMetadata() private static string WorkingDirectory => Path.GetFullPath("repo"); - private static TestResultsDirectoryResolver CreateResolver(string? resultsDirectory, ResultsDirectoryLayout layout, params TestModule[] modules) - => CreateResolver(resultsDirectory, layout, WorkingDirectory, modules); - - private static TestResultsDirectoryResolver CreateResolver(string? resultsDirectory, ResultsDirectoryLayout layout, string workingDirectory, params TestModule[] modules) + private static TestResultsDirectoryResolver CreateResolver( + string? resultsDirectory, + ResultsDirectoryLayout layout, + params TestModule[] modules) + => CreateResolver(resultsDirectory, layout, WorkingDirectory, layoutSpecified: false, modules); + + private static TestResultsDirectoryResolver CreateResolver( + string? resultsDirectory, + ResultsDirectoryLayout layout, + bool layoutSpecified, + params TestModule[] modules) + => CreateResolver(resultsDirectory, layout, WorkingDirectory, layoutSpecified, modules); + + private static TestResultsDirectoryResolver CreateResolver( + string? resultsDirectory, + ResultsDirectoryLayout layout, + string workingDirectory, + params TestModule[] modules) + => CreateResolver(resultsDirectory, layout, workingDirectory, layoutSpecified: false, modules); + + private static TestResultsDirectoryResolver CreateResolver( + string? resultsDirectory, + ResultsDirectoryLayout layout, + string workingDirectory, + bool layoutSpecified, + params TestModule[] modules) => TestResultsDirectoryResolver.Create( new PathOptions( ProjectOrSolutionPath: null, @@ -181,11 +246,19 @@ private static TestResultsDirectoryResolver CreateResolver(string? resultsDirect ResultsDirectoryPath: resultsDirectory, ResultsDirectoryLayout: layout, ConfigFilePath: null, - DiagnosticOutputDirectoryPath: null), + DiagnosticOutputDirectoryPath: null, + ResultsDirectoryLayoutSpecified: layoutSpecified), modules, workingDirectory); - private static TestModule CreateModule(string projectName = "ProjectA", string? parentDirectory = null, string runtimeIdentifier = "") + private static TestModule CreateModule( + string projectName = "ProjectA", + string? parentDirectory = null, + string runtimeIdentifier = "", + bool useArtifactsOutput = false, + string? artifactsPath = null, + string? artifactsProjectName = null, + string? artifactsPivots = null) { string projectDirectory = parentDirectory is null ? Path.Combine(WorkingDirectory, projectName) @@ -206,6 +279,10 @@ private static TestModule CreateModule(string projectName = "ProjectA", string? LaunchSettings: null, TargetPath: targetPath, DotnetRootArchVariableName: null, - EnvironmentVariables: ImmutableDictionary.Empty); + EnvironmentVariables: ImmutableDictionary.Empty, + UseArtifactsOutput: useArtifactsOutput, + ArtifactsPath: artifactsPath, + ArtifactsProjectName: artifactsProjectName, + ArtifactsPivots: artifactsPivots); } }