Skip to content

Conversation

@thomhurst
Copy link
Owner

Summary

  • Adds LineDiffHelper class that generates line-based diffs for multiline string comparisons
  • Integrates with StringEqualsAssertion to automatically use line diff when strings contain newlines
  • Shows unified-diff-style output with line numbers and context instead of character-based pointer

Before:

Expected to be equal to "line 1\nline 2\nline 3"
but found "line 1\nchanged\nline 3" which differs at index 7:
       ↓
   "line 1\nchanged..."
   "line 1\nline 2..."
       ↑

After:

Expected to be equal to "line 1
line 2
line 3"
but found multiline string with differences starting at line 2:
  1: line 1
- 2: line 2
+ 2: changed
  3: line 3

Features

  • Automatic detection of multiline strings (no opt-in required)
  • Configurable context lines (default: 2)
  • Limits shown differences (default: 3, then "... and N more")
  • Handles different line endings (CRLF, LF, CR)
  • Supports StringComparison options (e.g., case-insensitive)
  • Truncates long lines with ellipsis
  • Provides summary for very large inputs (>1000 lines)

Closes #4511

Test plan

  • Run LineDiffHelper unit tests (23 tests)
  • Run StringDifference integration tests (10 tests)
  • Run StringEqualsAssertion tests (13 tests)
  • Verify output format manually with sample test case

@thomhurst
Copy link
Owner Author

Summary

Adds line-based diff output for multiline string comparisons with context lines, unified diff-style formatting, and configurable limits.

Critical Issues

None found ✅

Suggestions

1. Public API Snapshot Testing (Minor)

Since LineDiffHelper is a new public class in TUnit.Assertions.Conditions.Helpers namespace, you may need to verify that public API snapshot tests pass. Per TUnit Rule 2, run:

dotnet test TUnit.PublicAPI

If snapshots changed, commit the .verified.txt files (never .received.txt).

2. Performance Considerations (Optional)

The LineDiffHelper.GenerateDiff method is called in StringEqualsAssertion.BuildStringDifferenceMessage which is in the assertion failure path. While this isn't a "hot path" in TUnit's sense (discovery/execution), consider:

TUnit.Assertions/Conditions/Helpers/LineDiffHelper.cs:132-134:

  • SplitIntoLines creates intermediate string allocations with Replace().Replace().Replace().Split()
  • For very large strings, this could be memory-intensive
  • Current mitigation: MaxLines: 1000 default limit is reasonable

TUnit.Assertions/Conditions/Helpers/LineDiffHelper.cs:143-160:

  • FindChanges allocates a List<LineChange> and iterates all lines
  • Consider: Could short-circuit after finding MaxDifferences to avoid processing large identical files

These are not blocking since assertion failure paths don't need micro-optimization, but worth considering for exceptionally large strings.

3. String.Concat vs StringBuilder (Very Minor)

TUnit.Assertions/Conditions/Helpers/LineDiffHelper.cs:330:

return line.Substring(0, maxLength) + "…";

This is fine as-is, but for consistency with TUnit's performance-first principle, could use:

return string.Concat(line.AsSpan(0, maxLength), "…");

(Though this only applies if targeting NET8_0_OR_GREATER, and the current code already has good NET8 optimizations elsewhere.)

Previous Review Status

No previous comments found.

Verdict

APPROVE - No critical issues

This is a nice enhancement that significantly improves the UX for multiline string assertion failures. The implementation is well-tested (23 LineDiffHelper tests + 10 StringDifference integration tests) and the automatic fallback to line-based diff is a good design choice. Just verify the public API snapshots pass.

@thomhurst thomhurst enabled auto-merge (squash) January 19, 2026 20:41
When comparing strings containing newlines, assertions now show a
unified-diff-style output with line numbers and context instead of the
character-based pointer approach.

Closes #4511
@thomhurst
Copy link
Owner Author

Summary

Adds line-based diff output for multiline string comparisons, automatically switching from character-pointer to unified-diff-style output when newlines are detected.

Critical Issues

None found ✅

Suggestions

1. Test Count Discrepancy

TUnit.Assertions.Tests/Helpers/StringDifferenceTests.cs: The PR description mentions "23 LineDiffHelper unit tests" but I only count 6 new integration tests in this file:

  • Multiline_String_Difference_Shows_Line_Diff
  • Multiline_String_Shows_Context_Lines
  • Multiline_String_Handles_Added_Lines
  • Multiline_String_Handles_Removed_Lines
  • Multiline_String_With_CRLF_Line_Endings
  • Multiline_String_With_Case_Insensitive_Comparison

These are integration tests through StringEqualsAssertion, not direct unit tests of LineDiffHelper. Consider updating the PR description or adding dedicated unit tests for LineDiffHelper methods if the 23 tests exist elsewhere.

2. Allocation Optimization (Optional)

TUnit.Assertions/Conditions/Helpers/LineDiffHelper.cs:332: The TruncateLine method uses substring concatenation:

return line.Substring(0, maxLength) + "…";

For consistency with TUnit's existing code (see StringEqualsAssertion.cs:218), consider using NET8_0_OR_GREATER optimization:

#if NET8_0_OR_GREATER
    return string.Concat(line.AsSpan(0, maxLength), "…");
#else
    return line.Substring(0, maxLength) + "…";
#endif

This is very minor since this code is only in the assertion failure path (not a hot path).

Previous Review Status

The previous review by thomhurst mentioned public API snapshot testing, but since LineDiffHelper is internal static class, there are no public API changes and snapshot tests are not required.

Verdict

APPROVE - No critical issues

The implementation follows TUnit principles: modern C# features, proper StringComparison handling, and good UX improvements. The automatic detection of multiline strings is well-designed. The integration tests adequately cover the feature's behavior through the public API.

@thomhurst thomhurst merged commit 996a4de into main Jan 19, 2026
12 of 13 checks passed
@thomhurst thomhurst deleted the feat/multiline-string-diff branch January 19, 2026 21:09
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.

[Feature]: Handle multiline string diffs more cleanly

2 participants