Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

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

Include exception message in JUnit failure/error bodies - #10286

Merged
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon
Jul 28, 2026
Merged

Include exception message in JUnit failure/error bodies#10286
Amaury Levé (Evangelink) merged 3 commits into
mainfrom
dev/amauryleve/bookish-bassoon

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes#10269

Include the exception message in <failure>/<error> bodies

Today the JUnit report writes only Exception.StackTrace into the element body, keeping the message in the message attribute. Consumers that render the body rather than the attribute — GitLab CI and CircleCI most notably — therefore show a bare stack trace with no indication of why the test failed. This is especially damaging for fluent assertion libraries, where the stack trace is mostly framework frames while the assertion message carries the actual diagnosis.

This is a fidelity gap, not a stylistic choice. Canonical JUnit bodies come from Java's Throwable.printStackTrace(), whose first line is com.example.AssertionError: expected:<1> but was:<2> — the message is inherently part of the body. .NET's Exception.StackTrace omits that header (only ToString() includes it), which is exactly why our output looked message-less next to a genuine Surefire report. GitLab isn't being quirky; it's reading the body assuming Java semantics.

The body now mirrors printStackTrace():

<failuremessage="Expected the operation not to throw, but it threw ..."type="Xunit.Sdk.XunitException">
Xunit.Sdk.XunitException: Expected the operation not to throw, but it threw ...
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
...
</failure>

Each part degrades gracefully: a missing exception type drops the header prefix, a missing message drops the : separator, and a missing stack trace yields a header-only body.

Why change the default instead of adding an option

