diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index 5ee91420958f..2cb9809360e7 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -136,17 +136,11 @@ This file should be imported by eng/Versions.props
11.0.0-preview.7.26363.11711.0.0-preview.7.26363.11711.0.0-preview.7.26363.117
-
- 2.4.0-preview.26367.7
+
+ 2.4.0-preview.26372.142.3.0-preview.26330.8
- 4.4.0-preview.26367.7
- 4.4.0-preview.26367.7
+ 4.4.0-preview.26372.14
+ 4.4.0-preview.26372.14
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 83ee7ddc9dd2..2d7c7b08cdad 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -544,18 +544,18 @@
-
+ https://github.com/microsoft/testfx
- fd744761254d82f054ebba7f8479af01f3822c5a
+ e40436111b52795981c425a49b4c8c2e9a70d9d8
-
+ https://github.com/microsoft/testfx
- fd744761254d82f054ebba7f8479af01f3822c5a
+ e40436111b52795981c425a49b4c8c2e9a70d9d8
-
+ https://github.com/microsoft/testfx
- fd744761254d82f054ebba7f8479af01f3822c5a
+ e40436111b52795981c425a49b4c8c2e9a70d9d8https://github.com/dotnet/dotnet
diff --git a/global.json b/global.json
index 1320db543286..b52fe928f53b 100644
--- a/global.json
+++ b/global.json
@@ -29,6 +29,6 @@
"Microsoft.Build.NoTargets": "3.7.134",
"Microsoft.Build.Traversal": "4.1.82",
"Microsoft.WixToolset.Sdk": "6.0.3-dotnet.6",
- "MSTest.Sdk": "4.4.0-preview.26367.7"
+ "MSTest.Sdk": "4.4.0-preview.26372.14"
}
}
diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx
index 300daaa0169d..fb2ad47dec04 100644
--- a/src/Cli/dotnet/Commands/CliCommandStrings.resx
+++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx
@@ -2669,6 +2669,18 @@ Proceed?
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+
A message of type '{0}' was received in help mode, which is not expected.
diff --git a/src/Cli/dotnet/Commands/Test/CliConstants.cs b/src/Cli/dotnet/Commands/Test/CliConstants.cs
index 70fe370a6e5f..486f6ef791f0 100644
--- a/src/Cli/dotnet/Commands/Test/CliConstants.cs
+++ b/src/Cli/dotnet/Commands/Test/CliConstants.cs
@@ -10,6 +10,8 @@ internal static class CliConstants
public const string DotNetTestPipeOptionKey = "--dotnet-test-pipe";
public const string ServerOptionValue = "dotnettestcli";
+ public const string ArtifactPostProcessingToolName = "internal-merge-artifacts";
+ public const string ArtifactPostProcessingManifestOptionKey = "--manifest";
public const string SemiColon = ";";
@@ -77,6 +79,11 @@ internal static class HandshakeMessagePropertyNames
// can belong to the same attempt. Older hosts omit it, so the SDK retains instance-based
// retry inference as a compatibility fallback.
internal const byte AttemptNumber = 13;
+
+ // Semicolon-separated reverse-DNS artifact kinds and lowercase file extensions
+ // supported by post-processors registered in the test application.
+ internal const byte SupportedPostProcessorKinds = 14;
+ internal const byte SupportedPostProcessorExtensionsLegacy = 15;
}
internal static class HandshakeMessageExecutionModes
@@ -89,6 +96,15 @@ internal static class HandshakeMessageExecutionModes
// The test host is going to discover tests (e.g. --list-tests).
internal const string Discover = "discover";
+
+ // The host is running a non-test tool.
+ internal const string Tool = "tool";
+}
+
+internal static class HandshakeMessageHostTypes
+{
+ internal const string TestHost = "TestHost";
+ internal const string ArtifactPostProcessor = "ArtifactPostProcessor";
}
internal static class ProtocolConstants
diff --git a/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs
new file mode 100644
index 000000000000..45029f9aed6b
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingManager.cs
@@ -0,0 +1,350 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+using System.Globalization;
+using System.Text.Json;
+using Microsoft.DotNet.Cli.Commands.Test.IPC.Models;
+using Microsoft.DotNet.Cli.Commands.Test.Terminal;
+
+namespace Microsoft.DotNet.Cli.Commands.Test;
+
+internal sealed class ArtifactPostProcessingManager
+{
+ private readonly Lock _lock = new();
+ private readonly Dictionary _applications = [];
+ private readonly List _artifacts = [];
+
+ public void RecordCapabilities(
+ TestModule module,
+ string? targetFramework,
+ string? architecture,
+ HandshakeMessage handshakeMessage)
+ {
+ string[] kinds = ParseCapabilities(handshakeMessage, HandshakeMessagePropertyNames.SupportedPostProcessorKinds);
+ string[] extensions = ParseCapabilities(handshakeMessage, HandshakeMessagePropertyNames.SupportedPostProcessorExtensionsLegacy)
+ .Select(extension => extension.ToLowerInvariant())
+ .ToArray();
+
+ if (kinds.Length == 0 && extensions.Length == 0)
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ if (!_applications.TryGetValue(module, out ApplicationState? application))
+ {
+ application = new ApplicationState(module, targetFramework, architecture);
+ _applications.Add(module, application);
+ }
+
+ application.SupportedKinds.UnionWith(kinds);
+ application.SupportedExtensions.UnionWith(extensions);
+ }
+ }
+
+ public void RecordArtifact(
+ TestModule module,
+ string? targetFramework,
+ string? architecture,
+ string executionId,
+ FileArtifactMessage artifact)
+ {
+ lock (_lock)
+ {
+ _artifacts.Add(new ArtifactPostProcessingArtifact(
+ artifact.FullPath!,
+ artifact.Kind,
+ module.TargetPath,
+ targetFramework,
+ architecture,
+ executionId));
+ }
+ }
+
+ public async Task ExecuteAsync(
+ BuildOptions buildOptions,
+ TerminalTestReporter output,
+ CtrlCCancellationManager ctrlC)
+ {
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan(
+ SnapshotApplications(),
+ SnapshotArtifacts());
+
+ foreach (ArtifactPostProcessingJob job in plan.Jobs)
+ {
+ if (ctrlC.Token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ string tempDirectory = Path.Combine(
+ Path.GetTempPath(),
+ $"dotnet-test-postproc-{Guid.NewGuid():N}");
+
+ try
+ {
+ Directory.CreateDirectory(tempDirectory);
+ string manifestPath = Path.Combine(tempDirectory, "manifest.json");
+ string outputDirectory = GetOutputDirectory(buildOptions, job);
+ Directory.CreateDirectory(outputDirectory);
+ WriteManifest(manifestPath, outputDirectory, job.Groups.SelectMany(group => group.Artifacts));
+
+ var invocation = new ArtifactPostProcessingInvocation(manifestPath);
+ var toolOptions = new TestOptions(
+ IsHelp: false,
+ IsDiscovery: false,
+ ListTestsFormat: TestListFormat.Text,
+ IsArtifactPostProcessing: true);
+
+ using var application = new TestApplication(
+ job.Application.Module,
+ buildOptions,
+ toolOptions,
+ output,
+ onHelpRequested: _ => { },
+ artifactPostProcessingManager: this,
+ artifactPostProcessingInvocation: invocation);
+
+ int exitCode = await application.RunAsync(ctrlC);
+ ApplyOutputs(output, job, invocation.SnapshotOutputs());
+
+ if (invocation.FailureMessage is { } failureMessage)
+ {
+ output.WriteWarningMessage(string.Format(
+ CultureInfo.CurrentCulture,
+ CliCommandStrings.ArtifactPostProcessingFailed,
+ job.Application.Module.TargetPath,
+ failureMessage));
+ }
+ else if (exitCode != ExitCode.Success)
+ {
+ output.WriteWarningMessage(string.Format(
+ CultureInfo.CurrentCulture,
+ CliCommandStrings.ArtifactPostProcessingProcessFailed,
+ job.Application.Module.TargetPath,
+ exitCode));
+ }
+ }
+ catch (Exception ex) when (ex is IOException
+ or UnauthorizedAccessException
+ or InvalidOperationException
+ or Win32Exception
+ or NotSupportedException
+ or TimeoutException)
+ {
+ output.WriteWarningMessage(string.Format(
+ CultureInfo.CurrentCulture,
+ CliCommandStrings.ArtifactPostProcessingFailed,
+ job.Application.Module.TargetPath,
+ ex.Message));
+ }
+ finally
+ {
+ try
+ {
+ if (Directory.Exists(tempDirectory))
+ {
+ Directory.Delete(tempDirectory, recursive: true);
+ }
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ Logger.LogTrace($"Failed to clean artifact post-processing temporary directory '{tempDirectory}': {ex}");
+ }
+ }
+ }
+ }
+
+ internal IReadOnlyList SnapshotApplications()
+ {
+ lock (_lock)
+ {
+ return
+ [
+ .. _applications.Values.Select(application => new ArtifactPostProcessingApplication(
+ application.Module,
+ application.TargetFramework,
+ application.Architecture,
+ new HashSet(application.SupportedKinds, StringComparer.Ordinal),
+ new HashSet(application.SupportedExtensions, StringComparer.Ordinal)))
+ ];
+ }
+ }
+
+ internal IReadOnlyList SnapshotArtifacts()
+ {
+ lock (_lock)
+ {
+ return [.. _artifacts];
+ }
+ }
+
+ private static string[] ParseCapabilities(HandshakeMessage handshakeMessage, byte propertyName)
+ => !handshakeMessage.Properties.TryGetValue(propertyName, out string? capabilities)
+ ? []
+ : capabilities
+ .Split(CliConstants.SemiColon, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Where(capability => capability.Length > 0)
+ .Distinct(StringComparer.Ordinal)
+ .ToArray();
+
+ private static string GetOutputDirectory(BuildOptions buildOptions, ArtifactPostProcessingJob job)
+ {
+ if (buildOptions.PathOptions.ResultsDirectoryPath is { } resultsDirectory)
+ {
+ return Path.GetFullPath(resultsDirectory);
+ }
+
+ string firstInputPath = job.Groups
+ .SelectMany(group => group.Artifacts)
+ .Select(artifact => artifact.Path)
+ .OrderBy(path => path, FileUtilities.PathComparer)
+ .First();
+
+ return Path.GetDirectoryName(Path.GetFullPath(firstInputPath))!;
+ }
+
+ private static void WriteManifest(
+ string manifestPath,
+ string outputDirectory,
+ IEnumerable artifacts)
+ {
+ using FileStream stream = File.Create(manifestPath);
+ using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
+
+ writer.WriteStartObject();
+ writer.WriteNumber("schemaVersion", 1);
+ writer.WriteString("outputDirectory", outputDirectory);
+ writer.WriteStartArray("inputs");
+ foreach (ArtifactPostProcessingArtifact artifact in artifacts
+ .OrderBy(artifact => artifact.Path, FileUtilities.PathComparer))
+ {
+ writer.WriteStartObject();
+ writer.WriteString("path", artifact.Path);
+ WriteNullableString(writer, "kind", artifact.Kind);
+ WriteNullableString(writer, "producingTestModule", artifact.ProducingTestModule);
+ WriteNullableString(writer, "targetFramework", artifact.TargetFramework);
+ WriteNullableString(writer, "architecture", artifact.Architecture);
+ writer.WriteString("executionId", artifact.ExecutionId);
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+
+ private static void WriteNullableString(Utf8JsonWriter writer, string propertyName, string? value)
+ {
+ if (value is null)
+ {
+ writer.WriteNull(propertyName);
+ }
+ else
+ {
+ writer.WriteString(propertyName, value);
+ }
+ }
+
+ internal static void ApplyOutputs(
+ TerminalTestReporter output,
+ ArtifactPostProcessingJob job,
+ IReadOnlyList processedArtifacts)
+ {
+ foreach (ArtifactPostProcessingArtifact processedArtifact in processedArtifacts)
+ {
+ string outputExtension = Path.GetExtension(processedArtifact.Path).ToLowerInvariant();
+ // The dispatcher gives one processor both its kind-tagged inputs and matching legacy
+ // extension inputs, so the returned output consumes both groups.
+ ArtifactPostProcessingGroup[] consumedGroups =
+ [
+ .. job.Groups.Where(group =>
+ group.IsKind
+ ? string.Equals(group.Key, processedArtifact.Kind, StringComparison.Ordinal)
+ : string.Equals(group.Key, outputExtension, StringComparison.Ordinal))
+ ];
+
+ if (consumedGroups.Length > 0)
+ {
+ var consumedPaths = new HashSet(
+ consumedGroups.SelectMany(group => group.Artifacts).Select(artifact => artifact.Path),
+ FileUtilities.PathComparer);
+ output.RemoveArtifacts(consumedPaths);
+ }
+
+ output.ArtifactAdded(
+ outOfProcess: true,
+ job.Application.Module.TargetPath,
+ job.Application.TargetFramework,
+ job.Application.Architecture,
+ processedArtifact.ExecutionId,
+ testName: null,
+ processedArtifact.Path);
+ }
+ }
+
+ private sealed class ApplicationState(TestModule module, string? targetFramework, string? architecture)
+ {
+ public TestModule Module { get; } = module;
+ public string? TargetFramework { get; } = targetFramework;
+ public string? Architecture { get; } = architecture;
+ public HashSet SupportedKinds { get; } = new(StringComparer.Ordinal);
+ public HashSet SupportedExtensions { get; } = new(StringComparer.Ordinal);
+ }
+}
+
+internal sealed class ArtifactPostProcessingInvocation(string manifestPath)
+{
+ private readonly Lock _lock = new();
+ private readonly List _outputs = [];
+ private string? _failureMessage;
+
+ public string ManifestPath { get; } = manifestPath;
+
+ public string? FailureMessage
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _failureMessage;
+ }
+ }
+ }
+
+ public void RecordFailure(string message)
+ {
+ lock (_lock)
+ {
+ _failureMessage ??= message;
+ }
+ }
+
+ public void RecordOutput(
+ TestModule module,
+ string? targetFramework,
+ string? architecture,
+ string executionId,
+ FileArtifactMessage artifact)
+ {
+ lock (_lock)
+ {
+ _outputs.Add(new ArtifactPostProcessingArtifact(
+ artifact.FullPath!,
+ artifact.Kind,
+ module.TargetPath,
+ targetFramework,
+ architecture,
+ executionId));
+ }
+ }
+
+ public IReadOnlyList SnapshotOutputs()
+ {
+ lock (_lock)
+ {
+ return [.. _outputs];
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingPlanner.cs b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingPlanner.cs
new file mode 100644
index 000000000000..7eab9a007cdc
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Test/MTP/ArtifactPostProcessingPlanner.cs
@@ -0,0 +1,162 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.Cli.Commands.Test.Terminal;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Commands.Test;
+
+internal sealed record ArtifactPostProcessingApplication(
+ TestModule Module,
+ string? TargetFramework,
+ string? Architecture,
+ IReadOnlySet SupportedKinds,
+ IReadOnlySet SupportedExtensions);
+
+internal sealed record ArtifactPostProcessingArtifact(
+ string Path,
+ string? Kind,
+ string ProducingTestModule,
+ string? TargetFramework,
+ string? Architecture,
+ string ExecutionId);
+
+internal sealed record ArtifactPostProcessingGroup(
+ string Key,
+ bool IsKind,
+ IReadOnlyList Artifacts,
+ IReadOnlyList Candidates);
+
+internal sealed record ArtifactPostProcessingJob(
+ ArtifactPostProcessingApplication Application,
+ IReadOnlyList Groups);
+
+internal sealed record ArtifactPostProcessingPlan(IReadOnlyList Jobs);
+
+internal static class ArtifactPostProcessingPlanner
+{
+ private const string MicrosoftCodeCoverageKind = "microsoft.codecoverage";
+ private const string MicrosoftCodeCoverageExtension = ".coverage";
+
+ public static ArtifactPostProcessingPlan Plan(
+ IReadOnlyList applications,
+ IReadOnlyList artifacts)
+ {
+ ArtifactPostProcessingArtifact[] distinctArtifacts =
+ [.. artifacts.DistinctBy(artifact => artifact.Path, FileUtilities.PathComparer)];
+ List groups = [];
+
+ foreach (IGrouping group in distinctArtifacts
+ .Where(artifact => artifact.Kind is not null)
+ .GroupBy(artifact => artifact.Kind!, StringComparer.Ordinal)
+ .OrderBy(group => group.Key, StringComparer.Ordinal))
+ {
+ AddGroup(group.Key, isKind: true, [.. group]);
+ }
+
+ foreach (IGrouping group in distinctArtifacts
+ .Where(artifact => artifact.Kind is null)
+ .GroupBy(artifact => Path.GetExtension(artifact.Path).ToLowerInvariant(), StringComparer.Ordinal)
+ .Where(group => group.Key.Length > 0)
+ .OrderBy(group => group.Key, StringComparer.Ordinal))
+ {
+ AddGroup(group.Key, isKind: false, [.. group]);
+ }
+
+ ArtifactPostProcessingGroup[] mergeableGroups =
+ [
+ .. groups.Where(group =>
+ group.Artifacts.Count >= 2
+ || groups.Any(other => CanCombineKindAndExtensionGroups(group, other)))
+ ];
+
+ var uncoveredGroups = new HashSet(mergeableGroups);
+ List jobs = [];
+ while (uncoveredGroups.Count > 0)
+ {
+ ArtifactPostProcessingApplication? winner = applications
+ .Select(application => new
+ {
+ Application = application,
+ Groups = uncoveredGroups.Where(group => group.Candidates.Contains(application)).ToArray(),
+ })
+ .Where(candidate => candidate.Groups.Length > 0)
+ .OrderByDescending(candidate => candidate.Groups.Length)
+ .ThenByDescending(candidate => candidate.Groups.Sum(group =>
+ group.Artifacts.Count(artifact => FileUtilities.PathComparer.Equals(
+ artifact.ProducingTestModule,
+ candidate.Application.Module.TargetPath))))
+ .ThenByDescending(candidate => GetFrameworkVersion(candidate.Application.TargetFramework))
+ .ThenBy(candidate => candidate.Application.Module.TargetPath, FileUtilities.PathComparer)
+ .Select(candidate => candidate.Application)
+ .FirstOrDefault();
+
+ if (winner is null)
+ {
+ break;
+ }
+
+ ArtifactPostProcessingGroup[] coveredGroups =
+ [.. uncoveredGroups.Where(group => group.Candidates.Contains(winner))];
+ jobs.Add(new ArtifactPostProcessingJob(winner, coveredGroups));
+ uncoveredGroups.ExceptWith(coveredGroups);
+ }
+
+ return new ArtifactPostProcessingPlan(jobs);
+
+ void AddGroup(string key, bool isKind, ArtifactPostProcessingArtifact[] inputs)
+ {
+ ArtifactPostProcessingApplication[] candidates =
+ [
+ .. applications.Where(application =>
+ (isKind
+ ? application.SupportedKinds.Contains(key)
+ : application.SupportedExtensions.Contains(key))
+ && IsArchitectureCompatible(key, isKind, application, inputs))
+ ];
+
+ if (candidates.Length > 0)
+ {
+ groups.Add(new ArtifactPostProcessingGroup(key, isKind, inputs, candidates));
+ }
+ }
+ }
+
+ private static bool CanCombineKindAndExtensionGroups(
+ ArtifactPostProcessingGroup first,
+ ArtifactPostProcessingGroup second)
+ {
+ ArtifactPostProcessingGroup kindGroup = first.IsKind ? first : second;
+ ArtifactPostProcessingGroup extensionGroup = first.IsKind ? second : first;
+
+ return first.IsKind != second.IsKind
+ && kindGroup.Artifacts.Any(artifact =>
+ string.Equals(
+ Path.GetExtension(artifact.Path),
+ extensionGroup.Key,
+ StringComparison.OrdinalIgnoreCase))
+ && kindGroup.Candidates.Intersect(extensionGroup.Candidates).Any()
+ && kindGroup.Artifacts.Count + extensionGroup.Artifacts.Count >= 2;
+ }
+
+ private static bool IsArchitectureCompatible(
+ string key,
+ bool isKind,
+ ArtifactPostProcessingApplication application,
+ IReadOnlyList artifacts)
+ {
+ bool isArchitectureSensitive = isKind
+ ? string.Equals(key, MicrosoftCodeCoverageKind, StringComparison.Ordinal)
+ : string.Equals(key, MicrosoftCodeCoverageExtension, StringComparison.Ordinal);
+
+ return !isArchitectureSensitive
+ || artifacts.All(artifact =>
+ artifact.Architecture is null
+ || string.Equals(artifact.Architecture, application.Architecture, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static Version GetFrameworkVersion(string? targetFramework)
+ => string.IsNullOrEmpty(targetFramework)
+ ? new Version()
+ : NuGetFramework.ParseFolder(targetFramework).Version;
+}
diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs
index 3d029803c677..cf41b679cf66 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs
@@ -81,12 +81,25 @@ public int Run(ParseResult parseResult, bool isHelp)
var output = InitializeOutput(degreeOfParallelism, parseResult, testOptions);
using var ctrlC = new CtrlCCancellationManager(output.StartCancelling);
+ var artifactPostProcessingManager = new ArtifactPostProcessingManager();
int? exitCode = null;
try
{
- var actionQueue = new TestApplicationActionQueue(degreeOfParallelism, buildOptions, testOptions, output, OnHelpRequested, ctrlC);
+ var actionQueue = new TestApplicationActionQueue(
+ degreeOfParallelism,
+ buildOptions,
+ testOptions,
+ output,
+ OnHelpRequested,
+ ctrlC,
+ artifactPostProcessingManager);
exitCode = testHandler.RunTestApplications(actionQueue);
+ if (!testOptions.IsHelp && !testOptions.IsDiscovery && !ctrlC.Token.IsCancellationRequested)
+ {
+ artifactPostProcessingManager.ExecuteAsync(buildOptions, output, ctrlC).GetAwaiter().GetResult();
+ }
+
// If all test apps exited with 0 exit code, but we detected that handshake didn't happen correctly, map that to generic failure.
if (exitCode == ExitCode.Success && output.HasHandshakeFailure)
{
diff --git a/src/Cli/dotnet/Commands/Test/MTP/Options.cs b/src/Cli/dotnet/Commands/Test/MTP/Options.cs
index ec649748ddbb..3c9425ea8639 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/Options.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/Options.cs
@@ -18,7 +18,7 @@ internal enum TestListFormat
Json,
}
-internal record TestOptions(bool IsHelp, bool IsDiscovery, TestListFormat ListTestsFormat);
+internal record TestOptions(bool IsHelp, bool IsDiscovery, TestListFormat ListTestsFormat, bool IsArtifactPostProcessing = false);
internal record PathOptions(string? ProjectOrSolutionPath, string? SolutionPath, string? TestModules, string? ResultsDirectoryPath, string? ConfigFilePath, string? DiagnosticOutputDirectoryPath);
diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs
index b6bf97983503..64e45c311a76 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs
@@ -39,6 +39,7 @@ internal sealed partial class TerminalTestReporter : IDisposable
private readonly Lock _assembliesLock = new();
private readonly List _artifacts = [];
+ private readonly Lock _artifactsLock = new();
private readonly TerminalTestReporterOptions _options;
@@ -224,7 +225,13 @@ public void TestExecutionCompleted(DateTimeOffset endTime, int? exitCode)
private void AppendTestRunSummary(ITerminal terminal, int? exitCode)
{
- IEnumerable> artifactGroups = _artifacts.GroupBy(a => a.OutOfProcess);
+ TestRunArtifact[] artifacts;
+ lock (_artifactsLock)
+ {
+ artifacts = [.. _artifacts];
+ }
+
+ IEnumerable> artifactGroups = artifacts.GroupBy(a => a.OutOfProcess);
if (artifactGroups.Any())
{
@@ -1040,7 +1047,20 @@ private static void AppendLongDuration(ITerminal terminal, TimeSpan duration, bo
public void Dispose() => _terminalWithProgress.Dispose();
public void ArtifactAdded(bool outOfProcess, string? assembly, string? targetFramework, string? architecture, string? executionId, string? testName, string path)
- => _artifacts.Add(new TestRunArtifact(outOfProcess, assembly, targetFramework, architecture, executionId, testName, path));
+ {
+ lock (_artifactsLock)
+ {
+ _artifacts.Add(new TestRunArtifact(outOfProcess, assembly, targetFramework, architecture, executionId, testName, path));
+ }
+ }
+
+ public void RemoveArtifacts(IReadOnlySet paths)
+ {
+ lock (_artifactsLock)
+ {
+ _artifacts.RemoveAll(artifact => paths.Contains(artifact.Path));
+ }
+ }
internal void WriteMessage(string text) =>
_terminalWithProgress.WriteToTerminal(terminal =>
diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs
index 95848a247919..afba396d872a 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs
@@ -20,15 +20,24 @@ internal sealed class TestApplication(
BuildOptions buildOptions,
TestOptions testOptions,
TerminalTestReporter output,
- Action onHelpRequested) : IDisposable
+ Action onHelpRequested,
+ ArtifactPostProcessingManager? artifactPostProcessingManager = null,
+ ArtifactPostProcessingInvocation? artifactPostProcessingInvocation = null) : IDisposable
{
private static readonly Version ProtocolVersion_1_1 = new(1, 1, 0);
+ private static readonly TimeSpan ArtifactPostProcessingTimeout = TimeSpan.FromMinutes(15);
private const int LiveOutputTailLineCount = 200;
private readonly Lock _requestLock = new();
private readonly BuildOptions _buildOptions = buildOptions;
private readonly Action _onHelpRequested = onHelpRequested;
- private readonly TestApplicationHandler _handler = new(output, module, testOptions);
+ private readonly TestApplicationHandler _handler = new(
+ output,
+ module,
+ testOptions,
+ artifactPostProcessingManager,
+ artifactPostProcessingInvocation);
+ private readonly ArtifactPostProcessingInvocation? _artifactPostProcessingInvocation = artifactPostProcessingInvocation;
private readonly string _pipeName = NamedPipeServer.GetPipeName(Guid.NewGuid().ToString("N"));
@@ -107,7 +116,28 @@ public async Task RunAsync(CtrlCCancellationManager ctrlC)
// WaitForExitAsync only waits for process exit (and doesn't wait for output) for our usage here.
// If we use BeginOutputReadLine/BeginErrorReadLine, it will also wait for output which can deadlock.
- await process.WaitForExitAsync();
+ bool artifactPostProcessingTimedOut = false;
+ if (_artifactPostProcessingInvocation is null)
+ {
+ await process.WaitForExitAsync();
+ }
+ else
+ {
+ try
+ {
+ await process.WaitForExitAsync().WaitAsync(ArtifactPostProcessingTimeout);
+ }
+ catch (TimeoutException)
+ {
+ if (!process.HasExited)
+ {
+ process.Kill(entireProcessTree: true);
+ await process.WaitForExitAsync();
+ }
+
+ artifactPostProcessingTimedOut = true;
+ }
+ }
// At this point, process already exited. Allow for 5 seconds to consume stdout/stderr.
// We might not be able to consume all the output if the test app has exited but left a child process alive.
@@ -119,6 +149,11 @@ public async Task RunAsync(CtrlCCancellationManager ctrlC)
{
}
+ if (artifactPostProcessingTimedOut)
+ {
+ throw new TimeoutException();
+ }
+
var exitCode = process.ExitCode;
_handler.OnTestProcessExited(exitCode, stdOutBuilder.GetOutput(), stdErrBuilder.GetOutput());
@@ -177,7 +212,8 @@ private ProcessStartInfo CreateProcessStartInfo()
processStartInfo.Environment[entry.Key] = entry.Value;
}
- if (!_buildOptions.NoLaunchProfileArguments &&
+ if (_artifactPostProcessingInvocation is null &&
+ !_buildOptions.NoLaunchProfileArguments &&
!string.IsNullOrEmpty(Module.LaunchSettings.CommandLineArgs))
{
processStartInfo.Arguments = $"{processStartInfo.Arguments} {Module.LaunchSettings.CommandLineArgs}";
@@ -209,7 +245,24 @@ private string GetArguments()
// RunArguments is intentionally not escaped. It can contain multiple arguments and spaces there shouldn't cause the whole
// value to be wrapped in double quotes. This matches dotnet run behavior.
// In short, it's expected to already be escaped properly.
- StringBuilder builder = new(Module.RunProperties.Arguments);
+ StringBuilder builder = new(
+ _artifactPostProcessingInvocation is null
+ ? Module.RunProperties.Arguments
+ : GetArtifactPostProcessingLaunchArguments(Module));
+
+ if (_artifactPostProcessingInvocation is not null)
+ {
+ builder.Append($" {CliConstants.ArtifactPostProcessingToolName}");
+ builder.Append($" {CliConstants.ArtifactPostProcessingManifestOptionKey} {ArgumentEscaper.EscapeSingleArg(_artifactPostProcessingInvocation.ManifestPath)}");
+
+ if (_buildOptions.PathOptions.DiagnosticOutputDirectoryPath is { } toolDiagnosticOutputDirectoryPath)
+ {
+ builder.Append($" {TestCommandDefinition.MicrosoftTestingPlatform.DiagnosticOutputDirectoryOptionName} {ArgumentEscaper.EscapeSingleArg(toolDiagnosticOutputDirectoryPath)}");
+ }
+
+ builder.Append($" {CliConstants.ServerOptionKey} {CliConstants.ServerOptionValue} {CliConstants.DotNetTestPipeOptionKey} {ArgumentEscaper.EscapeSingleArg(_pipeName)}");
+ return builder.ToString();
+ }
if (TestOptions.IsHelp)
{
@@ -246,6 +299,14 @@ private string GetArguments()
return builder.ToString();
}
+ internal static string GetArtifactPostProcessingLaunchArguments(TestModule module)
+ => string.Equals(
+ Path.GetFileNameWithoutExtension(module.RunProperties.Command),
+ "dotnet",
+ StringComparison.OrdinalIgnoreCase)
+ ? $"exec {ArgumentEscaper.EscapeSingleArg(module.TargetPath)}"
+ : string.Empty;
+
private async Task WaitConnectionAsync(CancellationToken token)
{
try
diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs
index b8ebac54a05f..7cb386662f5e 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs
@@ -17,14 +17,27 @@ internal class TestApplicationActionQueue
private readonly Lock _lock = new();
- public TestApplicationActionQueue(int degreeOfParallelism, BuildOptions buildOptions, TestOptions testOptions, TerminalTestReporter output, Action onHelpRequested, CtrlCCancellationManager ctrlC)
+ public TestApplicationActionQueue(
+ int degreeOfParallelism,
+ BuildOptions buildOptions,
+ TestOptions testOptions,
+ TerminalTestReporter output,
+ Action onHelpRequested,
+ CtrlCCancellationManager ctrlC,
+ ArtifactPostProcessingManager artifactPostProcessingManager)
{
_channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = false, SingleWriter = false });
_readers = new Task[degreeOfParallelism];
for (int i = 0; i < degreeOfParallelism; i++)
{
- _readers[i] = Task.Run(async () => await Read(buildOptions, testOptions, output, onHelpRequested, ctrlC));
+ _readers[i] = Task.Run(async () => await Read(
+ buildOptions,
+ testOptions,
+ output,
+ onHelpRequested,
+ ctrlC,
+ artifactPostProcessingManager));
}
}
@@ -48,7 +61,13 @@ public int CompleteEnqueueAndWait()
return _aggregateExitCode ?? ExitCode.ZeroTests;
}
- private async Task Read(BuildOptions buildOptions, TestOptions testOptions, TerminalTestReporter output, Action onHelpRequested, CtrlCCancellationManager ctrlC)
+ private async Task Read(
+ BuildOptions buildOptions,
+ TestOptions testOptions,
+ TerminalTestReporter output,
+ Action onHelpRequested,
+ CtrlCCancellationManager ctrlC,
+ ArtifactPostProcessingManager artifactPostProcessingManager)
{
try
{
@@ -59,7 +78,13 @@ private async Task Read(BuildOptions buildOptions, TestOptions testOptions, Term
ctrlC.Token.ThrowIfCancellationRequested();
int result = ExitCode.GenericFailure;
- var testApp = new TestApplication(module, buildOptions, testOptions, output, onHelpRequested);
+ var testApp = new TestApplication(
+ module,
+ buildOptions,
+ testOptions,
+ output,
+ onHelpRequested,
+ artifactPostProcessingManager);
try
{
using (testApp)
diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs
index b41b46cf491b..ad35be40f565 100644
--- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs
+++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationHandler.cs
@@ -11,17 +11,26 @@ internal sealed class TestApplicationHandler
private readonly TerminalTestReporter _output;
private readonly TestModule _module;
private readonly TestOptions _options;
+ private readonly ArtifactPostProcessingManager? _artifactPostProcessingManager;
+ private readonly ArtifactPostProcessingInvocation? _artifactPostProcessingInvocation;
private readonly Lock _lock = new();
private readonly Dictionary _testSessionEventCountPerSessionUid = new();
private (string? TargetFramework, string? Architecture, string ExecutionId)? _handshakeInfo;
private bool _receivedTestHostHandshake;
- public TestApplicationHandler(TerminalTestReporter output, TestModule module, TestOptions options)
+ public TestApplicationHandler(
+ TerminalTestReporter output,
+ TestModule module,
+ TestOptions options,
+ ArtifactPostProcessingManager? artifactPostProcessingManager = null,
+ ArtifactPostProcessingInvocation? artifactPostProcessingInvocation = null)
{
_output = output;
_module = module;
_options = options;
+ _artifactPostProcessingManager = artifactPostProcessingManager;
+ _artifactPostProcessingInvocation = artifactPostProcessingInvocation;
}
///
@@ -61,9 +70,19 @@ internal bool OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup
var tfm = TargetFrameworkParser.GetShortTargetFramework(framework);
var currentHandshakeInfo = (tfm, arch, executionId!);
+ if (_options.IsArtifactPostProcessing
+ && hostType != HandshakeMessageHostTypes.ArtifactPostProcessor)
+ {
+ ReportHandshakeFailure(string.Format(
+ CliCommandStrings.MismatchingHandshakeHostType,
+ hostType,
+ HandshakeMessageHostTypes.ArtifactPostProcessor));
+ return false;
+ }
+
// https://github.com/microsoft/testfx/blob/2a9a353ec2bb4ce403f72e8ba1f29e01e7cf1fd4/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs#L87-L97
string? instanceId = null;
- if (hostType == "TestHost"
+ if (hostType == HandshakeMessageHostTypes.TestHost
&& !TryGetRequiredHandshakeProperty(handshakeMessage, HandshakeMessagePropertyNames.InstanceId, out instanceId, out validationError))
{
ReportHandshakeFailure(validationError!);
@@ -80,7 +99,7 @@ internal bool OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup
return false;
}
- if (hostType == "TestHost")
+ if (hostType == HandshakeMessageHostTypes.TestHost)
{
int? attemptNumber = null;
// Invalid values fall back to legacy instance-based inference. Testfx normalizes malformed
@@ -121,12 +140,23 @@ internal bool OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup
return false;
}
+ if (!_options.IsArtifactPostProcessing)
+ {
+ _artifactPostProcessingManager?.RecordCapabilities(
+ _module,
+ _module.TargetFramework ?? tfm,
+ arch,
+ handshakeMessage);
+ }
+
return true;
}
private bool IsExpectedExecutionMode(string reportedMode, out string expectedMode)
{
- expectedMode = _options.IsHelp
+ expectedMode = _options.IsArtifactPostProcessing
+ ? HandshakeMessageExecutionModes.Tool
+ : _options.IsHelp
? HandshakeMessageExecutionModes.Help
: _options.IsDiscovery
? HandshakeMessageExecutionModes.Discover
@@ -147,7 +177,14 @@ private bool IsExpectedExecutionMode(string reportedMode, out string expectedMod
// HandshakeFailure with no actionable context. Explicit programmatic rejections here (unsupported
// protocol version, missing required property, mismatching handshake info, mismatching execution
// mode) are real protocol failures and must still be surfaced even when the SDK is in help mode.
- private void ReportHandshakeFailure(string failureMessage) =>
+ private void ReportHandshakeFailure(string failureMessage)
+ {
+ if (_artifactPostProcessingInvocation is not null)
+ {
+ _artifactPostProcessingInvocation.RecordFailure(failureMessage);
+ return;
+ }
+
_output.HandshakeFailure(
_module.TargetPath,
string.Empty,
@@ -155,6 +192,7 @@ private void ReportHandshakeFailure(string failureMessage) =>
failureMessage,
string.Empty,
reportEvenWhenHelp: true);
+ }
private static bool TryGetRequiredHandshakeProperty(HandshakeMessage handshakeMessage, byte propertyId, out string? value, out string? failureMessage)
{
@@ -199,6 +237,8 @@ private static string GetHandshakePropertyName(byte propertyId) =>
HandshakeMessagePropertyNames.IsIDE => nameof(HandshakeMessagePropertyNames.IsIDE),
HandshakeMessagePropertyNames.ExecutionMode => nameof(HandshakeMessagePropertyNames.ExecutionMode),
HandshakeMessagePropertyNames.AttemptNumber => nameof(HandshakeMessagePropertyNames.AttemptNumber),
+ HandshakeMessagePropertyNames.SupportedPostProcessorKinds => nameof(HandshakeMessagePropertyNames.SupportedPostProcessorKinds),
+ HandshakeMessagePropertyNames.SupportedPostProcessorExtensionsLegacy => nameof(HandshakeMessagePropertyNames.SupportedPostProcessorExtensionsLegacy),
_ => string.Empty,
};
@@ -367,7 +407,9 @@ internal void OnFileArtifactsReceived(FileArtifactMessages fileArtifactMessages)
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutHandshake, nameof(FileArtifactMessages)));
}
- if (!_receivedTestHostHandshake)
+ if (_options.IsArtifactPostProcessing
+ ? _artifactPostProcessingInvocation is null
+ : !_receivedTestHostHandshake)
{
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutTestHostHandshake, nameof(FileArtifactMessages)));
}
@@ -391,10 +433,28 @@ internal void OnFileArtifactsReceived(FileArtifactMessages fileArtifactMessages)
nameof(FileArtifactMessage.FullPath),
nameof(FileArtifactMessage));
- _output.ArtifactAdded(
- outOfProcess: false,
- _module.TargetPath, handshakeInfo.TargetFramework, handshakeInfo.Architecture, handshakeInfo.ExecutionId,
- artifact.TestDisplayName, fullPath);
+ if (_artifactPostProcessingInvocation is not null)
+ {
+ _artifactPostProcessingInvocation.RecordOutput(
+ _module,
+ handshakeInfo.TargetFramework,
+ handshakeInfo.Architecture,
+ handshakeInfo.ExecutionId,
+ artifact with { FullPath = fullPath });
+ }
+ else
+ {
+ _artifactPostProcessingManager?.RecordArtifact(
+ _module,
+ _module.TargetFramework ?? handshakeInfo.TargetFramework,
+ handshakeInfo.Architecture,
+ handshakeInfo.ExecutionId,
+ artifact with { FullPath = fullPath });
+ _output.ArtifactAdded(
+ outOfProcess: false,
+ _module.TargetPath, handshakeInfo.TargetFramework, handshakeInfo.Architecture, handshakeInfo.ExecutionId,
+ artifact.TestDisplayName, fullPath);
+ }
}
}
@@ -550,6 +610,14 @@ internal void WriteMessage(string? text)
internal void OnTestProcessExited(int exitCode, string outputData, string errorData)
{
+ if (_options.IsArtifactPostProcessing)
+ {
+ WriteMessage(outputData);
+ WriteMessage(errorData);
+ LogTestProcessExit(exitCode, outputData, errorData);
+ return;
+ }
+
if (_receivedTestHostHandshake && _handshakeInfo.HasValue)
{
// If we received a handshake from TestHostController but not from TestHost,
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
index b1e58cab23ce..d68a523b3fad 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
@@ -77,6 +77,16 @@
Při instalaci balíčku nástroje .NET povolte downgrade balíčku.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyÚloha Aspire je zastaralá a už není potřeba. Aspire je nyní k dispozici v podobě balíčků NuGet, které můžete přidat přímo do svých projektů. Další informace najdete na https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Nezobrazovat úvodní nápis ani zprávu o autorských právech
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
index 7957c660dd14..03f6b713c536 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
@@ -77,6 +77,16 @@
Paketdowngrade beim Installieren eines .NET-Toolpakets zulassen.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyDie Workload „Aspire“ ist veraltet und nicht mehr erforderlich. Aspire ist jetzt als NuGet-Pakete verfügbar, die Sie direkt Ihren Projekten hinzufügen können. Weitere Informationen finden Sie unter https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Zeigt kein Startbanner und keine Copyrightmeldung an.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
index 380aaa3a0754..f069245e21e0 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
@@ -77,6 +77,16 @@
Permitir la degradación del paquete al instalar un paquete de herramientas de .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyLa carga de trabajo Aspire está en desuso y ya no es necesaria. Ahora Aspire está disponible como paquetes NuGet que puedes añadir directamente a tus proyectos. Para obtener más información, consulta https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.No mostrar la pancarta de inicio ni el mensaje de copyright.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
index 682ef17c0211..5ad04dc966a1 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
@@ -77,6 +77,16 @@
Autoriser le passage à une version antérieure du package lors de l’installation d’un package d’outils .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyLa charge de travail Aspire est dépréciée et n’est plus nécessaire. Aspire est désormais disponible sous forme de packages NuGet que vous pouvez ajouter directement à vos projets. Pour plus d’informations, consultez https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.N'affiche pas la bannière de démarrage ni le message de copyright.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
index 5fe2071c60c5..d90f804ec634 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
@@ -77,6 +77,16 @@
Consente il downgrade del pacchetto durante l'installazione di un pacchetto di strumenti .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyIl carico di lavoro Aspire è deprecato e non è più necessario. Aspire ora è disponibile come pacchetti NuGet che puoi aggiungere direttamente ai tuoi progetti. Per altre informazioni, visita https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Evita la visualizzazione del messaggio di avvio o di copyright.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
index c13e31663d28..c9ee12ca6562 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
@@ -77,6 +77,16 @@
.NET ツール パッケージのインストール時にパッケージのダウングレードを許可します。
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyAspire ワークロードは非推奨となり、不要になりました。Aspire は現在、プロジェクトに直接追加できる NuGet パッケージとして利用可能です。詳細情報については、https://aka.ms/aspire/support-policy をご覧ください
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.著作権情報を表示しません。
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
index 4cb39d88ed6e..e8ee1ea33595 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
@@ -77,6 +77,16 @@
.NET 도구 패키지를 설치할 때 패키지 다운그레이드를 허용합니다.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyAspire 워크로드는 더 이상 사용되지 않으며 필요하지 않습니다. Aspire는 이제 NuGet 패키지로 제공되어 프로젝트에 직접 추가할 수 있습니다. 자세한 내용은 https://aka.ms/aspire/support-policy를 참조하세요.
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.시작 배너 또는 저작권 메시지를 표시하지 않습니다.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
index 8815b7f2bce4..dcf0c966444a 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
@@ -77,6 +77,16 @@
Zezwalaj na obniżanie wersji pakietu podczas instalowania pakietu narzędzi .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyObciążenie Aspire jest przestarzałe i nie jest już konieczne. Usługa Aspire jest teraz dostępna jako pakiety NuGet, które można dodawać bezpośrednio do projektów. Więcej informacji znajdziesz na stronie https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Nie wyświetlaj baneru początkowego ani komunikatu o prawach autorskich.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
index f7b819fdc97e..3abd4cc1b7fa 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
@@ -77,6 +77,16 @@
Permitir downgrade de pacote ao instalar um pacote de ferramentas do .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyA carga de trabalho Aspire está obsoleta e não é mais necessária. O Aspire agora está disponível como pacotes NuGet que você pode adicionar diretamente aos seus projetos. Para mais informações, consulte https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Não exibe a faixa de inicialização ou a mensagem de direitos autorais.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
index 3deeb5bc49fc..51f12fe5c977 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
@@ -77,6 +77,16 @@
Разрешить переход на использование более ранней версии пакета при установке пакета инструментов .NET.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyРабочая нагрузка Aspire является нерекомендуемой и больше не требуется. Теперь нагрузка Aspire доступна в виде пакетов NuGet, которые можно добавлять непосредственно в проекты. Дополнительные сведения см. на странице https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Не отображать начальный баннер или сообщение об авторских правах.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
index 05effa1a2e5d..edd13bf786fc 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
@@ -77,6 +77,16 @@
Bir .NET araç paketini yüklerken paketi eski sürüme düşürmeye izin verin.
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyAspire iş yükü kullanım dışı bırakıldı ve artık gerekli değil. Aspire, projelerinize doğrudan ekleyebileceğiniz NuGet paketleri olarak kullanılabilir. Daha fazla bilgi için bkz. https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.Başlangıç bandını veya telif hakkı iletisini görüntüleme.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
index 187c0fd21578..24b405e04627 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
@@ -77,6 +77,16 @@
安装 .NET 工具包时允许包降级。
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyAspire 工作负载已弃用,并且不再是必需项。Aspire 现在以 NuGet 包的形式提供,可以直接将其添加到项目中。有关详细信息,请参阅 https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.不显示启动版权标志或版权消息。
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
index 939e7e12d702..802dc1ccaf9c 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
@@ -77,6 +77,16 @@
安裝 .NET 工具套件時允許套件降級。
+
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ Artifact post-processing with '{0}' failed: {1} Original artifacts will be reported.
+ {0} is the test application path. {1} is the failure reason.
+
+
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ Artifact post-processing with '{0}' exited with code {1}. Any artifacts that were not merged will be reported individually.
+ {0} is the test application path. {1} is the process exit code.
+ The Aspire workload is deprecated and no longer necessary. Aspire is now available as NuGet packages that you can add directly to your projects. For more information, see https://aka.ms/aspire/support-policyAspire 工作負載已棄用,不再需要。Aspire 現已作為 NuGet 套件提供,您可以將它直接新增至專案。如需詳細資訊,請參閱 https://aka.ms/aspire/support-policy
@@ -147,6 +157,11 @@
The test host reported execution mode '{0}', but 'dotnet test' expected '{1}'. This typically happens when an option such as '--help', '-?' or '--list-tests' was injected into the test host through a non-CLI channel (e.g. the 'TestingPlatformCommandLineArguments' or 'RunArguments' MSBuild properties, or 'launchSettings.json' commandLineArgs). Pass these options directly to 'dotnet test' instead.{Locked="dotnet test"}{Locked="--help"}{Locked="-?"}{Locked="--list-tests"}{Locked="TestingPlatformCommandLineArguments"}{Locked="RunArguments"}{Locked="launchSettings.json"}{Locked="commandLineArgs"}
+
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ The test host reported host type '{0}', but 'dotnet test' expected '{1}'.
+ {Locked="dotnet test"}
+ Do not display the startup banner or the copyright message.不顯示啟始資訊或著作權訊息。
diff --git a/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs
new file mode 100644
index 000000000000..1de00153a755
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingManagerTests.cs
@@ -0,0 +1,169 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.Cli.Commands.Run;
+using Microsoft.DotNet.Cli.Commands.Test;
+using Microsoft.DotNet.Cli.Commands.Test.Terminal;
+using TestExitCode = Microsoft.DotNet.Cli.Commands.Test.ExitCode;
+
+namespace dotnet.Tests.CommandTests.Test;
+
+[TestClass]
+public class ArtifactPostProcessingManagerTests
+{
+ [TestMethod]
+ public void ApplyOutputs_MatchingKind_ReplacesOriginalArtifacts()
+ {
+ var console = new CapturingConsole();
+ using var reporter = CreateReporter(console);
+ ArtifactPostProcessingArtifact first = CreateArtifact("first.trx", "microsoft.testing.trx");
+ ArtifactPostProcessingArtifact second = CreateArtifact("second.trx", "microsoft.testing.trx");
+ ArtifactPostProcessingApplication application = CreateApplication();
+ var group = new ArtifactPostProcessingGroup(
+ "microsoft.testing.trx",
+ IsKind: true,
+ [first, second],
+ [application]);
+ var job = new ArtifactPostProcessingJob(application, [group]);
+ reporter.ArtifactAdded(false, "A.dll", "net10.0", "x64", "execution-1", null, first.Path);
+ reporter.ArtifactAdded(false, "B.dll", "net10.0", "x64", "execution-2", null, second.Path);
+ ArtifactPostProcessingArtifact merged = CreateArtifact("merged.trx", "microsoft.testing.trx");
+
+ ArtifactPostProcessingManager.ApplyOutputs(reporter, job, [merged]);
+ reporter.TestExecutionCompleted(DateTimeOffset.UtcNow, TestExitCode.Success);
+
+ string output = console.GetOutput();
+ output.Should().Contain("merged.trx");
+ output.Should().NotContain("first.trx");
+ output.Should().NotContain("second.trx");
+ }
+
+ [TestMethod]
+ public void ApplyOutputs_UnmatchedOutput_StillReportsOutputAndPreservesOriginals()
+ {
+ var console = new CapturingConsole();
+ using var reporter = CreateReporter(console);
+ ArtifactPostProcessingArtifact first = CreateArtifact("first.coverage", "microsoft.codecoverage");
+ ArtifactPostProcessingArtifact second = CreateArtifact("second.coverage", "microsoft.codecoverage");
+ ArtifactPostProcessingApplication application = CreateApplication();
+ var group = new ArtifactPostProcessingGroup(
+ "microsoft.codecoverage",
+ IsKind: true,
+ [first, second],
+ [application]);
+ var job = new ArtifactPostProcessingJob(application, [group]);
+ reporter.ArtifactAdded(false, "A.dll", "net10.0", "x64", "execution-1", null, first.Path);
+ reporter.ArtifactAdded(false, "B.dll", "net10.0", "x64", "execution-2", null, second.Path);
+ ArtifactPostProcessingArtifact converted = CreateArtifact("coverage.cobertura.xml", "cobertura");
+
+ ArtifactPostProcessingManager.ApplyOutputs(reporter, job, [converted]);
+ reporter.TestExecutionCompleted(DateTimeOffset.UtcNow, TestExitCode.Success);
+
+ string output = console.GetOutput();
+ output.Should().Contain("coverage.cobertura.xml");
+ output.Should().Contain("first.coverage");
+ output.Should().Contain("second.coverage");
+ }
+
+ [TestMethod]
+ public void ApplyOutputs_KindOutput_AlsoConsumesLegacyInputsWithSameExtension()
+ {
+ var console = new CapturingConsole();
+ using var reporter = CreateReporter(console);
+ ArtifactPostProcessingArtifact taggedFirst = CreateArtifact("tagged-first.xml", "example.junit");
+ ArtifactPostProcessingArtifact taggedSecond = CreateArtifact("tagged-second.xml", "example.junit");
+ ArtifactPostProcessingArtifact legacyFirst = CreateArtifact("legacy-first.xml", kind: null);
+ ArtifactPostProcessingArtifact legacySecond = CreateArtifact("legacy-second.xml", kind: null);
+ ArtifactPostProcessingApplication application = CreateApplication();
+ var taggedGroup = new ArtifactPostProcessingGroup(
+ "example.junit",
+ IsKind: true,
+ [taggedFirst, taggedSecond],
+ [application]);
+ var fallbackGroup = new ArtifactPostProcessingGroup(
+ ".xml",
+ IsKind: false,
+ [legacyFirst, legacySecond],
+ [application]);
+ var job = new ArtifactPostProcessingJob(application, [taggedGroup, fallbackGroup]);
+ foreach (ArtifactPostProcessingArtifact artifact in taggedGroup.Artifacts.Concat(fallbackGroup.Artifacts))
+ {
+ reporter.ArtifactAdded(false, "A.dll", "net10.0", "x64", artifact.ExecutionId, null, artifact.Path);
+ }
+
+ ArtifactPostProcessingManager.ApplyOutputs(
+ reporter,
+ job,
+ [CreateArtifact("merged.xml", "example.junit")]);
+ reporter.TestExecutionCompleted(DateTimeOffset.UtcNow, TestExitCode.Success);
+
+ string output = console.GetOutput();
+ output.Should().Contain("merged.xml");
+ output.Should().NotContain("tagged-first.xml");
+ output.Should().NotContain("tagged-second.xml");
+ output.Should().NotContain("legacy-first.xml");
+ output.Should().NotContain("legacy-second.xml");
+ }
+
+ [TestMethod]
+ public void GetArtifactPostProcessingLaunchArguments_DotnetCommand_UsesOnlyExecAndTargetPath()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication();
+
+ string arguments = TestApplication.GetArtifactPostProcessingLaunchArguments(application.Module);
+
+ arguments.Should().Be("exec A.dll");
+ }
+
+ [TestMethod]
+ public void GetArtifactPostProcessingLaunchArguments_AppHost_UsesNoTestArguments()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication();
+ TestModule appHostModule = application.Module with
+ {
+ RunProperties = new RunProperties("A.exe", "--filter injected", null),
+ };
+
+ string arguments = TestApplication.GetArtifactPostProcessingLaunchArguments(appHostModule);
+
+ arguments.Should().BeEmpty();
+ }
+
+ private static TerminalTestReporter CreateReporter(CapturingConsole console)
+ {
+ var reporter = new TerminalTestReporter(console, new TerminalTestReporterOptions
+ {
+ AnsiMode = AnsiMode.SimpleAnsi,
+ ShowProgress = false,
+ });
+ reporter.TestExecutionStarted(
+ DateTimeOffset.UtcNow,
+ workerCount: 1,
+ isDiscovery: false,
+ isHelp: false,
+ isRetry: false);
+ return reporter;
+ }
+
+ private static ArtifactPostProcessingApplication CreateApplication()
+ {
+ var module = new TestModule(
+ new RunProperties("dotnet", "A.dll", null),
+ ProjectFullPath: null,
+ TargetFramework: "net10.0",
+ IsTestingPlatformApplication: true,
+ LaunchSettings: null,
+ TargetPath: "A.dll",
+ DotnetRootArchVariableName: null,
+ EnvironmentVariables: new Dictionary());
+ return new ArtifactPostProcessingApplication(
+ module,
+ "net10.0",
+ "x64",
+ new HashSet(StringComparer.Ordinal) { "microsoft.testing.trx", "microsoft.codecoverage" },
+ new HashSet(StringComparer.Ordinal));
+ }
+
+ private static ArtifactPostProcessingArtifact CreateArtifact(string path, string? kind)
+ => new(path, kind, "A.dll", "net10.0", "x64", Guid.NewGuid().ToString("N"));
+}
diff --git a/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingPlannerTests.cs b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingPlannerTests.cs
new file mode 100644
index 000000000000..c1debaad7193
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Test/ArtifactPostProcessingPlannerTests.cs
@@ -0,0 +1,243 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.Cli.Commands.Run;
+using Microsoft.DotNet.Cli.Commands.Test;
+
+namespace dotnet.Tests.CommandTests.Test;
+
+[TestClass]
+public class ArtifactPostProcessingPlannerTests
+{
+ [TestMethod]
+ public void Plan_OneApplicationCoversAllGroups_CreatesOneJob()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx", "example.junit"],
+ []);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A-1.trx", "microsoft.testing.trx", "A.dll", "x64"),
+ CreateArtifact("B-1.trx", "microsoft.testing.trx", "B.dll", "x64"),
+ CreateArtifact("A-1.xml", "example.junit", "A.dll", "x64"),
+ CreateArtifact("B-1.xml", "example.junit", "B.dll", "x64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([application], artifacts);
+
+ plan.Jobs.Should().ContainSingle();
+ plan.Jobs[0].Application.Should().BeSameAs(application);
+ plan.Jobs[0].Groups.Select(group => group.Key)
+ .Should().BeEquivalentTo("microsoft.testing.trx", "example.junit");
+ }
+
+ [TestMethod]
+ public void Plan_ApplicationCoveringMostGroups_WinsMinimalSetCover()
+ {
+ ArtifactPostProcessingApplication trxOnly = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx"],
+ []);
+ ArtifactPostProcessingApplication both = CreateApplication(
+ "B.dll",
+ "net9.0",
+ "x64",
+ ["microsoft.testing.trx", "example.junit"],
+ []);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A.trx", "microsoft.testing.trx", "A.dll", "x64"),
+ CreateArtifact("B.trx", "microsoft.testing.trx", "B.dll", "x64"),
+ CreateArtifact("A.xml", "example.junit", "A.dll", "x64"),
+ CreateArtifact("B.xml", "example.junit", "B.dll", "x64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([trxOnly, both], artifacts);
+
+ plan.Jobs.Should().ContainSingle();
+ plan.Jobs[0].Application.Should().BeSameAs(both);
+ }
+
+ [TestMethod]
+ public void Plan_SplitCapabilities_CreatesOneJobPerApplication()
+ {
+ ArtifactPostProcessingApplication trx = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx"],
+ []);
+ ArtifactPostProcessingApplication junit = CreateApplication(
+ "B.dll",
+ "net10.0",
+ "x64",
+ ["example.junit"],
+ []);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A.trx", "microsoft.testing.trx", "A.dll", "x64"),
+ CreateArtifact("B.trx", "microsoft.testing.trx", "B.dll", "x64"),
+ CreateArtifact("A.xml", "example.junit", "A.dll", "x64"),
+ CreateArtifact("B.xml", "example.junit", "B.dll", "x64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([trx, junit], artifacts);
+
+ plan.Jobs.Should().HaveCount(2);
+ plan.Jobs.Select(job => job.Application).Should().Contain(trx).And.Contain(junit);
+ }
+
+ [TestMethod]
+ public void Plan_UntaggedArtifacts_UsesExtensionFallback()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ [],
+ [".trx"]);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A.TRX", kind: null, "A.dll", "x64"),
+ CreateArtifact("B.trx", kind: null, "B.dll", "x64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([application], artifacts);
+
+ plan.Jobs.Should().ContainSingle();
+ plan.Jobs[0].Groups.Should().ContainSingle();
+ plan.Jobs[0].Groups[0].Key.Should().Be(".trx");
+ plan.Jobs[0].Groups[0].IsKind.Should().BeFalse();
+ }
+
+ [TestMethod]
+ public void Plan_TaggedAndLegacyArtifactsTogether_MeetMergeThreshold()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx"],
+ [".trx"]);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A.trx", "microsoft.testing.trx", "A.dll", "x64"),
+ CreateArtifact("B.trx", kind: null, "B.dll", "x64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([application], artifacts);
+
+ plan.Jobs.Should().ContainSingle();
+ plan.Jobs[0].Groups.Should().HaveCount(2);
+ plan.Jobs[0].Groups.SelectMany(group => group.Artifacts).Should().HaveCount(2);
+ }
+
+ [TestMethod]
+ public void Plan_OneArtifactOrNoCapability_CreatesNoJobs()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx"],
+ []);
+
+ ArtifactPostProcessingPlan oneArtifact = ArtifactPostProcessingPlanner.Plan(
+ [application],
+ [CreateArtifact("A.trx", "microsoft.testing.trx", "A.dll", "x64")]);
+ ArtifactPostProcessingPlan unsupported = ArtifactPostProcessingPlanner.Plan(
+ [application],
+ [
+ CreateArtifact("A.xml", "example.junit", "A.dll", "x64"),
+ CreateArtifact("B.xml", "example.junit", "B.dll", "x64"),
+ ]);
+
+ oneArtifact.Jobs.Should().BeEmpty();
+ unsupported.Jobs.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public void Plan_SameArtifactReportedTwice_DoesNotCreateJob()
+ {
+ ArtifactPostProcessingApplication application = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.testing.trx"],
+ []);
+ ArtifactPostProcessingArtifact artifact =
+ CreateArtifact("A.trx", "microsoft.testing.trx", "A.dll", "x64");
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan(
+ [application],
+ [artifact, artifact with { ExecutionId = "another-execution" }]);
+
+ plan.Jobs.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public void Plan_CodeCoverage_RequiresArchitectureCompatibleApplication()
+ {
+ ArtifactPostProcessingApplication x64 = CreateApplication(
+ "A.dll",
+ "net10.0",
+ "x64",
+ ["microsoft.codecoverage"],
+ []);
+ ArtifactPostProcessingApplication arm64 = CreateApplication(
+ "B.dll",
+ "net10.0",
+ "arm64",
+ ["microsoft.codecoverage"],
+ []);
+ ArtifactPostProcessingArtifact[] artifacts =
+ [
+ CreateArtifact("A.coverage", "microsoft.codecoverage", "A.dll", "arm64"),
+ CreateArtifact("B.coverage", "microsoft.codecoverage", "B.dll", "arm64"),
+ ];
+
+ ArtifactPostProcessingPlan plan = ArtifactPostProcessingPlanner.Plan([x64, arm64], artifacts);
+
+ plan.Jobs.Should().ContainSingle();
+ plan.Jobs[0].Application.Should().BeSameAs(arm64);
+ }
+
+ private static ArtifactPostProcessingApplication CreateApplication(
+ string targetPath,
+ string targetFramework,
+ string architecture,
+ string[] kinds,
+ string[] extensions)
+ => new(
+ new TestModule(
+ new RunProperties("dotnet", targetPath, null),
+ ProjectFullPath: null,
+ TargetFramework: targetFramework,
+ IsTestingPlatformApplication: true,
+ LaunchSettings: null,
+ TargetPath: targetPath,
+ DotnetRootArchVariableName: null,
+ EnvironmentVariables: new Dictionary()),
+ targetFramework,
+ architecture,
+ new HashSet(kinds, StringComparer.Ordinal),
+ new HashSet(extensions, StringComparer.Ordinal));
+
+ private static ArtifactPostProcessingArtifact CreateArtifact(
+ string path,
+ string? kind,
+ string producingTestModule,
+ string architecture)
+ => new(
+ path,
+ kind,
+ producingTestModule,
+ "net10.0",
+ architecture,
+ Guid.NewGuid().ToString("N"));
+}
diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsArtifactPostProcessingMTP.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsArtifactPostProcessingMTP.cs
new file mode 100644
index 000000000000..68a9731b1d15
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsArtifactPostProcessingMTP.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Microsoft.DotNet.Cli.Commands.Test;
+using Microsoft.DotNet.Cli.Utils;
+using ExitCodes = Microsoft.NET.TestFramework.ExitCode;
+
+namespace Microsoft.DotNet.Cli.Test.Tests;
+
+[TestClass]
+public class GivenDotnetTestBuildsAndRunsArtifactPostProcessingMTP : SdkTest
+{
+ [TestMethod]
+ public void MultiProjectRun_MergesTrxArtifacts()
+ {
+ TestAsset testInstance = TestAssetsManager
+ .CopyTestAsset("MultiTestProjectSolutionWithTests", Guid.NewGuid().ToString())
+ .WithSource();
+ EnableTrxReport(testInstance.Path);
+ string resultsDirectory = Path.Combine(testInstance.Path, "TestResults");
+
+ CommandResult firstResult = Run(testInstance.Path, resultsDirectory);
+ string firstMergedTrxPath = GetMergedTrxPath(firstResult);
+
+ File.Exists(firstMergedTrxPath).Should().BeTrue();
+ Path.GetFileName(firstMergedTrxPath).Should().MatchRegex("^merged-[0-9a-f]{32}\\.trx$");
+ Directory.GetFiles(resultsDirectory, "*.trx", SearchOption.AllDirectories)
+ .Should().HaveCount(3, "the two original reports remain on disk beside the merged report");
+
+ CommandResult secondResult = Run(testInstance.Path, resultsDirectory);
+ string secondMergedTrxPath = GetMergedTrxPath(secondResult);
+
+ secondMergedTrxPath.Should().NotBe(
+ firstMergedTrxPath,
+ "each invocation has distinct execution IDs and must produce a non-colliding merged report");
+ File.Exists(secondMergedTrxPath).Should().BeTrue();
+ Directory.GetFiles(resultsDirectory, "*.trx", SearchOption.AllDirectories)
+ .Should().HaveCount(4, "the original reports are overwritten while each merged run is preserved");
+
+ XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
+ XDocument mergedTrx = XDocument.Load(secondMergedTrxPath);
+ mergedTrx.Descendants(ns + "Counters").Single().Attribute("total")!.Value.Should().Be("5");
+ }
+
+ private CommandResult Run(string workingDirectory, string resultsDirectory)
+ => new DotnetTestCommand(Log, disableNewOutput: false)
+ .WithWorkingDirectory(workingDirectory)
+ .Execute(
+ "--report-trx",
+ "--results-directory", resultsDirectory,
+ "--configuration", TestingConstants.Debug);
+
+ private static string GetMergedTrxPath(CommandResult result)
+ {
+ result.ExitCode.Should().Be(
+ ExitCodes.AtLeastOneTestFailed,
+ $"the test output was:{Environment.NewLine}{result.StdOut}{Environment.NewLine}{result.StdErr}");
+
+ MatchCollection artifactMatches = Regex.Matches(
+ result.StdOut ?? string.Empty,
+ @"(?m)^\s*-\s+(?.*\.trx)\s*$",
+ RegexOptions.CultureInvariant);
+ artifactMatches.Should().ContainSingle();
+
+ string mergedTrxPath = artifactMatches[0].Groups["path"].Value;
+ return mergedTrxPath;
+ }
+
+ private static void EnableTrxReport(string testAssetPath)
+ {
+ foreach (string projectPath in Directory.GetFiles(testAssetPath, "*TestProject.csproj", SearchOption.AllDirectories))
+ {
+ XDocument project = XDocument.Load(projectPath);
+ XElement packageReferenceGroup = project.Root!
+ .Elements("ItemGroup")
+ .Single(group => group.Elements("PackageReference").Any());
+ packageReferenceGroup.Elements("PackageReference")
+ .Single(reference => (string?)reference.Attribute("Include") == "Microsoft.Testing.Platform")
+ .SetAttributeValue("Version", "$(MicrosoftTestingPlatformVersion)");
+ packageReferenceGroup.Add(new XElement(
+ "PackageReference",
+ new XAttribute("Include", "Microsoft.Testing.Extensions.TrxReport"),
+ new XAttribute("Version", "$(MicrosoftTestingPlatformVersion)")));
+ project.Save(projectPath);
+ }
+
+ foreach (string programPath in Directory.GetFiles(testAssetPath, "Program.cs", SearchOption.AllDirectories))
+ {
+ string source = File.ReadAllText(programPath)
+ .Replace(
+ """
+ for (int i = 0; i < 3; i++)
+ {
+ Console.WriteLine(new string('a', 10000));
+ Console.Error.WriteLine(new string('e', 10000));
+ }
+
+ """,
+ string.Empty,
+ StringComparison.Ordinal)
+ .Replace(
+ "using Microsoft.Testing.Platform.Builder;",
+ """
+ using Microsoft.Testing.Extensions;
+ using Microsoft.Testing.Extensions.TrxReport.Abstractions;
+ using Microsoft.Testing.Platform.Builder;
+ """,
+ StringComparison.Ordinal)
+ .Replace(
+ "new TestFrameworkCapabilities()",
+ "new TestFrameworkCapabilities(new TrxReportCapability())",
+ StringComparison.Ordinal)
+ .Replace(
+ "testApplicationBuilder.RegisterTestFramework",
+ """
+ testApplicationBuilder.AddTrxReportProvider();
+
+ testApplicationBuilder.RegisterTestFramework
+ """,
+ StringComparison.Ordinal)
+ .Replace(
+ """
+ public async Task ExecuteRequestAsync(ExecuteRequestContext context)
+ {
+ """,
+ """
+ public async Task ExecuteRequestAsync(ExecuteRequestContext context)
+ {
+ var testMethodIdentifier = new TestMethodIdentifierProperty(
+ string.Empty, string.Empty, nameof(DummyTestAdapter), "Test", 0, [], string.Empty);
+ """,
+ StringComparison.Ordinal)
+ .Replace(
+ """new PassedTestNodeStateProperty("OK"))""",
+ """new PassedTestNodeStateProperty("OK"), testMethodIdentifier)""",
+ StringComparison.Ordinal)
+ .Replace(
+ """new SkippedTestNodeStateProperty("OK skipped!"))""",
+ """new SkippedTestNodeStateProperty("OK skipped!"), testMethodIdentifier)""",
+ StringComparison.Ordinal)
+ .Replace(
+ """new SkippedTestNodeStateProperty("skipped"))""",
+ """new SkippedTestNodeStateProperty("skipped"), testMethodIdentifier)""",
+ StringComparison.Ordinal)
+ .Replace(
+ """new FailedTestNodeStateProperty(new Exception("this is a failed test"), "not OK"))""",
+ """new FailedTestNodeStateProperty(new Exception("this is a failed test"), "not OK"), testMethodIdentifier)""",
+ StringComparison.Ordinal)
+ + """
+
+ public sealed class TrxReportCapability : ITrxReportCapability
+ {
+ bool ITrxReportCapability.IsSupported => true;
+ void ITrxReportCapability.Enable()
+ {
+ }
+ }
+ """;
+ File.WriteAllText(programPath, source);
+ }
+ }
+}
diff --git a/test/dotnet.Tests/CommandTests/Test/TestApplicationHandlerTests.cs b/test/dotnet.Tests/CommandTests/Test/TestApplicationHandlerTests.cs
index e924b970a243..29f2e791eb5e 100644
--- a/test/dotnet.Tests/CommandTests/Test/TestApplicationHandlerTests.cs
+++ b/test/dotnet.Tests/CommandTests/Test/TestApplicationHandlerTests.cs
@@ -156,6 +156,75 @@ public void OnHandshakeReceived_WithExplicitAttemptNumber_UsesReportedAttempt()
console.GetOutput().Should().Contain("(try 3)");
}
+ [TestMethod]
+ public void OnHandshakeReceived_WithArtifactPostProcessingCapabilities_RecordsApplication()
+ {
+ var manager = new ArtifactPostProcessingManager();
+ (TestApplicationHandler handler, _, _) = CreateHandler(
+ isHelp: false,
+ isDiscovery: false,
+ artifactPostProcessingManager: manager);
+ var handshake = BuildHandshake(
+ executionMode: HandshakeMessageExecutionModes.Run,
+ supportedPostProcessorKinds: "microsoft.testing.trx;example.junit",
+ supportedPostProcessorExtensions: ".trx;.xml");
+
+ bool accepted = handler.OnHandshakeReceived(handshake, gotSupportedVersion: true);
+
+ accepted.Should().BeTrue();
+ ArtifactPostProcessingApplication application = manager.SnapshotApplications().Should().ContainSingle().Subject;
+ application.SupportedKinds.Should().BeEquivalentTo("microsoft.testing.trx", "example.junit");
+ application.SupportedExtensions.Should().BeEquivalentTo(".trx", ".xml");
+ }
+
+ [TestMethod]
+ public void OnFileArtifactsReceived_RecordsArtifactMetadata()
+ {
+ var manager = new ArtifactPostProcessingManager();
+ (TestApplicationHandler handler, _, _) = CreateHandler(
+ isHelp: false,
+ isDiscovery: false,
+ artifactPostProcessingManager: manager);
+ handler.OnHandshakeReceived(
+ BuildHandshake(HandshakeMessageExecutionModes.Run),
+ gotSupportedVersion: true).Should().BeTrue();
+ string artifactPath = Path.GetFullPath("result.trx");
+
+ handler.OnFileArtifactsReceived(new FileArtifactMessages(
+ "exec-1",
+ "inst-1",
+ [new FileArtifactMessage(artifactPath, "TRX", null, null, null, null, "microsoft.testing.trx")]));
+
+ ArtifactPostProcessingArtifact artifact = manager.SnapshotArtifacts().Should().ContainSingle().Subject;
+ artifact.Path.Should().Be(artifactPath);
+ artifact.Kind.Should().Be("microsoft.testing.trx");
+ artifact.ProducingTestModule.Should().Be(TargetPath);
+ artifact.TargetFramework.Should().Be(TargetFramework);
+ artifact.Architecture.Should().Be("x64");
+ artifact.ExecutionId.Should().Be("exec-1");
+ }
+
+ [TestMethod]
+ public void OnHandshakeReceived_WhenArtifactPostProcessorHandshakeFails_DoesNotFailTestRun()
+ {
+ var invocation = new ArtifactPostProcessingInvocation("manifest.json");
+ (TestApplicationHandler handler, TerminalTestReporter reporter, _) = CreateHandler(
+ isHelp: false,
+ isDiscovery: false,
+ artifactPostProcessingInvocation: invocation);
+
+ bool accepted = handler.OnHandshakeReceived(
+ BuildHandshake(
+ HandshakeMessageExecutionModes.Tool,
+ hostType: HandshakeMessageHostTypes.TestHost),
+ gotSupportedVersion: true);
+
+ accepted.Should().BeFalse();
+ invocation.FailureMessage.Should().NotBeNullOrEmpty();
+ reporter.HasHandshakeFailure.Should().BeFalse(
+ "post-processing failures must not change the test run exit code");
+ }
+
///
/// Drives the new validation added in this change: if the host reports an execution mode that
/// doesn't match what dotnet test intended (e.g. RunArguments or
@@ -318,7 +387,12 @@ public void OnTestProcessExited_WhenSdkInHelpModeAndNoHandshakeReceived_DoesNotR
private const string ProjectPath = "/repo/MyTest.csproj";
private const string TargetFramework = "net9.0";
- private (TestApplicationHandler Handler, TerminalTestReporter Reporter, CapturingConsole Console) CreateHandler(bool isHelp, bool isDiscovery, bool showAssembly = false)
+ private (TestApplicationHandler Handler, TerminalTestReporter Reporter, CapturingConsole Console) CreateHandler(
+ bool isHelp,
+ bool isDiscovery,
+ bool showAssembly = false,
+ ArtifactPostProcessingManager? artifactPostProcessingManager = null,
+ ArtifactPostProcessingInvocation? artifactPostProcessingInvocation = null)
{
var capturingConsole = new CapturingConsole();
@@ -348,12 +422,30 @@ public void OnTestProcessExited_WhenSdkInHelpModeAndNoHandshakeReceived_DoesNotR
DotnetRootArchVariableName: null,
EnvironmentVariables: new Dictionary());
- var testOptions = new TestOptions(IsHelp: isHelp, IsDiscovery: isDiscovery, ListTestsFormat: TestListFormat.Text);
-
- return (new TestApplicationHandler(reporter, module, testOptions), reporter, capturingConsole);
+ var testOptions = new TestOptions(
+ IsHelp: isHelp,
+ IsDiscovery: isDiscovery,
+ ListTestsFormat: TestListFormat.Text,
+ IsArtifactPostProcessing: artifactPostProcessingInvocation is not null);
+
+ return (
+ new TestApplicationHandler(
+ reporter,
+ module,
+ testOptions,
+ artifactPostProcessingManager,
+ artifactPostProcessingInvocation),
+ reporter,
+ capturingConsole);
}
- private static HandshakeMessage BuildHandshake(string? executionMode, string hostType = "TestHost", bool includeInstanceId = true, int? attemptNumber = null)
+ private static HandshakeMessage BuildHandshake(
+ string? executionMode,
+ string hostType = "TestHost",
+ bool includeInstanceId = true,
+ int? attemptNumber = null,
+ string? supportedPostProcessorKinds = null,
+ string? supportedPostProcessorExtensions = null)
{
var properties = new Dictionary
{
@@ -382,6 +474,16 @@ private static HandshakeMessage BuildHandshake(string? executionMode, string hos
properties[HandshakeMessagePropertyNames.AttemptNumber] = attemptNumber.Value.ToString(CultureInfo.InvariantCulture);
}
+ if (supportedPostProcessorKinds is not null)
+ {
+ properties[HandshakeMessagePropertyNames.SupportedPostProcessorKinds] = supportedPostProcessorKinds;
+ }
+
+ if (supportedPostProcessorExtensions is not null)
+ {
+ properties[HandshakeMessagePropertyNames.SupportedPostProcessorExtensionsLegacy] = supportedPostProcessorExtensions;
+ }
+
return new HandshakeMessage(properties);
}
}