XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json.Serialization;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEExtension.NetCore.XUnit.Runtime;

/// <summary>
/// 表示生成器创建的强类型异步调用适配器。
/// </summary>
/// <param name="instance">实例方法的目标对象;静态方法为 <see langword="null"/>。</param>
/// <param name="arguments">按方法形参顺序排列的调用参数。</param>
/// <returns>表示方法完成的值任务;结果为装箱后的返回值,<see langword="void"/> 方法返回 <see langword="null"/>。</returns>
public delegate ValueTask<object?> XfeInvoker(object? instance, object?[] arguments);

/// <summary>
/// 保存测试和基准各阶段的强类型生命周期调用器。
/// </summary>
public sealed class XfeLifecycleHooks
{
    /// <summary>
    /// 获取测试类全部用例开始前执行一次的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> BeforeAll { get; init; } = [];

    /// <summary>
    /// 获取测试类全部用例结束后执行一次的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> AfterAll { get; init; } = [];

    /// <summary>
    /// 获取每个普通测试用例开始前执行的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> BeforeEach { get; init; } = [];

    /// <summary>
    /// 获取每个普通测试用例结束后执行的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> AfterEach { get; init; } = [];

    /// <summary>
    /// 获取单个基准工作进程开始测量前执行一次的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> GlobalSetup { get; init; } = [];

    /// <summary>
    /// 获取单个基准工作进程完成全部测量后执行一次的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> GlobalCleanup { get; init; } = [];

    /// <summary>
    /// 获取每轮基准实际测量前执行的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> IterationSetup { get; init; } = [];

    /// <summary>
    /// 获取每轮基准实际测量后执行的调用器。
    /// </summary>
    public IReadOnlyList<XfeInvoker> IterationCleanup { get; init; } = [];
}

/// <summary>
/// 描述一个已经展开参数、可由运行器直接执行的普通测试用例。
/// </summary>
public sealed class TestDescriptor
{
    /// <summary>
    /// 获取在生成注册表和工作进程通信中唯一标识该用例的字符串。
    /// </summary>
    public required string Id { get; init; }

    /// <summary>
    /// 获取控制台、筛选和报告使用的测试显示名称。
    /// </summary>
    public required string DisplayName { get; init; }

    /// <summary>
    /// 获取测试方法声明类型的完全限定名称。
    /// </summary>
    public required string TypeName { get; init; }

    /// <summary>
    /// 获取测试方法名称。
    /// </summary>
    public required string MethodName { get; init; }

    /// <summary>
    /// 获取传递给测试调用器的参数。
    /// </summary>
    public object?[] Arguments { get; init; } = [];

    /// <summary>
    /// 获取从测试类和方法合并得到的分类名称。
    /// </summary>
    public IReadOnlyList<string> Categories { get; init; } = [];

    /// <summary>
    /// 获取从测试类和方法合并得到的结构化特征。
    /// </summary>
    public IReadOnlyDictionary<string, string> Traits { get; init; } = new Dictionary<string, string>();

    /// <summary>
    /// 获取跳过原因;为空表示测试未被无条件跳过。
    /// </summary>
    public string? SkipReason { get; init; }

    /// <summary>
    /// 获取该用例是否仅在启用显式测试时运行。
    /// </summary>
    public bool Explicit { get; init; }

    /// <summary>
    /// 获取该用例是否禁止与其他测试并行执行。
    /// </summary>
    public bool NonParallel { get; init; }

    /// <summary>
    /// 获取该用例是否必须在独立工作进程中执行。
    /// </summary>
    public bool Isolated { get; init; }

    /// <summary>
    /// 获取该用例是否来自 3.x 兼容特性。
    /// </summary>
    public bool IsLegacy { get; init; }

    /// <summary>
    /// 获取该用例是否来自 3.x 的 <see cref="Attributes.SMTestAttribute"/> 单次执行特性。
    /// </summary>
    public bool IsLegacySingleRun { get; init; }

    /// <summary>
    /// 获取测试集合名称;为空时按声明类型分组。
    /// </summary>
    public string? Collection { get; init; }

    /// <summary>
    /// 获取硬超时毫秒数;0 表示使用全局默认值或不超时。
    /// </summary>
    public int TimeoutMilliseconds { get; init; }

    /// <summary>
    /// 获取首次失败后允许重新执行的次数。
    /// </summary>
    public int RetryCount { get; init; }

