feat: cross-process test report aggregation — merged HTML report + single GitHub summary#6415
Conversation
Multiple test projects run as separate processes, producing N summary blocks and N HTML reports with no way to merge them. Each process now persists a JSON sidecar of its report data; sibling processes cooperatively rewrite one marker-delimited summary block and one merged HTML report under a file lock (last finisher leaves the complete aggregate). On by default where a shared directory is resolvable (GitHub Actions or TUNIT_AGGREGATE_DIR); TUNIT_AGGREGATE_REPORTS=off to disable, =defer for multi-step pipelines with the new tunit-report dotnet tool (TUnit.Reporting.Tool) merging at the end.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 medium |
| CodeStyle | 52 minor |
🟢 Metrics 364 complexity
Metric Results Complexity 364
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Full artifact-based workflow: per-job sidecar upload (defer mode), download-artifact with merge-multiple onto one runner, tunit-report merge for the combined HTML + single summary block; plus gh run download for combining separate workflow runs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b71373e417
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@claude review |
|
Claude finished @thomhurst's task in 1m 58s —— View job Claude finished @thomhurst's taskTodo list
ReviewOverall this is a well-scoped design: no daemon/coordination protocol, atomic writes, and a genuinely simple last-writer-wins model over a monotonically growing input. The lock/splice/merge separation is clean and the "torn marker → append, never splice" rule in Confirmed — both Codex findings are real bugs:
Other observations (no action required, noted for awareness):
I did not run the build/test suite (no tool access to |
Single lock cycle: HtmlReporter now drives sidecar write, merged HTML and the summary region in one env-parse/lock/scan pass, calling back into GitHubReporter for the region write; previously both reporters independently parsed env, locked and re-read every sidecar. Sidecar JSON serializes once to UTF-8 bytes for both copies. Shared AtomicFile helper replaces two swap implementations. ReportTestResult is a record so merging uses 'with' instead of a drift-prone manual copy. Sidecar reader collapses eight loops into a generic ReadArray. Tool links real constants via new constants-only partials (TUnitActivitySource, ClassTimelineAttribute) instead of value-mirroring shims; merged report name and summary cap live in linked files. Duration formatting and the failures-per-group cap are shared between the per-suite and aggregated summaries.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5b1043510d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Code review (re-review after
|
Review findings (Codex + Claude): GetInt32() on a non-Int32-representable schemaVersion threw FormatException past the catch(JsonException) guard, letting one corrupt sidecar kill the whole merge; the tool's per-file read was unguarded, so a locked or unreadable sidecar exited the tool with code 2 instead of counting as skipped. schemaVersion and the int helpers now use TryGetInt32; the tool skips unreadable files like the engine does.
Code review (re-review after
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f8e61a0d71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Sidecar reader now treats wrong-shaped nested values (groups:[1] throws InvalidOperationException, not JsonException) and non-finite numeric literals (1e9999 parses to Infinity) as malformed input instead of letting them escape or poison duration math. Merged reports re-tag tunit.test.class span tags when class names are disambiguated, so per-class timelines survive multi-TFM merges. Source-control metadata and link templates are kept only when every suite agrees — merging sidecars from different commits no longer links tests to the wrong revision. SpanData is a record to support the retag via 'with'.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 00bd7bb18b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Code review (re-review of
|
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.59.0 to 1.61.15. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.61.15 <!-- Release notes generated using configuration in .github/release.yml at v1.61.15 --> ## What's Changed ### Other Changes * feat: Allow disabling the automatic html report upload to upload-artifacts@v7 by @Crashdummyy in thomhurst/TUnit#6447 * fix: stop retrying artifact upload on non-retryable status codes by @thomhurst in thomhurst/TUnit#6449 ### Dependencies * chore(deps): update tunit to 1.61.0 by @thomhurst in thomhurst/TUnit#6437 * chore(deps): update verify to 31.24.3 by @thomhurst in thomhurst/TUnit#6438 * chore(deps): update dependency opentelemetry.instrumentation.aspnetcore to 1.17.0 by @thomhurst in thomhurst/TUnit#6440 * chore(deps): update verify to 31.25.0 by @thomhurst in thomhurst/TUnit#6442 * chore(deps): update dependency docusaurus-plugin-llms to ^0.5.0 by @thomhurst in thomhurst/TUnit#6443 * chore(deps): update opentelemetry to 1.17.0 by @thomhurst in thomhurst/TUnit#6441 ## New Contributors * @Crashdummyy made their first contribution in thomhurst/TUnit#6447 **Full Changelog**: thomhurst/TUnit@v1.61.0...v1.61.15 ## 1.61.0 <!-- Release notes generated using configuration in .github/release.yml at v1.61.0 --> ## What's Changed ### Other Changes * docs: revamp README with feature showcase and auto-updating benchmarks by @thomhurst in thomhurst/TUnit#6430 * docs: align docs homepage messaging with revamped README by @thomhurst in thomhurst/TUnit#6431 * fix(engine): emit ECMA-335 metadata-format type names in TestMethodIdentifierProperty by @thomhurst in thomhurst/TUnit#6433 * fix(benchmarks): make SetupTeardownTests JSON tests Native AOT compatible by @thomhurst in thomhurst/TUnit#6434 * Organize repository into conventional .NET layout by @thomhurst in thomhurst/TUnit#6435 ### Dependencies * chore(deps): update dependency polyfill to v11 by @thomhurst in thomhurst/TUnit#6418 * chore(deps): update dependency tunit.aspire to 1.6* by @thomhurst in thomhurst/TUnit#6419 * chore(deps): update tunit to 1.6* by @thomhurst in thomhurst/TUnit#6420 * chore(deps): update dependency polyfill to 11.0.1 by @thomhurst in thomhurst/TUnit#6421 * chore(deps): update dependency polyfill to 11.0.1 by @thomhurst in thomhurst/TUnit#6422 * chore(deps): update verify to 31.24.2 by @thomhurst in thomhurst/TUnit#6424 * chore(deps): update microsoft.build to 18.8.2 by @thomhurst in thomhurst/TUnit#6426 * chore(deps): update dependency microsoft.net.stringtools to 18.8.2 by @thomhurst in thomhurst/TUnit#6425 * chore(deps): update actions/setup-dotnet action to v6 by @thomhurst in thomhurst/TUnit#6427 * chore(deps): bump websocket-driver from 0.7.4 to 0.7.5 in /docs by @dependabot[bot] in thomhurst/TUnit#6429 * chore(deps): update opentelemetry to 1.17.0 by @thomhurst in thomhurst/TUnit#6432 **Full Changelog**: thomhurst/TUnit@v1.60.0...v1.61.0 ## 1.60.0 <!-- Release notes generated using configuration in .github/release.yml at v1.60.0 --> ## What's Changed ### Other Changes * feat: cross-process test report aggregation — merged HTML report + single GitHub summary by @thomhurst in thomhurst/TUnit#6415 ### Dependencies * chore(deps): update tunit to 1.59.0 by @thomhurst in thomhurst/TUnit#6398 * chore(deps): update verify to 31.24.1 by @thomhurst in thomhurst/TUnit#6399 * chore(deps): update microsoft.testing to 2.3.2 by @thomhurst in thomhurst/TUnit#6401 * chore(deps): update mstest to 4.3.2 by @thomhurst in thomhurst/TUnit#6402 * chore(deps): update actions/setup-node action to v7 by @thomhurst in thomhurst/TUnit#6403 * chore(deps): update dependency microsoft.net.test.sdk to 18.8.0 by @thomhurst in thomhurst/TUnit#6404 * chore(deps): update dependency microsoft.net.test.sdk to 18.8.1 by @thomhurst in thomhurst/TUnit#6405 * chore(deps): update dependency fsharp.core to 10.1.302 by @thomhurst in thomhurst/TUnit#6406 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.302 by @thomhurst in thomhurst/TUnit#6407 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.10 by @thomhurst in thomhurst/TUnit#6410 * chore(deps): update dependency dotnet-sdk to v10.0.302 by @thomhurst in thomhurst/TUnit#6409 * chore(deps): update dependency system.commandline to 2.0.10 by @thomhurst in thomhurst/TUnit#6412 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.302 by @thomhurst in thomhurst/TUnit#6411 * chore(deps): update microsoft.aspnetcore to 10.0.10 by @thomhurst in thomhurst/TUnit#6413 * chore(deps): update microsoft.extensions to 10.0.10 by @thomhurst in thomhurst/TUnit#6414 * chore(deps): update microsoft.extensions to 10.8.0 by @thomhurst in thomhurst/TUnit#6416 * chore(deps): update dependency polyfill to v11 by @thomhurst in thomhurst/TUnit#6417 **Full Changelog**: thomhurst/TUnit@v1.59.0...v1.60.0 Commits viewable in [compare view](thomhurst/TUnit@v1.59.0...v1.61.15). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.59.0 to 1.61.15. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.61.15 <!-- Release notes generated using configuration in .github/release.yml at v1.61.15 --> ## What's Changed ### Other Changes * feat: Allow disabling the automatic html report upload to upload-artifacts@v7 by @Crashdummyy in thomhurst/TUnit#6447 * fix: stop retrying artifact upload on non-retryable status codes by @thomhurst in thomhurst/TUnit#6449 ### Dependencies * chore(deps): update tunit to 1.61.0 by @thomhurst in thomhurst/TUnit#6437 * chore(deps): update verify to 31.24.3 by @thomhurst in thomhurst/TUnit#6438 * chore(deps): update dependency opentelemetry.instrumentation.aspnetcore to 1.17.0 by @thomhurst in thomhurst/TUnit#6440 * chore(deps): update verify to 31.25.0 by @thomhurst in thomhurst/TUnit#6442 * chore(deps): update dependency docusaurus-plugin-llms to ^0.5.0 by @thomhurst in thomhurst/TUnit#6443 * chore(deps): update opentelemetry to 1.17.0 by @thomhurst in thomhurst/TUnit#6441 ## New Contributors * @Crashdummyy made their first contribution in thomhurst/TUnit#6447 **Full Changelog**: thomhurst/TUnit@v1.61.0...v1.61.15 ## 1.61.0 <!-- Release notes generated using configuration in .github/release.yml at v1.61.0 --> ## What's Changed ### Other Changes * docs: revamp README with feature showcase and auto-updating benchmarks by @thomhurst in thomhurst/TUnit#6430 * docs: align docs homepage messaging with revamped README by @thomhurst in thomhurst/TUnit#6431 * fix(engine): emit ECMA-335 metadata-format type names in TestMethodIdentifierProperty by @thomhurst in thomhurst/TUnit#6433 * fix(benchmarks): make SetupTeardownTests JSON tests Native AOT compatible by @thomhurst in thomhurst/TUnit#6434 * Organize repository into conventional .NET layout by @thomhurst in thomhurst/TUnit#6435 ### Dependencies * chore(deps): update dependency polyfill to v11 by @thomhurst in thomhurst/TUnit#6418 * chore(deps): update dependency tunit.aspire to 1.6* by @thomhurst in thomhurst/TUnit#6419 * chore(deps): update tunit to 1.6* by @thomhurst in thomhurst/TUnit#6420 * chore(deps): update dependency polyfill to 11.0.1 by @thomhurst in thomhurst/TUnit#6421 * chore(deps): update dependency polyfill to 11.0.1 by @thomhurst in thomhurst/TUnit#6422 * chore(deps): update verify to 31.24.2 by @thomhurst in thomhurst/TUnit#6424 * chore(deps): update microsoft.build to 18.8.2 by @thomhurst in thomhurst/TUnit#6426 * chore(deps): update dependency microsoft.net.stringtools to 18.8.2 by @thomhurst in thomhurst/TUnit#6425 * chore(deps): update actions/setup-dotnet action to v6 by @thomhurst in thomhurst/TUnit#6427 * chore(deps): bump websocket-driver from 0.7.4 to 0.7.5 in /docs by @dependabot[bot] in thomhurst/TUnit#6429 * chore(deps): update opentelemetry to 1.17.0 by @thomhurst in thomhurst/TUnit#6432 **Full Changelog**: thomhurst/TUnit@v1.60.0...v1.61.0 ## 1.60.0 <!-- Release notes generated using configuration in .github/release.yml at v1.60.0 --> ## What's Changed ### Other Changes * feat: cross-process test report aggregation — merged HTML report + single GitHub summary by @thomhurst in thomhurst/TUnit#6415 ### Dependencies * chore(deps): update tunit to 1.59.0 by @thomhurst in thomhurst/TUnit#6398 * chore(deps): update verify to 31.24.1 by @thomhurst in thomhurst/TUnit#6399 * chore(deps): update microsoft.testing to 2.3.2 by @thomhurst in thomhurst/TUnit#6401 * chore(deps): update mstest to 4.3.2 by @thomhurst in thomhurst/TUnit#6402 * chore(deps): update actions/setup-node action to v7 by @thomhurst in thomhurst/TUnit#6403 * chore(deps): update dependency microsoft.net.test.sdk to 18.8.0 by @thomhurst in thomhurst/TUnit#6404 * chore(deps): update dependency microsoft.net.test.sdk to 18.8.1 by @thomhurst in thomhurst/TUnit#6405 * chore(deps): update dependency fsharp.core to 10.1.302 by @thomhurst in thomhurst/TUnit#6406 * chore(deps): update dependency microsoft.templateengine.authoring.cli to v10.0.302 by @thomhurst in thomhurst/TUnit#6407 * chore(deps): update dependency microsoft.entityframeworkcore to 10.0.10 by @thomhurst in thomhurst/TUnit#6410 * chore(deps): update dependency dotnet-sdk to v10.0.302 by @thomhurst in thomhurst/TUnit#6409 * chore(deps): update dependency system.commandline to 2.0.10 by @thomhurst in thomhurst/TUnit#6412 * chore(deps): update dependency microsoft.templateengine.authoring.templateverifier to 10.0.302 by @thomhurst in thomhurst/TUnit#6411 * chore(deps): update microsoft.aspnetcore to 10.0.10 by @thomhurst in thomhurst/TUnit#6413 * chore(deps): update microsoft.extensions to 10.0.10 by @thomhurst in thomhurst/TUnit#6414 * chore(deps): update microsoft.extensions to 10.8.0 by @thomhurst in thomhurst/TUnit#6416 * chore(deps): update dependency polyfill to v11 by @thomhurst in thomhurst/TUnit#6417 **Full Changelog**: thomhurst/TUnit@v1.59.0...v1.60.0 Commits viewable in [compare view](thomhurst/TUnit@v1.59.0...v1.61.15). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Closes #4522. Addresses discussion #6408.
Problem
Running many test projects (solution
dotnet test, per-microservicedotnet run) spawns one process per suite. Each process emits its own HTML report and its own GitHub step summary block — N projects means N blocks and N artifacts, and failures get buried. Processes can't see each other, so nothing merges.Key idea: nobody needs to know who finishes last
GITHUB_STEP_SUMMARYis a per-step file that stays freely rewritable until the step ends (the runner reads it once, at step completion). So every finishing process persists its results, takes a cross-process file lock, and re-renders one merged block from all results present so far — the true last finisher naturally leaves the complete aggregate. Last-writer-wins over monotonically-growing input needs no coordination protocol, no daemon, no "am I last?" answer.What's included
1. JSON sidecar (foundation, on by default)
Every HTML report now gets a machine-readable sibling:
{asm}-{os}-{tfm}.tunit-report.json— a faithful, versioned serialization ofReportData(hand-writtenUtf8JsonWriter/JsonDocument, AOT-safe, forward-compatible reader). Opt out:TUNIT_DISABLE_JSON_REPORT. This is the substrate everything else consumes.2. Cooperative in-engine aggregation (ON by default where a shared dir is resolvable — GitHub Actions, or explicit
TUNIT_AGGREGATE_DIR; escape hatchTUNIT_AGGREGATE_REPORTS=off)For the single-step/many-processes layout (the #4522 scenario). Each process drops its sidecar into a shared dir (auto-derived on GHA from
RUNNER_TEMP+ run/attempt/job;TUNIT_AGGREGATE_DIRelsewhere), then under a lock regenerates:echo >>lines) is never touched. Conservative marker pairing + temp-file-and-File.Replacewrites mean even a killed process can't corrupt the file.3.
tunit-reportdotnet tool (new packageTUnit.Reporting.Tool)For layouts cooperative merging can't cover — one step per project, or matrix jobs across machines (steps/jobs each get their own summary file, so a later step can't rewrite an earlier block):
The tool compiles the engine's own serializer/merger/renderer sources via
<Compile Include>links (no MTP dependency, tiny payload), so the tool always reads exactly what the engine writes.4. Docs — new guide
docs/guides/report-aggregation+ cross-links from the HTML-report and CI/CD-reporting pages.Verified
TUnit.UnitTestsunder net8.0 + net10.0 with simulated GHA env sharing one summary file — got exactly one marked block ("2 suites", 482 tests, per-runtime labels), pre-existing foreign summary content preserved, merged HTML rendered both suites.tunit-report mergeon crafted sidecars: merged HTML + summary with failure grouping, flaky section, source links, artifact links.PackAsTool); added toGetPackageProjectsModuleandTUnit.slnx.Design notes
defer(documented). Plain local runs (no resolvable shared dir) silently no-op.RunEngineTestsModulesetsofffor its child processes — hundreds of short-lived, deliberately-failing suites would otherwise re-merge the job report on every exit.SourceLinkstemplates weren't persisted in the sidecar (merged reports would have lost clickable source links). Sidecar round-trip now covers them.TUNIT_DISABLE_HTML_REPORTERdisables sidecars and therefore aggregation.ReportData.TotalDurationMscame out as ~4ms for a 2.6s run when the session span is present — the merged outputs sidestep it by deriving wall-clock from test timestamps, but the per-suite HTML header may be affected on main too.