File: Assert\AssertEx.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using DiffPlex;
using DiffPlex.Chunkers;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
 
namespace Roslyn.Test.Utilities
{
    /// <summary>
    /// Assert style type to deal with the lack of features in xUnit's Assert type
    /// </summary>
    public static class AssertEx
    {
        private static readonly IChunker s_lineChunker = new LineChunker();
        private static readonly IChunker s_lineEndingsPreservingChunker = new LineEndingsPreservingChunker();
        private static readonly InlineDiffBuilder s_diffBuilder = new InlineDiffBuilder(new Differ());
 
        #region AssertEqualityComparer<T>
 
        private class AssertEqualityComparer<T> : IEqualityComparer<T>
        {
            public static readonly IEqualityComparer<T> Instance = new AssertEqualityComparer<T>();
 
            private static bool CanBeNull()
            {
                var type = typeof(T);
                return !type.GetTypeInfo().IsValueType ||
                    (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
            }
 
            public static bool IsNull(T @object)
            {
                if (!CanBeNull())
                {
                    return false;
                }
 
                return object.Equals(@object, default(T));
            }
 
            public static bool Equals(T left, T right)
            {
                return Instance.Equals(left, right);
            }
 
            bool IEqualityComparer<T>.Equals(T x, T y)
            {
                if (CanBeNull())
                {
                    if (object.Equals(x, default(T)))
                    {
                        return object.Equals(y, default(T));
                    }
 
                    if (object.Equals(y, default(T)))
                    {
                        return false;
                    }
                }
 
                if (x is IEquatable<T> equatable)
                {
                    return equatable.Equals(y);
                }
 
                if (x is IComparable<T> comparableT)
                {
                    return comparableT.CompareTo(y) == 0;
                }
 
                if (x is IComparable comparable)
                {
                    return comparable.CompareTo(y) == 0;
                }
 
                if (x is IEnumerable enumerableX && y is IEnumerable enumerableY)
                {
                    var enumeratorX = enumerableX.GetEnumerator();
                    var enumeratorY = enumerableY.GetEnumerator();
 
                    while (true)
                    {
                        bool hasNextX = enumeratorX.MoveNext();
                        bool hasNextY = enumeratorY.MoveNext();
 
                        if (!hasNextX || !hasNextY)
                        {
                            return hasNextX == hasNextY;
                        }
 
                        if (!Equals(enumeratorX.Current, enumeratorY.Current))
                        {
                            return false;
                        }
                    }
                }
 
                return object.Equals(x, y);
            }
 
            int IEqualityComparer<T>.GetHashCode(T obj)
            {
                throw new NotImplementedException();
            }
        }
 
        #endregion
 
        public static void AreEqual<T>(T expected, T actual, string message = null, IEqualityComparer<T> comparer = null)
        {
            if (ReferenceEquals(expected, actual))
            {
                return;
            }
 
            if (expected == null)
            {
                Fail("expected was null, but actual wasn't" + Environment.NewLine + message);
            }
            else if (actual == null)
            {
                Fail("actual was null, but expected wasn't" + Environment.NewLine + message);
            }
            else if (!(comparer ?? AssertEqualityComparer<T>.Instance).Equals(expected, actual))
            {
                string expectedAndActual;
                if (expected is IEnumerable expectedEnumerable && actual is IEnumerable actualEnumerable)
                {
                    expectedAndActual = GetAssertMessage(expectedEnumerable.OfType<object>(), actualEnumerable.OfType<object>(), comparer: null);
                }
                else
                {
                    expectedAndActual = $"""
                        Expected:
                        {expected}
                        Actual:
                        {actual}
                        """;
                }
 
                Fail(message + Environment.NewLine + expectedAndActual);
            }
        }
 
        public static void Equal<T>(ImmutableArray<T> expected, IEnumerable<T> actual, IEqualityComparer<T> comparer = null, string message = null)
        {
            if (actual == null || expected.IsDefault)
            {
                Assert.True((actual == null) == expected.IsDefault, message);
            }
            else
            {
                Equal((IEnumerable<T>)expected, actual, comparer, message);
            }
        }
 
        public static void Equal<T>(IEnumerable<T> expected, ImmutableArray<T> actual, IEqualityComparer<T> comparer = null, string message = null, string itemSeparator = null)
        {
            if (expected == null || actual.IsDefault)
            {
                Assert.True((expected == null) == actual.IsDefault, message);
            }
            else
            {
                Equal(expected, (IEnumerable<T>)actual, comparer, message, itemSeparator);
            }
        }
 
