Files
QWERTYkez.Mensura/QWERTYkez.Mensura.Tests/CastExtensionsTest.cs
2026-06-12 23:34:00 +07:00

165 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace QWERTYkez.Mensura.Tests;
public class CastExtensionsTest
{
private const double NormalValue1 = 42.42;
private const double NormalValue2 = 100.05;
#region 1. Слабое место: Побитовая идентичность и спец-значения (NaN/Inf)
// Так как используется Unsafe.As, нужно убедиться, что битовая сетка double
// не ломается при сохранении пограничных значений (NaN, Infinity).
[Theory]
[InlineData(NormalValue1)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
[InlineData(double.Epsilon)]
public void SingleConversion_Should_Preserve_Exact_BitPattern_For_Special_Doubles(double specialValue)
{
// Act
Length unit = specialValue.ToUnit<Length>();
double recovered = unit.ToDouble();
// Assert
// Используем BitConverter, чтобы проверить идентичность на уровне битов (особенно важно для NaN)
long originalBits = BitConverter.DoubleToInt64Bits(specialValue);
long recoveredBits = BitConverter.DoubleToInt64Bits(recovered);
Assert.Equal(originalBits, recoveredBits);
}
#endregion
#region 2. Слабое место: Взрыв CLR при изменении емкости (EnsureCapacity)
// При ReCast списков подменяется MethodTable самого списка и его внутреннего массива.
// Если мы начнем добавлять элементы, список выделит новый массив через Array.CreateInstance.
// Если тип в MethodTable не совпадет с тем, что ожидает среда исполнения, GC или рантайм упадет.
[Fact]
public void List_ReCast_Should_Survive_Massive_Resize_And_GC_Collections()
{
// Arrange
var originalList = new List<Length> { (Length)NormalValue1, (Length)NormalValue2 };
// Act
List<double> morphedList = originalList.ReCast();
// Провоцируем многократное выделение новой памяти (Resize внутреннего массива)
for (int i = 0; i < 100; i++)
{
morphedList.Add(i * 1.1);
}
// Вызываем сборщик мусора, чтобы проверить, не сошел ли он с ума от нашей подмены
GC.Collect();
GC.WaitForPendingFinalizers();
// Assert
Assert.Equal(102, morphedList.Count);
Assert.Equal(NormalValue1, morphedList[0]);
Assert.Equal(10 * 1.1, morphedList[12], 5); // Проверка случайного элемента
}
#endregion
#region 3. Слабое место: Ловушка `ArrayTypeMismatchException` при добавлении в WrapAsList
// WrapAsList создает фейковый список, подменяя MethodTable исходного массива на double[].
// Но когда мы вызываем List.Add(), рантайм внутри делает Array.Copy().
// Если CLR поймет, что исходный массив физически был массивом структур Length,
// вылетит ArrayTypeMismatchException.
[Fact]
public void WrapAsList_Must_Allow_Adding_Elements_Without_ArrayTypeMismatchException()
{
// Arrange
Length[] sourceArray = [(Length)NormalValue1, (Length)NormalValue2];
// Act
List<double> wrappedList = sourceArray.WrapAsList();
// Assert
// Это действие вызывает внутренний Array.Copy. Самое хрупкое место!
var exception = Record.Exception(() => wrappedList.Add(999.99));
Assert.Null(exception); // Тест провален, если здесь вылетит исключение типа массива
Assert.Equal(3, wrappedList.Count);
Assert.Equal(999.99, wrappedList[2]);
}
#endregion
#region 4. Слабое место: Выравнивание памяти и макет Nullable типов
// Структура Nullable<T> в памяти имеет размер больше, чем T (из-за флага HasValue и выравнивания).
// Подмена MethodTable для Nullable<Length>[] в Nullable<double>[] — это огромный риск
// смещения байт. Проверяем, что null остается null, а значения не затираются.
[Fact]
public void Nullable_Array_ReCast_Should_Not_Corrupt_Flags_And_Values()
{
// Arrange
Length?[] source = [(Length)NormalValue1, null, (Length)NormalValue2, null];
// Act
double?[] morphed = source.ReCast();
// Assert
Assert.Equal(source.Length, morphed.Length);
Assert.Equal(NormalValue1, morphed[0]);
Assert.Null(morphed[1]);
Assert.Equal(NormalValue2, morphed[2]);
Assert.Null(morphed[3]);
}
#endregion
#region 5. Слабое место: In-Place мутация (Побочный эффект "Вуду")
// Так как ReCast и WrapAsList меняют MethodTable *оригинального* объекта прямо в куче,
// старая ссылка на массив Length[] теперь указывает на объект, который думает, что он double[].
// Проверяем, как ведет себя оригинальная переменная после этого хака.
[Fact]
public void Verify_InPlace_Mutation_SideEffect_Does_Not_Crash_Old_Reference()
{
// Arrange
Length[] originalArray = [(Length)NormalValue1];
// Act
double[] morphedArray = originalArray.ReCast();
// Изменяем элемент через НОВЫЙ массив
morphedArray[0] = 777.77;
// Assert
// Внимание: из-за жесткой мутации в куче originalArray[0] теперь ТОЖЕ вернет 777.77,
// потому что они смотрят на одну память. Главное — чтобы CLR не упал при обращении к старой ссылке.
double valueFromOldRef = (double)originalArray[0];
Assert.Equal(777.77, valueFromOldRef);
Assert.Same(originalArray, morphedArray); // Физически это один и тот же объект в памяти
}
#endregion
#region 6. Слабое место: Фильтры CLR и оптимизации LINQ
// Компилятор и JIT часто оптимизируют методы вроде .Select() или .ToArray(),
// опираясь на тип MethodTable. Проверяем, съедят ли механизмы LINQ наш "поддельный" тип.
[Fact]
public void Morphed_Array_Should_Pass_Through_Linq_And_Native_Sorting_Validations()
{
// Arrange
Length[] originalArray = [(Length)NormalValue2, (Length)NormalValue1]; // [100.05, 42.42]
// Act
double[] morphedArray = originalArray.ReCast();
// 1. Проверка LINQ фильтрации
double[] processedViaLinq = [.. morphedArray.Where(x => x > 50.0)];
Assert.Single(processedViaLinq);
Assert.Equal(NormalValue2, processedViaLinq[0]);
// 2. Проверка работы встроенной нативной сортировки (Array.Sort использует JIT-оптимизации)
var sortException = Record.Exception(() => Array.Sort(morphedArray));
Assert.Null(sortException);
Assert.True(morphedArray[0] < morphedArray[1]); // Теперь [42.42, 100.05]
}
#endregion
}