コード例 #1
0
        private static async Task VerifyFixAsync(Solution sln, IReadOnlyList <ImmutableArray <Diagnostic> > diagnostics, DiagnosticAnalyzer analyzer, CodeFixProvider fix, string fixedCode, string fixTitle = null, AllowCompilationErrors allowCompilationErrors = AllowCompilationErrors.No)
        {
            var fixableDiagnostics = diagnostics.SelectMany(x => x)
                                     .Where(x => fix.FixableDiagnosticIds.Contains(x.Id))
                                     .ToArray();

            if (fixableDiagnostics.Length == 0)
            {
                var message = $"Code analyzed with {analyzer} did not generate any diagnostics fixable by {fix}.{Environment.NewLine}" +
                              $"The analyzed code contained the following diagnostics: {{{string.Join(", ", diagnostics.SelectMany(x => x).Select(d => d.Id))}}}{Environment.NewLine}" +
                              $"The code fix supports the following diagnostics: {{{string.Join(", ", fix.FixableDiagnosticIds)}}}";
                throw new AssertException(message);
            }

            if (fixableDiagnostics.Length > 1)
            {
                var message = $"Code analyzed with {analyzer} generated more than one diagnostic fixable by {fix}.{Environment.NewLine}" +
                              $"The analyzed code contained the following diagnostics: {{{string.Join(", ", diagnostics.SelectMany(x => x).Select(d => d.Id))}}}{Environment.NewLine}" +
                              $"The code fix supports the following diagnostics: {{{string.Join(", ", fix.FixableDiagnosticIds)}}}{Environment.NewLine}" +
                              $"Maybe you meant to call AnalyzerAssert.FixAll?";
                throw new AssertException(message);
            }

            var diagnostic    = fixableDiagnostics.Single();
            var fixedSolution = await Fix.ApplyAsync(sln, fix, diagnostic, fixTitle).ConfigureAwait(false);

            if (ReferenceEquals(sln, fixedSolution))
            {
                throw new AssertException($"{fix} did not change any document.");
            }

            var fixedSource = await CodeReader.GetStringFromDocumentAsync(
                fixedSolution.GetDocument(sln.GetDocument(diagnostic.Location.SourceTree).Id),
                Formatter.Annotation,
                CancellationToken.None)
                              .ConfigureAwait(false);

            CodeAssert.AreEqual(fixedCode, fixedSource);

            if (allowCompilationErrors == AllowCompilationErrors.No)
            {
                await VerifyNoCompilerErrorsAsync(fix, fixedSolution).ConfigureAwait(false);
            }
        }
コード例 #2
0
        private static async Task AreEqualAsync(IReadOnlyList <string> expected, Solution actual, string?messageHeader)
        {
            var actualCount = actual.Projects.SelectMany(x => x.Documents).Count();

            if (expected.Count != actualCount)
            {
                throw new AssertException($"Expected {expected.Count} documents the fixed solution has {actualCount} documents.");
            }

            foreach (var project in actual.Projects)
            {
                foreach (var document in project.Documents)
                {
                    var fixedSource = await CodeReader.GetStringFromDocumentAsync(document, CancellationToken.None).ConfigureAwait(false);

                    CodeAssert.AreEqual(FindExpected(fixedSource), fixedSource, messageHeader);
                }
            }

            string FindExpected(string fixedSource)
            {
                var fixedNamespace = CodeReader.Namespace(fixedSource);
                var fixedFileName  = CodeReader.FileName(fixedSource);
                var match          = expected.FirstOrDefault(x => x == fixedSource);

                if (match != null)
                {
                    return(match);
                }

                foreach (var candidate in expected)
                {
                    if (CodeReader.Namespace(candidate) == fixedNamespace &&
                        CodeReader.FileName(candidate) == fixedFileName)
                    {
                        return(candidate);
                    }
                }

                throw new AssertException($"The fixed solution contains a document {fixedFileName} in namespace {fixedNamespace} that is not in the expected documents.");
            }
        }
コード例 #3
0
        /// <summary>
        /// Create a new instance of <see cref="ExpectedDiagnostic"/> with position.
        /// </summary>
        /// <param name="codeWithErrorsIndicated">The code with error position indicated..</param>
        /// <param name="cleanedSources"><paramref name="codeWithErrorsIndicated"/> without errors indicated.</param>
        /// <returns>A new instance of <see cref="ExpectedDiagnostic"/>.</returns>
        public ExpectedDiagnostic WithPositionFromCodeWithErrorsIndicated(string codeWithErrorsIndicated, out string cleanedSources)
        {
            var positions = CodeReader.FindLinePositions(codeWithErrorsIndicated).ToArray();

            if (positions.Length == 0)
            {
                throw new ArgumentException("Expected one error position indicated, was zero.", nameof(codeWithErrorsIndicated));
            }

            if (positions.Length > 1)
            {
                throw new ArgumentException($"Expected one error position indicated, was {positions.Length}.", nameof(codeWithErrorsIndicated));
            }

            cleanedSources = codeWithErrorsIndicated.Replace("↓", string.Empty);
            var fileName = CodeReader.FileName(codeWithErrorsIndicated);
            var position = positions[0];

            return(new ExpectedDiagnostic(this.Id, this.Message, new FileLinePositionSpan(fileName, position, position)));
        }
コード例 #4
0
        /// <summary>
        /// Get the expected diagnostics and cleaned sources.
        /// </summary>
        /// <param name="analyzerId">The descriptor diagnosticId that is expected to produce diagnostics.</param>
        /// <param name="message">The expected message for the diagnostics, can be null.</param>
        /// <param name="code">The code with errors indicated.</param>
        /// <returns>An instance of <see cref="DiagnosticsAndSources"/>.</returns>
        public static DiagnosticsAndSources CreateFromCodeWithErrorsIndicated(string analyzerId, string?message, IReadOnlyList <string> code)
        {
            if (analyzerId is null)
            {
                throw new ArgumentNullException(nameof(analyzerId));
            }

            if (code is null)
            {
                throw new ArgumentNullException(nameof(code));
            }

            var diagnostics    = new List <ExpectedDiagnostic>();
            var cleanedSources = new List <string>();

            foreach (var source in code)
            {
                var positions = CodeReader.FindLinePositions(source).ToArray();
                if (positions.Length == 0)
                {
                    cleanedSources.Add(source);
                    continue;
                }

                cleanedSources.Add(source.Replace("↓", string.Empty));
                var fileName = CodeReader.FileName(source);
                diagnostics.AddRange(positions.Select(p => new ExpectedDiagnostic(analyzerId, message, new FileLinePositionSpan(fileName, p, p))));
            }

            if (diagnostics.Count == 0)
            {
                throw new InvalidOperationException("Expected code to have at least one error position indicated with '↓'");
            }

            return(new DiagnosticsAndSources(diagnostics, cleanedSources));
        }
コード例 #5
0
 public SourceMetadata(string code)
 {
     this.Code      = code;
     this.FileName  = CodeReader.FileName(code);
     this.Namespace = CodeReader.Namespace(code);
 }
コード例 #6
0
        private static async Task AssertNoCompilerErrorsAsync(CodeFixProvider codeFix, Solution fixedSolution)
        {
            var diagnostics = await Analyze.GetDiagnosticsAsync(fixedSolution).ConfigureAwait(false);

            var introducedDiagnostics = diagnostics
                                        .SelectMany(x => x)
                                        .Where(IsIncluded)
                                        .ToArray();

            if (introducedDiagnostics.Select(x => x.Id)
                .Except(DiagnosticSettings.AllowedErrorIds())
                .Any())
            {
                var errorBuilder = StringBuilderPool.Borrow();
                errorBuilder.AppendLine($"{codeFix} introduced syntax error{(introducedDiagnostics.Length > 1 ? "s" : string.Empty)}.");
                foreach (var introducedDiagnostic in introducedDiagnostics)
                {
                    var errorInfo = await introducedDiagnostic.ToStringAsync(fixedSolution).ConfigureAwait(false);

                    errorBuilder.AppendLine($"{errorInfo}");
                }

                errorBuilder.AppendLine("First source file with error is:");
                var sources = await Task.WhenAll(fixedSolution.Projects.SelectMany(p => p.Documents).Select(d => CodeReader.GetStringFromDocumentAsync(d, Formatter.Annotation, CancellationToken.None)));

                var lineSpan = introducedDiagnostics.First().Location.GetMappedLineSpan();
                var match    = sources.SingleOrDefault(x => CodeReader.FileName(x) == lineSpan.Path);
                errorBuilder.Append(match);
                errorBuilder.AppendLine();
                throw AssertException.Create(errorBuilder.Return());
            }
        }
コード例 #7
0
        /// <summary>
        /// Create a <see cref="Solution"/> for <paramref name="code"/>
        /// Each unique namespace in <paramref name="code"/> is added as a project.
        /// </summary>
        /// <param name="code">The code to create the solution from.</param>
        /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/>.</param>
        /// <param name="metadataReferences">The metadata references.</param>
        /// <param name="languageVersion">The <see cref="LanguageVersion"/>.</param>
        /// <returns>A <see cref="Solution"/>.</returns>
        public static Solution CreateSolution(IEnumerable <string> code, CSharpCompilationOptions compilationOptions, IEnumerable <MetadataReference> metadataReferences = null, LanguageVersion languageVersion = LanguageVersion.Latest)
        {
            var solutionInfo = SolutionInfo.Create(
                SolutionId.CreateNewId("Test.sln"),
                VersionStamp.Default,
                projects: GetProjectInfos());
            var solution = EmptySolution;

            foreach (var projectInfo in solutionInfo.Projects)
            {
                solution = solution.AddProject(projectInfo.WithProjectReferences(FindReferences(projectInfo)));
            }

            return(solution);

            IEnumerable <ProjectInfo> GetProjectInfos()
            {
                var byNamespace = new SortedDictionary <string, List <string> >();

                foreach (var document in code)
                {
                    var ns = CodeReader.Namespace(document);
                    if (byNamespace.TryGetValue(ns, out var doc))
                    {
                        doc.Add(document);
                    }
                    else
                    {
                        byNamespace[ns] = new List <string> {
                            document
                        };
                    }
                }

                var byProject = new SortedDictionary <string, List <KeyValuePair <string, List <string> > > >();

                foreach (var kvp in byNamespace)
                {
                    var last = byProject.Keys.LastOrDefault();
                    var ns   = kvp.Key;
                    if (last != null &&
                        ns.Contains(last))
                    {
                        byProject[last].Add(kvp);
                    }
                    else
                    {
                        byProject.Add(ns, new List <KeyValuePair <string, List <string> > > {
                            kvp
                        });
                    }
                }

                foreach (var kvp in byProject)
                {
                    var assemblyName = kvp.Key;
                    var projectId    = ProjectId.CreateNewId(assemblyName);
                    yield return(ProjectInfo.Create(
                                     projectId,
                                     VersionStamp.Default,
                                     assemblyName,
                                     assemblyName,
                                     LanguageNames.CSharp,
                                     compilationOptions: compilationOptions,
                                     metadataReferences: metadataReferences,
                                     documents: kvp.Value.SelectMany(x => x.Value)
                                     .Select(
                                         x =>
                    {
                        var documentName = CodeReader.FileName(x);
                        return DocumentInfo.Create(
                            DocumentId.CreateNewId(projectId, documentName),
                            documentName,
                            sourceCodeKind: SourceCodeKind.Regular,
                            loader: new StringLoader(x));
                    }))
                                 .WithParseOptions(CSharpParseOptions.Default.WithLanguageVersion(languageVersion)));
                }
            }

            IEnumerable <ProjectReference> FindReferences(ProjectInfo projectInfo)
            {
                var references = new List <ProjectReference>();

                foreach (var other in solutionInfo.Projects.Where(x => x.Id != projectInfo.Id))
                {
                    if (projectInfo.Documents.Any(x => x.TextLoader is StringLoader stringLoader &&
                                                  (stringLoader.Code.Contains($"using {other.Name};") ||
                                                   stringLoader.Code.Contains($"{other.Name}."))))
                    {
                        references.Add(new ProjectReference(other.Id));
                    }
                }

                return(references);
            }
        }
コード例 #8
0
        /// <summary>
        /// Verify that two strings of code are equal. Agnostic to end of line characters.
        /// </summary>
        /// <param name="expected">The expected code.</param>
        /// <param name="actual">The actual code.</param>
        /// <param name="messageHeader">The first line to add to the exception message on error.</param>
        internal static void AreEqual(string expected, string actual, string messageHeader)
        {
            var expectedPos = 0;
            var actualPos   = 0;
            var line        = 1;

            while (expectedPos < expected.Length && actualPos < actual.Length)
            {
                var ec = expected[expectedPos];
                var ac = actual[actualPos];
                if (ec == '\r' || ac == '\r')
                {
                    if (ec == '\r')
                    {
                        expectedPos++;
                    }

                    if (ac == '\r')
                    {
                        actualPos++;
                    }

                    continue;
                }

                if (ec != ac)
                {
                    if (IsAt(expected, expectedPos, "\\r") ||
                        IsAt(actual, actualPos, "\\r"))
                    {
                        if (IsAt(expected, expectedPos, "\\r"))
                        {
                            expectedPos += 2;
                        }

                        if (IsAt(actual, actualPos, "\\r"))
                        {
                            actualPos += 2;
                        }

                        continue;
                    }

                    var errorBuilder = StringBuilderPool.Borrow();
                    if (messageHeader != null)
                    {
                        errorBuilder.AppendLine(messageHeader);
                    }

                    if (!IsSingleLine(expected) ||
                        !IsSingleLine(actual))
                    {
                        errorBuilder.AppendLine(
                            CodeReader.TryGetFileName(expected, out var fileName)
                                ? $"Mismatch on line {line} of file {fileName}."
                                : $"Mismatch on line {line}.");
                    }

                    var expectedLine = expected.Split('\n')[line - 1].Trim('\r');
                    var actualLine   = actual.Split('\n')[line - 1].Trim('\r');
                    var diffPos      = DiffPos(expectedLine, actualLine);

                    errorBuilder.AppendLine($"Expected: {expectedLine}")
                    .AppendLine($"Actual:   {actualLine}")
                    .AppendLine($"          {new string(' ', diffPos)}^");

                    if (!IsSingleLine(expected) ||
                        !IsSingleLine(actual))
                    {
                        errorBuilder.AppendLine("Expected:")
                        .Append(expected)
                        .AppendLine()
                        .AppendLine("Actual:")
                        .Append(actual)
                        .AppendLine();
                    }

                    throw new AssertException(errorBuilder.Return());
                }

                if (ec == '\n')
                {
                    line++;
                }

                expectedPos++;
                actualPos++;
            }

            while (expectedPos < expected.Length && expected[expectedPos] == '\r')
            {
                expectedPos++;
            }

            while (actualPos < actual.Length && actual[actualPos] == '\r')
            {
                actualPos++;
            }

            if (expectedPos == expected.Length && actualPos == actual.Length)
            {
                return;
            }

            var messageBuilder = StringBuilderPool.Borrow();

            if (messageHeader != null)
            {
                messageBuilder.AppendLine(messageHeader);
            }

            messageBuilder.AppendLine(CodeReader.TryGetFileName(expected, out var name) ? $"Mismatch at end of file {name}." : "Mismatch at end.")
            .Append("Expected: ").AppendLine(GetEnd(expected))
            .Append("Actual:   ").AppendLine(GetEnd(actual))
            .AppendLine($"          {new string(' ', DiffPos(GetEnd(expected), GetEnd(actual)))}^");

            if (!IsSingleLine(expected) ||
                !IsSingleLine(actual))
            {
                messageBuilder.AppendLine("Expected:")
                .Append(expected)
                .AppendLine()
                .AppendLine("Actual:")
                .Append(actual)
                .AppendLine();
            }

            throw new AssertException(messageBuilder.Return());

            bool IsSingleLine(string text)
            {
                bool foundNewLine = false;

                foreach (var c in text)
                {
                    switch (c)
                    {
                    case '\r':
                    case '\n':
                        foundNewLine = true;
                        break;

                    default:
                        if (foundNewLine)
                        {
                            return(false);
                        }

                        break;
                    }
                }

                return(true);
            }

            int DiffPos(string expectedLine, string actualLine)
            {
                var diffPos = Math.Min(expectedLine.Length, actualLine.Length);

                for (var i = 0; i < Math.Min(expectedLine.Length, actualLine.Length); i++)
                {
                    if (expectedLine[i] != actualLine[i])
                    {
                        diffPos = i;
                        break;
                    }
                }

                return(diffPos);
            }

            string GetEnd(string text)
            {
                bool lastLine = false;
                var  builder  = StringBuilderPool.Borrow();

                for (var i = text.Length - 1; i >= 0; i--)
                {
                    switch (text[i])
                    {
                    case '\r':
                        if (lastLine)
                        {
                            return(builder.Return());
                        }

                        builder.Insert(0, "\\r");
                        break;

                    case '\n':
                        if (lastLine)
                        {
                            return(builder.Return());
                        }

                        builder.Insert(0, "\\n");
                        break;

                    default:
                        lastLine = true;
                        builder.Insert(0, text[i]);
                        break;
                    }
                }

                return(builder.Return());
            }
        }
コード例 #9
0
        private static async Task VerifyNoCompilerErrorsAsync(CodeFixProvider fix, Solution fixedSolution)
        {
            var diagnostics = await Analyze.GetDiagnosticsAsync(fixedSolution).ConfigureAwait(false);

            var introducedDiagnostics = diagnostics
                                        .SelectMany(x => x)
                                        .Where(IsIncluded)
                                        .ToArray();

            if (introducedDiagnostics.Select(x => x.Id)
#pragma warning disable CS0618 // Suppress until removed. Will be replaced with Metadatareferences.FromAttributes()
                .Except(SuppressedDiagnostics)
#pragma warning restore CS0618 // Suppress until removed. Will be replaced with Metadatareferences.FromAttributes()
                .Any())
            {
                var errorBuilder = StringBuilderPool.Borrow();
                errorBuilder.AppendLine($"{fix.GetType().Name} introduced syntax error{(introducedDiagnostics.Length > 1 ? "s" : string.Empty)}.");
                foreach (var introducedDiagnostic in introducedDiagnostics)
                {
                    errorBuilder.AppendLine($"{introducedDiagnostic.ToErrorString()}");
                }

                var sources = await Task.WhenAll(fixedSolution.Projects.SelectMany(p => p.Documents).Select(d => CodeReader.GetStringFromDocumentAsync(d, CancellationToken.None)));

                errorBuilder.AppendLine("First source file with error is:");
                var lineSpan = introducedDiagnostics.First().Location.GetMappedLineSpan();
                if (sources.TrySingle(x => CodeReader.FileName(x) == lineSpan.Path, out var match))
                {
                    errorBuilder.AppendLine(match);
                }
                else if (sources.TryFirst(x => CodeReader.FileName(x) == lineSpan.Path, out _))
                {
                    errorBuilder.AppendLine($"Found more than one document for {lineSpan.Path}.");
                    foreach (string source in sources.Where(x => CodeReader.FileName(x) == lineSpan.Path))
                    {
                        errorBuilder.AppendLine(source);
                    }
                }
                else
                {
                    errorBuilder.AppendLine($"Did not find a single document for {lineSpan.Path}.");
                }

                throw new AssertException(errorBuilder.Return());
            }
        }
コード例 #10
0
ファイル: CodeAssert.cs プロジェクト: forki/Gu.Roslyn.Asserts
        /// <summary>
        /// Verify that two strings of code are equal. Agnostic to end of line characters.
        /// </summary>
        /// <param name="expected">The expected code.</param>
        /// <param name="actual">The actual code.</param>
        /// <param name="messageHeader">The first line to add to the exception message on error.</param>
        internal static void AreEqual(string expected, string actual, string messageHeader)
        {
            var expectedPos = 0;
            var actualPos   = 0;
            var line        = 1;

            while (expectedPos < expected.Length && actualPos < actual.Length)
            {
                var ec = expected[expectedPos];
                var ac = actual[actualPos];
                if (ec == '\r' || ac == '\r')
                {
                    if (ec == '\r')
                    {
                        expectedPos++;
                    }

                    if (ac == '\r')
                    {
                        actualPos++;
                    }

                    continue;
                }

                if (ec != ac)
                {
                    var errorBuilder = StringBuilderPool.Borrow();
                    if (messageHeader != null)
                    {
                        errorBuilder.AppendLine(messageHeader);
                    }

                    errorBuilder.AppendLine($"Mismatch on line {line} of file {CodeReader.FileName(expected)}");
                    var expectedLine = expected.Split('\n')[line - 1].Trim('\r');
                    var actualLine   = actual.Split('\n')[line - 1].Trim('\r');
                    var diffPos      = Math.Min(expectedLine.Length, actualLine.Length);
                    for (var i = 0; i < Math.Min(expectedLine.Length, actualLine.Length); i++)
                    {
                        if (expectedLine[i] != actualLine[i])
                        {
                            diffPos = i;
                            break;
                        }
                    }

                    errorBuilder.AppendLine($"Expected: {expectedLine}")
                    .AppendLine($"Actual:   {actualLine}")
                    .AppendLine($"          {new string(' ', diffPos)}^")
                    .AppendLine("Expected:")
                    .Append(expected)
                    .AppendLine()
                    .AppendLine("Actual:")
                    .Append(actual)
                    .AppendLine();

                    throw AssertException.Create(errorBuilder.Return());
                }

                if (ec == '\n')
                {
                    line++;
                }

                expectedPos++;
                actualPos++;
            }

            while (expectedPos < expected.Length && expected[expectedPos] == '\r')
            {
                expectedPos++;
            }

            while (actualPos < actual.Length && actual[actualPos] == '\r')
            {
                actualPos++;
            }

            if (expectedPos == expected.Length && actualPos == actual.Length)
            {
                return;
            }

            if (messageHeader != null)
            {
                var message = StringBuilderPool.Borrow()
                              .AppendLine(messageHeader)
                              .AppendLine($"Mismatch at end of file {CodeReader.FileName(expected)}")
                              .AppendLine("Expected:")
                              .Append(expected)
                              .AppendLine()
                              .AppendLine("Actual:")
                              .Append(actual)
                              .AppendLine()
                              .Return();
                throw AssertException.Create(message);
            }
            else
            {
                var message = StringBuilderPool.Borrow()
                              .AppendLine($"Mismatch at end of file {CodeReader.FileName(expected)}")
                              .AppendLine("Expected:")
                              .Append(expected)
                              .AppendLine()
                              .AppendLine("Actual:")
                              .Append(actual)
                              .AppendLine()
                              .Return();
                throw AssertException.Create(message);
            }
        }
コード例 #11
0
        /// <summary>
        /// Verifies that
        /// 1. <paramref name="solution"/> produces the expected diagnostics
        /// 2. The code fix fixes the code.
        /// </summary>
        /// <param name="analyzer">The analyzer to run on the code..</param>
        /// <param name="fix">The code fix to apply.</param>
        /// <param name="expectedDiagnostic">The expected diagnostic.</param>
        /// <param name="solution">The code with error positions indicated.</param>
        /// <param name="fixedCode">The expected code produced by the code fix.</param>
        /// <param name="fixTitle">The title of the fix to apply if more than one.</param>
        /// <param name="allowCompilationErrors">If compilation errors are accepted in the fixed code.</param>
        public static void CodeFix(DiagnosticAnalyzer analyzer, CodeFixProvider fix, ExpectedDiagnostic expectedDiagnostic, Solution solution, string fixedCode, string fixTitle = null, AllowCompilationErrors allowCompilationErrors = AllowCompilationErrors.No)
        {
            VerifyAnalyzerSupportsDiagnostic(analyzer, expectedDiagnostic);
            VerifyCodeFixSupportsAnalyzer(analyzer, fix);
            var diagnostics           = Analyze.GetDiagnostics(analyzer, solution);
            var diagnosticsAndSources = DiagnosticsAndSources.Create(expectedDiagnostic, solution.Projects.SelectMany(x => x.Documents).Select(x => CodeReader.GetCode(x, null)).ToArray());

            VerifyDiagnostics(diagnosticsAndSources, diagnostics);
            VerifyFix(solution, diagnostics, analyzer, fix, fixedCode, fixTitle, allowCompilationErrors);
        }
