Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Share terminal retry counting model + port SDK oracle test (4/n) - #9267

Merged
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model
Jun 20, 2026
Merged

Share terminal retry counting model + port SDK oracle test (4/n)#9267
Amaury Levé (Evangelink) merged 7 commits into
mainfrom
copilot/share-terminal-retry-model

Conversation

@Evangelink

Copy link
Copy Markdown
Member

What

Slice 4/n (the high-risk one) of the test-first terminal-reporter superset port: adopt the SDK's instanceId-based retry counting model so retried assemblies render the /r{N} segment and the summary reflects the latest attempt's results.

Stacked on #9266. This completes the four reporter behaviors the dotnet/sdk orchestrator relies on (handshake, per-assembly counts, multi-assembly summary, retry).

How

  • TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test reported results keyed by (testNodeUid, attempt), and TotalTests is computed. A new instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests. Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
  • TestCompleted threads the instance id — the in-process host uses its single fixed execution id as the instance id (one attempt), the orchestrator overload passes the real one — and routes counting through the Report* methods.
  • AssemblyRunStarted registers the attempt via NotifyHandshake.
  • Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report* (the counters are no longer directly settable).

Oracle test ported

AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount — ported from the SDK's TerminalTestReporterTests: attempt 1 fails flaky-1, attempt 2 (new instance id, same execution id) passes it, so the final tally is [+1/x0/?0/r1].

Why it's safe for the in-process host

The in-process path reports each test once under a single fixed instance id, so the retry model collapses to the same single-attempt counts. The ~89 byte-exact in-process tests are the proof.

Verification

  • Platform clean on net8.0 / net9.0 / netstandard2.0 (0 warnings incl. IDE/SA).
  • Full Microsoft.Testing.Platform.UnitTests suite green: 1202 total, 0 failed (incl. all 4 ported SDK oracle tests + the byte-exact in-process tests).
  • Standalone TerminalReporterContract consumer compiles.

Stacked PR — review/merge #9263#9265#9266 first.

First slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's own reporter test as the
expected-UI oracle.
Adds the orchestrator handshake surface to the shared reporter (additive; the
in-process host never reports handshake failures, so output stays byte-identical):
- HandshakeFailure(assemblyPath, tfm, exitCode, output, error, reportEvenWhenHelp)
+ HasHandshakeFailure + end-of-run AppendHandshakeFailureRecap.
- Rich AssemblyRunCompleted(executionId, exitCode, outputData, errorData) overload
that surfaces an unknown execution id as a handshake failure (no throw) and prints
the executable summary (exit code + stdout/stderr) on non-zero exit.
- TerminalTestReporter(IConsole, options) ctor overload for orchestrator callers that
drive cancellation out-of-band via StartCancelling.
- TestProgressState.Success; string-based AppendAssemblyLinkTargetFrameworkAndArchitecture
overload; AppendExecutableSummary; new TerminalResources HandshakeFailuresHeader/ExitCode
(resx + xlf regenerated via UpdateXlf, hand-maintained !IS_CORE_MTP accessors updated).
Ports the SDK's AssemblyRunCompleted_WhenExecutionIdUnknown test (dotnet/sdk#51608) to
MSTest, asserting both HasHandshakeFailure and the rendered failure context.
Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings incl. IDE/SA);
full Microsoft.Testing.Platform.UnitTests suite green (1199, 0 failed); standalone
TerminalReporterContract consumer still compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork, validated by porting the SDK's per-assembly-counts test.
Adds the orchestrator per-assembly summary rendering (additive; gated on the new
ShowAssembly options which default off, so the in-process host is unaffected and
its output stays byte-identical):
- ShowAssembly + ShowAssemblyStartAndComplete options.
- Rich AssemblyRunCompleted now prints the per-assembly summary line (link + result
+ compact counts + duration) when ShowAssembly && ShowAssemblyStartAndComplete.
- AppendAssemblySummary/AppendAssemblyResult/AppendAssemblyTestCounts/AppendGlyphCount
porting the compact '[+P/xF/?S]' (ASCII) / '[checkP/xF/downS]' (ANSI) counts block,
with the '/rN' retry segment wired to TestProgressState.RetriedFailedTests.
- Orchestrator TestCompleted overload (assembly, tfm, arch, executionId, instanceId,
...) that updates per-assembly counts (delegates to the shared per-execution-id
path; instanceId-based retry attribution arrives in the retry slice).
- New TerminalResources.FailedWithErrors (resx + xlf + !IS_CORE_MTP accessor).
Ports the SDK's AssemblyRunCompleted_WithShowAssemblyStartAndComplete_PrintsPerAssemblyCounts
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+3/x0/?1]' counts block.
Stacked on #9263. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1200, 0 failed); standalone contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3/n)
Third slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork. Adds the per-assembly listing to the FINAL run summary: when
ShowAssembly is set and more than one assembly ran, each assembly is listed under
the run-level verdict with its own result + compact counts. Additive — the
in-process host leaves ShowAssembly off, so its summary stays byte-identical.
Ports the SDK's TestExecutionCompleted_WithMultipleAssemblies_PrintsPerAssemblyCountsInSummary
test (dotnet/sdk#52128) to MSTest, asserting the rendered '[+2/x1/?0]' and '[+5/x0/?2]'
per-assembly count blocks.
Stacked on #9265. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1201, 0 failed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fourth slice of unifying the shared MTP terminal reporter with the dotnet/sdk
orchestrator fork: adopt the instanceId-based retry counting model so retried
assemblies render the '/r{N}' segment and report the latest attempt's results.
- TestProgressState now derives PassedTests/FailedTests/SkippedTests from per-test
reported results keyed by (testNodeUid, attempt). TotalTests is computed. A new
instance id (registered via NotifyHandshake on AssemblyRunStarted) is a retry
attempt that supersedes the previous attempt's tally and bumps RetriedFailedTests.
Adds ReportPassingTest/ReportSkippedTest/ReportFailedTest, NotifyHandshake, TryCount.
- TestCompleted now threads the instance id (the in-process host uses its single
fixed execution id as the instance id; the orchestrator overload passes the real
one) and routes through the Report* methods.
- AssemblyRunStarted registers the attempt via NotifyHandshake.
- Updated the SilenceDrivenHeartbeatRenderer test helper to seed counts via Report*.
Ports the SDK's AssemblyRunCompleted_WhenTestsWereRetried_ShowsRetriedCount test,
asserting the rendered '[+1/x0/?0/r1]' block.
Stacked on #9266. Verified: platform clean net8.0/net9.0/netstandard2.0 (0 warnings);
full unit suite green (1202, 0 failed) — the ~89 byte-exact in-process tests confirm
the single-attempt counts are unchanged; contract consumer compiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

PR #9267 — Share terminal retry counting model + port SDK oracle test (4/n)

#DimensionVerdict
2Threading & Concurrency🟡 1 MODERATE
13Test Completeness🟡 1 MODERATE
17Documentation Accuracy🔵 1 NIT

✅ 19/22 dimensions clean.

  • Threading_testUidToResults (Dictionary) and _orderedInstanceIds (List) in TestProgressState are non-thread-safe but undocumented as single-threaded-only. The current architecture (single pipeline consumer per assembly) prevents actual races; the ask is a doc comment to make the invariant auditable and prevent future regressions.
  • Test Completeness — the retry oracle test covers 2 attempts; the 3-attempt (fail→fail→pass) accumulation of RetriedFailedTests is untested. TryCount is also never directly asserted.
  • NITinstanceId: executionId in the in-process TestCompleted overload relies on the caller passing the same value to AssemblyRunStarted; that pairing is undocumented.

Algorithmic correctness (✅)

The retry-deduplication state machine in ReportGenericTestResult is correct. Traced all four transitions:

Previous attemptCurrent attemptRetriedFailedTests changeCounter change
(none)any+0+1 in new category
FailPass+prev.Failed (+1)−1 fail, +1 pass
FailFail+prev.Failed (+1)net 0 (old subtracted, new added)
PassPass+0net 0

TotalTests stays invariant across retries (correct: same test, new attempt). RetriedFailedTests accumulates only value.Failed so a pass→pass "retry" (degenerate case) correctly contributes 0.

TotalTests refactor (✅)

Removing asm.TotalTests++ from TestDiscovered in TerminalTestReporter.Summary.cs is safe: in discovery mode TotalTests is now DiscoveredTests (incremented via DiscoveredTests++); in execution mode it is PassedTests + SkippedTests + FailedTests. Both are correct.

Performance (✅)

_orderedInstanceIds.IndexOf is O(N) where N = retry count (almost always ≤ 3). _testUidToResults is O(1) lookup. The static lambdas in ReportPassingTest/ReportSkippedTest/ReportFailedTest are compiler-cached (no per-call delegate allocation). No concerns.

Public API surface (✅)

All new and modified types are internal sealed; no PublicAPI.Unshipped.txt changes needed.

- Document the single-consumer threading invariant on TestProgressState (its members are
unsynchronized because the message pipeline delivers one executionId's events serially).
- Document the in-process host instanceId == executionId contract at the AssemblyRunStarted
call site, mirroring the note on the in-process TestCompleted overload.
- Add a TestProgressState unit test asserting TryCount: a new instance id is a new attempt,
re-seeing the current attempt is a no-op, and three distinct attempts accumulate to 3.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AssemblyRunCompleted_WhenTestsWereRetriedTwice_AccumulatesRetriedCount covering the fail->fail->pass path so RetriedFailedTests must accumulate (rendered as /r2), guarding against a regression that resets rather than accumulates the retry count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from copilot/share-terminal-summary-per-assembly to mainJune 20, 2026 08:53
…l-retry-model
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Handshake.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporterOptions.cs
#	src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs
#	test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs
CopilotAI review requested due to automatic review settings June 20, 2026 09:02

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (slice 4/n of the terminal reporter unification work) ports the dotnet/sdk orchestrator’s instanceId-based retry counting model into the shared Microsoft.Testing.Platform terminal reporter, so retried tests are de-duplicated and the per-assembly counts render the /r{N} retry segment.

Changes:

  • Reworks TestProgressState to derive pass/fail/skip totals from per-test results keyed by (testNodeUid, attempt) and to track retry attempts via instanceId handshakes (including TryCount and RetriedFailedTests).
  • Threads instanceId through TerminalTestReporter.TestCompleted and registers attempts via AssemblyRunStarted(..., instanceId) so retries supersede earlier attempt tallies instead of double-counting.
  • Ports the SDK oracle test asserting retry rendering ([+1/x0/?0/r1]) and updates heartbeat-renderer tests to seed counts via the new reporting APIs.
Show a summary per file
FileDescription
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.csAdds the ported SDK oracle test for retry rendering and extends ExpectedCounts to include /r{N}.
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/SilenceDrivenHeartbeatRendererTests.csUpdates the test helper to seed progress counts via NotifyHandshake + Report*Test methods (since counters are no longer directly settable).
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.SessionLifecycle.csEnsures in-process host registers an instanceId consistent with the in-process execution id to satisfy attempt lookup.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.csIntroduces per-test/per-attempt result tracking, retry attempt registration, computed TotalTests, and retry attribution (RetriedFailedTests, TryCount).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.csRoutes counting through TestProgressState.Report*Test and forwards the instanceId (including mapping in-process instanceId == executionId).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.csAdjusts discovery counting to rely on computed TotalTests (derived from DiscoveredTests in discovery mode).
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Lifecycle.csRegisters retry attempts by calling NotifyHandshake(instanceId) on every (re-)start for a given execution id.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 1

Comment on lines +149 to +159
private int GetAttemptNumberFromInstanceId(string instanceId)
{
int index = _orderedInstanceIds.IndexOf(instanceId);
if (index < 0)
{
throw ApplicationStateGuard.Unreachable();
}

// Attempt numbers are 1-based.
return index + 1;
}
@Evangelink
Amaury Levé (Evangelink) merged commit 86ed21b into mainJun 20, 2026
30 of 33 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the copilot/share-terminal-retry-model branch June 20, 2026 10:01
Amaury Levé (Evangelink) added a commit that referenced this pull request Jun 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Evangelink