170 lines
7.3 KiB
C#
170 lines
7.3 KiB
C#
|
|
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<Length> units);
|
|||
|
|
private delegate Length SpanNullableDelegate(ReadOnlySpan<Length?> 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<Length> data)
|
|||
|
|
{
|
|||
|
|
var method = FindMethod(methodName, typeof(ReadOnlySpan<>), isNullable: false);
|
|||
|
|
var closedMethod = method.MakeGenericMethod(typeof(Length));
|
|||
|
|
var del = closedMethod.CreateDelegate<SpanDelegate>();
|
|||
|
|
return del(data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Length InvokeSpanNullable(string methodName, ReadOnlySpan<Length?> data)
|
|||
|
|
{
|
|||
|
|
var method = FindMethod(methodName, typeof(ReadOnlySpan<>), isNullable: true);
|
|||
|
|
var closedMethod = method.MakeGenericMethod(typeof(Length));
|
|||
|
|
var del = closedMethod.CreateDelegate<SpanNullableDelegate>();
|
|||
|
|
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<T>
|
|||
|
|
ReadOnlySpan<Length> span = _standardData;
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeSpan(operation, span));
|
|||
|
|
|
|||
|
|
// 2. Тест List<T>
|
|||
|
|
var list = _standardData.ToList();
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(List<>), list, isNullable: false));
|
|||
|
|
|
|||
|
|
// 3. Тест IReadOnlyCollection<T>
|
|||
|
|
IReadOnlyCollection<Length> readOnlyCollection = _standardData;
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IReadOnlyCollection<>), readOnlyCollection, isNullable: false));
|
|||
|
|
|
|||
|
|
// 4. Тест IEnumerable<T>
|
|||
|
|
IEnumerable<Length> 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<T?>
|
|||
|
|
ReadOnlySpan<Length?> span = _nullableData;
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeSpanNullable(operation, span));
|
|||
|
|
|
|||
|
|
// 2. Тест List<T?>
|
|||
|
|
var list = _nullableData.ToList();
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(List<>), list, isNullable: true));
|
|||
|
|
|
|||
|
|
// 3. Тест IReadOnlyCollection<T?>
|
|||
|
|
IReadOnlyCollection<Length?> readOnlyCollection = _nullableData;
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IReadOnlyCollection<>), readOnlyCollection, isNullable: true));
|
|||
|
|
|
|||
|
|
// 4. Тест IEnumerable<T?>
|
|||
|
|
IEnumerable<Length?> enumerable = _nullableData.Select(x => x);
|
|||
|
|
Assert.Equal(expected, Invoker.InvokeCollection(operation, typeof(IEnumerable<>), enumerable, isNullable: true));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|