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..b633257511 100644 --- a/src/Verify.Tests/NewLineTests.cs +++ b/src/Verify.Tests/NewLineTests.cs @@ -56,6 +56,38 @@ 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("*.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); @@ -65,32 +97,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 +122,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..e265e3b4be 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, 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 new file mode 100644 index 0000000000..24c1192b53 --- /dev/null +++ b/src/Verify/Compare/VerifiedLineEndingException.cs @@ -0,0 +1,12 @@ +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.{extension} 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(