From bf5d077e1520200e3c11010053889ea03de37ab8 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 16 Jul 2026 22:49:54 +1000 Subject: [PATCH 1/2] Improve the invalid line ending failure The verified file line ending check threw before received was written, so a CRLF verified file produced no output at all. That reads as "verify did nothing" rather than a line ending problem, and the cause was buried: VerifyEngine wraps every exception in "Failed to compare files", so the outer message runners surface said nothing about line endings. Write received before throwing, so the failure produces a snapshot, and accepting it rewrites verified with \n endings. Throw a dedicated VerifiedLineEndingException that VerifyEngine rethrows unwrapped, and name the likely cause (git checking the file out with \r\n) and the .gitattributes fix in the message. Surfaced by VerifyTests/Verify.DiffPlex#474, where the missing output was misattributed to Verify.DiffPlex. Also correct two dead guards. NET10 is not a symbol the SDK defines (it is NET10_0), so #if NET10 was always false, and TrailingNewlinesRaw was additionally nested inside the #if NET9_0 above it, requiring both symbols. TrailingNewlinesRaw, SecondsFractionUpperLong and SecondsFractionUpperShort had never compiled; all three pass once the guards are corrected. Notably TrailingNewlinesRaw covers the trailing newline half of #1762, which until now had no running test. TrailingNewlinesRaw hand-manages its verified file (delete, write, verify, delete), so the tracked NewLineTests.TrailingNewlinesRaw.verified.txt was a leftover that every run deletes, leaving a dirty tree. Untrack it, matching StringWithDifferingNewline which uses the same pattern. Also remove the TrailingNewlinesObject snapshot, whose test is commented out. --- ...eTests.TrailingNewlinesObject.verified.txt | 3 - ...LineTests.TrailingNewlinesRaw.verified.txt | 1 - src/Verify.Tests/NewLineTests.cs | 71 ++++++++++++------- .../Serialization/SerializationTests.cs | 3 +- src/Verify/Compare/Comparer.cs | 6 +- .../Compare/VerifiedLineEndingException.cs | 12 ++++ src/Verify/Verifier/VerifyEngine.cs | 6 ++ 7 files changed, 70 insertions(+), 32 deletions(-) delete mode 100644 src/Verify.Tests/NewLineTests.TrailingNewlinesObject.verified.txt delete mode 100644 src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt create mode 100644 src/Verify/Compare/VerifiedLineEndingException.cs diff --git a/src/Verify.Tests/NewLineTests.TrailingNewlinesObject.verified.txt b/src/Verify.Tests/NewLineTests.TrailingNewlinesObject.verified.txt deleted file mode 100644 index 52ce6eea7a..0000000000 --- a/src/Verify.Tests/NewLineTests.TrailingNewlinesObject.verified.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ - s: a -} diff --git a/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt b/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt deleted file mode 100644 index 7a2a9ee4f5..0000000000 --- a/src/Verify.Tests/NewLineTests.TrailingNewlinesRaw.verified.txt +++ /dev/null @@ -1 +0,0 @@ -a diff --git a/src/Verify.Tests/NewLineTests.cs b/src/Verify.Tests/NewLineTests.cs index 6a2ead546a..8152e841fe 100644 --- a/src/Verify.Tests/NewLineTests.cs +++ b/src/Verify.Tests/NewLineTests.cs @@ -56,6 +56,25 @@ public async Task StringWithDifferingNewline() var cr = await Assert.ThrowsAnyAsync(() => Verify("a\nb", settings)); Assert.Contains("carriage return", cr.ToString()); + // The rejection writes received, so the run is not silent, and is not wrapped in a + // generic "Failed to compare files" that hides the cause. + // Globbed because received carries the namer uniqueness suffix, while the verified + // file above is the unsuffixed fallback. + var directory = Path.GetDirectoryName(fullPath)!; + const string receivedPattern = "NewLineTests.StringWithDifferingNewline*.received.txt"; + foreach (var stale in Directory.EnumerateFiles(directory, receivedPattern)) + { + File.Delete(stale); + } + + await File.WriteAllTextAsync(fullPath, "a\r\nb"); + var rejection = await Assert.ThrowsAnyAsync(() => Verify("a\nb", settings)); + Assert.DoesNotContain("Failed to compare files", rejection.Message); + Assert.Contains(".gitattributes", rejection.Message); + var received = Directory.EnumerateFiles(directory, receivedPattern).Single(); + Assert.Equal("a\nb", await File.ReadAllTextAsync(received)); + File.Delete(received); + // A verified file using \n still matches received content normalized to \n await File.WriteAllTextAsync(fullPath, "a\nb"); await Verify("a\r\nb", settings); @@ -65,32 +84,6 @@ public async Task StringWithDifferingNewline() File.Delete(fullPath); } -#if NET10 - [Fact] - public async Task TrailingNewlinesRaw() - { - var file = CurrentFile.Relative("NewLineTests.TrailingNewlinesRaw.verified.txt"); - File.Delete(file); - var settings = new VerifySettings(); - settings.DisableRequireUniquePrefix(); - - // A verified file containing \r is rejected - await File.WriteAllTextAsync(file, "a\r\n"); - var exception = await Assert.ThrowsAnyAsync(() => Verify("a\n", settings)); - Assert.Contains("carriage return", exception.ToString()); - - // Trailing newlines are now compared exactly, with no tolerance - await File.WriteAllTextAsync(file, "a\n"); - await Verify("a\n", settings); - await Assert.ThrowsAsync(() => Verify("a", settings)); - - await File.WriteAllTextAsync(file, "a\n\n"); - await Verify("a\n\n", settings); - await Assert.ThrowsAsync(() => Verify("a\n", settings)); - File.Delete(file); - } -#endif - //TODO: add test for trailing newlines // [Fact] // public async Task TrailingNewlinesObject() @@ -116,4 +109,30 @@ public async Task TrailingNewlinesRaw() // } #endif + +#if NET10_0 + [Fact] + public async Task TrailingNewlinesRaw() + { + var file = CurrentFile.Relative("NewLineTests.TrailingNewlinesRaw.verified.txt"); + File.Delete(file); + var settings = new VerifySettings(); + settings.DisableRequireUniquePrefix(); + + // A verified file containing \r is rejected + await File.WriteAllTextAsync(file, "a\r\n"); + var exception = await Assert.ThrowsAnyAsync(() => Verify("a\n", settings)); + Assert.Contains("carriage return", exception.ToString()); + + // Trailing newlines are now compared exactly, with no tolerance + await File.WriteAllTextAsync(file, "a\n"); + await Verify("a\n", settings); + await Assert.ThrowsAsync(() => Verify("a", settings)); + + await File.WriteAllTextAsync(file, "a\n\n"); + await Verify("a\n\n", settings); + await Assert.ThrowsAsync(() => Verify("a\n", settings)); + File.Delete(file); + } +#endif } diff --git a/src/Verify.Tests/Serialization/SerializationTests.cs b/src/Verify.Tests/Serialization/SerializationTests.cs index 9a118a8557..8f3ba88fe6 100644 --- a/src/Verify.Tests/Serialization/SerializationTests.cs +++ b/src/Verify.Tests/Serialization/SerializationTests.cs @@ -4545,7 +4545,8 @@ public Task Dictionary_ScrubDictionaryKeys() => }, }) .AddScrubber(_ => _.Replace("key", "scrubbed")); -#if NET10 + +#if NET10_0 [Fact] Task SecondsFractionUpperLong() => Verify(""" diff --git a/src/Verify/Compare/Comparer.cs b/src/Verify/Compare/Comparer.cs index 688e150132..ca0316a7c7 100644 --- a/src/Verify/Compare/Comparer.cs +++ b/src/Verify/Compare/Comparer.cs @@ -14,7 +14,11 @@ public static async Task Text(FilePair filePair, StringBuilder r var verified = await File.ReadAllTextAsync(filePair.VerifiedPath, VerifierSettings.Encoding); 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"); + // Write received before throwing. Otherwise the run produces no output at all, + // which reads as "verify silently did nothing" rather than a line ending problem. + // Accepting the received file also rewrites verified with \n endings. + IoHelpers.WriteText(filePair.ReceivedPath, received); + throw new VerifiedLineEndingException(filePair.VerifiedPath); } var result = await CompareStrings(filePair.Extension, received, verified, settings, bypassComparer); diff --git a/src/Verify/Compare/VerifiedLineEndingException.cs b/src/Verify/Compare/VerifiedLineEndingException.cs new file mode 100644 index 0000000000..a87b5a7ff2 --- /dev/null +++ b/src/Verify/Compare/VerifiedLineEndingException.cs @@ -0,0 +1,12 @@ +class VerifiedLineEndingException(string path) : + Exception( + $""" + Verified file must use \n line endings, but it contains a \r (carriage return). + Path: {path} + The usual cause is git checking the file out with \r\n. Add to .gitattributes: + *.verified.txt text eol=lf working-tree-encoding=UTF-8 + then re-checkout the files: + git rm --cached -r . + git reset --hard + See https://github.com/verifytests/verify#text-file-settings + """); diff --git a/src/Verify/Verifier/VerifyEngine.cs b/src/Verify/Verifier/VerifyEngine.cs index 562a50a88b..f90cc874b8 100644 --- a/src/Verify/Verifier/VerifyEngine.cs +++ b/src/Verify/Verifier/VerifyEngine.cs @@ -36,6 +36,12 @@ static async Task GetResult(VerifySettings settings, FilePair fi stream.MoveToStart(); return await FileComparer.DoCompare(settings, file, textHasFailed || bypassComparers, stream); } + catch (VerifiedLineEndingException) + { + // Already names the file and the fix. Wrapping it hides that behind a generic + // "failed to compare" message. + throw; + } catch (Exception exception) { throw new( From 42e439803563a9bdcbdae8f7a46c0a96ba098a57 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 16 Jul 2026 22:56:52 +1000 Subject: [PATCH 2/2] Use the failing file's extension in the line ending message The suggested .gitattributes line hardcoded *.verified.txt, but the verified file may be json, xml, or any other registered text extension, so the suggestion did not match the file it was reported against. Interpolate FilePair.Extension instead, and cover it with a json case. The extension being unread would now also fail the build (CS9113). --- src/Verify.Tests/NewLineTests.cs | 15 ++++++++++++++- src/Verify/Compare/Comparer.cs | 2 +- src/Verify/Compare/VerifiedLineEndingException.cs | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Verify.Tests/NewLineTests.cs b/src/Verify.Tests/NewLineTests.cs index 8152e841fe..b633257511 100644 --- a/src/Verify.Tests/NewLineTests.cs +++ b/src/Verify.Tests/NewLineTests.cs @@ -70,11 +70,24 @@ public async Task StringWithDifferingNewline() await File.WriteAllTextAsync(fullPath, "a\r\nb"); var rejection = await Assert.ThrowsAnyAsync(() => Verify("a\nb", settings)); Assert.DoesNotContain("Failed to compare files", rejection.Message); - Assert.Contains(".gitattributes", rejection.Message); + Assert.Contains("*.verified.txt text eol=lf", rejection.Message); var received = Directory.EnumerateFiles(directory, receivedPattern).Single(); Assert.Equal("a\nb", await File.ReadAllTextAsync(received)); File.Delete(received); + // The suggested .gitattributes line uses the extension of the failing file, + // which is not always txt + var jsonPath = CurrentFile.Relative("NewLineTests.StringWithDifferingNewline.verified.json"); + await File.WriteAllTextAsync(jsonPath, "{\r\n}"); + var json = await Assert.ThrowsAnyAsync( + () => Verify("{\n}", extension: "json", settings: settings)); + Assert.Contains("*.verified.json text eol=lf", json.Message); + File.Delete(jsonPath); + foreach (var stale in Directory.EnumerateFiles(directory, "NewLineTests.StringWithDifferingNewline*.received.json")) + { + File.Delete(stale); + } + // A verified file using \n still matches received content normalized to \n await File.WriteAllTextAsync(fullPath, "a\nb"); await Verify("a\r\nb", settings); diff --git a/src/Verify/Compare/Comparer.cs b/src/Verify/Compare/Comparer.cs index ca0316a7c7..e265e3b4be 100644 --- a/src/Verify/Compare/Comparer.cs +++ b/src/Verify/Compare/Comparer.cs @@ -18,7 +18,7 @@ public static async Task Text(FilePair filePair, StringBuilder r // which reads as "verify silently did nothing" rather than a line ending problem. // Accepting the received file also rewrites verified with \n endings. IoHelpers.WriteText(filePair.ReceivedPath, received); - throw new VerifiedLineEndingException(filePair.VerifiedPath); + throw new VerifiedLineEndingException(filePair.VerifiedPath, filePair.Extension); } var result = await CompareStrings(filePair.Extension, received, verified, settings, bypassComparer); diff --git a/src/Verify/Compare/VerifiedLineEndingException.cs b/src/Verify/Compare/VerifiedLineEndingException.cs index a87b5a7ff2..24c1192b53 100644 --- a/src/Verify/Compare/VerifiedLineEndingException.cs +++ b/src/Verify/Compare/VerifiedLineEndingException.cs @@ -1,10 +1,10 @@ -class VerifiedLineEndingException(string path) : +class VerifiedLineEndingException(string path, string extension) : Exception( $""" Verified file must use \n line endings, but it contains a \r (carriage return). Path: {path} The usual cause is git checking the file out with \r\n. Add to .gitattributes: - *.verified.txt text eol=lf working-tree-encoding=UTF-8 + *.verified.{extension} text eol=lf working-tree-encoding=UTF-8 then re-checkout the files: git rm --cached -r . git reset --hard