The issue proposed a --report-junit-failure-body-format option. We went with changing the behavior outright:

  • Microsoft.Testing.Extensions.JUnitReport has not shipped a stable version (only 1.0.0-alpha.* on NuGet), so there is no back-compat cost to fixing the default now. This is the one moment where it's free.
  • A CLI option is a permanent contract, and it would only let users opt into less diagnostic information. Adding one later is non-breaking; removing one is not.
  • Both schemas allow it: neither the Ant/windyroad JUnit.xsd nor Surefire's surefire-test-report.xsd constrains the body — it's free-form text (the Ant schema's "e.g., a stack trace" is illustrative, not normative).

The message and type attributes are unchanged, so consumers reading them directly are unaffected. The resulting duplication between attribute and body is exactly what every Maven/Surefire report already exhibits, so consumers rendering both have long handled it.

Unlike spekt's FailureBodyFormat=Verbose, we deliberately do not fold standard output into the failure body — it's already emitted as <system-out> per test case, and duplicating it would bloat reports for noisier suites.

Always emit the type attribute

The Ant/windyroad JUnit.xsd marks type as use="required" on <failure>/<error> (Surefire relaxes it to optional). MTP only supplies an exception type when the state property carried an actual Exception — frameworks that report a failure through Explanation alone leave it null, so the attribute was previously omitted entirely in that case.

type is now always written, falling back to the element name (failure / error) when no exception type is available. That keeps the document valid under the stricter schema without inventing a bogus exception type name.

Testing

  • New unit tests (JUnitReportFailureBodyTests) covering every combination of present/absent exception type, message and stack trace, plus the type fallback for both <failure> and <error>, asserted against real generated XML.
  • Updated acceptance snapshot in Microsoft.Testing.Platform.Acceptance.IntegrationTests/JUnitReportTests.cs — the <failure>/<error> elements are no longer self-closing.
  • Verified locally: 795 unit tests pass, 21 MTP JUnit acceptance tests pass across all TFMs, and the MSTest JUnit retry acceptance test (which produces real stack traces) passes.

Docs

docs/RFCs/016-JUnit-Report.md updated with a "Failure and error body format" section, a "The type attribute" section, and the revised outcome-mapping table.

.NET's Exception.StackTrace omits the leading ype: message header that
Java's Throwable.printStackTrace() (and Exception.ToString()) include, so
writing the stack trace alone into the <failure>/<error> body dropped the
single most useful piece of diagnostic information. Consumers that render
the body rather than the message attribute -- GitLab CI and CircleCI most
notably -- showed only a stack trace with no indication of why a test failed.
The body now mirrors printStackTrace() shape, degrading gracefully when the
exception type, message or stack trace is absent. The message and ype
attributes are unchanged, so consumers reading them directly are unaffected.
Also always emit the ype attribute. The Ant/windyroad JUnit.xsd marks it
use="required" (Surefire relaxes it to optional), but MTP only supplies an
exception type when the state property carried an actual Exception --
frameworks reporting via Explanation alone leave it null. Fall back to the
element name so the document stays valid under the stricter schema.
Fixes#10269
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 08:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates JUnit reports to provide complete failure diagnostics in body-rendering CI systems.

Changes:

  • Adds exception type/message headers to failure and error bodies.
  • Always emits the required type attribute.
  • Adds unit, acceptance, API tracking, and RFC updates.
Show a summary per file
FileDescription
JUnitXmlWriter.csBuilds diagnostic failure/error bodies and type fallbacks.
InternalAPI.Unshipped.txtTracks the new internal helper.
JUnitReportFailureBodyTests.csTests body composition and XML output.
JUnitReportTests.csUpdates acceptance snapshots.
016-JUnit-Report.mdDocuments the revised format.

Review details

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

Comment threaddocs/RFCs/016-JUnit-Report.md Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Build Failure Analysis

Root cause: All 6 build legs fail with the same error in src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csprojnot in any file changed by this PR:

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'.
Run `msbuild /t:UpdateXlf` to update .xlf files

Why this PR is affected: The most recent commit to src/Analyzers/MSTest.Analyzers/Resources.resx on main was #10248 (2026-07-27), which added new analyzer resource strings but did not regenerate the .xlf files. This PR was branched after or rebased onto that commit, so it inherits the broken base.

This PR’s changes are unrelated — all modifications are in src/Platform/Microsoft.Testing.Extensions.JUnitReport/ and its tests.

Fix

Regenerate the XLF files for MSTest.Analyzers by running:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

Then commit the updated xlf/*.xlf files. This can be done either:

  • On main directly (fixing the base for all open PRs), or
  • In this PR branch if you want to unblock it immediately.

i️ Per the repo guidelines, .xlf files must never be edited by hand — always regenerate via UpdateXlf.

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 34 AIC · ⌖ 5.29 AIC · ⊞ 8K · [◷]( · )

The outcome-mapping table and the XML skeleton both rendered <failure> and
<error> as self-closing while the table also claimed a body, which is an
impossible XML shape. Now that these elements always carry a body, show them
with explicit start/end tags in both places, and correct the <skipped> row:
the writer emits only the message attribute, never a body.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

This comment has been minimized.

WriteXmlAsync_ErroredTestWithoutException_FallsBackToElementNameForTypeAttribute
verified the element name and the type fallback but left the message attribute
and the body content unasserted, so a regression in either would have gone
undetected by this test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a565c557-8c36-4279-ab7b-41311abcd836
CopilotAI review requested due to automatic review settings July 28, 2026 12:22

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10286

All 9 new test methods are in JUnitReportFailureBodyTests (new file). The diff in JUnitReportTests.cs only touched helper-method bodies, not [TestMethod]-decorated methods, so those are excluded.

GradeTestMutationNotesHow to improve
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithExceptionTypeMessageAndStackTrace_
MirrorsPrintStackTraceShape
4/4 killedExact string equality kills all meaningful mutations: type prefix, ": " separator, "\n" join, stack trace.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithNoExceptionDetails_
ReturnsNull
1/1 killedNull return on all-null input; Assert.IsNull catches any non-null mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutErrorMessage_
OmitsTheSeparator
2/2 killedExact string confirms no ": " appears and stack trace is still appended with "\n".
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutExceptionType_
OmitsTheTypePrefix
2/2 killedExact string kills the early-return guard mutation and the "\n" join mutation.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithoutStackTrace_
StillContainsTypeAndMessage
2/2 killedExact string kills removing type prefix and removing ": " separator in the no-stack-trace path.
A (90–100)new JUnitReportFailureBodyTests.
BuildFailureBody_
WithStackTraceOnly_
ReturnsStackTraceUnchanged
2/2 killedExact string kills both the "return null" mutation and any wrapper/prefix mutation on the early-return path.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
ErroredTestWithoutException_
FallsBackToElementNameForTypeAttribute
4/4 killedAsserts element name, type fallback, message attribute, and body — all four attributes of the errored outcome.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTest_
WritesMessageIntoTheFailureBodyAndKeepsAttributes
4/4 killedAsserts element name, both XML attributes, and the full body text — comprehensive for the failure element.
A (90–100)new JUnitReportFailureBodyTests.
WriteXmlAsync_
FailedTestWithoutException_
FallsBackToElementNameForTypeAttribute
2/2 killedAsserts type-attribute fallback to element name and body-only content when no exception type is present.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 91.9 AIC · ⌖ 4.71 AIC · ⊞ 10.3K · [◷]( · )

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@Evangelink
Amaury Levé (Evangelink) merged commit e131c91 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/bookish-bassoon branch July 28, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option to include exception messages in Microsoft.Testing.Extensions.JUnitReport failure bodies for GitLab compatibility.

3 participants

@Evangelink@0101