From 56e783cecbea6793c6d888c8abe3a32b0e8474bb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 29 Jun 2026 10:27:55 +1000 Subject: [PATCH 1/3] Reject carriage returns in verified files instead of normalizing The text comparison previously read the verified file via ReadStringBuilderWithFixedLines, silently converting \r\n and \r to \n. Now it reads the file directly and throws if it contains a \r, linking to the text-file-settings docs. Verified files are LF-only (enforced by .gitattributes), so this surfaces misconfigured line endings rather than masking them. Drop the now-unused ReadStringBuilderWithFixedLines(string) overload and rewrite the two net9 NewLineTests to assert the throw. --- src/Verify.Tests/NewLineTests.cs | 49 ++++++++++++-------------------- src/Verify/Compare/Comparer.cs | 10 +++++-- src/Verify/IoHelpers.cs | 6 ---- 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/src/Verify.Tests/NewLineTests.cs b/src/Verify.Tests/NewLineTests.cs index f36f35d85e..7910eaf028 100644 --- a/src/Verify.Tests/NewLineTests.cs +++ b/src/Verify.Tests/NewLineTests.cs @@ -44,35 +44,27 @@ public async Task StringWithDifferingNewline() { var fullPath = CurrentFile.Relative("NewLineTests.StringWithDifferingNewline.verified.txt"); File.Delete(fullPath); + var settings = new VerifySettings(); + settings.DisableRequireUniquePrefix(); + + // A verified file containing \r is rejected rather than silently normalized await File.WriteAllTextAsync(fullPath, "a\r\nb"); - await Verify("a\r\nb"); - PrefixUnique.Clear(); - await Verify("a\rb"); - PrefixUnique.Clear(); - await Verify("a\nb"); - PrefixUnique.Clear(); + var crlf = await Assert.ThrowsAnyAsync(() => Verify("a\nb", settings)); + Assert.Contains("carriage return", crlf.ToString()); - File.Delete(fullPath); + await File.WriteAllTextAsync(fullPath, "a\rb"); + var cr = await Assert.ThrowsAnyAsync(() => Verify("a\nb", settings)); + Assert.Contains("carriage return", cr.ToString()); + + // A verified file using \n still matches received content normalized to \n await File.WriteAllTextAsync(fullPath, "a\nb"); - await Verify("a\r\nb"); - PrefixUnique.Clear(); - await Verify("a\rb"); - PrefixUnique.Clear(); - await Verify("a\nb"); - PrefixUnique.Clear(); + await Verify("a\r\nb", settings); + await Verify("a\rb", settings); + await Verify("a\nb", settings); File.Delete(fullPath); - await File.WriteAllTextAsync(fullPath, "a\rb"); - await Verify("a\r\nb"); - PrefixUnique.Clear(); - await Verify("a\rb"); - PrefixUnique.Clear(); - await Verify("a\nb"); - File.Delete(fullPath); } -#if NET9_0 - [Fact] public async Task TrailingNewlinesRaw() { @@ -81,16 +73,12 @@ public async Task TrailingNewlinesRaw() var settings = new VerifySettings(); settings.DisableRequireUniquePrefix(); + // A verified file containing \r is rejected await File.WriteAllTextAsync(file, "a\r\n"); - await Verify("a\r\n", settings); - await Verify("a\n", settings); - await Verify("a", settings); - - await File.WriteAllTextAsync(file, "a\r\n\r\n"); - await Verify("a\r\n\r\n", settings); - await Verify("a\n\n", settings); - await Verify("a\n", settings); + var exception = await Assert.ThrowsAnyAsync(() => Verify("a\n", settings)); + Assert.Contains("carriage return", exception.ToString()); + // Trailing \n tolerance still applies for \n-only verified files await File.WriteAllTextAsync(file, "a\n"); await Verify("a\n", settings); await Verify("a", settings); @@ -100,7 +88,6 @@ public async Task TrailingNewlinesRaw() await Verify("a\n", settings); File.Delete(file); } -#endif //TODO: add test for trailing newlines // [Fact] diff --git a/src/Verify/Compare/Comparer.cs b/src/Verify/Compare/Comparer.cs index 9dde538f52..47a47bb346 100644 --- a/src/Verify/Compare/Comparer.cs +++ b/src/Verify/Compare/Comparer.cs @@ -9,7 +9,13 @@ public static async Task Text(FilePair filePair, StringBuilder r return new(Equality.New, null, received, null); } - var verified = await IoHelpers.ReadStringBuilderWithFixedLines(filePair.VerifiedPath); + var verifiedText = await File.ReadAllTextAsync(filePair.VerifiedPath); + if (verifiedText.Contains('\r')) + { + throw new($@"Verified file must use \n line endings, but it contains a \r (carriage return). Path: {filePair.VerifiedPath}. See https://github.com/verifytests/verify#text-file-settings"); + } + + var verified = new StringBuilder(verifiedText); var result = await CompareStrings(filePair.Extension, received, verified, settings, bypassComparer); if (result.IsEqual) { @@ -52,4 +58,4 @@ static Task CompareStrings(string extension, StringBuilder receiv return Task.FromResult(new CompareResult(isEqual)); } -} \ No newline at end of file +} diff --git a/src/Verify/IoHelpers.cs b/src/Verify/IoHelpers.cs index 540bdee621..5fe104ee3b 100644 --- a/src/Verify/IoHelpers.cs +++ b/src/Verify/IoHelpers.cs @@ -208,12 +208,6 @@ internal static string ResolveDirectoryFromSourceFile(string sourceFile) throw new($"Unable to resolve directory. sourceFile: {sourceFile}"); } - public static async Task ReadStringBuilderWithFixedLines(string path) - { - using var stream = OpenRead(path); - return await stream.ReadStringBuilderWithFixedLines(); - } - public static async Task WriteStream(string path, Stream stream) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); From 11199604580deded136292c373f9ee3f5ffcdd70 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 29 Jun 2026 13:03:28 +1000 Subject: [PATCH 2/3] . --- ...onTests.ImmutableArray_Empty.verified.json | 2 +- ...ImmutableArray_Uninitialized.verified.json | 2 +- .../ExceptionMessageFormatSamples.cs | 4 +-- .../ExceptionParsingTests.cs | 10 +++--- .../VerifyExceptionMessageBuilderTests.cs | 10 +++--- .../NoMatchTests.NoAttributes.verified.txt | 2 +- src/Verify.Tests/NewLineTests.cs | 6 ++-- src/Verify/Compare/Comparer.cs | 32 ++++--------------- src/Verify/Verifier/EqualityResult.cs | 6 ++-- src/Verify/Verifier/NotEqualResult.cs | 6 ++-- 10 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/StrictJsonTests/SerializationTests.ImmutableArray_Empty.verified.json b/src/StrictJsonTests/SerializationTests.ImmutableArray_Empty.verified.json index 5c755d6793..de4b275ff3 100644 --- a/src/StrictJsonTests/SerializationTests.ImmutableArray_Empty.verified.json +++ b/src/StrictJsonTests/SerializationTests.ImmutableArray_Empty.verified.json @@ -1,3 +1,3 @@ { "Array": null -} +} \ No newline at end of file diff --git a/src/StrictJsonTests/SerializationTests.ImmutableArray_Uninitialized.verified.json b/src/StrictJsonTests/SerializationTests.ImmutableArray_Uninitialized.verified.json index 5c755d6793..de4b275ff3 100644 --- a/src/StrictJsonTests/SerializationTests.ImmutableArray_Uninitialized.verified.json +++ b/src/StrictJsonTests/SerializationTests.ImmutableArray_Uninitialized.verified.json @@ -1,3 +1,3 @@ { "Array": null -} +} \ No newline at end of file diff --git a/src/Verify.ExceptionParsing.Tests/ExceptionMessageFormatSamples.cs b/src/Verify.ExceptionParsing.Tests/ExceptionMessageFormatSamples.cs index cb91008ae3..6b740c2b87 100644 --- a/src/Verify.ExceptionParsing.Tests/ExceptionMessageFormatSamples.cs +++ b/src/Verify.ExceptionParsing.Tests/ExceptionMessageFormatSamples.cs @@ -14,7 +14,7 @@ public Task AllCategories() }; var notEquals = new List { - new(new("txt", Dir("MyTests.Test2.received.txt"), Dir("MyTests.Test2.verified.txt")), null, new("received text"), new("verified text")) + new(new("txt", Dir("MyTests.Test2.received.txt"), Dir("MyTests.Test2.verified.txt")), null, new("received text"), "verified text") }; var delete = new List { @@ -33,7 +33,7 @@ public Task NotEqualWithMessage() { var notEquals = new List { - new(new("txt", Dir("MyTests.Test1.received.txt"), Dir("MyTests.Test1.verified.txt")), "The comparer reported a difference", new("received text"), new("verified text")) + new(new("txt", Dir("MyTests.Test1.received.txt"), Dir("MyTests.Test1.verified.txt")), "The comparer reported a difference", new("received text"), "verified text") }; return BuildVerify([], notEquals, [], []); diff --git a/src/Verify.ExceptionParsing.Tests/ExceptionParsingTests.cs b/src/Verify.ExceptionParsing.Tests/ExceptionParsingTests.cs index 25cc71d2e7..271b88f8f0 100644 --- a/src/Verify.ExceptionParsing.Tests/ExceptionParsingTests.cs +++ b/src/Verify.ExceptionParsing.Tests/ExceptionParsingTests.cs @@ -31,7 +31,7 @@ public Task WithMessage() { var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), "TheMessage", new("receivedText"), new("verifiedText")), + new(new("txt", receivedTxt, verifiedTxt), "TheMessage", new("receivedText"), "verifiedText"), new(new("bin", receivedBin, verifiedBin), "TheMessage", null, null) }; @@ -132,7 +132,7 @@ public Task SingleNotEqual() { var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), new("verifiedText")) + new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), "verifiedText") }; return ParseVerify([], notEquals, [], []); @@ -153,7 +153,7 @@ public Task MultipleItem() }; var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), new("verifiedText")), + new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), "verifiedText"), new(new("bin", receivedBin, verifiedBin), null, null, null) }; var delete = new List @@ -178,7 +178,7 @@ public Task SingleItem() }; var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), new("verifiedText")) + new(new("txt", receivedTxt, verifiedTxt), null, new("receivedText"), "verifiedText") }; var delete = new List { @@ -242,4 +242,4 @@ static Task ParseVerify( result }); } -} \ No newline at end of file +} diff --git a/src/Verify.ExceptionParsing.Tests/VerifyExceptionMessageBuilderTests.cs b/src/Verify.ExceptionParsing.Tests/VerifyExceptionMessageBuilderTests.cs index 3402baca11..768613c692 100644 --- a/src/Verify.ExceptionParsing.Tests/VerifyExceptionMessageBuilderTests.cs +++ b/src/Verify.ExceptionParsing.Tests/VerifyExceptionMessageBuilderTests.cs @@ -42,7 +42,7 @@ public Task SingleNotEqual_Text() => @new: [], notEquals: [ - new(new("txt", receivedTxt, verifiedTxt), null, new("received content"), new("verified content")) + new(new("txt", receivedTxt, verifiedTxt), null, new("received content"), "verified content") ], delete: [], equal: []); @@ -53,7 +53,7 @@ public Task SingleNotEqual_WithMessage() => @new: [], notEquals: [ - new(new("txt", receivedTxt, verifiedTxt), "Comparer reported difference", new("received content"), new("verified content")) + new(new("txt", receivedTxt, verifiedTxt), "Comparer reported difference", new("received content"), "verified content") ], delete: [], equal: []); @@ -108,7 +108,7 @@ public Task AllCategories() }; var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), null, new("received"), new("verified")) + new(new("txt", receivedTxt, verifiedTxt), null, new("received"), "verified") }; var delete = new List { @@ -139,8 +139,8 @@ public Task MultipleNotEqual_MixedMessageAndNoMessage() { var notEquals = new List { - new(new("txt", receivedTxt, verifiedTxt), null, new("received text"), new("verified text")), - new(new("txt", receivedTxt, verifiedTxt), "Custom comparison message", new("received2"), new("verified2")) + new(new("txt", receivedTxt, verifiedTxt), null, new("received text"), "verified text"), + new(new("txt", receivedTxt, verifiedTxt), "Custom comparison message", new("received2"), "verified2") }; return BuildVerify([], notEquals, [], []); diff --git a/src/Verify.MSTest.SourceGenerator.Tests/NoMatchTests.NoAttributes.verified.txt b/src/Verify.MSTest.SourceGenerator.Tests/NoMatchTests.NoAttributes.verified.txt index f117788923..22fdca1b26 100644 --- a/src/Verify.MSTest.SourceGenerator.Tests/NoMatchTests.NoAttributes.verified.txt +++ b/src/Verify.MSTest.SourceGenerator.Tests/NoMatchTests.NoAttributes.verified.txt @@ -1 +1 @@ -{} +{} \ No newline at end of file diff --git a/src/Verify.Tests/NewLineTests.cs b/src/Verify.Tests/NewLineTests.cs index 7910eaf028..6bcda41281 100644 --- a/src/Verify.Tests/NewLineTests.cs +++ b/src/Verify.Tests/NewLineTests.cs @@ -78,14 +78,14 @@ public async Task TrailingNewlinesRaw() var exception = await Assert.ThrowsAnyAsync(() => Verify("a\n", settings)); Assert.Contains("carriage return", exception.ToString()); - // Trailing \n tolerance still applies for \n-only verified files + // Trailing newlines are now compared exactly, with no tolerance await File.WriteAllTextAsync(file, "a\n"); await Verify("a\n", settings); - await Verify("a", settings); + await Assert.ThrowsAsync(() => Verify("a", settings)); await File.WriteAllTextAsync(file, "a\n\n"); await Verify("a\n\n", settings); - await Verify("a\n", settings); + await Assert.ThrowsAsync(() => Verify("a\n", settings)); File.Delete(file); } diff --git a/src/Verify/Compare/Comparer.cs b/src/Verify/Compare/Comparer.cs index 47a47bb346..21aeb7aa13 100644 --- a/src/Verify/Compare/Comparer.cs +++ b/src/Verify/Compare/Comparer.cs @@ -9,13 +9,12 @@ public static async Task Text(FilePair filePair, StringBuilder r return new(Equality.New, null, received, null); } - var verifiedText = await File.ReadAllTextAsync(filePair.VerifiedPath); - if (verifiedText.Contains('\r')) + var verified = await File.ReadAllTextAsync(filePair.VerifiedPath); + if (verified.Contains('\r')) { throw new($@"Verified file must use \n line endings, but it contains a \r (carriage return). Path: {filePair.VerifiedPath}. See https://github.com/verifytests/verify#text-file-settings"); } - var verified = new StringBuilder(verifiedText); var result = await CompareStrings(filePair.Extension, received, verified, settings, bypassComparer); if (result.IsEqual) { @@ -26,35 +25,16 @@ public static async Task Text(FilePair filePair, StringBuilder r return new(Equality.NotEqual, result.Message, received, verified); } - static Task CompareStrings(string extension, StringBuilder received, StringBuilder verified, VerifySettings settings, bool bypassComparer) + static Task CompareStrings(string extension, StringBuilder received, string verified, VerifySettings settings, bool bypassComparer) { - if (verified.Length > 0 && - verified.Length - 1 == received.Length && - verified.LastChar() == '\n') - { - verified.Length -= 1; - } - - // StringBuilder is broken on older .net https://github.com/dotnet/runtime/issues/27684 -#if NET6_0_OR_GREATER - var isEqual = verified.Equals(received); - if (!isEqual && - !bypassComparer && - settings.TryFindStringComparer(extension, out var compare)) - { - return compare(received.ToString(), verified.ToString(), settings.Context); - } -#else - var receivedString = received.ToString(); - var verifiedString = verified.ToString(); - var isEqual = receivedString.Equals(verifiedString); + // StringBuilder.Equals(ReadOnlySpan) is native on net6+ and provided by Polyfill on net framework + var isEqual = received.Equals(verified.AsSpan()); if (!isEqual && !bypassComparer && settings.TryFindStringComparer(extension, out var compare)) { - return compare(receivedString, verifiedString, settings.Context); + return compare(received.ToString(), verified, settings.Context); } -#endif return Task.FromResult(new CompareResult(isEqual)); } diff --git a/src/Verify/Verifier/EqualityResult.cs b/src/Verify/Verifier/EqualityResult.cs index 0faa23c7b1..66ade7b318 100644 --- a/src/Verify/Verifier/EqualityResult.cs +++ b/src/Verify/Verifier/EqualityResult.cs @@ -1,7 +1,7 @@ -readonly struct EqualityResult(Equality equality, string? message, StringBuilder? receivedText, StringBuilder? verifiedText) +readonly struct EqualityResult(Equality equality, string? message, StringBuilder? receivedText, string? verifiedText) { public Equality Equality { get; } = equality; public string? Message { get; } = message; public StringBuilder? ReceivedText { get; } = receivedText; - public StringBuilder? VerifiedText { get; } = verifiedText; -} \ No newline at end of file + public string? VerifiedText { get; } = verifiedText; +} diff --git a/src/Verify/Verifier/NotEqualResult.cs b/src/Verify/Verifier/NotEqualResult.cs index c6c30114f2..bba5fb7618 100644 --- a/src/Verify/Verifier/NotEqualResult.cs +++ b/src/Verify/Verifier/NotEqualResult.cs @@ -2,10 +2,10 @@ FilePair file, string? message, StringBuilder? receivedText, - StringBuilder? verifiedText) + string? verifiedText) { public FilePair File { get; } = file; public string? Message { get; } = message; public StringBuilder? ReceivedText { get; } = receivedText; - public StringBuilder? VerifiedText { get; } = verifiedText; -} \ No newline at end of file + public string? VerifiedText { get; } = verifiedText; +} From 65a1d7d03e08fa1f1c03834febdcfda5cddc88ee Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 29 Jun 2026 17:34:26 +1000 Subject: [PATCH 3/3] . --- src/Directory.Build.props | 2 +- src/Verify/Compare/Comparer.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c5e2119511..48d70101ec 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CA1822;CS1591;CS0649;xUnit1026;xUnit1013;CS1573;VerifyTestsProjectDir;VerifySetParameters;PolyFillTargetsForNuget;xUnit1051;NU1608;NU1109 - 31.20.0 + 31.21.0 enable preview 1.0.0 diff --git a/src/Verify/Compare/Comparer.cs b/src/Verify/Compare/Comparer.cs index 21aeb7aa13..3e4a27b850 100644 --- a/src/Verify/Compare/Comparer.cs +++ b/src/Verify/Compare/Comparer.cs @@ -27,7 +27,6 @@ public static async Task Text(FilePair filePair, StringBuilder r static Task CompareStrings(string extension, StringBuilder received, string verified, VerifySettings settings, bool bypassComparer) { - // StringBuilder.Equals(ReadOnlySpan) is native on net6+ and provided by Polyfill on net framework var isEqual = received.Equals(verified.AsSpan()); if (!isEqual && !bypassComparer &&