165 lines
7.9 KiB
C#
165 lines
7.9 KiB
C#
|
|
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
|
|||
|
|
}
|