BryanWilhite / SonghayCore

core reusable, opinionated concerns for *all* 🧐 of my C# projects
http://songhayblog.azurewebsites.net/
MIT License
1 stars 0 forks source link

update `Orderers` classes #166

Closed BryanWilhite closed 7 months ago

BryanWilhite commented 7 months ago

/// <summary>
/// Provides ordered test assertions.
/// </summary>
/// <remarks>
/// For more detail, see “How to Order xUnit Tests and Collections” by Tom DuPont
/// [http://www.tomdupont.net/2016/04/how-to-order-xunit-tests-and-collections.html]
/// </remarks>
[TestCaseOrderer(TestCaseOrderer.TypeName, TestCaseOrderer.AssemblyName)]
public abstract class OrderedTestBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="OrderedTestBase"/> class.
    /// </summary>
    public OrderedTestBase()
    {
        AppDomain.CurrentDomain.FirstChanceException += (source, e) =>
        {
            if (xUnitExceptionHasOccurred || !isXunitException(e.Exception)) return;

            xUnitExceptionHasOccurred = true;
            lastException = e.Exception;
        };
    }

    /// <summary>
    /// The expected ordinal of the current test.
    /// </summary>
    protected static int TestOrdinal;

    /// <summary>
    /// Asserts there are no exceptions of type <c>xUnit.Sdk.*</c>
    /// to prevent ordered tests from running.
    /// </summary>
    /// <remarks>
    /// See https://github.com/xunit/xunit/issues/856
    /// </remarks>
    protected static void AssertNoXUnitException() =>
        Assert.False(xUnitExceptionHasOccurred, $"Assertion Failed: An exception has occurred under test [message: `{lastException?.Message}`].");

    /// <summary>
    /// Asserts the name of the test.
    /// </summary>
    /// <param name="testName">Name of the test.</param>
    protected void AssertTestName([CallerMemberName] string testName = "")
    {
        var type = GetType();

        Assert.False(string.IsNullOrEmpty(type.FullName));

        var queue = TestCaseOrderer.QueuedTests[type.FullName];
        var result = queue.TryDequeue(out string? dequeuedName);

        Assert.True(result);
        Assert.Equal(testName, dequeuedName);
    }

    protected static bool xUnitExceptionHasOccurred;

    static Exception? lastException;

    /// <summary>
    /// Determines whether the specified <see cref="Exception"/> is from an xUnit assertion.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns>
    ///   <c>true</c> if it is a xUnit <see cref="Exception"/>; otherwise, <c>false</c>.
    /// </returns>
    /// <remarks>
    /// <see cref="AppDomain.FirstChanceException"/> will detect ALL exceptions,
    /// including those NOT on the current path of execution!
    /// 
    /// This means a huge amount of exceptions can pass through <see cref="AppDomain.FirstChanceException"/>.
    /// </remarks>
    bool isXunitException(Exception ex) =>
        ex?.GetType().FullName?.ToLowerInvariant().StartsWith("xunit.sdk") == true;
}

/// <summary>
/// Implementation of <see cref="ITestCaseOrderer"/>
/// for the use of <see cref="TestOrderAttribute"/>.
/// </summary>
/// <seealso cref="ITestCaseOrderer" />
/// <remarks>
/// For more detail, see “How to Order xUnit Tests and Collections” by Tom DuPont.
/// [ see http://www.tomdupont.net/2016/04/how-to-order-xunit-tests-and-collections.html ]
/// </remarks>
public class TestCaseOrderer : ITestCaseOrderer
{
    /// <summary>The queued tests</summary>
    public static readonly ConcurrentDictionary<string, ConcurrentQueue<string>> QueuedTests = new();

    /// <summary>Orders test cases for execution.</summary>
    /// <typeparam name="TTestCase"></typeparam>
    /// <param name="testCases">The test cases to be ordered.</param>
    /// <returns>The test cases in the order to be run.</returns>
    public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases)
        where TTestCase : ITestCase
    {
        TTestCase[] orderedCases = testCases.OrderBy(GetOrder).ToArray();

        return orderedCases;
    }

    static int GetOrder<TTestCase>(TTestCase testCase) where TTestCase : ITestCase
    {
        // Enqueue the test name.
        QueuedTests
            .GetOrAdd(testCase.TestMethod.TestClass.Class.Name, _ => new ConcurrentQueue<string>())
            .Enqueue(testCase.TestMethod.Method.Name);

        // Order the test based on the attribute.
        var attr = testCase.TestMethod.Method
            .ToRuntimeMethod()
            .GetCustomAttribute<TestOrderAttribute>();

        return attr?.Ordinal ?? 0;
    }
}

/// <summary>
/// Defines the attribute used to order tests
/// by the specified ordinal.
/// </summary>
/// <seealso cref="Attribute" />
public class TestOrderAttribute : Attribute
{
    /// <summary>Initializes a new instance of the <see cref="TestOrderAttribute"/> class.</summary>
    /// <param name="ordinal">The ordinal.</param>
    public TestOrderAttribute(int ordinal) => Ordinal = ordinal;

    /// <summary>Initializes a new instance of the <see cref="TestOrderAttribute"/> class.</summary>
    /// <param name="ordinal">The ordinal.</param>
    /// <param name="reason">The reason.</param>
    public TestOrderAttribute(int ordinal, string? reason)
    {
        Ordinal = ordinal;
        Reason = reason;
    }

    /// <summary>Gets the ordinal.</summary>
    /// <value>The ordinal.</value>
    public int Ordinal { get; }

    /// <summary>Gets the reason for choosing the order.</summary>
    /// <value>The reason.</value>
    public string? Reason { get; }
}