    /// <summary>
    /// 获取兼容执行器是否需要比较方法返回值。
    /// </summary>
    public bool HasExpectedResult { get; init; }

    /// <summary>
    /// 获取兼容执行器期望的方法返回值。
    /// </summary>
    public object? ExpectedResult { get; init; }

    /// <summary>
    /// 获取创建测试类实例的工厂;该成员不会序列化到结果文件。
    /// </summary>
    [JsonIgnore] public required Func<object?> Factory { get; init; }

    /// <summary>
    /// 获取直接调用测试方法的强类型适配器;该成员不会序列化到结果文件。
    /// </summary>
    [JsonIgnore] public required XfeInvoker Invoker { get; init; }

    /// <summary>
    /// 获取测试类的生命周期调用器集合。
    /// </summary>
    [JsonIgnore] public XfeLifecycleHooks Lifecycle { get; init; } = new();
}

/// <summary>
/// 描述一个已经展开方法参数和 <see cref="Attributes.ParamsAttribute"/> 组合的基准用例。
/// </summary>
public sealed class BenchmarkDescriptor
{
    /// <summary>
    /// 获取在生成注册表和工作进程通信中唯一标识该基准的字符串。
    /// </summary>
    public required string Id { get; init; }

    /// <summary>
    /// 获取控制台、筛选和报告使用的基准显示名称。
    /// </summary>
    public required string DisplayName { get; init; }

    /// <summary>
    /// 获取基准方法声明类型的完全限定名称。
    /// </summary>
    public required string TypeName { get; init; }

    /// <summary>
    /// 获取基准方法名称。
    /// </summary>
    public required string MethodName { get; init; }

    /// <summary>
    /// 获取传递给基准调用器的方法参数。
    /// </summary>
    public object?[] Arguments { get; init; } = [];

    /// <summary>
    /// 获取从基准类和方法合并得到的分类名称。
    /// </summary>
    public IReadOnlyList<string> Categories { get; init; } = [];

    /// <summary>
    /// 获取该基准是否为相同参数组合的比率基线。
    /// </summary>
    public bool Baseline { get; init; }

    /// <summary>
    /// 获取该基准是否来自 3.x 单方法计时兼容特性。
    /// </summary>
    public bool IsLegacy { get; init; }

    /// <summary>
    /// 获取用于匹配相同 <see cref="Attributes.ParamsAttribute"/> 组合基线的稳定键。
    /// </summary>
    public string ParameterKey { get; init; } = string.Empty;

    /// <summary>
    /// 获取该基准采用的测量策略。
    /// </summary>
    public BenchmarkStrategy Strategy { get; init; } = BenchmarkStrategy.Throughput;

    /// <summary>
    /// 获取创建基准类实例的工厂。
    /// </summary>
    [JsonIgnore] public required Func<object?> Factory { get; init; }

    /// <summary>
    /// 获取直接调用基准方法的强类型适配器。
    /// </summary>
    [JsonIgnore] public required XfeInvoker Invoker { get; init; }

    /// <summary>
    /// 获取具有相同返回形状但不执行用户工作负载的开销调用器。
    /// </summary>
    [JsonIgnore] public XfeInvoker OverheadInvoker { get; init; } = static (_, _) => new ValueTask<object?>((object?)null);

    /// <summary>
    /// 获取把当前 <see cref="Attributes.ParamsAttribute"/> 组合应用到基准实例的委托。
    /// </summary>
    [JsonIgnore] public Action<object?> ApplyParameters { get; init; } = static _ => { };

    /// <summary>
    /// 获取基准类的全局和迭代生命周期调用器。
    /// </summary>
    [JsonIgnore] public XfeLifecycleHooks Lifecycle { get; init; } = new();
}

/// <summary>
/// 保存生成器发现的普通测试和基准描述符。
/// </summary>
public sealed class XfeRegistry
{
    private readonly List<TestDescriptor> _tests = [];
    private readonly List<BenchmarkDescriptor> _benchmarks = [];

    /// <summary>
    /// 获取注册的普通测试用例只读列表。
    /// </summary>
    public IReadOnlyList<TestDescriptor> Tests => _tests;

    /// <summary>
    /// 获取注册的基准用例只读列表。
    /// </summary>
    public IReadOnlyList<BenchmarkDescriptor> Benchmarks => _benchmarks;