        public static void Equal<T>(ImmutableArray<T> expected, ImmutableArray<T> actual, IEqualityComparer<T> comparer = null, string message = null, string itemSeparator = null)
        {
            Equal(expected, (IEnumerable<T>)actual, comparer, message, itemSeparator);
        }
 
        public static void Equal(string expected, string actual)
        {
            if (string.Equals(expected, actual, StringComparison.Ordinal))
            {
                return;
            }
 
            var message = new StringBuilder();
            message.AppendLine();
            message.AppendLine("Expected:");
            message.AppendLine(expected);
            message.AppendLine("Actual:");
            message.AppendLine(actual);
 
            Assert.True(false, message.ToString());
        }
 
        public static void Equal<T>(
            IEnumerable<T> expected,
            IEnumerable<T> actual,
            IEqualityComparer<T> comparer = null,
            string message = null,
            string itemSeparator = null,
            Func<T, string> itemInspector = null,
            string expectedValueSourcePath = null,
            int expectedValueSourceLine = 0)
        {
            if (expected == null)
            {
                Assert.Null(actual);
            }
            else
            {
                Assert.NotNull(actual);
            }
 
            if (SequenceEqual(expected, actual, comparer))
            {
                return;
            }
 
            Assert.True(false, GetAssertMessage(expected, actual, comparer, message, itemInspector, itemSeparator, expectedValueSourcePath, expectedValueSourceLine));
        }
 
        public static void Equal<T>(
            ReadOnlySpan<T> expected,
            ReadOnlySpan<T> actual,
            IEqualityComparer<T> comparer = null,
            string message = null,
            string itemSeparator = null,
            Func<T, string> itemInspector = null,
            string expectedValueSourcePath = null,
            int expectedValueSourceLine = 0)
        {
            if (SequenceEqual(expected, actual, comparer))
                return;
 
            Assert.True(false, GetAssertMessage(expected, actual, comparer, message, itemInspector, itemSeparator, expectedValueSourcePath, expectedValueSourceLine));
        }
 
        /// <summary>
        /// Asserts that two strings are equal, and prints a diff between the two if they are not.
        /// </summary>
        /// <param name="expected">The expected string. This is presented as the "baseline/before" side in the diff.</param>
        /// <param name="actual">The actual string. This is presented as the changed or "after" side in the diff.</param>
        /// <param name="message">The message to precede the diff, if the values are not equal.</param>
        public static void EqualOrDiff(string expected, string actual, string message = null)
        {
            if (expected == actual)
            {
                return;
            }
 
            var diff = s_diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false, ignoreCase: false, s_lineChunker);
            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_diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false, ignoreCase: false, s_lineEndingsPreservingChunker);
            }
 
            foreach (var line in diff.Lines)
            {
                switch (line.Type)
                {
                    case ChangeType.Inserted:
                        messageBuilder.Append('+');
                        break;
                    case ChangeType.Deleted:
                        messageBuilder.Append('-');
                        break;
                    default:
                        messageBuilder.Append(' ');
                        break;
                }
 
                messageBuilder.AppendLine(line.Text.Replace("\r", "<CR>").Replace("\n", "<LF>"));
            }
 
