File: Assert\AssertXml.cs
Web Access
Project: src\src\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj (Microsoft.CodeAnalysis.Test.Utilities)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Roslyn.Utilities;
using Xunit;
using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer;
 
namespace Roslyn.Test.Utilities
{
    /// <summary>
    /// There are many ways to compare XML documents.  This class aims to provide functionality somewhere
    /// between a straight string comparison and a fully-configurable XML tree comparison.  In particular,
    /// given a shallow comparer (i.e. one that does not consider children), it will compare the root elements
    /// and, if they are equal, match up children by shallow equality, recursing on each pair.
    /// </summary>
    public static class AssertXml
    {
        public static void Equal(string expected, string actual)
        {
            Equal(XElement.Parse(expected), XElement.Parse(actual), message: null, expectedValueSourcePath: null, expectedValueSourceLine: 0, expectedIsXmlLiteral: true);
        }
 
        public static void Equal(XElement expected, XElement actual)
        {
            Equal(expected, actual, message: null, expectedValueSourcePath: null, expectedValueSourceLine: 0, expectedIsXmlLiteral: false);
        }
 
        /// <summary>
        /// Compare two XElements.  Assumed to be non-null.
        /// </summary>
        public static void Equal(
            XElement expectedRoot,
            XElement actualRoot,
            string message,
            string expectedValueSourcePath,
            int expectedValueSourceLine,
            bool expectedIsXmlLiteral)
        {
            if (!CheckEqual(expectedRoot, actualRoot, ShallowElementComparer.Instance, out var firstMismatch))
            {
                Assert.True(false, message +
                    GetAssertText(
                        GetXmlString(expectedRoot, expectedIsXmlLiteral),
                        GetXmlString(actualRoot, expectedIsXmlLiteral),
                        expectedRoot,
                        firstMismatch,
                        expectedValueSourcePath,
                        expectedValueSourceLine,
                        expectedIsXmlLiteral));
            }
        }
 
        private static string GetXmlString(XElement node, bool expectedIsXmlLiteral)
        {
            using (var sw = new StringWriter(CultureInfo.InvariantCulture))
            {
                var ws = new XmlWriterSettings()
                {
                    IndentChars = expectedIsXmlLiteral ? "    " : "  ",
                    OmitXmlDeclaration = true,
                    Indent = true
                };
 
                using (var w = XmlWriter.Create(sw, ws))
                {
                    node.WriteTo(w);
                }
 
                return sw.ToString();
            }
        }
 
        /// <summary>
        /// Helpful diff output message.  Can be printed as either an XML literal (VB) or a string literal (C#).
        /// </summary>
        private static string GetAssertText(
            string expected,
            string actual,
            XElement expectedRoot,
            Tuple<XElement, XElement> firstMismatch,
            string expectedValueSourcePath,
            int expectedValueSourceLine,
            bool expectedIsXmlLiteral)
        {
            StringBuilder assertText = new StringBuilder();
 
            string actualString = expectedIsXmlLiteral ? actual.Replace(" />\r\n", "/>\r\n") : string.Format("@\"{0}\"", actual.Replace("\"", "\"\""));
            string expectedString = expectedIsXmlLiteral ? expected.Replace(" />\r\n", "/>\r\n") : string.Format("@\"{0}\"", expected.Replace("\"", "\"\""));
 
            if (AssertEx.TryGenerateExpectedSourceFileAndGetDiffLink(actualString, expectedString.Count(c => c == '\n') + 1, expectedValueSourcePath, expectedValueSourceLine, out var link))
            {
                assertText.AppendLine(link);
            }
            else
            {
                assertText.AppendLine("Expected");
                assertText.AppendLine("====");
                assertText.AppendLine(expectedString);
                assertText.AppendLine();
 
                if (firstMismatch.Item1 != expectedRoot)
                {
                    assertText.AppendLine("First Difference");
                    assertText.AppendLine("====");
                    assertText.AppendLine("Expected Fragment");
                    assertText.AppendLine("----");
                    assertText.AppendLine(firstMismatch.Item1.ToString());
                    assertText.AppendLine();
                    assertText.AppendLine("Actual Fragment");
                    assertText.AppendLine("----");
                    assertText.AppendLine(firstMismatch.Item2.ToString());
                    assertText.AppendLine();
                }
 
                assertText.AppendLine("Actual");
                assertText.AppendLine("====");
                assertText.AppendLine(actualString);
                assertText.AppendLine();
            }
 
            return assertText.ToString();
        }
 
