Skip to content

RFC 018: Artifact post-processing for dotnet test (MTP)#9187

Merged
Evangelink merged 4 commits into
mainfrom
dev/amauryleve/rfc-artifact-post-processing
Jul 10, 2026
Merged

RFC 018: Artifact post-processing for dotnet test (MTP)#9187
Evangelink merged 4 commits into
mainfrom
dev/amauryleve/rfc-artifact-post-processing

Conversation

@Evangelink

@Evangelink Evangelink commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Opens RFC 018 — Artifact post-processing for dotnet test (MTP) for discussion (docs/RFCs/018-Artifact-Post-Processing.md).

Renumber note. This was originally drafted as "RFC 017". 017 is now taken on main by 017-TestHost-Launcher.md (merged in #9349), so the file and this PR are renumbered to 018. No new PR — same branch, renamed file + reworked content.

The RFC proposes a mechanism so that, after a multi-module dotnet test run, artifacts of the same kind (TRX, .coverage, Cobertura, custom) are consolidated into single files instead of listed one-per-module — the MTP analogue of VSTest's collect/post-process flow.

Addresses dotnet/sdk#47613; related to #7345, #7471, #6586.

Status: Under discussion

The design is now a decisive, layered recommendation rather than an open menu of orchestration routes:

  • Phase 1 (recommended to implement first). A typed, invocation-agnostic IArtifactPostProcessor engine contract (experimental-gated) plus a user-facing ITool per kind (e.g. merge-trx), both delegating to one shared merge engine. This ships user-visible value (Using TrxReport with --test-modules should include test assembly name and target framework in trx file name #7345) with no SDK or protocol change (one platform-API change: promote ITool to public), is user-runnable from a shell, and is discoverable via --info.
  • Phase 2 (automatic experience). In-memory election computed from data the SDK already has — handshake-advertised capabilities + the artifacts that already stream live over the dotnet-test pipe — then a platform-owned internal dispatcher ITool relaunched once per elected app, with merged artifacts flowing back over the existing pipe as FileArtifactMessage (re-entering the normal reporter path).

Key design refinements vs. the original draft:

  • Tool-first orchestration over a bespoke --internal-post-process-artifacts switch: --tool is the platform's existing non-test execution path (precedent: ms-trxcompare), so the manual and automated routes share one mechanism. The reserved switch is kept as the documented runner-up (§11.1).
  • Reuse the existing pipe rather than a parallel manifest-in/result-JSON-out channel: DotnetTestConnection already streams artifacts live, so the SDK already has the full attributed input list; the manifest is transitional and largely redundant (§7.7, §11.2).
  • Election gathered during the run (no dedicated collect phase) and computed as a minimal set-cover (fewest relaunches), arch-constrained for binary coverage (§7.5, §7.12).

Feedback wanted

  1. Orchestration primitive: dispatcher ITool (recommended) vs. reserved switch vs. specialized live-channel host (§11.1/§11.2, Open question Initial commit! 🎉 #1).
  2. Handshake Option A vs. B (§7.4).
  3. Set-cover vs. per-kind election (§7.5).
  4. Kind as string vs. a typed wrapper, and Kind-versioning policy (Open questions Porting BugFix#260653: DataRowTests not getting appended by arguments #9/Packaging mstest for .net core #10).

cc @Evangelink

Two-phase artifact post-processing to consolidate per-module artifacts (TRX, coverage, custom) at the end of a dotnet test run via a typed IArtifactPostProcessor extension contract, a reserved host mode, handshake-advertised capabilities, and SDK-side election/orchestration.

Opened as an RFC for discussion. Notably raises two unresolved orchestration alternatives: using the existing ITool route as the invocation primitive, and a specialized post-processing host with a live IPC channel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 16, 2026 14:22

Copilot AI 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.

⚠️ Not ready to approve

The RFC includes a few documentation inaccuracies/ambiguities (repo path attribution and an internally inconsistent election tie-break snippet) that could mislead implementers.

Pull request overview

Adds a new design RFC (017) to the docs/RFCs set, describing a two-phase artifact post-processing/merging mechanism for Microsoft.Testing.Platform (MTP) runs orchestrated by dotnet test, including a proposed IArtifactPostProcessor contract, host invocation modes, and SDK election/orchestration.

Changes:

  • Introduces RFC 017 documenting artifact consolidation/post-processing for multi-module dotnet test runs under MTP.
  • Specifies the proposed extension contract (IArtifactPostProcessor), manifest/result schemas, handshake capability advertisement, and election algorithm.
  • Documents alternative orchestration primitives (--tool vs reserved switch vs specialized host) and open questions.
File summaries
File Description
docs/RFCs/017-Artifact-Post-Processing.md New RFC describing the proposed artifact post-processing contract and SDK/host orchestration flow for consolidating artifacts after multi-module dotnet test runs.

Copilot's findings

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

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment thread docs/RFCs/017-Artifact-Post-Processing.md Outdated
Comment thread docs/RFCs/017-Artifact-Post-Processing.md Outdated
Comment thread docs/RFCs/017-Artifact-Post-Processing.md Outdated
Evangelink and others added 2 commits June 19, 2026 22:29
- Note that src/Cli/dotnet/Commands/Test/MTP/ lives in the dotnet/sdk repo.
- Reword the informal 'already-ish present' Warning comment into a concrete statement.
- Fix the A.2 election tie-break: compare ProducingTestModule (a file name) against
  Path.GetFileName(ModulePath) instead of the full path, so it actually matches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
017 is already taken on main by 017-TestHost-Launcher.md (#9349), so rename
docs/RFCs/017-Artifact-Post-Processing.md -> 018-Artifact-Post-Processing.md.

Rework the design into a layered, decisive recommendation:
- Phase 1 (ship first): typed, invocation-agnostic IArtifactPostProcessor engine
  contract (experimental) + a user-facing ITool per kind (merge-trx) sharing one
  engine. Delivers user value with no SDK/protocol change (one platform-API change:
  promote ITool to public).
- Phase 2: in-memory election (from data the SDK already has via handshake + live
  FileArtifactMessages), a platform-owned internal dispatcher ITool, and merged
  artifacts returned over the existing dotnet-test pipe as FileArtifactMessage.
- Election is minimal set-cover (fewest relaunches), arch-constrained for binary
  coverage; live-channel alternative reframed to lead with reuse of the existing
  pipe; experimental gating; cross-arch notes.

Also fixes markdownlint issues and corrects codebase-accuracy points surfaced in
review (ITool is internal today; TrxReportEngine has no file-merge path yet; no
--list-tools switch; public records must avoid synthesized init accessors).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 5, 2026 09:29
@Evangelink Evangelink changed the title RFC 017: Artifact post-processing for dotnet test (MTP) RFC 018: Artifact post-processing for dotnet test (MTP) Jul 5, 2026

Copilot AI 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.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread docs/RFCs/018-Artifact-Post-Processing.md Outdated
… path wins)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 14:33

Copilot AI 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.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 0 new
  • Review effort level: Low

@Evangelink
Evangelink merged commit c66515a into main Jul 10, 2026
23 checks passed
@Evangelink
Evangelink deleted the dev/amauryleve/rfc-artifact-post-processing branch July 10, 2026 14:38

@github-actions github-actions Bot 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.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

RFC 018 Review — Artifact Post-Processing

This is an excellent, thorough RFC. The layered phasing (ship the engine + user tool first, SDK orchestration second) is a strong strategy that delivers value early and de-risks the harder integration work. The design correctly leverages MTP's existing pipe infrastructure instead of reinventing VSTest's disk-tagging plumbing.

Verdict Table

# Dimension Verdict
1 Algorithmic Correctness ✅ Clean — election algorithm is well-specified with clear edge cases
2 Public API ⚠️ Record init accessor issue — code samples use positional records; the note acknowledges it but the samples should show the compliant form (see inline)
3 IPC Contract Stability ⚠️ Handshake byte keys and exit codes need concrete value allocation (see inline)
4 Backward Compatibility ✅ Clean — graceful degradation matrix in §10 is comprehensive
5 Security ⚠️ Temp directory permissions on Linux — TOCTOU risk in manifest path (see inline)
6 Error Handling ✅ Clean — §7.9 is thorough; never-fail-the-run invariant is well-maintained
7 Performance ✅ Clean — minimal set-cover minimizes relaunches; pipe reuse avoids extra I/O
8 Cross-TFM ✅ Clean — §7.12 addresses arch constraints for binary kinds
9 Testing Strategy ✅ Clean — §9 covers unit, integration, and acceptance layers
10 Localization N/A (no user-facing strings in this RFC)
11–22 Remaining dimensions N/A (documentation-only PR, no code changes)

Summary

3 actionable items (all inline), none blocking. The RFC is well-structured, internally consistent, and addresses its stated goals. The main items to tighten before implementation begins:

  1. Show the init-free DTO form in the code samples (not just the footnote).
  2. Specify concrete byte key values for the new handshake properties.
  3. Harden the temp-directory security story for Linux.

No blocking issues found. Good to discuss and iterate.

Comment on lines +175 to +190
public sealed record InputArtifact(
string Path,
string? Kind, // null when the producer didn't declare one
string? ProducingTestModule,
string? TargetFramework,
string? Architecture,
string? ExecutionId);

public sealed record ProcessedArtifact(
string Path,
string Kind, // processor MUST tag its output
string DisplayName,
string? Description);
```

> **Public-API note (no `init`).** testfx bans `init` accessors on *new* public API. C# positional records synthesize `{ get; init; }`, so if `InputArtifact` / `ProcessedArtifact` are public they must instead be declared with get-only auto-properties and an explicit constructor (the positional form above is shorthand for readability only). Alternatively keep the DTOs `internal` and expose only the interface. This must be settled at API-review time, not discovered there.

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.

Public API / no-init (Principle 2). The RFC correctly flags the init accessor issue in the note below, but the code sample itself still uses positional record syntax which synthesizes { get; init; }. Since this is the most-copied snippet in the RFC, consider showing the actual intended form (explicit constructor + get-only properties) inline so implementers don't cargo-cult the shorthand:

public sealed class InputArtifact
{
    public InputArtifact(string path, string? kind, string? producingTestModule,
        string? targetFramework, string? architecture, string? executionId)
    {
        Path = path;
        Kind = kind;
        // ...
    }

    public string Path { get; }
    public string? Kind { get; }
    // ...
}

This avoids the "settled at API-review time, not discovered there" scenario the note warns about by making the RFC itself the reference.

- **Group has only 1 input** -> not planned (the `>= 2` filter). Original listed as today; no relaunch.
- **Two processors in the same app advertise the same Kind** -> first-wins inside the dispatcher with a warning surfaced to the SDK. (Packaging bug for the user to fix.)
- **Election succeeds but the relaunch fails** -> warning; originals listed; run exit code unchanged.
- **Same file matches both a Kind and a fallback extension** -> Kind wins; an artifact is never double-routed.

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.

Edge case gap. What happens when two different extensions in two different apps each advertise the same Kind with incompatible merge logic? The "two processors in the same app" case is covered (first-wins + warning), but the cross-app case is only addressed implicitly by election (one app wins). Worth stating explicitly that the losing app's processor is never invoked, so incompatibility is sidestepped by election rather than detected.

Comment on lines +243 to +247
Each test app advertises the kinds (and legacy file extensions) it can post-process so the orchestrator can elect. `HandshakeMessage` is already a `Dictionary<byte, string>` whose values are frequently semicolon-joined lists (e.g. `SupportedProtocolVersions` = `"1.0.0;1.1.0;1.2.0;1.3.0"`), so this is additive and cheap:

```text
SupportedPostProcessorKinds: "microsoft.testing.trx;microsoft.codecoverage"
SupportedPostProcessorExtensionsLegacy: ".trx;.coverage"

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.

IPC backward compatibility. The handshake property keys are shown as strings here but HandshakeMessage uses byte keys (Dictionary<byte, string>). The RFC should specify the actual byte values (or at least note they need allocation from the HandshakeMessagePropertyNames enum) to avoid collisions with existing or concurrently-planned handshake properties.

- [ ] Implementation
- [ ] Shipped

> **Renumber note.** This design was originally drafted as "RFC 017" in [microsoft/testfx#9187](https://github.com/microsoft/testfx/pull/9187). `017` has since been taken by `017-TestHost-Launcher.md` (merged in [#9349](https://github.com/microsoft/testfx/pull/9349)), so this document is renumbered to **018**.

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.

Nit: The renumber note references this PR (#9187) as the original source but says 017 was "taken by" #9349. If #9349 is already merged, this is fine. If it hasn't merged yet, there could be a race on the numbering. Worth confirming the 017 slot is definitively claimed before this merges.


### 7.10 Security considerations

- The manifest is created by the orchestrator in a process-private temp directory (`Path.GetTempPath()/dotnet-test-postproc-<guid>/manifest.json`), cleaned up after use.

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.

Security (Dimension 17). The manifest is written to Path.GetTempPath() which is world-readable on some Linux configurations. A local attacker could race (TOCTOU) to replace the manifest between write and read, injecting arbitrary file paths into the merge inputs. Since the invariant "a local attacker who can tamper with the manifest can equally tamper with the test binaries" holds on Windows (user-private %TEMP%) but is weaker on Linux (/tmp shared), consider:

  1. Explicitly stating the temp directory should be created with 0700 permissions (or using Directory.CreateTempSubdirectory on .NET 7+ which does this).
  2. Alternatively, writing the manifest under the --results-directory / TestResults folder which is already user-owned.

Comment on lines +400 to +402
**Transitional result path: result JSON.** If the pipe-composition task in §7.6 is deferred, the dispatcher writes a result JSON (path supplied in the manifest) and the SDK performs the swap. This keeps the same typed contract and the same manifest; only the *return channel* changes. Keeping the manifest/result schema `schemaVersion`-versioned means the return channel can switch from files to the pipe without touching `IArtifactPostProcessor`.

Dispatcher exit codes (final values to be finalized so they don't overlap existing MTP `ExitCode` values — see §12 Q11):

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.

IPC contract stability (Principle 6). The exit codes are (tbd) — please add a note that these must be allocated from a range that doesn't conflict with existing ExitCodes in Microsoft.Testing.Platform.Helpers.ExitCodes (currently 0–10). Open question 11 mentions this, but it would be good to have the constraint documented here where the codes are listed, so an implementer doesn't accidentally pick overlapping values.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Overview

The build failed with 4 CA1416 errors in test/UnitTests/Microsoft.Testing.Platform.UnitTests/Logging/FileLoggerTests.cs. This PR only modifies docs/RFCs/018-Artifact-Post-Processing.md (a documentation file) — these errors are pre-existing on the base branch, not introduced by this PR.

Root Cause

ITask.RunLongRunning() is annotated with [UnsupportedOSPlatform("browser")] (in src/Platform/Microsoft.Testing.Platform/Helpers/System/ITask.cs:16). Two inner test helper classes in FileLoggerTests.cs delegate to _inner.RunLongRunning(...) without suppressing CA1416:

Class Line Error
RecordingTask 465 CA1416: RunLongRunning unsupported on 'browser'
NeverCompletingTask 490 CA1416: RunLongRunning unsupported on 'browser'

The errors appear twice (2 TFMs), totaling 4 errors.

Suggested Fix (on main)

Add #pragma warning disable CA1416 / #pragma warning restore CA1416 around the RunLongRunning calls at lines 464–465 and 489–490 of FileLoggerTests.cs, matching the pattern already used at lines 301–303 and 313–315 in the same file.

Impact on This PR

This PR is documentation-only and did not introduce the failure. The fix should be applied separately on main.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · 137.6 AIC · ⌖ 5.69 AIC · ⊞ 7.3K · [◷]( · )

Sign up for free to 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