Skip to content

fix: truncate large stdout/stderr in HTML report to prevent JSON serialization failure#5485

Merged
thomhurst merged 2 commits intomainfrom
fix/html-report-large-output-truncation
Apr 10, 2026
Merged

fix: truncate large stdout/stderr in HTML report to prevent JSON serialization failure#5485
thomhurst merged 2 commits intomainfrom
fix/html-report-large-output-truncation

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

Fixes #5479

System.Text.Json's Utf8JsonWriter limits a single string token to int.MaxValue / 6 ≈ 357,913,941 characters (worst-case UTF-16 → \uXXXX escaping). Test suites with excessive per-test logging (~380MB+) hit this limit during HTML report serialization, producing:

\
Warning: HTML report generation failed: The JSON value of length 397639975 is too large and not supported.
\\

Fix

In ExtractTestResult, stdout and stderr are now passed through TruncateOutput() which caps each at 1 MB (1,048,576 chars). If a value is truncated, a note with the original character count is appended:

\
[... output truncated — 397,639,975 total characters]
\\

This ensures the HTML report always generates successfully, even for test suites with extremely verbose output.

Files changed

  • TUnit.Engine/Reporters/Html/HtmlReporter.cs

…alization failure

System.Text.Json's Utf8JsonWriter limits a single string token to
int.MaxValue / 6 characters. Test suites with excessive logging
(~380MB+ output per test) would hit this limit during HTML report
generation, resulting in:

  Warning: HTML report generation failed: The JSON value of length
  397639975 is too large and not supported.

Fix: cap stdout and stderr at 1MB each in ExtractTestResult.
If truncated, a message noting the original character count is appended.

Fixes #5479"
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

Overall: Clean, targeted fix for a real user-facing bug. The approach is correct and the implementation is clear.


What the PR does

Caps / at 1 MB each in before they reach , preventing the System.Text.Json hard limit (~357M chars) from silently dropping the entire HTML report.


Observations

1. Missing unit tests for TruncateOutput

The helper is pure and easy to unit test. Without a test, future refactors could silently regress the truncation message format or the boundary condition (value.Length == MaxOutputLength should pass through unmodified).

A minimal test would cover:

  • nullnull
  • String at exactly the limit → returned as-is (no truncation note appended)
  • String one char over the limit → truncated with the note

2. String is allocated twice on the truncation path

return value[..MaxOutputLength] + $"\n[... output truncated — {value.Length:N0} total characters]";

value[..MaxOutputLength] materialises a new 1 MB string, then the interpolation concatenates it into a second string. Since this path is only hit for pathologically large outputs this is not a hot-path concern, but string.Concat or string.Create would express the intent more efficiently:

return string.Concat(value.AsSpan(0, MaxOutputLength),
    $"\n[... output truncated — {value.Length:N0} total characters]");

This still allocates once (the final string) rather than twice.

3. Hardcoded 1 MB limit

1 MB is a very reasonable default, but there is no escape hatch for test suites that intentionally produce dense structured output (e.g. binary-encoded logs) and want more. A possible future improvement: respect an environment variable like TUNIT_HTML_MAX_OUTPUT_LENGTH. Not required for this fix, just something to keep in mind.

4. Surrogate-pair boundary (theoretical)

value[..MaxOutputLength] slices at a UTF-16 char boundary, not a Unicode scalar boundary. If the 1,048,576th char is the high surrogate of a supplementary character, the truncated string ends with an unpaired surrogate — which is technically invalid UTF-16. Utf8JsonWriter will throw on it. In practice this is astronomically unlikely for test output, but char.IsHighSurrogate(value[MaxOutputLength - 1]) → back off by 1 would make the code provably correct.

5. Placement of MaxOutputLength constant

The constant is declared after the method that uses it, sandwiched between two methods. C# allows this, but grouping constants at the top of the class (or at least before the first method) is the prevailing convention in this file and would make it easier to discover if someone wants to adjust the value.


Summary

Correctness Fixes the reported crash
Safety Null-safe, handles edge cases
Missing Unit tests for TruncateOutput
Minor Double allocation on truncation path; theoretical surrogate split
Non-blocking Hardcoded limit, constant placement

The fix does what it says and the PR description is excellent. Addressing the missing tests would make this production-ready with more confidence.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

Overall: Clean, targeted fix for a real user-facing bug. The approach is correct and the implementation is clear.


What the PR does

Caps stdout/stderr at 1 MB each in ExtractTestResult before they reach Utf8JsonWriter, preventing the System.Text.Json hard limit (~357M chars) from silently dropping the entire HTML report.


Observations

1. Missing unit tests for TruncateOutput

The helper is pure and easy to unit test. Without a test, future refactors could silently regress the truncation message format or the boundary condition (value.Length == MaxOutputLength should pass through unmodified).

A minimal test would cover:

  • null returns null
  • String at exactly the limit is returned as-is (no truncation note appended)
  • String one char over the limit is truncated with the note

2. String is allocated twice on the truncation path

return value[..MaxOutputLength] + $"\n[... output truncated - {value.Length:N0} total characters]";

value[..MaxOutputLength] materialises a new 1 MB string, then the interpolation concatenates it into a second string. Since this path is only hit for pathologically large outputs this is not a hot-path concern, but using string.Concat with a span overload would allocate once rather than twice:

return string.Concat(value.AsSpan(0, MaxOutputLength),
    $"\n[... output truncated - {value.Length:N0} total characters]");

3. Hardcoded 1 MB limit

1 MB is a very reasonable default, but there is no escape hatch for test suites that intentionally produce dense structured output and want more. A possible future improvement: respect an environment variable like TUNIT_HTML_MAX_OUTPUT_LENGTH. Not required for this fix, just worth noting.

4. Surrogate-pair boundary (theoretical)

value[..MaxOutputLength] slices at a UTF-16 char boundary, not a Unicode scalar boundary. If the 1,048,576th char is the high surrogate of a supplementary character, the truncated string ends with an unpaired surrogate - technically invalid UTF-16, and Utf8JsonWriter may throw on it. In practice this is extremely unlikely for test output, but checking char.IsHighSurrogate(value[MaxOutputLength - 1]) and backing off by 1 would make the code provably correct.

5. Placement of MaxOutputLength constant

The constant is declared after the method that uses it, sandwiched between two methods. Grouping it at the top of the class (or at least before the first method) would make it easier to discover if someone wants to adjust the value.


Summary

Aspect Assessment
Correctness Fixes the reported crash
Safety Null-safe, handles edge cases
Missing Unit tests for TruncateOutput
Minor Double allocation on truncation path; theoretical surrogate split
Non-blocking Hardcoded limit, constant placement

The fix does what it says and the PR description is excellent. Addressing the missing tests would make this production-ready with more confidence.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 10, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 12 complexity

Metric Results
Complexity 12

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

…lloc, add tests

- Move MaxOutputLength const to top of class (before fields)
- Make TruncateOutput internal so TUnit.UnitTests can access it
- Back off cut point by 1 if it lands on a high surrogate to avoid
  producing invalid UTF-16 that Utf8JsonWriter would reject
- Use StringBuilder.Append(str, start, count) to avoid materialising
  the 1MB intermediate string from the slice (works on netstandard2.0)
- Add HtmlReporterTruncateOutputTests covering null, empty, at limit,
  one-over-limit, large, and surrogate-pair-at-boundary cases"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant