File: Language\IntegrationTests\IntermediateNodeVerifier.cs
Web Access
Project: src\src\Razor\src\Shared\Microsoft.AspNetCore.Razor.Test.Common\Microsoft.AspNetCore.Razor.Test.Common.csproj (Microsoft.AspNetCore.Razor.Test.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Xunit;
using Xunit.Sdk;
 
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests;
 
public static class IntermediateNodeVerifier
{
    public static void Verify(IntermediateNode node, string[] baseline)
    {
        var walker = new Walker(baseline);
        walker.Visit(node);
        walker.AssertReachedEndOfBaseline();
    }
 
    private sealed class Walker : IntermediateNodeWalker
    {
        private readonly string[] _baseline;
        private readonly IntermediateNodeWriter _visitor;
        private readonly StringWriter _writer;
 
        private int _index;
 
        public Walker(string[] baseline)
        {
            _baseline = baseline;
            _writer = new StringWriter();
            _visitor = new IntermediateNodeWriter(_writer);
        }
 
        public override void VisitDefault(IntermediateNode node)
        {
            var expected = _index < _baseline.Length ? _baseline[_index++] : null;
 
            // Write the node as text for comparison
            _writer.GetStringBuilder().Clear();
            _visitor.Visit(node);
            var actual = _writer.GetStringBuilder().ToString();
 
            AssertNodeEquals(node, Ancestors, expected, actual);
 
            _visitor.Depth++;
            base.VisitDefault(node);
            _visitor.Depth--;
        }
 
        public void AssertReachedEndOfBaseline()
        {
            // Since we're walking the nodes of our generated code there's the chance that our baseline is longer.
            Assert.True(_baseline.Length == _index, "Not all lines of the baseline were visited!");
        }
 
        private static void AssertNodeEquals(IntermediateNode node, ReadOnlySpan<IntermediateNode> ancestors, string? expected, string actual)
        {
            if (string.Equals(expected, actual))
            {
                // YAY!!! everything is great.
                return;
            }
 
            if (expected == null)
            {
                var message = "The node is missing from baseline.";
                throw new IntermediateNodeBaselineException(node, ancestors, expected, actual, message);
            }
 
            var charsVerified = 0;
            AssertNestingEqual(node, ancestors, expected, actual, ref charsVerified);
            AssertNameEqual(node, ancestors, expected, actual, ref charsVerified);
            AssertDelimiter(expected, actual, true, ref charsVerified);
            AssertLocationEqual(node, ancestors, expected, actual, ref charsVerified);
            AssertDelimiter(expected, actual, false, ref charsVerified);
            AssertContentEqual(node, ancestors, expected, actual, ref charsVerified);
 
            throw new InvalidOperationException("We can't figure out HOW these two things are different. This is a bug.");
        }
 
        private static void AssertNestingEqual(
            IntermediateNode node, ReadOnlySpan<IntermediateNode> ancestors, string expected, string actual, ref int charsVerified)
        {
            var i = 0;
            for (; i < expected.Length; i++)
            {
                if (expected[i] != ' ')
                {
                    break;
                }
            }
 
            var failed = false;
            var j = 0;
            for (; j < i; j++)
            {
                if (actual.Length <= j || actual[j] != ' ')
                {
                    failed = true;
                    break;
                }
            }
 
            if (actual.Length <= j + 1 || actual[j] == ' ')
            {
                failed = true;
            }
 
            if (failed)
            {
                var message = "The node is at the wrong level of nesting. This usually means a child is missing.";
                throw new IntermediateNodeBaselineException(node, ancestors, expected, actual, message);
            }
 
            charsVerified = j;
        }
 
        private static void AssertNameEqual(
            IntermediateNode node, ReadOnlySpan<IntermediateNode> ancestors, string expected, string actual, ref int charsVerified)
        {
            var expectedName = GetName(expected, charsVerified);
            var actualName = GetName(actual, charsVerified);
 
            if (!string.Equals(expectedName, actualName))
            {
                var message = "Node names are not equal.";
                throw new IntermediateNodeBaselineException(node, ancestors, expected, actual, message);
            }
 
            charsVerified += expectedName.Length;
        }
 
        // Either both strings need to have a delimiter next or neither should.
        private static void AssertDelimiter(string expected, string actual, bool required, ref int charsVerified)
        {
            if (charsVerified == expected.Length && required)
            {
                throw new InvalidOperationException($"Baseline text is not well-formed: '{expected}'.");
            }
 
            if (charsVerified == actual.Length && required)
            {
                throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
            }
 
            if (charsVerified == expected.Length && charsVerified == actual.Length)
            {
                return;
            }
 
            var expectedDelimiter = expected.IndexOf(" - ", charsVerified, StringComparison.Ordinal);
            if (expectedDelimiter != charsVerified && expectedDelimiter != -1)
            {
                throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
            }
 
            var actualDelimiter = actual.IndexOf(" - ", charsVerified, StringComparison.Ordinal);
            if (actualDelimiter != charsVerified && actualDelimiter != -1)
            {
                throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'.");
            }
 
            Assert.Equal(expectedDelimiter, actualDelimiter);
 
            charsVerified += 3;
        }
 
        private static void AssertLocationEqual(
            IntermediateNode node, ReadOnlySpan<IntermediateNode> ancestors, string expected, string actual, ref int charsVerified)
        {
            var expectedLocation = GetLocation(expected, charsVerified);
            var actualLocation = GetLocation(actual, charsVerified);
 
            if (expectedLocation != actualLocation)
            {
                var message = "Locations are not equal.";
                throw new IntermediateNodeBaselineException(node, ancestors, expected, actual, message);
            }
 
            charsVerified += expectedLocation.Length;
        }
 
        private static void AssertContentEqual(
            IntermediateNode node, ReadOnlySpan<IntermediateNode> ancestors, string expected, string actual, ref int charsVerified)
        {
            var expectedContent = GetContent(expected, charsVerified);
            var actualContent = GetContent(actual, charsVerified);
 
            if (expectedContent != actualContent)
            {
                var message = "Contents are not equal.";
                throw new IntermediateNodeBaselineException(node, ancestors, expected, actual, message);
            }
 
            charsVerified += expectedContent.Length;
        }
 
        private static string GetName(string text, int start)
        {
            var delimiter = text.IndexOf(" - ", start, StringComparison.Ordinal);
            if (delimiter == -1)
            {
                throw new InvalidOperationException($"Baseline text is not well-formed: '{text}'.");
            }
 
            return text[start..delimiter];
        }
 
        private static string GetLocation(string text, int start)
        {
            var delimiter = text.IndexOf(" - ", start, StringComparison.Ordinal);
            return delimiter == -1 ? text[start..] : text[start..delimiter];
        }
 
        private static string GetContent(string text, int start)
        {
            return start == text.Length ? string.Empty : text[start..];
        }
 
        private sealed class IntermediateNodeBaselineException : XunitException
        {
            public IntermediateNode Node { get; }
            public string? Actual { get; }
            public string? Expected { get; }
 
            public IntermediateNodeBaselineException(
                IntermediateNode node,
                ReadOnlySpan<IntermediateNode> ancestors,
                string? expected,
                string? actual,
                string userMessage)
                : base(Format(ancestors, expected, actual, userMessage))
            {
                Node = node;
                Expected = expected;
                Actual = actual;
            }
 
            private static string Format(ReadOnlySpan<IntermediateNode> ancestors, string? expected, string? actual, string userMessage)
            {
                using var _ = StringBuilderPool.GetPooledObject(out var builder);
 
                builder.AppendLine(userMessage);
                builder.AppendLine();
 
                if (expected != null)
                {
                    builder.Append("Expected: ");
                    builder.AppendLine(expected);
                }
 
                if (actual != null)
                {
                    builder.Append("Actual: ");
                    builder.AppendLine(actual);
                }
 
                if (!ancestors.IsEmpty)
                {
                    builder.AppendLine();
                    builder.AppendLine("Path:");
 
                    foreach (var ancestor in ancestors)
                    {
                        builder.AppendLine(ancestor.ToString());
                    }
                }
 
                return builder.ToString();
            }
        }
    }
}