This commit is contained in:
melekhin
2026-06-11 15:42:01 +07:00
parent 343996ef46
commit 790a5f8e10
35 changed files with 5128 additions and 3595 deletions

View File

@@ -0,0 +1,165 @@
namespace QWERTYkez.Mensura.Tests;
public class CastExtensions
{
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
}