Skip to content

Commit 8efa138

Browse files
authored
perf: use EnumerateLines for line splitting in HtmlReportGenerator (#6089)
* perf: use EnumerateLines for line splitting in HtmlReportGenerator Replace `message.Replace("\r\n", "\n").Split('\n')` with span-based `MemoryExtensions.EnumerateLines()` on net8+, eliminating the Replace string and Split array allocations per rendered test result. The netstandard2.0 path retains the existing Replace/Split fallback. Behavior is identical: both code paths match the same line prefixes. Closes #6034 * perf: early-exit TryExtractExpectedActual once both values found Address review on PR #6089: stop scanning the assertion message as soon as both the Expected and Actual lines have been captured, instead of iterating every remaining line (stack traces, diff blocks). Applied to both the #if NET EnumerateLines path and the netstandard2.0 Split fallback.
1 parent 93ad4ff commit 8efa138

1 file changed

Lines changed: 38 additions & 1 deletion

File tree

TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,27 @@ private static void TryExtractExpectedActual(string message, out string? expecte
672672
expected = null;
673673
actual = null;
674674
if (string.IsNullOrEmpty(message)) return;
675+
#if NET
676+
// EnumerateLines splits on \n and \r\n natively — no Replace/Split allocations.
677+
foreach (var rawLine in message.AsSpan().EnumerateLines())
678+
{
679+
var line = rawLine.TrimStart();
680+
if (expected is null && line.StartsWith("Expected:", StringComparison.OrdinalIgnoreCase))
681+
{
682+
expected = line.Slice("Expected:".Length).Trim().ToString();
683+
}
684+
else if (actual is null && (line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("But was:", StringComparison.OrdinalIgnoreCase)))
685+
{
686+
var prefixLen = line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) ? "Actual:".Length : "But was:".Length;
687+
actual = line.Slice(prefixLen).Trim().ToString();
688+
}
689+
690+
if (expected is not null && actual is not null)
691+
{
692+
break;
693+
}
694+
}
695+
#else
675696
var lines = message.Replace("\r\n", "\n").Split('\n');
676697
foreach (var raw in lines)
677698
{
@@ -685,7 +706,13 @@ private static void TryExtractExpectedActual(string message, out string? expecte
685706
var prefixLen = line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) ? "Actual:".Length : "But was:".Length;
686707
actual = line.Substring(prefixLen).Trim();
687708
}
709+
710+
if (expected is not null && actual is not null)
711+
{
712+
break;
713+
}
688714
}
715+
#endif
689716
if (expected is { Length: 0 }) expected = null;
690717
if (actual is { Length: 0 }) actual = null;
691718
}
@@ -767,14 +794,24 @@ private static bool IsDuplicateKey(ReportKeyValue[] items, int index, string key
767794
{
768795
if (string.IsNullOrEmpty(stderr)) return stderr;
769796
if (stderr!.IndexOf("[TUnit]", StringComparison.Ordinal) < 0) return stderr;
770-
var lines = stderr.Replace("\r\n", "\n").Split('\n');
771797
var sb = new StringBuilder(stderr.Length);
798+
#if NET
799+
// EnumerateLines splits on \n and \r\n natively — no Replace/Split allocations.
800+
foreach (var line in stderr.AsSpan().EnumerateLines())
801+
{
802+
if (line.TrimStart().StartsWith("[TUnit]", StringComparison.Ordinal)) continue;
803+
if (sb.Length > 0) sb.Append('\n');
804+
sb.Append(line);
805+
}
806+
#else
807+
var lines = stderr.Replace("\r\n", "\n").Split('\n');
772808
foreach (var line in lines)
773809
{
774810
if (line.TrimStart().StartsWith("[TUnit]", StringComparison.Ordinal)) continue;
775811
if (sb.Length > 0) sb.Append('\n');
776812
sb.Append(line);
777813
}
814+
#endif
778815
return sb.Length == 0 ? null : sb.ToString();
779816
}
780817

0 commit comments

Comments
 (0)