using System.Reflection; namespace QWERTYkez.Mensura.Tests; public class AggregateUnitExtensions { // Вспомогательный метод для создания объекта Length. // Если у вас используется фабричный метод (например, Length.FromMeters), замените код внутри. private static Length CreateLength(double value) => value * Length._MilliMeter; #region Инфраструктура Рефлексии (Invoker) private delegate Length SpanDelegate(ReadOnlySpan units); private delegate Length SpanNullableDelegate(ReadOnlySpan units); private static class Invoker { private static readonly Type ExtType; static Invoker() { // Находим внутренний класс AggregateUnitExtensions в целевой сборке ExtType = typeof(Length).Assembly.GetType("QWERTYkez.Mensura.Extensions.AggregateUnitExtensions") ?? throw new InvalidOperationException("Не удалось найти класс AggregateUnitExtensions через рефлексию."); } public static Length InvokeSpan(string methodName, ReadOnlySpan data) { var method = FindMethod(methodName, typeof(ReadOnlySpan<>), isNullable: false); var closedMethod = method.MakeGenericMethod(typeof(Length)); var del = closedMethod.CreateDelegate(); return del(data); } public static Length InvokeSpanNullable(string methodName, ReadOnlySpan data) { var method = FindMethod(methodName, typeof(ReadOnlySpan<>), isNullable: true); var closedMethod = method.MakeGenericMethod(typeof(Length)); var del = closedMethod.CreateDelegate(); return del(data); } public static Length InvokeCollection(string methodName, Type genericContainer, object data, bool isNullable) { var method = FindMethod(methodName, genericContainer, isNullable); var closedMethod = method.MakeGenericMethod(typeof(Length)); return (Length)closedMethod.Invoke(null, [data])!; } private static MethodInfo FindMethod(string name, Type genericContainerType, bool isNullable) { var methods = ExtType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public) .Where(m => m.Name == name); foreach (var method in methods) { var parameters = method.GetParameters(); if (parameters.Length != 1) continue; var paramType = parameters[0].ParameterType; if (!paramType.IsGenericType) continue; // Проверяем базовый контейнер (List<>, IEnumerable<>, ReadOnlySpan<>) if (paramType.GetGenericTypeDefinition() != genericContainerType) continue; // Проверяем внутренний аргумент типа на Nullable var genericArgument = paramType.GetGenericArguments()[0]; bool currentIsNullable = genericArgument.IsGenericType && genericArgument.GetGenericTypeDefinition() == typeof(Nullable<>); if (currentIsNullable == isNullable) { return method; } } throw new MethodAccessException($"Метод {name} для контейнера {genericContainerType.Name} (Nullable: {isNullable}) не найден."); } } #endregion #region Тесты для НЕ-выделяющих Nullable типов (Обычные структуры) private readonly Length[] _standardData = [CreateLength(10), CreateLength(20), CreateLength(30)]; private readonly Length _expectedSum = CreateLength(60); private readonly Length _expectedAvg = CreateLength(20); private readonly Length _expectedMax = CreateLength(30); private readonly Length _expectedMin = CreateLength(10); [Theory] [InlineData("Sum")] [InlineData("Avg")] [InlineData("Max")] [InlineData("Min")] public void StandardContainers_ShouldCalculateCorrectly(string operation) { // Набор ожидаемых значений Length expected = operation switch { "Sum" => _expectedSum, "Avg" => _expectedAvg, "Max" => _expectedMax, "Min" => _expectedMin, _ => throw new ArgumentException(operation) }; // 1. Тест ReadOnlySpan ReadOnlySpan span = _standardData; Assert.Equal(expected, Invoker.InvokeSpan(operation, span)); // 2. Тест List var list = _standardData.ToList(); Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(List<>), list, isNullable: false)); // 3. Тест IReadOnlyCollection IReadOnlyCollection readOnlyCollection = _standardData; Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IReadOnlyCollection<>), readOnlyCollection, isNullable: false)); // 4. Тест IEnumerable IEnumerable enumerable = _standardData.Select(x => x); Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IEnumerable<>), enumerable, isNullable: false)); } #endregion #region Тесты для Nullable типов (T?) // Тестируем смесь значений с null-элементами private readonly Length?[] _nullableData = [CreateLength(10), null, CreateLength(30)]; private readonly Length _expectedNullSum = CreateLength(40); private readonly Length _expectedNullAvg = CreateLength(20); // 40 / 2 значения private readonly Length _expectedNullMax = CreateLength(30); private readonly Length _expectedNullMin = CreateLength(10); [Theory] [InlineData("Sum")] [InlineData("Avg")] [InlineData("Max")] [InlineData("Min")] public void NullableContainers_ShouldCalculateCorrectly(string operation) { Length expected = operation switch { "Sum" => _expectedNullSum, "Avg" => _expectedNullAvg, "Max" => _expectedNullMax, "Min" => _expectedNullMin, _ => throw new ArgumentException(operation) }; // 1. Тест ReadOnlySpan ReadOnlySpan span = _nullableData; Assert.Equal(expected, Invoker.InvokeSpanNullable(operation, span)); // 2. Тест List var list = _nullableData.ToList(); Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(List<>), list, isNullable: true)); // 3. Тест IReadOnlyCollection IReadOnlyCollection readOnlyCollection = _nullableData; Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IReadOnlyCollection<>), readOnlyCollection, isNullable: true)); // 4. Тест IEnumerable IEnumerable enumerable = _nullableData.Select(x => x); Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IEnumerable<>), enumerable, isNullable: true)); } #endregion }