        /// <summary>
        /// Compare the root elements and, if they are equal, match up children by shallow equality, recursing on each pair.
        /// </summary>
        /// <returns>True if the elements are equal, false otherwise (in which case, firstMismatch will try to indicate a point of disagreement).</returns>
        private static bool CheckEqual(XElement expectedRoot, XElement actualRoot, IEqualityComparer<XElement> shallowComparer, out Tuple<XElement, XElement> firstMismatch)
        {
            Assert.NotNull(expectedRoot);
            Assert.NotNull(actualRoot);
            Assert.NotNull(shallowComparer);
 
            Tuple<XElement, XElement> rootPair = new Tuple<XElement, XElement>(expectedRoot, actualRoot);
 
            if (!shallowComparer.Equals(expectedRoot, actualRoot))
            {
                firstMismatch = rootPair;
                return false;
            }
 
            Stack<Tuple<XElement, XElement>> stack = new Stack<Tuple<XElement, XElement>>();
            stack.Push(rootPair);
 
            while (stack.Count > 0)
            {
                Tuple<XElement, XElement> pair = stack.Pop();
                firstMismatch = pair; // Will be overwritten if this pair is a match.
                Debug.Assert(shallowComparer.Equals(pair.Item1, pair.Item2)); // Shouldn't have been pushed otherwise.
 
                XElement[] children1 = pair.Item1.Elements().ToArray();
                MultiDictionary<XElement, XElement> children2Dict = new MultiDictionary<XElement, XElement>(shallowComparer);
 
                int children2Count = 0;
                foreach (XElement child in pair.Item2.Elements())
                {
                    children2Dict.Add(child, child);
                    children2Count++;
                }
 
                if (children1.Length != children2Count)
                {
                    return false;
                }
 
                HashSet<XElement> children2Used = new HashSet<XElement>(ReferenceEqualityComparer.Instance);
                foreach (XElement child1 in children1)
                {
                    XElement child2 = null;
                    foreach (var candidate in children2Dict[child1])
                    {
                        if (!children2Used.Contains(candidate))
                        {
                            child2 = candidate;
                            break;
                        }
                    }
 
                    if (child2 == null)
                    {
                        return false;
                    }
                    else
                    {
                        children2Used.Add(child2);
                        stack.Push(new Tuple<XElement, XElement>(child1, child2));
                    }
                }
 
                if (children2Used.Count < children1.Length)
                {
                    return false;
                }
            }
 
            firstMismatch = null;
            return true;
        }
 
        private class ShallowElementComparer : IEqualityComparer<XElement>
        {
            public static readonly IEqualityComparer<XElement> Instance = new ShallowElementComparer();
 
            private ShallowElementComparer() { }
 
            public bool Equals(XElement element1, XElement element2)
            {
                Assert.NotNull(element1);
                Assert.NotNull(element2);
 
                return element1.Name == "customDebugInfo"
                    ? element1.ToString() == element2.ToString()
                    : AssertXml.NameAndAttributeComparer.Instance.Equals(element1, element2);
            }
 
            public int GetHashCode(XElement element)
            {
                return element.Name.GetHashCode();
            }
        }
 
        /// <summary>
        /// Convenience shallow element comparer.  Checks names and attribute name-value pairs (ignoring order).
        /// </summary>
        private class NameAndAttributeComparer : IEqualityComparer<XElement>
        {
            public static readonly IEqualityComparer<XElement> Instance = new NameAndAttributeComparer();
 
            private NameAndAttributeComparer() { }
 
            public bool Equals(XElement element1, XElement element2)
            {
                Assert.NotNull(element1);
                Assert.NotNull(element2);
 
                if (element1.Name != element2.Name)
                {
                    return false;
                }
 
                IEnumerable<Tuple<XName, string>> attributes1 = element1.Attributes().Select(MakeAttributeTuple);
                IEnumerable<Tuple<XName, string>> attributes2 = element2.Attributes().Select(MakeAttributeTuple);
 
                return attributes1.SetEquals(attributes2);
            }
 
            public int GetHashCode(XElement element)
            {
                return element.Name.GetHashCode();
            }
 
            private static Tuple<XName, string> MakeAttributeTuple(XAttribute attribute)
            {
                return new Tuple<XName, string>(attribute.Name, attribute.Value);
            }
        }
    }
}