diff --git a/TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs b/TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs index 4298f11857..66857ab5f0 100644 --- a/TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs +++ b/TUnit.Engine/Reporters/Html/HtmlReportGenerator.cs @@ -672,6 +672,27 @@ private static void TryExtractExpectedActual(string message, out string? expecte expected = null; actual = null; if (string.IsNullOrEmpty(message)) return; +#if NET + // EnumerateLines splits on \n and \r\n natively — no Replace/Split allocations. + foreach (var rawLine in message.AsSpan().EnumerateLines()) + { + var line = rawLine.TrimStart(); + if (expected is null && line.StartsWith("Expected:", StringComparison.OrdinalIgnoreCase)) + { + expected = line.Slice("Expected:".Length).Trim().ToString(); + } + else if (actual is null && (line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("But was:", StringComparison.OrdinalIgnoreCase))) + { + var prefixLen = line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) ? "Actual:".Length : "But was:".Length; + actual = line.Slice(prefixLen).Trim().ToString(); + } + + if (expected is not null && actual is not null) + { + break; + } + } +#else var lines = message.Replace("\r\n", "\n").Split('\n'); foreach (var raw in lines) { @@ -685,7 +706,13 @@ private static void TryExtractExpectedActual(string message, out string? expecte var prefixLen = line.StartsWith("Actual:", StringComparison.OrdinalIgnoreCase) ? "Actual:".Length : "But was:".Length; actual = line.Substring(prefixLen).Trim(); } + + if (expected is not null && actual is not null) + { + break; + } } +#endif if (expected is { Length: 0 }) expected = null; if (actual is { Length: 0 }) actual = null; } @@ -767,14 +794,24 @@ private static bool IsDuplicateKey(ReportKeyValue[] items, int index, string key { if (string.IsNullOrEmpty(stderr)) return stderr; if (stderr!.IndexOf("[TUnit]", StringComparison.Ordinal) < 0) return stderr; - var lines = stderr.Replace("\r\n", "\n").Split('\n'); var sb = new StringBuilder(stderr.Length); +#if NET + // EnumerateLines splits on \n and \r\n natively — no Replace/Split allocations. + foreach (var line in stderr.AsSpan().EnumerateLines()) + { + if (line.TrimStart().StartsWith("[TUnit]", StringComparison.Ordinal)) continue; + if (sb.Length > 0) sb.Append('\n'); + sb.Append(line); + } +#else + var lines = stderr.Replace("\r\n", "\n").Split('\n'); foreach (var line in lines) { if (line.TrimStart().StartsWith("[TUnit]", StringComparison.Ordinal)) continue; if (sb.Length > 0) sb.Append('\n'); sb.Append(line); } +#endif return sb.Length == 0 ? null : sb.ToString(); }