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(); 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)NormalValue1, (Length)NormalValue2 }; // Act List 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 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 (из-за флага HasValue и выравнивания). // Подмена MethodTable для Nullable[] в Nullable[] — это огромный риск // смещения байт. Проверяем, что 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 }