Report MSTest [Retry] attempts to the platform instead of discarding them - #10295
Conversation
`UnitTestRunner` kept only the last retry attempt, so an in-process `[Retry]` was invisible everywhere: the terminal showed a plain passing test and the CTRF report reported `flaky: 0` with no `retryAttempts[]`, even though `CtrfReportEngine` already implements the correct rule. Every attempt is now published as its own `TestNodeUpdateMessage` under the same test node uid, tagged with a new `RetryAttemptProperty` (`AttemptNumber` + `IsSuperseded`): - Terminal: attempts are annotated (`failed (try 1) MyTest`), the test is counted once and the superseded failures feed the `(+N retried)` summary. `TestProgressState` now orders attempts by the pair (host attempt, in-process attempt) so this composes with `--retry-failed-tests` instead of colliding with it. - `TestApplicationResult`: superseded attempts are not counted, so a retried-then-passing test no longer fails the build. - TRX / JUnit / Azure DevOps: superseded attempts are filtered out, so those reports keep exactly one row per test. - CTRF and HTML: unchanged, they already collapse/annotate per uid. - VSTest: the recorder drops superseded attempts, so Test Explorer and the VSTest TRX behave exactly as before. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Pull request overview
Surfaces every MSTest [Retry] attempt to Microsoft.Testing.Platform, enabling terminal retry reporting and CTRF flaky-test detection.
Changes:
- Adds
RetryAttemptPropertyand propagates retry metadata. - Reconciles attempts in terminal output and filters superseded results from selected consumers.
- Adds unit, acceptance, API, RFC, and changelog updates.
Show a summary per file
| File | Description |
|---|---|
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs |
Tests exit-code filtering. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs |
Tests retry rendering and totals. |
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/RetryTests.cs |
Validates retry terminal output. |
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/JUnitReportTests.cs |
Verifies final-only JUnit results. |
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CtrfReportTests.cs |
Verifies CTRF flaky reporting. |
test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceAssert.cs |
Supports retried summary assertions. |
src/TestFramework/TestFramework/InternalAPI/InternalAPI.Unshipped.txt |
Tracks new internal result properties. |
src/TestFramework/TestFramework/Attributes/TestMethod/TestResult.cs |
Stores retry attribution. |
src/TestFramework/TestFramework/Attributes/TestMethod/RetryResult.cs |
Documents attempt publication. |
src/TestFramework/TestFramework/Attributes/TestMethod/RetryBaseAttribute.cs |
Updates retry contract documentation. |
src/TestFramework/TestFramework/Attributes/TestMethod/RetryAttribute.cs |
Updates retry behavior documentation. |
src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs |
Ignores superseded outcomes. |
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt |
Declares the new public property. |
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs |
Passes retry metadata to terminal reporting. |
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs |
Reconciles host and in-process attempts. |
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs |
Annotates and counts attempts. |
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.Summary.cs |
Documents retried totals. |
src/Platform/Microsoft.Testing.Platform/Messages/RetryAttemptProperty.cs |
Defines retry attempt metadata. |
src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxDataConsumer.cs |
Filters superseded TRX results. |
src/Platform/Microsoft.Testing.Extensions.JUnitReport/JUnitReportGenerator.cs |
Filters superseded JUnit results. |
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.cs |
Filters superseded published results. |
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs |
Suppresses superseded annotations. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs |
Flattens and tags retry attempts. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs |
Uses final outcomes for verdicts. |
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs |
Adds retry properties to test nodes. |
src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs |
Preserves VSTest final-only behavior. |
docs/RFCs/016-JUnit-Report.md |
Documents JUnit retry handling. |
docs/Changelog.md |
Records the MSTest behavior change. |
docs/Changelog-Platform.md |
Records the platform API change. |
Review details
Comments suppressed due to low confidence (1)
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:357
- A custom
RetryBaseAttributemay legally add an empty final result array. PreviouslyTryGetLast()returned that empty array, allowingSendTestResultsAsyncto callRecordEmptyResultAsync; this combination instead retains only earlier superseded results, so no final outcome is recorded andAllPassedcan return true after skipping them. Preserve the existing empty-result behavior when the final retry attempt is empty.
IReadOnlyList<TestResult[]> retryAttempts = retryResult.AllResults;
if (retryAttempts.Count == 0)
{
// A RetryBaseAttribute implementation is expected to add at least one attempt.
throw ApplicationStateGuard.Unreachable();
- Files reviewed: 29/29 changed files
- Comments generated: 7
- Review effort level: Medium
Follow-up to the design discussion on the placement of the retry attribution. Keeping `RetryAttemptProperty` as a sibling of the state property rather than folding it into `TestNodeStateProperty`: the state types are sealed with public constructors and repo policy bans `init` on new public API, so carrying the data there would mean ~12-15 new public constructors across Passed/Failed/Error/Timeout/Skipped/Cancelled, plus an equality change on already-shipped types. Attempt data is also the same category as `TimingProperty` and the output properties - it describes the execution occurrence, not the outcome - and applies to in-progress updates too. Two concrete improvements from that review: - Add `TestNode.IsSupersededRetryAttempt()` so the "is this the final outcome?" question has a single implementation instead of the same pattern-match duplicated across four consumers. This is the one real downside of the sibling placement (the state property alone cannot answer it) and is now contained in one place. - Register the property in BOTH JSON-RPC serializers. An unhandled property falls through the type chain and is silently dropped, so server-mode clients previously saw several updates for one test node uid with no way to tell the attempts apart. Documented the new `retry.attempt` / `retry.is-superseded` keys in the protocol intro and covered both serializer implementations with tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (4)
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs:95
- Publishing these nodes exposes superseded failures to several unfiltered consumers.
GitHubActionsAnnotationReporter.cs:88-117emits a durable::error,MSBuildConsumer.cs:118-193forwards/counts it as a real failure,SimplifiedConsoleOutputDeviceBase.TestEvents.cs:168-197produces a failed Browser/WASI summary, andAzureDevOpsArtifactUploader.cs:138-140enables failure-only uploads. A fail-then-pass[Retry]test therefore still appears failed or triggers failure side effects on those surfaces. Filter superseded attempts in each final-outcome consumer and add regression tests.
// Surface an in-process retry (MSTest's [Retry]) so the platform can tell the attempts of one test apart
// instead of seeing repeated results for the same uid. Only added when a retry actually happened, so a
// regular test node is byte-identical to before.
if (result.RetryAttemptNumber > 1 || result.IsSupersededRetryAttempt)
{
testNode.Properties.Add(new RetryAttemptProperty(result.RetryAttemptNumber, result.IsSupersededRetryAttempt));
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs:1
- This edit removed the UTF-8 BOM, but
.editorconfig:65-67requiresutf-8-bomfor every C# file. Restore the BOM to avoid an encoding-policy violation.
// Copyright (c) Microsoft Corporation. All rights reserved.
src/Platform/Microsoft.Testing.Platform/Messages/RetryAttemptProperty.cs:1
- This new C# file is missing the UTF-8 BOM required by
.editorconfig:65-67. Save it as UTF-8 with BOM.
// Copyright (c) Microsoft Corporation. All rights reserved.
src/Platform/Microsoft.Testing.Platform/Messages/RetryAttemptPropertyExtensions.cs:1
- This new C# file is missing the UTF-8 BOM required by
.editorconfig:65-67. Save it as UTF-8 with BOM.
// Copyright (c) Microsoft Corporation. All rights reserved.
- Files reviewed: 35/35 changed files
- Comments generated: 3
- Review effort level: Medium
Integrates #10329 ("Report retried and flaky tests by name"), which landed in main and reworked the same terminal retry subsystem. The two changes are complementary and now compose: #10329 reports flaky tests by name for out-of-process host retries, and in-process `[Retry]` attempts feed the same reporting, so a fail-then-pass `[Retry]` test is now listed under "Flaky tests:" with its attempt count. My ad-hoc "(+N retried)" total suffix is dropped in favor of #10329's richer `flaky:` / `retried:` lines. Review findings addressed: - Superseded attempts reached five outcome-oriented consumers that were missed in the original audit, each of which treats every terminal update as a real outcome: `AbortForMaxFailedTestsExtension` (a fail-then-pass `[Retry]` could trip `--maximum-failed-tests` and force exit code 13), `RetryDataConsumer` (requested an out-of-process host relaunch for a test whose final in-process attempt passed), `MSBuildConsumer` and `GitHubActionsAnnotationReporter` (logged build errors / annotations for a test that went on to pass), and `SimplifiedConsoleOutputDeviceBase` (counted and printed superseded failures). All now filter via the shared `IsSupersededRetryAttempt()` helper. - Class and assembly cleanup results are appended after the retry sequence and kept the default attempt number 1. Reported under the same test node uid as a test that finished on attempt 2+, the terminal's attempt ordering saw an out-of-order arrival and threw `Unreachable()`. Cleanup results now inherit the attempt the test finished on. - An empty final retry attempt was misreported. `SendTestResultsAsync` treats an empty array as an execution error, but the combined array was non-empty (superseded results only), so that path was skipped, every entry was then filtered out, and `AllPassed` vacuously returned true - turning a missing result into a passing test that unblocked dependents. The empty-result contract is now preserved. - Restored the UTF-8 BOM that a scripted edit stripped from TerminalOutputDevice.DataConsumption.cs, and added it to the two new files, per `.editorconfig` (`charset = utf-8-bom`). - Declared the new `TestProgressState` overloads in InternalAPI.Unshipped.txt, matching how the existing overloads are tracked. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (5)
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs:526
- When both retry mechanisms are present, the number of executions is not the maximum of the two final coordinates. For example, two in-process attempts in host attempt 1 followed by three in host attempt 2 means five executions, but this reports three. Track the number of distinct
(hostAttempt, retryAttempt)pairs per UID and use that count here; otherwise the flaky-test detail contradicts the retry totals.
// The attempt count is the pair (host attempt, in-process retry attempt): a test retried
// in-process reports 1 host attempt but several retry attempts, so the larger of the two is
// the number of times the test actually ran.
TestNodeInfoEntry resultEntry = _testUidToResults[entry.Key];
flaky.Add((entry.Value, Math.Max(resultEntry.LastAttemptNumber, resultEntry.LastRetryAttemptNumber)));
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:412
- The cleanup-attempt alignment that prevents the reported out-of-order terminal crash has no automated coverage in this change. Add a
UnitTestRunnerTestscase where a retry finishes on attempt 2+ and class or assembly cleanup produces a result, then assert that the cleanup result inherits the final attempt number and is not superseded.
private static TestResult AlignAttemptNumber(TestResult result, TestResult[] precedingResults)
{
for (int i = precedingResults.Length - 1; i >= 0; i--)
{
if (!precedingResults[i].IsSupersededRetryAttempt)
{
result.RetryAttemptNumber = precedingResults[i].RetryAttemptNumber;
docs/RFCs/016-JUnit-Report.md:220
- These additions leave two descriptions of each retry mechanism, and the old MSTest bullet directly contradicts the new one by saying only the final attempt is surfaced. Keep the updated MSTest bullet and the existing detailed orchestrator bullet below, removing the duplicate abbreviated orchestrator entry and stale MSTest entry.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries in-process and publishes **every** attempt as a `TestNodeUpdateMessage` under the same test node uid, tagged with `RetryAttemptProperty`. The JUnit report generator drops the attempts marked `IsSuperseded` (JUnit has no notion of attempts, so keeping them would inflate the suite totals), so the report still contains a single `<testcase>` row per logical test with its eventual outcome. No per-attempt disambiguation is applied.
- **`Microsoft.Testing.Extensions.Retry` (MTP-level orchestrator, `--retry-failed-tests`)** — The orchestrator re-runs the entire test-host child process on failure. Each attempt is a separate process and therefore produces **its own JUnit XML file** under the run's results directory. No special handling is required inside the report engine.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries internally and surfaces only the **final** attempt as a `TestNodeUpdateMessage`. The JUnit report therefore contains a single `<testcase>` row per logical test, with its eventual outcome. No per-attempt disambiguation is applied.
docs/Changelog.md:22
- The PR explicitly dropped the
(+N retried)total suffix after #10329, and the new terminal test expects a dedicatedretried:line. This changelog example therefore documents output that this change does not produce.
* Report every `[Retry]` attempt to the test host instead of discarding all but the last one. A retried test now shows its attempts in the terminal (`failed (try 1) MyTest`), is reconciled in the summary (`total: 1 (+1 retried)`), and is finally detected as `flaky` (with `retries` / `retryAttempts[]`) by the CTRF report. Superseded attempts do not affect the process exit code and are filtered out of the TRX and JUnit reports, so those keep one row per test in [#10292](https://github.com/microsoft/testfx/issues/10292)
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:367
- This special empty-final-attempt path is not exercised by the added tests: the acceptance assets use the built-in retry implementation and always produce a final result, and
UnitTestRunnerTestscurrently has no retry cases. Add a unit test with a customRetryBaseAttributereturning an empty final array and verify that the empty-result/error path is preserved rather than treating the test and its dependents as passed.
This issue also appears on line 406 of the same file.
if (retryAttempts[retryAttempts.Count - 1].Length == 0)
{
return [];
- Files reviewed: 38/38 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Two conflicts, both purely additive - main added `AssertionFailureProperty` alongside this branch's `RetryAttemptProperty`: - `PublicAPI.Unshipped.txt`: kept both API blocks. - `TerminalOutputDevice.DataConsumption.cs`: kept both locals and both property-bag switch cases, so the single-pass walk collects the assertion pair and the retry attribution together. Main's expected/actual wiring on the failed-state call site auto-merged and is preserved. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (11)
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.TestCompletion.cs:135
DotnetTestDataConsumeremits every newly surfaced attempt, but its success/failure IPC messages have no retry fields. Hard-coding(1, false)here means a fail-then-pass[Retry]reaches the parent under the same(host attempt, retry attempt)pair;ReportGenericTestResultthen counts both outcomes instead of replacing the failure. This can makedotnet testreport one ultimately passing test as two results with one failure. Carry the retry attribution through the IPC model/serializers, or filter superseded attempts before transport until that protocol exists.
// The orchestrator attributes retries per host instance; it does not surface a test framework's
// in-process retry attempt, so results arriving through this path are always attempt 1 of their host
// attempt.
retryAttemptNumber: 1,
isRetryAttempt: false);
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs:526
Math.Maxis not the number of executions when both retry mechanisms compose. For example, three in-process executions in host attempt 1 followed by one execution in host attempt 2 ran four times, but this reports two. Track the count of distinct(hostAttempt, retryAttempt)pairs per UID and use that count here.
// The attempt count is the pair (host attempt, in-process retry attempt): a test retried
// in-process reports 1 host attempt but several retry attempts, so the larger of the two is
// the number of times the test actually ran.
TestNodeInfoEntry resultEntry = _testUidToResults[entry.Key];
flaky.Add((entry.Value, Math.Max(resultEntry.LastAttemptNumber, resultEntry.LastRetryAttemptNumber)));
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsPublisher.cs:267
- The same filter is missing from
AzureDevOpsArtifactUploader.ConsumeAsync: that consumer sets_hasTestFailuresfor any failed state and later emits thehas-test-failuresbuild tag. Consequently, a fail-then-pass[Retry]still tags the Azure build as having test failures even though this publisher, the reporter, and the process exit code treat it as passed. ApplyIsSupersededRetryAttempt()there and add a tag regression test.
// A test framework that retries a test in-process reports every attempt under the same test
// node uid. Azure DevOps keys results by automated test name, so publishing the superseded
// attempts would create duplicate rows for one test; only the final attempt is published.
if (testNodeUpdateMessage.TestNode.IsSupersededRetryAttempt())
{
return;
}
docs/RFCs/016-JUnit-Report.md:219
- These bullets were inserted without replacing the existing retry bullets immediately below. The RFC now both says MSTest publishes every attempt and says it surfaces only the final attempt, while the orchestrator description is duplicated. Remove the stale MSTest bullet and one of the two orchestrator bullets.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries in-process and publishes **every** attempt as a `TestNodeUpdateMessage` under the same test node uid, tagged with `RetryAttemptProperty`. The JUnit report generator drops the attempts marked `IsSuperseded` (JUnit has no notion of attempts, so keeping them would inflate the suite totals), so the report still contains a single `<testcase>` row per logical test with its eventual outcome. No per-attempt disambiguation is applied.
- **`Microsoft.Testing.Extensions.Retry` (MTP-level orchestrator, `--retry-failed-tests`)** — The orchestrator re-runs the entire test-host child process on failure. Each attempt is a separate process and therefore produces **its own JUnit XML file** under the run's results directory. No special handling is required inside the report engine.
docs/Changelog.md:22
- The
(+N retried)total suffix was removed by #10329; the implementation and new terminal test now emit a dedicatedretried: N test(s), M extra run(s)line. Update this changelog example so it describes the shipped output.
* Report every `[Retry]` attempt to the test host instead of discarding all but the last one. A retried test now shows its attempts in the terminal (`failed (try 1) MyTest`), is reconciled in the summary (`total: 1 (+1 retried)`), and is finally detected as `flaky` (with `retries` / `retryAttempts[]`) by the CTRF report. Superseded attempts do not affect the process exit code and are filtered out of the TRX and JUnit reports, so those keep one row per test in [#10292](https://github.com/microsoft/testfx/issues/10292)
src/TestFramework/TestFramework/Attributes/TestMethod/RetryResult.cs:24
RetryResultis shared by the MTP and VSTest paths, but the VSTest recorder intentionally drops superseded results. Therefore “All attempts are reported to the test host” is not true for VSTest. Scope this statement to Microsoft.Testing.Platform or describe the results as retained for host integrations.
/// <remarks>
/// All attempts are reported to the test host: the last one as the test's outcome, the earlier ones tagged as
/// superseded so tooling can surface the retry (see the platform's <c>RetryAttemptProperty</c>).
src/TestFramework/TestFramework/Attributes/TestMethod/RetryBaseAttribute.cs:34
- This return documentation applies to both host integrations, but VSTest intentionally filters the superseded results. Scope the reporting claim to Microsoft.Testing.Platform so consumers are not promised per-attempt VSTest results.
/// Returns a <see cref="RetryResult"/> object that contains the results of all attempts. The last added
/// element determines the test outcome; the earlier attempts are reported to the test host as superseded
/// attempts so tooling can surface the retry (attempt count, flaky detection, per-attempt error messages).
src/TestFramework/TestFramework/Attributes/TestMethod/RetryAttribute.cs:88
- This return documentation applies to both host integrations, but VSTest intentionally filters the superseded results. Scope the reporting claim to Microsoft.Testing.Platform so consumers are not promised per-attempt VSTest results.
/// Returns a <see cref="RetryResult"/> object that contains the results of all attempts. The last added
/// element determines the test outcome; the earlier attempts are reported to the test host as superseded
/// attempts so tooling can surface the retry.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:367
- This branch preserves a subtle empty-result contract, but there is no
UnitTestRunnertest exercising a customRetryBaseAttributewhose final attempt returns an empty array. Add a regression test proving the runner returns an empty result and does not treat the test as passing; otherwise this false-pass fix can regress unnoticed.
if (retryAttempts[retryAttempts.Count - 1].Length == 0)
{
return [];
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:413
- The cleanup-attempt alignment fixes an out-of-order terminal crash, but no unit or acceptance test covers a retry followed by a class/assembly cleanup result. Add a regression test that verifies the cleanup result inherits the final retry number and is not marked superseded.
if (!precedingResults[i].IsSupersededRetryAttempt)
{
result.RetryAttemptNumber = precedingResults[i].RetryAttemptNumber;
break;
test/IntegrationTests/MSTest.Acceptance.IntegrationTests/CtrfReportTests.cs:185
- This generated test asset's private static field does not follow the repository's
s_camel-case rule (.editorconfig:601-603). Rename it tos_attemptsand update its uses.
- Files reviewed: 38/38 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
Brings in #10358 (expanded OpenTelemetry support), which touches `TestApplicationResult` — the same `ConsumeAsync` that carries this branch's superseded-retry-attempt guard. One conflict, purely additive: `InternalAPI.Unshipped.txt` gained the new OpenTelemetry surface alongside this branch's `TestProgressState.Report*Test` overloads. Kept both. `TestApplicationResult.cs` auto-merged, and the result is semantically correct: the retry guard sits above the outcome switch, so a superseded attempt now also skips the new OpenTelemetry notifications. That is the desired behavior — an attempt a later one supersedes is not the test's outcome, so it must not emit a failed span or count toward the OTel metrics, exactly as it must not count toward the exit code. The activity accounting stays balanced across a retry: `NotifyInProgress` enqueues one activity per test (MSTest reports in-progress once per test, not once per attempt, and in-progress nodes carry no `RetryAttemptProperty`), and the guard ensures exactly one dequeue from the final attempt rather than one per attempt. Added `ConsumeAsync_SupersededRetryAttempt_DoesNotCloseTheTestActivity` to lock that interaction in. Verified by mutation: disabling the guard fails this test along with the two existing superseded-attempt tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs:104
- Publishing the superseded failures now also reaches
VideoRecorderSessionHandler, which does not inspectRetryAttemptProperty: it marks_anyTestFailed, appends oneTestRecordper attempt, and in defaulton-failuremode retains video for a test whose final outcome passed. Per-test/always mode also emits duplicate clips and session chapters for the same logical test. Filter superseded updates in the video consumer (or explicitly model attempt history there) so its persistence and per-test artifact semantics follow the final outcome.
if (result.RetryAttemptNumber > 1 || result.IsSupersededRetryAttempt)
{
testNode.Properties.Add(new RetryAttemptProperty(result.RetryAttemptNumber, result.IsSupersededRetryAttempt));
docs/RFCs/016-JUnit-Report.md:220
- This adds replacement retry bullets without removing the old ones below. The section now says both that MSTest publishes every attempt and that it surfaces only the final attempt, and describes the orchestrator twice. Keep the new MSTest bullet and the existing detailed orchestrator bullet at line 221, removing the duplicate/stale entries.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries in-process and publishes **every** attempt as a `TestNodeUpdateMessage` under the same test node uid, tagged with `RetryAttemptProperty`. The JUnit report generator drops the attempts marked `IsSuperseded` (JUnit has no notion of attempts, so keeping them would inflate the suite totals), so the report still contains a single `<testcase>` row per logical test with its eventual outcome. No per-attempt disambiguation is applied.
- **`Microsoft.Testing.Extensions.Retry` (MTP-level orchestrator, `--retry-failed-tests`)** — The orchestrator re-runs the entire test-host child process on failure. Each attempt is a separate process and therefore produces **its own JUnit XML file** under the run's results directory. No special handling is required inside the report engine.
docs/Changelog.md:22
- The changelog still advertises the removed
total: 1 (+1 retried)suffix. The integrated #10329 output uses a dedicatedretried: 1 test(s), 1 extra run(s)line, as asserted by the new terminal test, so update this example to avoid documenting output users will never see.
* Report every `[Retry]` attempt to the test host instead of discarding all but the last one. A retried test now shows its attempts in the terminal (`failed (try 1) MyTest`), is reconciled in the summary (`total: 1 (+1 retried)`), and is finally detected as `flaky` (with `retries` / `retryAttempts[]`) by the CTRF report. Superseded attempts do not affect the process exit code and are filtered out of the TRX and JUnit reports, so those keep one row per test in [#10292](https://github.com/microsoft/testfx/issues/10292)
- Files reviewed: 38/38 changed files
- Comments generated: 1
- Review effort level: Balanced
The new 14-parameter internal `TerminalTestReporter.TestCompleted` overload was the one tracked-internal signature still missing from `InternalAPI.Unshipped.txt`; its siblings are tracked in `InternalAPI.Shipped.txt`. Adds it next to the `TestProgressState` overloads declared for the same feature. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (4)
src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs:120
- The Azure DevOps failure handling is still incomplete:
AzureDevOpsArtifactUploader.ConsumeAsyncat lines 138-140 marks every failed update as_hasTestFailures, and lines 182-185 consequently emit theTestFailuresTag. A fail-then-pass[Retry]run will therefore still receive a test-failures build tag from its superseded attempt. Apply the same superseded-attempt guard in that consumer.
if (nodeUpdateMessage.TestNode.IsSupersededRetryAttempt())
docs/RFCs/016-JUnit-Report.md:221
- This adds a second pair of retry-mechanism bullets without removing the existing pair below. The stale MSTest bullet still says only the final attempt is surfaced, directly contradicting the new behavior, while the orchestrator is documented twice. Keep the new MSTest explanation and the existing detailed orchestrator explanation only.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries in-process and publishes **every** attempt as a `TestNodeUpdateMessage` under the same test node uid, tagged with `RetryAttemptProperty`. The JUnit report generator drops the attempts marked `IsSuperseded` (JUnit has no notion of attempts, so keeping them would inflate the suite totals), so the report still contains a single `<testcase>` row per logical test with its eventual outcome. No per-attempt disambiguation is applied.
- **`Microsoft.Testing.Extensions.Retry` (MTP-level orchestrator, `--retry-failed-tests`)** — The orchestrator re-runs the entire test-host child process on failure. Each attempt is a separate process and therefore produces **its own JUnit XML file** under the run's results directory. No special handling is required inside the report engine.
src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestProgressState.cs:526
Math.Maxundercounts when both retry mechanisms advance. For example,(host 1, retry 1),(host 1, retry 2), then(host 2, retry 1)represents three executions, but the final tuple makes this report2 attempts. Track the number of distinct(host attempt, in-process attempt)pairs per UID as transitions arrive instead of deriving it from only the final pair, and cover a combined-retry sequence.
flaky.Add((entry.Value, Math.Max(resultEntry.LastAttemptNumber, resultEntry.LastRetryAttemptNumber)));
docs/Changelog.md:22
- The terminal no longer uses the
total: N (+M retried)suffix after #10329; it emits a dedicatedretried:line, as the updated terminal test asserts. Update this release note so it does not promise output that this PR cannot produce.
* Report every `[Retry]` attempt to the test host instead of discarding all but the last one. A retried test now shows its attempts in the terminal (`failed (try 1) MyTest`), is reconciled in the summary (`total: 1 (+1 retried)`), and is finally detected as `flaky` (with `retries` / `retryAttempts[]`) by the CTRF report. Superseded attempts do not affect the process exit code and are filtered out of the TRX and JUnit reports, so those keep one row per test in [#10292](https://github.com/microsoft/testfx/issues/10292)
- Files reviewed: 38/38 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
`TestResult` is `[Serializable]` on .NET Framework because it crosses AppDomain boundaries. Adding a field to a `[Serializable]` type breaks version-tolerant deserialization of payloads written before that field existed, which is why the previously added members carry `[field: OptionalField]` under `#if NETFRAMEWORK`. The two retry members were missing it; matched the existing convention. Documented the resulting defaults, including the subtlety that `RetryAttemptNumber` deserializes as 0 rather than its initializer's 1 (field initializers do not run during deserialization). That is harmless: every consumer keys off `IsSupersededRetryAttempt`, which defaults to false, so an older payload is treated as a final outcome either way. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (4)
src/Platform/Microsoft.Testing.Platform/OutputDevice/SimplifiedConsoleOutputDeviceBase.TestEvents.cs:146
isSupersededAttemptsuppresses the outcome switch, but the active-test tracker is completed before that switch. MSTest emits oneInProgressupdate for the whole retry sequence, so a superseded terminal update removes the UID from slow-test tracking before the final outcome;HangDumpActivityIndicatorcorrectly preserves the same UID until the final attempt. Gate_activeTestTracker.Completeon!isSupersededAttempt.
bool isSupersededAttempt = testNodeStateChanged.TestNode.IsSupersededRetryAttempt();
src/Platform/Microsoft.Testing.Platform/Messages/RetryAttemptProperty.cs:16
- The new stream contract also affects
SlowTestReporterBase: its terminal-state branch atSharedExtensionHelpers/SlowTestReporterBase.cs:141-145removes the UID for every result, including a superseded one. Azure DevOps and GitHub slow-test reporters inherit that behavior, so a framework that uses oneInProgressupdate for the retry sequence stops receiving slow-test diagnostics after the first attempt. Apply the same superseded-attempt guard used by the hang-dump and simplified-console consumers.
/// A test framework that retries a test in-process (for example MSTest's <c>[Retry]</c> attribute) publishes one
/// <see cref="TestNodeUpdateMessage"/> per attempt, each carrying this property. Consumers that want a single row
/// per test - TRX, JUnit, the process exit code - ignore updates whose <see cref="IsSuperseded"/> is
/// <see langword="true"/>. Consumers that want the whole history - CTRF's <c>retryAttempts[]</c> / <c>flaky</c>,
/// the HTML report, the terminal's retried/flaky counters - keep them all.
docs/RFCs/016-JUnit-Report.md:220
- The next bullet still describes exposing every MSTest attempt as “future MSTest behavior,” which now directly contradicts this updated bullet. Reword that generic case to cover frameworks whose repeated updates are not tagged with
RetryAttemptProperty.
- **MSTest `[Retry]` attribute** — The MSTest adapter retries in-process and publishes **every** attempt as a `TestNodeUpdateMessage` under the same test node uid, tagged with `RetryAttemptProperty`. The JUnit report generator drops the attempts marked `IsSuperseded` (JUnit has no notion of attempts, so keeping them would inflate the suite totals), so the report still contains a single `<testcase>` row per logical test with its eventual outcome. No per-attempt disambiguation is applied.
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:410
- Stamping a class/assembly cleanup result with the final retry number prevents the ordering crash, but it also makes the terminal classify that ancillary cleanup result as another retry execution. For a fail-then-pass test whose cleanup fails,
ReportGenericTestResultsees the cleanup in the same retry pair and incrementsRetriedExecutionsagain, so the summary reports two extra runs although the test was retried once. Preserve the ordering metadata without counting cleanup as a test-method retry (for example by carrying an ancillary-result marker through the terminal path).
private static TestResult AlignAttemptNumber(TestResult result, TestResult[] precedingResults)
{
for (int i = precedingResults.Length - 1; i >= 0; i--)
{
if (!precedingResults[i].IsSupersededRetryAttempt)
- Files reviewed: 42/42 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The "per-attempt messages" bullet still described exposing each attempt as future MSTest behavior and promised every duplicate is preserved. Both are now stale: MSTest tags its attempts with `RetryAttemptProperty`, and the generator filters the superseded ones, so only *unattributed* duplicates reach the preserve-and-disambiguate path. Scoped the bullet accordingly so it no longer contradicts the two bullets above it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:412
- Stamping an appended cleanup result with the final retry number avoids the out-of-order exception, but it also makes the terminal count that cleanup result as another retry execution. After attempt 1 fails and attempt 2 passes,
_retriedUidsalready contains the UID; the cleanup result then entersTestProgressStatewith the same(host, retry)pair, so the same-attempt branch increments_retriedExecutionsagain. A single retry followed by a class/assembly cleanup failure therefore reports2 extra run(s)instead of1. Cleanup results need to retain ordering without participating in retry-execution accounting (and this scenario should be covered for both cleanup levels).
result.RetryAttemptNumber = precedingResults[i].RetryAttemptNumber;
- Files reviewed: 42/42 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestNodeConverter.cs:104
- These newly published superseded terminal updates also reach
SlowTestReporterBase, whoseConsumeAsyncremoves a UID on any non-in-progress state (SharedExtensionHelpers/SlowTestReporterBase.cs:141-145). Both the GitHub Actions and Azure DevOps slow-test reporters therefore stop tracking at the first superseded attempt instead of the final outcome. Apply the same superseded-attempt guard there and cover both derived reporters.
if (result.RetryAttemptNumber > 1 || result.IsSupersededRetryAttempt)
{
testNode.Properties.Add(new RetryAttemptProperty(result.RetryAttemptNumber, result.IsSupersededRetryAttempt));
src/Platform/Microsoft.Testing.Platform/OutputDevice/SimplifiedConsoleOutputDeviceBase.TestEvents.cs:146
- The active-test bookkeeping still treats this superseded result as terminal. MSTest emits one
RecordStartAsyncbeforeRunSingleTestAsyncand then publishes the flattened results, so line 158 removes the UID fromActiveTestTrackerand line 209 clears its progress display on attempt 1. Defer both actions until!isSupersededAttempt; otherwise active/slow-test tracking ends before the final outcome is published.
// A test framework that retries in-process reports every attempt under the same test node uid. A
// superseded attempt is not the test's outcome, so it must not be counted or printed as a failure -
// a fail-then-pass [Retry] test would otherwise show a failed summary here. The active-test
// bookkeeping below still runs: each attempt has its own in-progress/terminal pair.
bool isSupersededAttempt = testNodeStateChanged.TestNode.IsSupersededRetryAttempt();
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs:218
- This reduces the retry property to its number/presence and drops
IsSuperseded.TerminalTestReporter.TestCompletedconsequently removes the UID fromTestNodeResultsStatefor every attempt, so--show-active-testsdrops a test on the first superseded result even though the sequence has only one in-progress update. Carry the superseded flag through and remove the running node only for the final attempt.
// A test framework that retries in-process reports every attempt under the same uid; the attempt
// number lets the reporter replace (rather than double-count) the earlier attempt and annotate the
// per-test line with "(try N)".
int retryAttemptNumber = retryAttempt?.AttemptNumber ?? 1;
bool isRetryAttempt = retryAttempt is not null;
- Files reviewed: 42/42 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Two more consumers whose state machines my change disturbed, both the same shape as the hang-dump fix: a retry sequence has one in-progress update for the whole test but a terminal update per attempt, so completing on a superseded attempt drops the test while its next attempt is still running. - `SimplifiedConsoleOutputDeviceBase` completed `_activeTestTracker` before the superseded gate, so active-test progress stopped after attempt 1. The comment there also asserted the opposite of what actually happens - "each attempt has its own in-progress/terminal pair" - which is wrong: MSTest reports in-progress once per test, outside the retry loop. Corrected both. - `SlowTestReporterBase` removed the uid from `_inProgress` on every terminal update, so a retried test stopped producing slow-test diagnostics after its first attempt. This is a shared base, so the Azure DevOps and GitHub slow-test reporters inherited the behavior. Also merges main (#10362). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
There was a problem hiding this comment.
Review details
Suppressed comments (2)
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:306
- The same retry-accounting inflation occurs for an assembly-cleanup failure: after the real final retry result has registered the UID as retried, this aligned same-pair result takes the branch that increments
_retriedExecutions. The summary therefore counts assembly cleanup as another retry-driven test run. Preserve the ordering fix, but mark or handle cleanup outcomes so they do not increase the retry execution count.
result = [.. result, AlignAttemptNumber(assemblyCleanupResult, result)];
src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs:278
- Aligning a class-cleanup failure to the final retry pair makes
ReportGenericTestResulttreat it as another result in that retry attempt. Because the UID is already in_retriedUids, the same-pair branch increments_retriedExecutions, so a test that ran twice and then hadClassCleanupfail is reported as having two extra runs instead of one. Cleanup results need to preserve ordering without contributing to retry execution accounting (likely via explicit result-kind metadata or equivalent terminal-state handling).
This issue also appears on line 306 of the same file.
result = [.. result, AlignAttemptNumber(cleanupResult, result)];
- Files reviewed: 43/43 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
Third state machine disturbed by publishing one terminal update per attempt: `TerminalTestReporter.TestCompleted` removed the uid from `TestNodeResultsState` on every attempt, so `--show-active-tests` dropped a retried test on its first superseded result even though the sequence has a single in-progress update. The root cause was that the in-process overload took the retry attribution as two loose values (`retryAttemptNumber`, `isRetryAttempt`) and simply had no way to express "superseded". Rather than adding a third flag - the third time this class of omission has surfaced - the overload now takes the `RetryAttemptProperty` itself, so the attempt number, the is-a-retry fact and the superseded flag travel together and a caller cannot forward some and drop the rest. Net parameter count goes down by one. Also removes a stale `|| isSupersededAttempt` in `SimplifiedConsoleOutputDeviceBase` that cleared the "running" progress message for a superseded attempt. It was correct when that consumer still completed the active-test tracker on superseded results; now that the test stays tracked, its progress message has to stay too. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (5)
src/TestFramework/TestFramework/Attributes/TestMethod/RetryResult.cs:24
- This states that every attempt reaches the test host, but the VSTest recorder explicitly drops superseded results in
TestResultRecorderExtensions.cs:65-71; only Microsoft.Testing.Platform receives the full history. Qualify the behavior so consumers do not expect VSTest tooling to exposeAllResults.
src/Platform/Microsoft.Testing.Platform/OutputDevice/TerminalOutputDevice.DataConsumption.cs:1 - This changed file is still BOM-less, but
.editorconfig:67requires UTF-8 with BOM for every C# file. Restore the BOM so the file complies with the repository encoding rule.
// Copyright (c) Microsoft Corporation. All rights reserved.
src/TestFramework/TestFramework/Attributes/TestMethod/RetryBaseAttribute.cs:34
- The earlier attempts are not reported to VSTest:
TestResultRecorderExtensions.cs:65-71drops superseded results before calling the VSTest recorder. Qualify this as Microsoft.Testing.Platform behavior so custom retry authors are not promised per-attempt tooling on both hosts.
src/TestFramework/TestFramework/Attributes/TestMethod/RetryAttribute.cs:88 - This is not true for the VSTest path, which deliberately drops superseded results in
TestResultRecorderExtensions.cs:65-71. Qualify the sentence as Microsoft.Testing.Platform behavior to keep this public override's documentation accurate.
src/TestFramework/TestFramework/Attributes/TestMethod/TestResult.cs:225 - This claim conflicts with the VSTest behavior documented below and implemented in
TestResultRecorderExtensions.cs:65-71: VSTest receives only the final result, while Microsoft.Testing.Platform receives every attempt. Qualify this remark so it does not describe the platform-specific behavior as universal.
- Files reviewed: 43/43 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…atform Two fixes from the latest review. The BOM on `TerminalOutputDevice.DataConsumption.cs` was stripped again by my own scripted `WriteAllText` while updating call sites - the same mistake, in the same file, that an earlier round already caught. Restored, and I audited every `.cs` file this PR touches against `origin/main`: it is the only one that regressed. The other BOM-less files in the diff were already BOM-less on main, so they are pre-existing drift and deliberately left alone rather than turned into an unrelated bulk change. The public retry docs stated that every attempt reaches "the test host" unconditionally, which is only true for Microsoft.Testing.Platform - the VSTest recorder deliberately drops superseded results so Test Explorer keeps its historical one-result-per-test behavior. Qualified the four affected doc comments (`RetryResult`, `RetryBaseAttribute`, `RetryAttribute`, `TestResult`) so custom retry authors are not promised per-attempt tooling on both hosts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
🧪 Test quality grade — PR #10295No new or modified test methods were identified in the changed regions Re-run with
|
Implements #10292.
Problem
UnitTestRunner.RunSingleTestAsynckept only the last retry attempt:so an in-process
[Retry]was invisible everywhere — the terminal showed a plain passing test, and the CTRF report claimedflaky: 0with noretryAttempts[]even thoughCtrfReportEngine.CollapseAttempts/IsFlakyalready implement the correct rule.Approach
Publish every attempt as its own
TestNodeUpdateMessageunder the sameTestNode.Uid, tagged with a new platform property:IsSupersededlets each consumer opt in or out without buffering the stream, via the sharedTestNode.IsSupersededRetryAttempt()helper.Composes with #10329
#10329 ("Report retried and flaky tests by name") landed in main mid-review and reworked the same terminal subsystem. The two are complementary and now integrated — in-process
[Retry]attempts feed the shared flaky reporting:TestProgressStateorders attempts by the pair(host attempt, in-process attempt), and tracks the execution count per uid so the two mechanisms compose rather than collide. CTRF additionally emits"flaky": 1,"retries": 1and a populatedretryAttempts[].Design note: sibling property vs. part of the state property
Raised in review — should the attempt data hang off
Passed/Failed/…? Two real points in favour:PropertyBaghas a dedicated O(1) field for the state property, and enforces exactly one, which would make illegal states unrepresentable.Kept as a sibling because the state types are sealed with public constructors and repo policy bans
initon new public API, so carrying two values there means ~12–15 new public constructors plus an equality change on shipped types. Attempt number is also the same category asTimingProperty(it describes the execution occurrence, not the outcome) and applies toInProgresstoo. The O(n) lookup is not a new class of cost —TestApplicationResultalready does an O(n)Any<TestNodeExecutionCompletedProperty>()walk per node.Consumer audit
After several review rounds each surfaced another affected consumer, I swept every file referencing terminal state properties and classified each one.
Filter superseded attempts (want one row per test):
TestApplicationResult(exit code),AbortForMaxFailedTestsExtension,RetryDataConsumer,MSBuildConsumer,GitHubActionsAnnotationReporter,AzureDevOpsReporter,AzureDevOpsArtifactUploader,VideoRecorderSessionHandler,TrxDataConsumer,JUnitReportGenerator.Keep the test tracked until the final attempt (state machines, not tallies — a retry sequence has one in-progress update but a terminal update per attempt, so completing on a superseded one drops the test while it is still running):
HangDumpActivityIndicator,SimplifiedConsoleOutputDeviceBase,SlowTestReporterBase.Deliberately unchanged:
CtrfReportEngineandHtmlReportEngine(keep the history by design — this is what fixesflaky),CrashDumpSequenceLogger(a diagnostic trace; the attempts leading to a crash are exactly what you want to see, so a guard would have been actively harmful),PerRequestServerDataConsumerService(already dedupes),DotnetTestDataConsumer(forwards by design).VSTest is unaffected: its recorder drops superseded attempts, so Test Explorer and the VSTest TRX are byte-identical to before.
Other correctness fixes found in review
Unreachable(). Cleanup results now inherit the attempt the test finished on.AllPassedvacuously returnedtrue— turning a missing result into a passing test that unblocked dependents.Math.Max(host, retry)undercounts when both mechanisms advance; now tracked per uid.--server. Registered in both (System.Text.Json + Jsonite), documented asretry.attempt/retry.is-superseded.TestResultis[Serializable]on .NET Framework; the two new members needed[field: OptionalField]to match the existing convention for version-tolerant AppDomain deserialization..editorconfig.Remaining follow-ups (not blockers)
dotnet testorchestrator path does not carry the in-process attempt through the IPC wire model, so underdotnet testa[Retry]result is keyed(host attempt, 1)for every attempt. Needs a versioned wire-model field.Progress renderer advances once per attempt, so a retry-heavy run can exceed the discovered count. Verified pre-existing on
mainfor host retries; the fix should cover both mechanisms at once.Per-attempt timestamps are coarse.
TestExecutionManagermeasuresstartTime/endTimeonce around the wholeRunSingleTestAsynccall, so every flattened attempt carries the same window while keeping its own (correct)Duration. CTRF'sretryAttempts[]therefore shows each attempt spanning the whole sequence — attempt 1'sstoplands after attempt 2 began. Fixing it properly means capturing per-attempt boundaries, which needs new serializableTestResultmembers; simply omitting the timing would also drop the correct per-attempt duration.Cleanup results inflate the retried-execution count. A class/assembly cleanup failure is reported under the test's uid and is stamped with the final attempt number (so the attempt ordering does not see it as out-of-order). It then lands on
TestProgressState's same-attempt branch, which treats an extra result for an already-retried uid as another execution — so one retry plus a cleanup failure reports2 extra run(s)instead of1. That layer cannot distinguish a cleanup result from a data-driven row, so a fix needs a distinguishing signal through the three tracked-internal-APIReport*Testoverloads. Cosmetic and edge-case.outcome-kindis documented in the protocol ("passed-with-retries") but implemented nowhere; now that attempts flow it could be populated for IDE filtering.Validation
Microsoft.Testing.Platform.UnitTests1983 ·Microsoft.Testing.Extensions.UnitTests879 ·MSTestAdapter.PlatformServices.UnitTests1071 ·TestFramework.UnitTests1502 (net9.0) and 1376 (net48, the[Serializable]path) — all passing-pack: MSTest retry/CTRF/JUnit/TRX and platform retry + max-failed-tests + MSBuild — all passingFixes #10292