    /// <summary>
    /// 向注册表添加一个普通测试用例。
    /// </summary>
    /// <param name="descriptor">要注册的测试描述符。</param>
    public void AddTest(TestDescriptor descriptor) => _tests.Add(descriptor);

    /// <summary>
    /// 向注册表添加一个基准用例。
    /// </summary>
    /// <param name="descriptor">要注册的基准描述符。</param>
    public void AddBenchmark(BenchmarkDescriptor descriptor) => _benchmarks.Add(descriptor);
}

/// <summary>
/// 连接增量生成器输出与运行时的进程内注册表入口。
/// </summary>
public static class XfeGeneratedRegistry
{
    private static Func<XfeRegistry>? s_factory;

    /// <summary>
    /// 设置用于创建当前程序集测试注册表的生成工厂。
    /// </summary>
    /// <param name="factory">生成器发出的注册表工厂。</param>
    public static void SetFactory(Func<XfeRegistry> factory) => s_factory = factory;

    /// <summary>
    /// 创建当前程序集的测试注册表;没有生成工厂时返回空注册表。
    /// </summary>
    /// <returns>新创建的测试和基准注册表。</returns>
    public static XfeRegistry Create() => s_factory?.Invoke() ?? new XfeRegistry();
}

/// <summary>
/// 为生成代码提供测试类、基准类和 Fixture 的构造及释放支持。
/// </summary>
public static class XfeObjectFactory
{
    private static readonly AsyncLocal<Dictionary<Type, object>?> s_fixtures = new();
    private static readonly AsyncLocal<ITestActivator?> s_activator = new();

    internal static IDisposable UseActivator(ITestActivator? activator)
    {
        var previous = s_activator.Value;
        s_activator.Value = activator;
        return new ActivatorScope(previous);
    }

    internal static IDisposable BeginFixtureScope(out IReadOnlyCollection<object> fixtures)
    {
        var previous = s_fixtures.Value;
        var current = new Dictionary<Type, object>();
        s_fixtures.Value = current;
        fixtures = current.Values;
        return new FixtureScope(previous);
    }

    /// <summary>
    /// 创建指定类型,优先使用已注册的 <see cref="ITestActivator"/>,然后匹配构造参数或注入 Fixture。
    /// </summary>
    /// <param name="type">要创建的测试、基准或 Fixture 类型。</param>
    /// <param name="arguments">显式传递给构造函数的参数。</param>
    /// <returns>创建的实例;静态类返回 <see langword="null"/>。</returns>
    /// <exception cref="MissingMethodException">没有构造函数能够接收给定参数,且无法使用 Fixture 构造。</exception>
    public static object? Create(Type type, object?[] arguments)
    {
        if (type.IsAbstract && type.IsSealed)
            return null;
        if (arguments.Length == 0 && s_activator.Value?.CreateInstance(type) is { } activated)
            return activated;
        var constructors = type.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
        foreach (var constructor in constructors)
        {
            var parameters = constructor.GetParameters();
            if (parameters.Length != arguments.Length)
                continue;
            var compatible = true;
            for (var index = 0; index < parameters.Length; index++)
            {
                var value = arguments[index];
                if (value is null ? parameters[index].ParameterType.IsValueType && Nullable.GetUnderlyingType(parameters[index].ParameterType) is null : !parameters[index].ParameterType.IsInstanceOfType(value))
                {
                    compatible = false;
                    break;
                }
            }
            if (compatible)
                return constructor.Invoke(arguments);
        }
        if (arguments.Length == 0)
        {
            var fixtureConstructor = constructors.OrderByDescending(static constructor => constructor.GetParameters().Length).FirstOrDefault();
            if (fixtureConstructor is not null)
            {
                var fixtureArguments = fixtureConstructor.GetParameters().Select(parameter => GetOrCreateFixture(parameter.ParameterType)).ToArray();
                return fixtureConstructor.Invoke(fixtureArguments);
            }
        }
        throw new MissingMethodException($"No constructor on {type.FullName} accepts {arguments.Length} supplied argument(s).");
    }

