From c4166ce627e4503439f90bcc97ed85a034141a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 14:43:32 +0200 Subject: [PATCH 01/11] Add orchestrator TestInProgress/TestDiscovered overloads for SDK parity The dotnet/sdk orchestrator calls TestInProgress with the full (assembly, tfm, arch, executionId, instanceId, uid, displayName) signature and TestDiscovered with (executionId, displayName, uid, filePath, lineNumber). Add those overloads to the shared reporter (additive; they delegate to the existing execution-id-keyed core methods) so the SDK can consume the shared source without call-site changes. Discovered while plugging the Internal.DotnetTest package into dotnet/sdk. Verified: platform clean net8.0/net9.0/netstandard2.0; terminal tests 108/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Terminal/TerminalTestReporter.Messaging.cs | 15 +++++++++++++++ .../Terminal/TerminalTestReporter.Summary.cs | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Messaging.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Messaging.cs index 64461e536f..5807f3f16c 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Messaging.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Messaging.cs @@ -43,6 +43,21 @@ private void WriteMessage(string text, TerminalColor? color = null, int? padding } }); + /// + /// Orchestrator overload (dotnet test): carries the assembly/target-framework/architecture and per-attempt + /// instance id known to the multi-process orchestrator. The shared in-progress tracking is keyed by execution id, + /// so those extra arguments are accepted for signature parity and the overload delegates to the core method. + /// + public void TestInProgress( + string assembly, + string? targetFramework, + string? architecture, + string executionId, + string instanceId, + string testNodeUid, + string displayName) + => TestInProgress(executionId, testNodeUid, displayName); + public void TestInProgress( string executionId, string testNodeUid, diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs index 061f4201c4..40971d0e8b 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs @@ -153,6 +153,14 @@ private void AppendTestRunSummary(ITerminal terminal) AppendHandshakeFailureRecap(terminal); } + /// + /// Orchestrator overload (dotnet test): the multi-process orchestrator also knows each discovered test's + /// uid, file path and line number. The shared discovery summary currently lists display names only, so those are + /// accepted for signature parity and the overload delegates to the core method. + /// + internal void TestDiscovered(string executionId, string? displayName, string? uid, string? filePath, int? lineNumber) + => TestDiscovered(executionId, displayName ?? string.Empty); + internal void TestDiscovered(string executionId, string displayName) { if (!_assemblies.TryGetValue(executionId, out TestProgressState? asm)) From 8c422600700f2e40b7ad97c5fa1f5c2cae7cfc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:06:26 +0200 Subject: [PATCH 02/11] Make Internal.DotnetTest package consumable by dotnet/sdk (Arcade resx, EmbeddedAttribute, nullable) Fixes found by actually plugging the package into dotnet/sdk's dotnet.csproj: - build props: switch TerminalResources from manual StronglyTyped generation to the Arcade EmbeddedResource GenerateSource convention. The manual StronglyTyped metadata collided with the consumer's XliffTasks (per-culture resx inherit it -> MSB3573 'more than one source file'). - ship the Microsoft.CodeAnalysis.EmbeddedAttribute polyfill as source: the shared source marks types [Embedded] and a vanilla consumer doesn't define the attribute (CS0246). - ExceptionFlattener: build the inner-exception fallback as Exception?[] so it compiles under a strict-nullable consumer (CS8601). Verified: platform builds clean; the package now compiles into dotnet/sdk's CLI (the 31-file terminal fork is replaced by this package source). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...esting.Platform.Internal.DotnetTest.csproj | 10 +++++++ ...Testing.Platform.Internal.DotnetTest.props | 28 +++++++++---------- .../Terminal/ExceptionFlattener.cs | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj index 047cc06b06..037decc47c 100644 --- a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj +++ b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj @@ -84,6 +84,16 @@ This is an INTERNAL, source-only package: it shares - as compiled source - the p BuildAction="Compile" PackagePath="contentFiles/cs/any/TerminalReporter/%(Filename)%(Extension)" /> + + + diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/ExceptionFlattener.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/ExceptionFlattener.cs index 8002d4296e..72938eee78 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/ExceptionFlattener.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/ExceptionFlattener.cs @@ -28,7 +28,7 @@ internal static FlatException[] Flatten(string? errorMessage, Exception? excepti IEnumerable aggregateExceptions = exception switch { AggregateException aggregate => aggregate.Flatten().InnerExceptions, - _ => [exception?.InnerException], + _ => new Exception?[] { exception?.InnerException }, }; foreach (Exception? aggregate in aggregateExceptions) From 907d86eb430d8252c42296acaf3d7ec159648a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:13:28 +0200 Subject: [PATCH 03/11] Address review: fall back to uid for null displayName in TestDiscovered overload The orchestrator TestDiscovered overload previously substituted string.Empty for a null displayName, which would add a blank entry to the discovery summary. Fall back to uid (then string.Empty as last resort) so the summary stays informative and the discovered-test count stays accurate, and document the behavior in the XML summary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OutputDevice/Terminal/TerminalTestReporter.Summary.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs index 40971d0e8b..f79e4bf0a3 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs @@ -156,10 +156,12 @@ private void AppendTestRunSummary(ITerminal terminal) /// /// Orchestrator overload (dotnet test): the multi-process orchestrator also knows each discovered test's /// uid, file path and line number. The shared discovery summary currently lists display names only, so those are - /// accepted for signature parity and the overload delegates to the core method. + /// accepted for signature parity and the overload delegates to the core method. When + /// is we fall back to so the discovery summary stays informative + /// (and the discovered-test count stays accurate) instead of adding a blank entry. /// internal void TestDiscovered(string executionId, string? displayName, string? uid, string? filePath, int? lineNumber) - => TestDiscovered(executionId, displayName ?? string.Empty); + => TestDiscovered(executionId, displayName ?? uid ?? string.Empty); internal void TestDiscovered(string executionId, string displayName) { From 5048d8c89194c9f6e618452ebbd287513725322c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:18:09 +0200 Subject: [PATCH 04/11] Correct stale comment: package ships its own EmbeddedAttribute polyfill The build.props comment claimed dotnet/sdk already provides Microsoft.CodeAnalysis.EmbeddedAttribute, but consuming the package surfaced CS0246 (the attribute is not present in a vanilla consumer). The package now ships the polyfill itself as a source contentFile (see the .csproj), so correct the comment to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Testing.Platform.Internal.DotnetTest.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/build/Microsoft.Testing.Platform.Internal.DotnetTest.props b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/build/Microsoft.Testing.Platform.Internal.DotnetTest.props index fb19cfaa8b..fae972d4b3 100644 --- a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/build/Microsoft.Testing.Platform.Internal.DotnetTest.props +++ b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/build/Microsoft.Testing.Platform.Internal.DotnetTest.props @@ -28,8 +28,8 @@ The shared reporter source is authored against the testfx global usings (ImplicitUsings=enable plus these namespaces). Supply them here so the consumer compiles the source without per-file usings. These only take effect when the consumer has ImplicitUsings enabled (the package also relies on LangVersion supporting the - 'field' keyword, i.e. preview/latest, and on the Microsoft.CodeAnalysis.EmbeddedAttribute polyfill - which - dotnet/sdk already provides). See PACKAGE.md. + 'field' keyword, i.e. preview/latest). The Microsoft.CodeAnalysis.EmbeddedAttribute the shared types need is + shipped by this package itself as a source polyfill (see the .csproj). See PACKAGE.md. --> From 5277d86656bd0b1be63183390b47f8ef43d7fc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:20:07 +0200 Subject: [PATCH 05/11] Address review: skip discovery summary entry when displayName is null Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Terminal/TerminalTestReporter.Summary.cs | 12 ++++--- .../Terminal/TerminalTestReporterTests.cs | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs index f79e4bf0a3..1e0ac67b1c 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs @@ -156,12 +156,16 @@ private void AppendTestRunSummary(ITerminal terminal) /// /// Orchestrator overload (dotnet test): the multi-process orchestrator also knows each discovered test's /// uid, file path and line number. The shared discovery summary currently lists display names only, so those are - /// accepted for signature parity and the overload delegates to the core method. When - /// is we fall back to so the discovery summary stays informative - /// (and the discovered-test count stays accurate) instead of adding a blank entry. + /// accepted for signature parity and the overload delegates to the core method when + /// is known. /// internal void TestDiscovered(string executionId, string? displayName, string? uid, string? filePath, int? lineNumber) - => TestDiscovered(executionId, displayName ?? uid ?? string.Empty); + { + if (displayName is not null) + { + TestDiscovered(executionId, displayName); + } + } internal void TestDiscovered(string executionId, string displayName) { diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index 07f2fc4ea8..20c9bbabfc 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -1137,6 +1137,39 @@ public void TerminalTestReporter_WhenInDiscoveryMode_ShouldIncrementDiscoveredTe Assert.IsTrue(output.Contains('2') || output.Contains("TestMethod1"), "Output should contain information about discovered tests"); } + [TestMethod] + public void TerminalTestReporter_WhenOrchestratorDiscoveryDisplayNameIsNull_ShouldNotAddBlankSummaryEntry() + { + // Arrange + string assembly = "test.dll"; + string targetFramework = "net8.0"; + string architecture = "x64"; + var stringBuilderConsole = new StringBuilderConsole(); + var terminalReporter = new TerminalTestReporter(stringBuilderConsole, static () => false, new TerminalTestReporterOptions + { + ShowPassedTests = () => false, + AnsiMode = AnsiMode.NoAnsi, + ShowProgress = () => false, + }); + + DateTimeOffset startTime = DateTimeOffset.MinValue; + DateTimeOffset endTime = DateTimeOffset.MaxValue; + + // Act + terminalReporter.TestExecutionStarted(startTime, 1, isDiscovery: true, isHelp: false, isRetry: false); + terminalReporter.AssemblyRunStarted(assembly, targetFramework, architecture, "0", "0"); + terminalReporter.TestDiscovered("0", displayName: null, uid: "uid", filePath: null, lineNumber: null); + terminalReporter.TestDiscovered("0", "TestMethod1", uid: "uid-1", filePath: null, lineNumber: null); + terminalReporter.AssemblyRunCompleted("0"); + terminalReporter.TestExecutionCompleted(endTime, exitCode: null); + + string[] outputLines = stringBuilderConsole.Output.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'); + + // Assert + Assert.DoesNotContain(TerminalTestReporter.SingleIndentation, outputLines); + Assert.Contains($"{TerminalTestReporter.SingleIndentation}TestMethod1", outputLines); + } + [TestMethod] public void TerminalTestReporter_WhenMultipleAssemblies_AggregatesCountsAndOmitsAssemblyLinkOnVerdict() { From 2e8dafca5aed23b056f18ab723119162cf9c9317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:37:24 +0200 Subject: [PATCH 06/11] Escalate run verdict to 'Failed!' on handshake failure (dotnet/sdk#51608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dotnet test orchestrator acceptance test RunConsoleAppDoesNothing_ShouldReprintHandshakeFailureRecapAndPrintFailedSummary (dotnet/sdk#51608) drove out a real gap in the shared reporter: when an assembly fails to hand-shake and contributes zero tests, the run-summary headline showed the benign 'Zero tests ran' even though the run is failed. A handshake failure must not be masked as an empty run. GetVerdictText now takes an optional hasHandshakeFailures flag; when set, the verdict escalates to 'Failed!' ahead of the 'Zero tests ran' branch. The orchestrator summary passes HasHandshakeFailure; in-process callers (and FormatSummaryText) pass false, so their verdict — and the byte-exact in-process UI — is unchanged. The per-assembly immediate-failure context still legitimately renders 'Zero tests ran' (the assembly really did register zero tests); only the run-level verdict escalates. Updated the two orchestrator handshake tests to assert the escalated 'Test run summary: Failed!' verdict. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Terminal/TerminalTestReporter.Summary.cs | 2 +- .../OutputDevice/TestRunSummaryHelper.cs | 11 +++++++++-- .../Terminal/TerminalTestReporterTests.cs | 10 ++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs index 1e0ac67b1c..5e6af5a6c5 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs @@ -61,7 +61,7 @@ private void AppendTestRunSummary(ITerminal terminal) terminal.Append(TerminalResources.TestRunSummary); terminal.Append(' '); - terminal.Append(TestRunSummaryHelper.GetVerdictText(totalTests, totalFailedTests, totalSkippedTests, WasCancelled, _options.MinimumExpectedTests)); + terminal.Append(TestRunSummaryHelper.GetVerdictText(totalTests, totalFailedTests, totalSkippedTests, WasCancelled, _options.MinimumExpectedTests, HasHandshakeFailure)); // For a single assembly (the in-process host) the verdict is followed by the assembly link, exactly as // before. For multiple assemblies (the dotnet test orchestrator) the per-assembly identity is rendered in diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TestRunSummaryHelper.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TestRunSummaryHelper.cs index 0b6a29c1ec..c67ecb53fa 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/TestRunSummaryHelper.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/TestRunSummaryHelper.cs @@ -27,13 +27,20 @@ internal static bool IsRunFailed(int totalTests, int failedTests, int skippedTes /// /// Computes the verdict string for the test run. /// - internal static string GetVerdictText(int totalTests, int failedTests, int skippedTests, bool wasCancelled, int minimumExpectedTests) + /// + /// When is (multi-assembly orchestrator only), at + /// least one assembly failed to hand-shake. Such a failure must surface as "Failed!" and must NOT be masked by the + /// benign "Zero tests ran" wording, even though the failing assembly contributed zero tests. In-process callers + /// never have handshake failures and pass , so their verdict is unchanged. + /// + internal static string GetVerdictText(int totalTests, int failedTests, int skippedTests, bool wasCancelled, int minimumExpectedTests, bool hasHandshakeFailures = false) => true switch { _ when wasCancelled => TerminalResources.Aborted, _ when totalTests < minimumExpectedTests => string.Format(CultureInfo.CurrentCulture, TerminalResources.MinimumExpectedTestsPolicyViolation, totalTests, minimumExpectedTests), - _ when totalTests == 0 || totalTests == skippedTests => TerminalResources.ZeroTestsRan, _ when failedTests > 0 => $"{TerminalResources.Failed}!", + _ when hasHandshakeFailures => $"{TerminalResources.Failed}!", + _ when totalTests == 0 || totalTests == skippedTests => TerminalResources.ZeroTestsRan, _ => $"{TerminalResources.Passed}!", }; diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index 20c9bbabfc..c7059e605a 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -1284,8 +1284,10 @@ public void AssemblyRunCompleted_WhenExecutionIdUnknown_SummaryReprintsRecapAndR Assert.Contains($"{TerminalResources.ExitCode}: 1", output); Assert.Contains("the err", output); - // No test ran, so the run verdict is the red "Zero tests ran" (runFailed also includes HasHandshakeFailure). - Assert.Contains(TerminalResources.ZeroTestsRan, output); + // The summary verdict escalates to "Failed!" rather than the benign "Zero tests ran": a handshake failure + // must not be masked as an empty run (dotnet/sdk#51608). The per-assembly immediate-failure context above + // still legitimately says "Zero tests ran" (the assembly really did register zero tests). + Assert.Contains($"{TerminalResources.TestRunSummary} {TerminalResources.Failed}!", output); // Per-run state is reset after completion so a subsequent session starts fresh. Assert.IsFalse(terminalReporter.HasHandshakeFailure); @@ -1561,6 +1563,10 @@ public void TestExecutionCompleted_WhenHandshakeFailures_PrintsRecapAndFailsRun( Assert.Contains(TerminalResources.HandshakeFailuresHeader, output); Assert.Contains("A failed", output); Assert.Contains("B failed", output); + + // With every assembly failing to handshake and zero tests, the summary verdict is "Failed!", not the + // benign "Zero tests ran" (dotnet/sdk#51608). + Assert.Contains($"{TerminalResources.TestRunSummary} {TerminalResources.Failed}!", output); } [TestMethod] From f6c00cce8d40365a5584089a18f1d8874f42f835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 20 Jun 2026 15:46:56 +0200 Subject: [PATCH 07/11] Address review: count unnamed discoveries with uid fallback, test orchestrator overloads, pin resource manifest name - TestDiscovered orchestrator overload now falls back to uid when displayName is null and still increments the discovered count (with no blank summary entry) when neither is available, so the discovery total stays correct. - Updated the discovery test to assert TotalTests counting and uid fallback, and added a parity test for the orchestrator TestInProgress overload. - Dropped Link and pinned ManifestResourceName on the build-extension EmbeddedResource so the generated accessor resolves; refreshed the now-stale resource-wiring comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...esting.Platform.Internal.DotnetTest.csproj | 8 +- ...Testing.Platform.Internal.DotnetTest.props | 9 ++- .../Terminal/TerminalTestReporter.Summary.cs | 22 +++++- .../Terminal/TerminalTestReporterTests.cs | 73 ++++++++++++++++++- 4 files changed, 98 insertions(+), 14 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj index 037decc47c..bbfc1f3147 100644 --- a/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj +++ b/src/Platform/Microsoft.Testing.Platform.Internal.DotnetTest/Microsoft.Testing.Platform.Internal.DotnetTest.csproj @@ -96,10 +96,10 @@ This is an INTERNAL, source-only package: it shares - as compiled source - the p MSB3573). * Namespace pins the generated accessor to Microsoft.Testing.Platform.OutputDevice.Terminal (the namespace the shared reporter source expects) regardless of the consumer's RootNamespace; the class name defaults to the - resx file name (TerminalResources), so the manifest name resolves to - Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalResources. + resx file name (TerminalResources). ManifestResourceName pins the embedded resource name to + Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalResources so the generated accessor's ResourceManager + resolves it. We deliberately do NOT set Link: the resx lives outside the consumer's project cone (it ships in + this package), and a Link would feed into the derived manifest resource name / accessor binding, which is + exactly what TerminalReporterContract.props avoids for the same reason. --> + ManifestResourceName="Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalResources" />