コード例 #12
0
        /// <summary>
        /// Verify that two strings of code are equal. Agnostic to end of line characters.
        /// </summary>
        /// <param name="expected">The expected code.</param>
        /// <param name="actual">The actual code.</param>
        /// <param name="messageHeader">The first line to add to the exception message on error.</param>
        internal static void AreEqual(string expected, string actual, string?messageHeader)
        {
            var expectedPos = 0;
            var actualPos   = 0;
            var line        = 1;

            while (expectedPos < expected.Length && actualPos < actual.Length)
            {
                var ec = expected[expectedPos];
                var ac = actual[actualPos];
                if (ec == '\r' || ac == '\r')
                {
                    if (ec == '\r')
                    {
                        expectedPos++;
                    }

                    if (ac == '\r')
                    {
                        actualPos++;
                    }

                    continue;
                }

                if (ec != ac)
                {
                    if (IsAt(expected, expectedPos, "\\r") ||
                        IsAt(actual, actualPos, "\\r"))
                    {
                        if (IsAt(expected, expectedPos, "\\r"))
                        {
                            expectedPos += 2;
                        }

                        if (IsAt(actual, actualPos, "\\r"))
                        {
                            actualPos += 2;
                        }

                        continue;
                    }

                    var errorBuilder = StringBuilderPool.Borrow();
                    if (messageHeader != null)
                    {
                        errorBuilder.AppendLine(messageHeader);
                    }

                    if (!IsSingleLine(expected) ||
                        !IsSingleLine(actual))
                    {
                        errorBuilder.AppendLine(
                            CodeReader.TryGetFileName(expected, out var fileName)
                                ? $"Mismatch on line {line} of file {fileName}."
                                : $"Mismatch on line {line}.");
                    }

                    var expectedLine = expected.Split('\n')[line - 1].Trim('\r');
                    var actualLine   = actual.Split('\n')[line - 1].Trim('\r');
                    var diffPos      = DiffPos(expectedLine, actualLine);

                    errorBuilder.AppendLine($"Expected: {expectedLine}")
                    .AppendLine($"Actual:   {actualLine}")
                    .AppendLine($"          {new string(' ', diffPos)}^");

                    if (!IsSingleLine(expected) ||
                        !IsSingleLine(actual))
                    {
                        errorBuilder.AppendLine("Expected:")
                        .Append(expected)
                        .AppendLine()
                        .AppendLine("Actual:")
                        .Append(actual)
                        .AppendLine();
                    }

                    throw new AssertException(errorBuilder.Return());
                }

                if (ec == '\n')
                {
                    line++;
                }

                expectedPos++;
                actualPos++;
            }

            while (expectedPos < expected.Length && expected[expectedPos] == '\r')
            {
                expectedPos++;
            }

            while (actualPos < actual.Length && actual[actualPos] == '\r')
            {
                actualPos++;
            }

            if (expectedPos == expected.Length && actualPos == actual.Length)
            {
                return;
            }

            var messageBuilder = StringBuilderPool.Borrow();

            if (messageHeader != null)
            {
                messageBuilder.AppendLine(messageHeader);
            }

            messageBuilder.AppendLine(CodeReader.TryGetFileName(expected, out var name) ? $"Mismatch at end of file {name}." : "Mismatch at end.")
            .Append("Expected: ").AppendLine(GetEnd(expected))
            .Append("Actual:   ").AppendLine(GetEnd(actual))
            .AppendLine($"          {new string(' ', DiffPos(GetEnd(expected), GetEnd(actual)))}^");

            if (!IsSingleLine(expected) ||
                !IsSingleLine(actual))
            {
                messageBuilder.AppendLine("Expected:")
                .Append(expected)
                .AppendLine()
                .AppendLine("Actual:")
                .Append(actual)
                .AppendLine();
            }

            throw new AssertException(messageBuilder.Return());