    /// <summary>
    /// 使用已注册激活器或标准释放接口异步释放实例。
    /// </summary>
    /// <param name="instance">要释放的对象;静态测试可传入 <see langword="null"/>。</param>
    /// <returns>表示释放完成的值任务。</returns>
    public static async ValueTask DisposeAsync(object? instance)
    {
        if (s_activator.Value is { } activator)
        {
            await activator.DisposeAsync(instance).ConfigureAwait(false);
            return;
        }
        if (instance is IAsyncDisposable asyncDisposable)
            await asyncDisposable.DisposeAsync().ConfigureAwait(false);
        else if (instance is IDisposable disposable)
            disposable.Dispose();
    }

    private static object GetOrCreateFixture(Type type)
    {
        var fixtures = s_fixtures.Value ?? throw new InvalidOperationException("Fixture construction is only available inside an active test collection.");
        if (fixtures.TryGetValue(type, out var fixture))
            return fixture;
        fixture = s_activator.Value?.CreateInstance(type)
            ?? Activator.CreateInstance(type, true)
            ?? throw new InvalidOperationException($"Could not construct fixture {type.FullName}.");
        fixtures.Add(type, fixture);
        return fixture;
    }

    private sealed class FixtureScope(Dictionary<Type, object>? previous) : IDisposable
    {
        public void Dispose() => s_fixtures.Value = previous;
    }

    private sealed class ActivatorScope(ITestActivator? previous) : IDisposable
    {
        public void Dispose() => s_activator.Value = previous;
    }
}

/// <summary>
/// 为生成代码解析 <see cref="Attributes.MemberDataAttribute"/> 指定的静态数据成员。
/// </summary>
public static class XfeMemberData
{
    /// <summary>
    /// 读取静态字段、属性或无参方法,并把数据源规范化为参数数组序列。
    /// </summary>
    /// <param name="sourceType">声明数据成员的类型。</param>
    /// <param name="memberName">静态数据成员名称。</param>
    /// <returns>测试参数行序列。</returns>
    /// <exception cref="MissingMemberException">找不到指定的静态成员。</exception>
    /// <exception cref="InvalidOperationException">成员值既不是 <see cref="ITestCaseDataSource"/> 也不是可枚举数据。</exception>
    public static IEnumerable<object?[]> Get(Type sourceType, string memberName)
    {
        const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
        object? value = sourceType.GetProperty(memberName, flags)?.GetValue(null)
            ?? sourceType.GetField(memberName, flags)?.GetValue(null)
            ?? sourceType.GetMethod(memberName, flags, null, Type.EmptyTypes, null)?.Invoke(null, null)
            ?? throw new MissingMemberException(sourceType.FullName, memberName);
        if (value is ITestCaseDataSource dataSource)
        {
            foreach (var row in dataSource.GetData())
                yield return row;
            yield break;
        }
        if (value is not System.Collections.IEnumerable rows)
            throw new InvalidOperationException($"Member data source {sourceType.FullName}.{memberName} must implement IEnumerable.");
        foreach (var row in rows)
        {
            if (row is object?[] array)
                yield return array;
            else if (row is System.Runtime.CompilerServices.ITuple tuple)
            {
                var values = new object?[tuple.Length];
                for (var index = 0; index < tuple.Length; index++)
                    values[index] = tuple[index];
                yield return values;
            }
            else
                yield return [row];
        }
    }
}

/// <summary>
/// 为生成代码把 <see cref="Attributes.ParamsAttribute"/> 的值应用到基准实例成员。
/// </summary>
public static class XfeParameterBinder
{
    /// <summary>
    /// 设置基准实例上的字段或属性值。
    /// </summary>
    /// <param name="instance">包含目标成员的基准实例。</param>
    /// <param name="memberName">要设置的字段或属性名称。</param>
    /// <param name="value">要应用的参数值。</param>
    /// <exception cref="InvalidOperationException"><paramref name="instance"/> 为 <see langword="null"/>。</exception>
    /// <exception cref="MissingMemberException">实例类型上不存在指定字段或属性。</exception>
    public static void Set(object? instance, string memberName, object? value)
    {
        if (instance is null)
            throw new InvalidOperationException($"Cannot set benchmark parameter {memberName} on a static fixture.");
        const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
        var type = instance.GetType();
        if (type.GetProperty(memberName, flags) is { } property)
        {
            property.SetValue(instance, value);
            return;
        }
        if (type.GetField(memberName, flags) is { } field)
        {
            field.SetValue(instance, value);
            return;
        }
        throw new MissingMemberException(type.FullName, memberName);
    }
}