This commit is contained in:
2026-06-12 23:34:00 +07:00
parent 790a5f8e10
commit d7e9bb7b5b
27 changed files with 1167 additions and 1020 deletions

View File

@@ -0,0 +1,170 @@
using System.Reflection;
namespace QWERTYkez.Mensura.Tests;
public class AggregateUnitExtensionsTest
{
// Вспомогательный метод для создания объекта 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
}