            Assert.True(false, messageBuilder.ToString());
        }
 
        public static void NotEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual, IEqualityComparer<T> comparer = null, string message = null)
        {
            if (ReferenceEquals(expected, actual))
            {
                Fail("expected and actual references are identical\r\n" + message);
            }
 
            if (expected == null || actual == null)
            {
                return;
            }
            else if (SequenceEqual(expected, actual, comparer))
            {
                Fail("expected and actual sequences match\r\n" + message);
            }
        }
 
        private static bool SequenceEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual, IEqualityComparer<T> comparer = null)
        {
            if (ReferenceEquals(expected, actual))
            {
                return true;
            }
 
            var enumerator1 = expected.GetEnumerator();
            var enumerator2 = actual.GetEnumerator();
 
            while (true)
            {
                var hasNext1 = enumerator1.MoveNext();
                var hasNext2 = enumerator2.MoveNext();
 
                if (hasNext1 != hasNext2)
                {
                    return false;
                }
 
                if (!hasNext1)
                {
                    break;
                }
 
                var value1 = enumerator1.Current;
                var value2 = enumerator2.Current;
 
                if (!(comparer != null ? comparer.Equals(value1, value2) : AssertEqualityComparer<T>.Equals(value1, value2)))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private static bool SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual, IEqualityComparer<T> comparer = null)
        {
            if (expected.Length != actual.Length)
                return false;
 
            for (int i = 0; i < expected.Length; i++)
            {
                if (!(comparer is not null ? comparer.Equals(expected[i], actual[i]) : AssertEqualityComparer<T>.Equals(expected[i], actual[i])))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        public static void SetEqual(IEnumerable<string> expected, IEnumerable<string> actual, IEqualityComparer<string> comparer = null, string message = null, string itemSeparator = "\r\n", Func<string, string> itemInspector = null)
        {
            var indexes = new Dictionary<string, int>(comparer);
            int counter = 0;
            foreach (var expectedItem in expected)
            {
                if (!indexes.ContainsKey(expectedItem))
                {
                    indexes.Add(expectedItem, counter++);
                }
            }
 
            SetEqual<string>(expected, actual.OrderBy(e => getIndex(e)), comparer, message, itemSeparator, itemInspector);
 
            int getIndex(string item)
            {
                // exact match to expected items
                if (indexes.TryGetValue(item, out var index))
                {
                    return index;
                }
 
                // closest match to expected items
                int closestDistance = int.MaxValue;
                string closestItem = null;
                foreach (var expectedItem in indexes.Keys)
                {
                    var distance = levenshtein(item, expectedItem);
                    if (distance < closestDistance)
                    {
                        closestDistance = distance;
                        closestItem = expectedItem;
                    }
                }
 
                if (closestItem != null)
                {
                    _ = indexes.TryGetValue(closestItem, out index);
                    return index;
                }
 
                return -1;
            }
 
            // Adapted from Toub's https://blogs.msdn.microsoft.com/toub/2006/05/05/generic-levenshtein-edit-distance-with-c/
            int levenshtein(string first, string second)
            {
                // Get the length of both.  If either is 0, return
                // the length of the other, since that number of insertions
                // would be required.
                int n = first.Length, m = second.Length;
                if (n == 0)
                    return m;
                if (m == 0)
                    return n;
 
                // Rather than maintain an entire matrix (which would require O(n*m) space),
                // just store the current row and the next row, each of which has a length m+1,
                // so just O(m) space. Initialize the current row.
                int curRow = 0, nextRow = 1;
                int[][] rows = new int[][] { new int[m + 1], new int[m + 1] };
                for (int j = 0; j <= m; ++j)
                    rows[curRow][j] = j;
 
                // For each virtual row (since we only have physical storage for two)
                for (int i = 1; i <= n; ++i)
                {
                    // Fill in the values in the row
                    rows[nextRow][0] = i;
                    for (int j = 1; j <= m; ++j)
                    {
                        int dist1 = rows[curRow][j] + 1;
                        int dist2 = rows[nextRow][j - 1] + 1;
                        int dist3 = rows[curRow][j - 1] + (first[i - 1].Equals(second[j - 1]) ? 0 : 1);
                        rows[nextRow][j] = Math.Min(dist1, Math.Min(dist2, dist3));
                    }
 
                    // Swap the current and next rows
                    if (curRow == 0)
                    {
                        curRow = 1;
                        nextRow = 0;
                    }
                    else
                    {
                        curRow = 0;
                        nextRow = 1;
                    }
                }
 
                // Return the computed edit distance
                return rows[curRow][m];
            }
        }
 
        public static void SetEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual, IEqualityComparer<T> comparer = null, string message = null, string itemSeparator = "\r\n", Func<T, string> itemInspector = null)
        {
            var expectedSet = new HashSet<T>(expected, comparer);
            var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual);
            if (!result)
            {
                Assert.True(result, GetAssertMessage(
                    ToString(expected, itemSeparator, itemInspector),
                    ToString(actual, itemSeparator, itemInspector),
                    prefix: message));
            }
        }
 
        public static void SetEqual<T>(T[] expected, T[] actual)
            => SetEqual((IEnumerable<T>)actual, expected);
 
        public static void SetEqual<T>(IEnumerable<T> actual, params T[] expected)
        {
            var expectedSet = new HashSet<T>(expected);
            if (!expectedSet.SetEquals(actual))
            {
                var message = GetAssertMessage(ToString(expected, ",\r\n", itemInspector: withQuotes), ToString(actual, ",\r\n", itemInspector: withQuotes));
                Assert.True(false, message);
            }
 
            string withQuotes(T t) => $"\"{Convert.ToString(t)}\"";
        }
 
        public static void None<T>(IEnumerable<T> actual, Func<T, bool> predicate)
        {
            var none = !actual.Any(predicate);
            if (!none)
            {
                Assert.True(none, string.Format(
                    "Unexpected item found among existing items: {0}\nExisting items: {1}",
                    ToString(actual.First(predicate)),
                    ToString(actual)));
            }
        }
 
        public static void Any<T>(IEnumerable<T> actual, Func<T, bool> predicate)
        {
            var any = actual.Any(predicate);
            Assert.True(any, string.Format("No expected item was found.\nExisting items: {0}", ToString(actual)));
        }
 
        public static void All<T>(IEnumerable<T> actual, Func<T, bool> predicate)
        {
            var all = actual.All(predicate);
            if (!all)
            {
                Assert.True(all, string.Format(
                    "Not all items satisfy condition:\n{0}",
                    ToString(actual.Where(i => !predicate(i)))));
            }
        }
 
        public static string ToString(object o)
        {
            return Convert.ToString(o);
        }
 
        public static string ToString<T>(IEnumerable<T> list, string separator = ", ", Func<T, string> itemInspector = null)
        {
            if (itemInspector == null)
            {
                itemInspector = i => Convert.ToString(i);
            }
 
            return string.Join(separator, list.Select(itemInspector));
        }
 
        public static void Fail(string message)
        {
            throw new Xunit.Sdk.XunitException(message);
        }
 
        public static void Fail(string format, params object[] args)
        {
            throw new Xunit.Sdk.XunitException(string.Format(format, args));
        }
 
        public static void NotNull<T>([NotNull] T @object, string message = null)
        {
            Assert.False(AssertEqualityComparer<T>.IsNull(@object), message);
        }
 
        // compares against a baseline
        public static void AssertEqualToleratingWhitespaceDifferences(
            string expected,
            string actual,
            string message = null,
            bool escapeQuotes = false,
            [CallerFilePath] string expectedValueSourcePath = null,
            [CallerLineNumber] int expectedValueSourceLine = 0)
        {
            var normalizedExpected = NormalizeWhitespace(expected);
            var normalizedActual = NormalizeWhitespace(actual);
 
            if (normalizedExpected != normalizedActual)
            {
                Assert.True(false, GetAssertMessage(expected, actual, message, escapeQuotes, expectedValueSourcePath, expectedValueSourceLine));
            }
        }
 
        // compares two results (no baseline)
        public static void AssertResultsEqual(string result1, string result2)
        {
            if (result1 != result2)
            {
                string message;
 
                if (DiffToolAvailable)
                {
                    string file1 = Path.GetTempFileName();
                    File.WriteAllText(file1, result1);
 
                    string file2 = Path.GetTempFileName();
                    File.WriteAllText(file2, result2);
 
                    message = MakeDiffToolLink(file1, file2);
                }
                else
                {
                    message = GetAssertMessage(result1, result2);
                }
 
                Assert.True(false, message);
            }
        }
 
        public static void AssertContainsToleratingWhitespaceDifferences(string expectedSubString, string actualString)
        {
            expectedSubString = NormalizeWhitespace(expectedSubString);
            actualString = NormalizeWhitespace(actualString);
            Assert.Contains(expectedSubString, actualString, StringComparison.Ordinal);
        }
 
        public static void AssertStartsWithToleratingWhitespaceDifferences(string expectedSubString, string actualString)
        {
            expectedSubString = NormalizeWhitespace(expectedSubString);
            actualString = NormalizeWhitespace(actualString);
            Assert.StartsWith(expectedSubString, actualString, StringComparison.Ordinal);
        }
 
        internal static string NormalizeWhitespace(string input)
        {
            var output = new StringBuilder();
            var inputLines = input.Split('\n', '\r');
            foreach (var line in inputLines)
            {
                var trimmedLine = line.Trim();
                if (trimmedLine.Length > 0)
                {
                    if (!(trimmedLine[0] == '{' || trimmedLine[0] == '}'))
                    {
                        output.Append("  ");
                    }
 
                    output.AppendLine(trimmedLine);
                }
            }
 
            return output.ToString();
        }
 
        public static string GetAssertMessage(string expected, string actual, string prefix = null, bool escapeQuotes = false, string expectedValueSourcePath = null, int expectedValueSourceLine = 0)
            => GetAssertMessage(DiffUtil.Lines(expected), DiffUtil.Lines(actual), prefix, escapeQuotes, expectedValueSourcePath, expectedValueSourceLine);
 
        public static string GetAssertMessage<T>(IEnumerable<T> expected, IEnumerable<T> actual, string prefix = null, bool escapeQuotes = false, string expectedValueSourcePath = null, int expectedValueSourceLine = 0)
        {
            Func<T, string> itemInspector = escapeQuotes ? new Func<T, string>(t => t.ToString().Replace("\"", "\"\"")) : null;
            return GetAssertMessage(expected, actual, prefix: prefix, itemInspector: itemInspector, itemSeparator: "\r\n", expectedValueSourcePath: expectedValueSourcePath, expectedValueSourceLine: expectedValueSourceLine);
        }
 
        private static readonly string s_diffToolPath = Environment.GetEnvironmentVariable("ROSLYN_DIFFTOOL");
 
        public static string GetAssertMessage<T>(
            IEnumerable<T> expected,
            IEnumerable<T> actual,
            IEqualityComparer<T> comparer = null,
            string prefix = null,
            Func<T, string> itemInspector = null,
            string itemSeparator = null,
            string expectedValueSourcePath = null,
            int expectedValueSourceLine = 0)
        {
            if (itemInspector == null)
            {
                if (typeof(T) == typeof(byte))
                {
                    itemInspector = b => $"0x{b:X2}";
                }
                else
                {
                    itemInspector = new Func<T, string>(obj => (obj != null) ? obj.ToString() : "<null>");
                }
            }
 
            if (itemSeparator == null)
            {
                if (typeof(T) == typeof(byte))
                {
                    itemSeparator = ", ";
                }
                else
                {
                    itemSeparator = "," + Environment.NewLine;
                }
            }
 
            var expectedString = string.Join(itemSeparator, expected.Take(10).Select(itemInspector));
            var actualString = string.Join(itemSeparator, actual.Select(itemInspector));
            var diffString = DiffUtil.DiffReport(expected, actual, itemSeparator, comparer, itemInspector);
 
            if (DifferOnlyInWhitespace(expectedString, actualString))
            {
                expectedString = VisualizeWhitespace(expectedString);
                actualString = VisualizeWhitespace(actualString);
                diffString = VisualizeWhitespace(diffString);
            }
 
            var message = new StringBuilder();
 
            if (!string.IsNullOrEmpty(prefix))
            {
                message.AppendLine(prefix);
                message.AppendLine();
            }
 
            message.AppendLine("Expected:");
            message.AppendLine(expectedString);
            if (expected.Count() > 10)
            {
                message.AppendLine("... truncated ...");
            }
 
            message.AppendLine("Actual:");
            message.AppendLine(actualString);
            message.AppendLine("Differences:");
            message.AppendLine(diffString);
 
            if (TryGenerateExpectedSourceFileAndGetDiffLink(actualString, expected.Count(), expectedValueSourcePath, expectedValueSourceLine, out var link))
            {
                message.AppendLine(link);
            }
 
            return message.ToString();
        }
 
        private static bool DifferOnlyInWhitespace(IEnumerable<char> expected, IEnumerable<char> actual)
            => expected.Where(c => !char.IsWhiteSpace(c)).SequenceEqual(actual.Where(c => !char.IsWhiteSpace(c)));
 
        private static string VisualizeWhitespace(string str)
        {
            var result = new StringBuilder(str.Length);
 
            var i = 0;
            while (i < str.Length)
            {
                var c = str[i++];
                if (c == '\r' && i < str.Length && str[i] == '\n')
                {
                    result.Append("␍␊\r\n");
                    i++;
                }
                else
                {
                    result.Append(c switch
                    {
                        ' ' => "·",
                        '\t' => "→",
                        '\r' => "␍\r",
                        '\n' => "␊\n",
                        _ => c,
                    });
                }
            }
 
            return result.ToString();
        }
 
        public static string GetAssertMessage<T>(
            ReadOnlySpan<T> expected,
            ReadOnlySpan<T> actual,
            IEqualityComparer<T> comparer = null,
            string prefix = null,
            Func<T, string> itemInspector = null,
            string itemSeparator = null,
            string expectedValueSourcePath = null,
            int expectedValueSourceLine = 0)
        {
            if (itemInspector == null)
            {
                if (typeof(T) == typeof(byte))
                {
                    itemInspector = b => $"0x{b:X2}";
                }
                else
                {
                    itemInspector = new Func<T, string>(obj => (obj != null) ? obj.ToString() : "<null>");
                }
            }
 
            if (itemSeparator == null)
            {
                if (typeof(T) == typeof(byte))
                {
                    itemSeparator = ", ";
                }
                else
                {
                    itemSeparator = "," + Environment.NewLine;
                }
            }
 
            const int maxDisplayedExpectedEntries = 10;
            var expectedString = join(itemSeparator, expected[..Math.Min(expected.Length, maxDisplayedExpectedEntries)], itemInspector);
            var actualString = join(itemSeparator, actual, itemInspector);
 
            var message = new StringBuilder();
 
            if (!string.IsNullOrEmpty(prefix))
            {
                message.AppendLine(prefix);
                message.AppendLine();
            }
 
            message.AppendLine("Expected:");
            message.AppendLine(expectedString);
            if (expected.Length > maxDisplayedExpectedEntries)
            {
                message.AppendLine("... truncated ...");
            }
 
            message.AppendLine("Actual:");
            message.AppendLine(actualString);
            message.AppendLine("Differences:");
            message.AppendLine(DiffUtil.DiffReport(expected.ToArray(), actual.ToArray(), itemSeparator, comparer, itemInspector));
 
            if (TryGenerateExpectedSourceFileAndGetDiffLink(actualString, expected.Length, expectedValueSourcePath, expectedValueSourceLine, out var link))
            {
                message.AppendLine(link);
            }
 
            return message.ToString();
 
            static string join(string itemSeparator, ReadOnlySpan<T> items, Func<T, string> itemInspector)
            {
                var result = new StringBuilder();
                var iter = items.GetEnumerator();
 
                if (iter.MoveNext())
                    result.Append(itemInspector(iter.Current));
 
                while (iter.MoveNext())
                    result.Append($"{itemSeparator}{itemInspector(iter.Current)}");
 
                return result.ToString();
            }
        }
 
        internal static bool TryGenerateExpectedSourceFileAndGetDiffLink(string actualString, int expectedLineCount, string expectedValueSourcePath, int expectedValueSourceLine, out string link)
        {
            // add a link to a .cmd file that opens a diff tool:
            if (DiffToolAvailable && expectedValueSourcePath != null && expectedValueSourceLine != 0)
            {
                var actualFile = Path.GetTempFileName();
                var testFileLines = File.ReadAllLines(expectedValueSourcePath);
 
                File.WriteAllLines(actualFile, testFileLines.Take(expectedValueSourceLine));
                File.AppendAllText(actualFile, actualString);
                File.AppendAllLines(actualFile, testFileLines.Skip(expectedValueSourceLine + expectedLineCount));
 
                link = MakeDiffToolLink(actualFile, expectedValueSourcePath);
 
                return true;
            }
 
            link = null;
            return false;
        }
 
        internal static bool DiffToolAvailable => !string.IsNullOrEmpty(s_diffToolPath);
 
        internal static string MakeDiffToolLink(string actualFilePath, string expectedFilePath)
        {
            var compareCmd = Path.GetTempFileName() + ".cmd";
            File.WriteAllText(compareCmd, string.Format("\"{0}\" \"{1}\" \"{2}\"", s_diffToolPath, actualFilePath, expectedFilePath));
 
            return "file://" + compareCmd;
        }
 
        public static void Empty<T>(IEnumerable<T> items, string message = "")
        {
            // realize the list in case it can't be traversed twice via .Count()/.Any() and .Select()
            var list = items.ToList();
            if (list.Count != 0)
            {
                Fail($"Expected 0 items but found {list.Count}: {message}\r\nItems:\r\n    {string.Join("\r\n    ", list)}");
            }
        }
 
        private sealed class LineComparer : IEqualityComparer<string>
        {
            public static readonly LineComparer Instance = new LineComparer();
 
            public bool Equals(string left, string right) => left.Trim() == right.Trim();
            public int GetHashCode(string str) => str.Trim().GetHashCode();
        }
 
        private static IEnumerable<string> GetLines(string str) =>
                str.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
 
        public static void AssertLinesEqual(string expected, string actual)
        {
            AssertEx.Equal(
                GetLines(expected),
                GetLines(actual),
                comparer: LineComparer.Instance);
        }
 
        public static void AssertLinesEqual(string expected, string actual, string message, string expectedValueSourcePath, int expectedValueSourceLine, bool escapeQuotes)
        {
            AssertEx.Equal(
                GetLines(expected),
                GetLines(actual),
                comparer: LineComparer.Instance,
                message: message,
                itemInspector: escapeQuotes ? new Func<string, string>(line => line.Replace("\"", "\"\"")) : null,
                itemSeparator: Environment.NewLine,
                expectedValueSourcePath: expectedValueSourcePath,
                expectedValueSourceLine: expectedValueSourceLine);
        }
 
        public static void Equal(bool[,] expected, Func<int, int, bool> getResult, int size)
        {
            Equal<bool>(expected, getResult, (b1, b2) => b1 == b2, b => b ? "true" : "false", "{0,-6:G}", size);
        }
 
        public static void Equal<T>(T[,] expected, Func<int, int, T> getResult, Func<T, T, bool> valuesEqual, Func<T, string> printValue, string format, int size)
        {
            bool mismatch = false;
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    if (!valuesEqual(expected[i, j], getResult(i, j)))
                    {
                        mismatch = true;
                    }
                }
            }
 
            if (mismatch)
            {
                var builder = new StringBuilder();
                builder.AppendLine("Actual result: ");
                for (int i = 0; i < size; i++)
                {
                    builder.Append("{ ");
                    for (int j = 0; j < size; j++)
                    {
                        string resultWithComma = printValue(getResult(i, j));
                        if (j < size - 1)
                        {
                            resultWithComma += ",";
                        }
 
                        builder.Append(string.Format(format, resultWithComma));
                        if (j < size - 1)
                        {
                            builder.Append(' ');
                        }
                    }
                    builder.AppendLine("},");
                }
 
                Assert.True(false, builder.ToString());
            }
        }
 
        /// <summary>
        /// Run multiple assertions at once and collect the result.
        /// This is useful when you want to verify multiple assertions but don't want to re-run to adjust every other case.
        /// </summary>
        public static void Multiple(params Action[] assertions)
        {
            Multiple(includeStackTrace: false, assertions);
        }
 
        /// <inheritdoc cref="Multiple(System.Action[])"/>
        public static void Multiple(bool includeStackTrace, params Action[] assertions)
        {
            List<(int, Exception)> exceptions = null;
 
            // Run assertions in reverse order so that line numbers don't change as we adjust the baseline.
            for (int index = assertions.Length - 1; index >= 0; --index)
            {
                try
                {
                    assertions[index]();
                }
                catch (Exception ex)
                {
                    (exceptions ??= new()).Add((index, ex));
                }
            }
 
            if (exceptions is null)
                return;
 
            var stringBuilder = new StringBuilder()
                .AppendLine($"{exceptions.Count} out of {assertions.Length} assertions failed.")
                .AppendLine();
            foreach (var (index, ex) in exceptions)
            {
                var stack = ex.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
                stringBuilder
                    .AppendLine($"Assertion failed at index {index}:")
                    .AppendLine(stack[^2]) // Prints the failing line in the original test case.
                    .AppendLine(ex.Message);
                if (includeStackTrace)
                    stringBuilder.AppendLine(ex.StackTrace);
                stringBuilder
                    .AppendLine()
                    .AppendLine();
            }
 
            Fail(stringBuilder.ToString());
        }
 
#nullable enable
        public static void NotNull<T>([NotNull] T value)
        {
            Assert.NotNull(value);
            Debug.Assert(value is object);
        }
 
        public static void Contains<T>(IEnumerable<T> collection, Predicate<T> filter, Func<T, string>? itemInspector = null, string? itemSeparator = null)
        {
            foreach (var item in collection)
            {
                if (filter(item))
                {
                    return;
                }
            }
 
            Fail("Filter does not match any item in the collection: " + Environment.NewLine +
                ToString(collection, itemSeparator ?? Environment.NewLine, itemInspector));
        }
    }
}