From c3008802d969ae5c7f5eceaa6977a25bccb15ec6 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 28 Jun 2021 10:22:27 -0700 Subject: [PATCH] Show differences when only end-of-line changes Fixes #876 --- .../Extensions/IVerifierExtensions.cs | 91 ++++++++++++++++++- .../VerifierExtensionsTests.cs | 82 +++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs index 0bffc225c..c43333919 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs @@ -2,10 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using DiffPlex; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; +using DiffPlex.Model; namespace Microsoft.CodeAnalysis.Testing { @@ -14,6 +18,9 @@ namespace Microsoft.CodeAnalysis.Testing /// public static class IVerifierExtensions { + private static readonly InlineDiffBuilder s_diffWithoutLineEndings = new InlineDiffBuilder(new Differ()); + private static readonly InlineDiffBuilder s_diffWithLineEndings = new InlineDiffBuilder(new DifferWithLineEndings()); + /// /// Asserts that two strings are equal, and prints a diff between the two if they are not. /// @@ -27,14 +34,19 @@ public static void EqualOrDiff(this IVerifier verifier, string expected, string if (expected != actual) { - var diffBuilder = new InlineDiffBuilder(new Differ()); - var diff = diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false); + var diff = s_diffWithoutLineEndings.BuildDiffModel(expected, actual, ignoreWhitespace: false); var messageBuilder = new StringBuilder(); messageBuilder.AppendLine( string.IsNullOrEmpty(message) ? "Actual and expected values differ. Expected shown in baseline of diff:" : message); + if (!diff.Lines.Any(line => line.Type == ChangeType.Inserted || line.Type == ChangeType.Deleted)) + { + // We have a failure only caused by line ending differences; recalculate with line endings visible + diff = s_diffWithLineEndings.BuildDiffModel(expected, actual, ignoreWhitespace: false); + } + foreach (var line in diff.Lines) { switch (line.Type) @@ -56,5 +68,80 @@ public static void EqualOrDiff(this IVerifier verifier, string expected, string verifier.Fail(messageBuilder.ToString()); } } + + private class DifferWithLineEndings : IDiffer + { + private const string CarriageReturnText = ""; + private const string LineFeedText = ""; + + private static readonly char[] s_endOfLineCharacters = { '\r', '\n' }; + private static readonly Differ s_differ = new Differ(); + + public DiffResult CreateCharacterDiffs(string oldText, string newText, bool ignoreWhitespace) + => s_differ.CreateCharacterDiffs(oldText, newText, ignoreWhitespace); + + public DiffResult CreateCharacterDiffs(string oldText, string newText, bool ignoreWhitespace, bool ignoreCase) + => s_differ.CreateCharacterDiffs(oldText, newText, ignoreWhitespace, ignoreCase); + + public DiffResult CreateCustomDiffs(string oldText, string newText, bool ignoreWhiteSpace, Func chunker) + => s_differ.CreateCustomDiffs(oldText, newText, ignoreWhiteSpace, chunker); + + public DiffResult CreateCustomDiffs(string oldText, string newText, bool ignoreWhiteSpace, bool ignoreCase, Func chunker) + => s_differ.CreateCustomDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, chunker); + + public DiffResult CreateLineDiffs(string oldText, string newText, bool ignoreWhitespace) + => CreateLineDiffs(oldText, newText, ignoreWhitespace, ignoreCase: false); + + public DiffResult CreateLineDiffs(string oldText, string newText, bool ignoreWhitespace, bool ignoreCase) + { + Func chunker = s => + { + var lines = new List(); + + var nextChar = 0; + while (nextChar < s.Length) + { + var nextEol = s.IndexOfAny(s_endOfLineCharacters, nextChar); + if (nextEol == -1) + { + lines.Add(s.Substring(nextChar)); + break; + } + + var currentLine = s.Substring(nextChar, nextEol - nextChar); + + switch (s[nextEol]) + { + case '\r': + currentLine += CarriageReturnText; + if (nextEol < s.Length - 1 && s[nextEol + 1] == '\n') + { + currentLine += LineFeedText; + nextEol++; + } + + break; + + case '\n': + currentLine += LineFeedText; + break; + } + + lines.Add(currentLine); + nextChar = nextEol + 1; + } + + return lines.ToArray(); + }; + + return CreateCustomDiffs(oldText, newText, ignoreWhitespace, ignoreCase, chunker); + } + + public DiffResult CreateWordDiffs(string oldText, string newText, bool ignoreWhitespace, char[] separators) + => s_differ.CreateWordDiffs(oldText, newText, ignoreWhitespace, separators); + + public DiffResult CreateWordDiffs(string oldText, string newText, bool ignoreWhitespace, bool ignoreCase, char[] separators) + => s_differ.CreateWordDiffs(oldText, newText, ignoreWhitespace, ignoreCase, separators); + } } } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs new file mode 100644 index 000000000..eb850de69 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class VerifierExtensionsTests + { + [Fact] + [WorkItem(876, "https://github.com/dotnet/roslyn-sdk/issues/876")] + public void VerifyContentWithMixedLineEndings1() + { + var baseline = + "Line 1\r\n" + + "Line 2\r\n" + + "Line 3\r\n" + + "Line 4\r\n" + + "Line 5\r\n" + + "Line 6\r\n"; + var modified = + "Line 1\r" + + "Line 2\r\n" + + "Line 3\n" + + "Line 4\r" + + "Line 5\r\n" + + "Line 6\n"; + + var exception = Assert.Throws(() => new DefaultVerifier().EqualOrDiff(baseline, modified)); + Assert.Equal( + $"Actual and expected values differ. Expected shown in baseline of diff:{Environment.NewLine}" + + $"-Line 1{Environment.NewLine}" + + $"+Line 1{Environment.NewLine}" + + $" Line 2{Environment.NewLine}" + + $"-Line 3{Environment.NewLine}" + + $"-Line 4{Environment.NewLine}" + + $"+Line 3{Environment.NewLine}" + + $"+Line 4{Environment.NewLine}" + + $" Line 5{Environment.NewLine}" + + $"-Line 6{Environment.NewLine}" + + $"+Line 6{Environment.NewLine}", + exception.Message); + } + + [Fact] + [WorkItem(876, "https://github.com/dotnet/roslyn-sdk/issues/876")] + public void VerifyContentWithMixedLineEnding2() + { + var baseline = + "Line 1\n" + + "Line 2\n" + + "Line 3\n" + + "Line 4\n" + + "Line 5\n" + + "Line 6\n"; + var modified = + "Line 1\r" + + "Line 2\r\n" + + "Line 3\n" + + "Line 4\r" + + "Line 5\r\n" + + "Line 6\n"; + + var exception = Assert.Throws(() => new DefaultVerifier().EqualOrDiff(baseline, modified)); + Assert.Equal( + $"Actual and expected values differ. Expected shown in baseline of diff:{Environment.NewLine}" + + $"-Line 1{Environment.NewLine}" + + $"-Line 2{Environment.NewLine}" + + $"+Line 1{Environment.NewLine}" + + $"+Line 2{Environment.NewLine}" + + $" Line 3{Environment.NewLine}" + + $"-Line 4{Environment.NewLine}" + + $"-Line 5{Environment.NewLine}" + + $"+Line 4{Environment.NewLine}" + + $"+Line 5{Environment.NewLine}" + + $" Line 6{Environment.NewLine}", + exception.Message); + } + } +}