Files
Sergio Pedri 12ae36b594 Refactor unit tests to use new assertion helpers
Replaces usages of Assert.AreEqual, Assert.IsTrue, and similar assertions with new helper methods such as Assert.HasCount, Assert.IsEmpty, Assert.IsNull, and Assert.IsGreaterThanOrEqualTo across multiple test files. This improves test readability and consistency with updated assertion APIs.
2025-11-09 18:34:09 -08:00

607 lines
23 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CommunityToolkit.HighPerformance;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CommunityToolkit.HighPerformance.UnitTests.Extensions;
[TestClass]
public class Test_MemoryExtensions
{
[TestMethod]
public void Test_MemoryExtensions_Cast_Empty()
{
// Casting an empty memory of any size should always be valid
// and result in another empty memory, regardless of types.
Memory<byte> m1 = default;
Memory<byte> mc1 = m1.Cast<byte, byte>();
Assert.IsTrue(mc1.IsEmpty);
Memory<byte> m2 = default;
Memory<float> mc2 = m2.Cast<byte, float>();
Assert.IsTrue(mc2.IsEmpty);
Memory<short> m3 = default;
Memory<Guid> mc3 = m3.Cast<short, Guid>();
Assert.IsTrue(mc3.IsEmpty);
// Same as above, but with a sliced memory (length 12, slide from 12, so length of 0)
Memory<byte> m4 = new byte[12].AsMemory(12);
Memory<int> mc4 = m4.Cast<byte, int>();
Assert.IsTrue(mc4.IsEmpty);
// Same as above, but slicing to 12 in two steps
Memory<byte> m5 = new byte[12].AsMemory().Slice(4).Slice(8);
Memory<int> mc5 = m5.Cast<byte, int>();
Assert.IsTrue(mc5.IsEmpty);
}
[TestMethod]
public void Test_MemoryExtensions_Cast_TooShort()
{
// One int is 4 bytes, so casting from 3 rounds down to 0
Memory<byte> m1 = new byte[3];
Memory<int> mc1 = m1.Cast<byte, int>();
Assert.IsTrue(mc1.IsEmpty);
// Same as above, 13 / sizeof(int) == 3
Memory<byte> m2 = new byte[13];
Memory<float> mc2 = m2.Cast<byte, float>();
Assert.AreEqual(3, mc2.Length);
// 16 - 5 = 11 ---> 11 / sizeof(int) = 2
Memory<byte> m3 = new byte[16].AsMemory(5);
Memory<float> mc3 = m3.Cast<byte, float>();
Assert.AreEqual(2, mc3.Length);
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastFromByte()
{
// Test for a standard cast from bytes with an evenly divisible length
Memory<byte> memoryOfBytes = new byte[128];
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Assert.AreEqual(128 / sizeof(float), memoryOfFloats.Length);
Span<byte> spanOfBytes = memoryOfBytes.Span;
Span<float> spanOfFloats = memoryOfFloats.Span;
// We also need to check that the Span<T> returned from the cast memory
// actually has the initial reference pointing to the same location as
// the one to the same item in the span from the original memory.
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfBytes[0],
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastToByte()
{
// Cast from float to bytes to verify casting works when the target type
// as a smaller byte size as well (so the resulting length will be larger).
Memory<float> memoryOfFloats = new float[128];
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(128 * sizeof(float), memoryOfBytes.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<byte> spanOfBytes = memoryOfBytes.Span;
// Same as above, we need to verify that the resulting span has matching
// starting references with the one produced by the original memory.
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastToShort()
{
// Similar test as above, just with different types to double check
Memory<float> memoryOfFloats = new float[128];
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
Assert.AreEqual(128 * sizeof(float) / sizeof(short), memoryOfShorts.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<short> spanOfShorts = memoryOfShorts.Span;
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastFromByteAndBack()
{
// Here we start from a byte array, get a memory, then cast to float and then
// back to byte. We want to verify that the final memory is both valid and
// consistent, as well that our internal optimization works and that the final
// memory correctly skipped the indirect memory managed and just wrapped the original
// array instead. This is documented in the custom array memory manager in the package.
byte[]? data = new byte[128];
Memory<byte> memoryOfBytes = data;
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
// Here we get the array from the final memory and check that it does exist and
// the associated parameters match the ones we'd expect here (same length, offset of 0).
Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out ArraySegment<byte> segment));
Assert.AreSame(segment.Array!, data);
Assert.AreEqual(0, segment.Offset);
Assert.HasCount(segment.Count, data);
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
Span<byte> span1 = memoryOfBytes.Span;
Span<byte> span2 = memoryBack.Span;
// Also validate the initial and final spans for reference equality
Assert.IsTrue(span1 == span2);
}
[TestMethod]
public void Test_MemoryExtensions_Cast_TooShort_WithSlice()
{
// Like we did above, we have some more tests where we slice an initial memory and
// validate the length of the resulting, accounting for the expected rounding down.
Memory<byte> m1 = new byte[8].AsMemory().Slice(4, 3);
Memory<int> mc1 = m1.Cast<byte, int>();
Assert.IsTrue(mc1.IsEmpty);
Memory<byte> m2 = new byte[20].AsMemory().Slice(4, 13);
Memory<float> mc2 = m2.Cast<byte, float>();
Assert.AreEqual(3, mc2.Length);
Memory<byte> m3 = new byte[16].AsMemory().Slice(5);
Memory<float> mc3 = m3.Cast<byte, float>();
Assert.AreEqual(2, mc3.Length);
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastFromByte_WithSlice()
{
// Same exact test as the cast from byte to float did above, but with a slice. This is done
// to ensure the cast still works correctly when the memory is internally storing an offset.
Memory<byte> memoryOfBytes = new byte[512].AsMemory().Slice(128, 128);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Assert.AreEqual(128 / sizeof(float), memoryOfFloats.Length);
Span<byte> spanOfBytes = memoryOfBytes.Span;
Span<float> spanOfFloats = memoryOfFloats.Span;
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfBytes[0],
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastToByte_WithSlice()
{
// Same as above, just with inverted source and destination types
Memory<float> memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(128 * sizeof(float), memoryOfBytes.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<byte> spanOfBytes = memoryOfBytes.Span;
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastToShort_WithSlice()
{
// Once again the same test but with types both different in size than 1. We're mostly
// just testing the rounding logic in a number of different case to ensure it's correct.
Memory<float> memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
Assert.AreEqual(128 * sizeof(float) / sizeof(short), memoryOfShorts.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<short> spanOfShorts = memoryOfShorts.Span;
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice()
{
// Just like the equivalent test above, but with a slice thrown in too
byte[]? data = new byte[512];
Memory<byte> memoryOfBytes = data.AsMemory().Slice(128, 128);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
// Here we now also have to validate the starting offset from the extracted array
Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out ArraySegment<byte> segment));
Assert.AreSame(segment.Array!, data);
Assert.AreEqual(128, segment.Offset);
Assert.HasCount(128, segment);
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
Span<byte> span1 = memoryOfBytes.Span;
Span<byte> span2 = memoryBack.Span;
Assert.IsTrue(span1 == span2);
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastFromByte()
{
// This test is just like the ones above, but this time we're casting a memory
// that wraps a custom memory manager and not an array. This is done to ensure
// the casting logic works correctly in all cases, as it'll use a different
// memory manager internally (a memory can wrap a string, an array or a manager).
Memory<byte> memoryOfBytes = new ArrayMemoryManager<byte>(128);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Assert.AreEqual(128 / sizeof(float), memoryOfFloats.Length);
Span<byte> spanOfBytes = memoryOfBytes.Span;
Span<float> spanOfFloats = memoryOfFloats.Span;
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfBytes[0],
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastToByte()
{
// Same as above, but with inverted types
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(128);
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(128 * sizeof(float), memoryOfBytes.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<byte> spanOfBytes = memoryOfBytes.Span;
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastToShort()
{
// Same as above, but with types different in size than 1, just in case
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(128);
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
Assert.AreEqual(128 * sizeof(float) / sizeof(short), memoryOfShorts.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<short> spanOfShorts = memoryOfShorts.Span;
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack()
{
// Equivalent to the one with an array, but with a memory manager
ArrayMemoryManager<byte>? data = new(128);
Memory<byte> memoryOfBytes = data;
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
// Here we expect to get back the original memory manager, due to the same optimization we
// checked for when using an array. We need to check they're the same, and the other parameters.
Assert.IsTrue(MemoryMarshal.TryGetMemoryManager<byte, ArrayMemoryManager<byte>>(memoryBack, out ArrayMemoryManager<byte>? manager, out int start, out int length));
Assert.AreSame(manager!, data);
Assert.AreEqual(0, start);
Assert.AreEqual(128, length);
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
Span<byte> span1 = memoryOfBytes.Span;
Span<byte> span2 = memoryBack.Span;
Assert.IsTrue(span1 == span2);
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastFromByte_WithSlice()
{
// Same as the ones with an array, but with an extra slice
Memory<byte> memoryOfBytes = new ArrayMemoryManager<byte>(512).Memory.Slice(128, 128);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Assert.AreEqual(128 / sizeof(float), memoryOfFloats.Length);
Span<byte> spanOfBytes = memoryOfBytes.Span;
Span<float> spanOfFloats = memoryOfFloats.Span;
Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfBytes[0],
ref Unsafe.As<float, byte>(ref spanOfFloats[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastToByte_WithSlice()
{
// Same as above, but with inverted types
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(512).Memory.Slice(128, 128);
Memory<byte> memoryOfBytes = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(128 * sizeof(float), memoryOfBytes.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<byte> spanOfBytes = memoryOfBytes.Span;
Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<byte, float>(ref spanOfBytes[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastToShort_WithSlice()
{
// Same as above but with different types
Memory<float> memoryOfFloats = new ArrayMemoryManager<float>(512).Memory.Slice(128, 128);
Memory<short> memoryOfShorts = memoryOfFloats.Cast<float, short>();
Assert.AreEqual(128 * sizeof(float) / sizeof(short), memoryOfShorts.Length);
Span<float> spanOfFloats = memoryOfFloats.Span;
Span<short> spanOfShorts = memoryOfShorts.Span;
Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
Assert.IsTrue(Unsafe.AreSame(
ref spanOfFloats[0],
ref Unsafe.As<short, float>(ref spanOfShorts[0])));
}
[TestMethod]
public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlice()
{
// Just like the one above, but with the slice
ArrayMemoryManager<byte>? data = new(512);
Memory<byte> memoryOfBytes = data.Memory.Slice(128, 128);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>();
Memory<byte> memoryBack = memoryOfFloats.Cast<float, byte>();
Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
// Here we also need to validate that the offset was maintained
Assert.IsTrue(MemoryMarshal.TryGetMemoryManager<byte, ArrayMemoryManager<byte>>(memoryBack, out ArrayMemoryManager<byte>? manager, out int start, out int length));
Assert.AreSame(manager!, data);
Assert.AreEqual(128, start);
Assert.AreEqual(128, length);
Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
Span<byte> span1 = memoryOfBytes.Span;
Span<byte> span2 = memoryBack.Span;
Assert.IsTrue(span1 == span2);
}
[TestMethod]
[DataRow(64, 0, 0)]
[DataRow(64, 4, 0)]
[DataRow(64, 0, 4)]
[DataRow(64, 4, 4)]
[DataRow(256, 16, 0)]
[DataRow(256, 4, 16)]
[DataRow(256, 64, 0)]
[DataRow(256, 64, 8)]
public unsafe void Test_MemoryExtensions_FromArray_CastFromByte_Pin(int size, int preOffset, int postOffset)
{
// Here we need to validate that pinning works correctly in a number of cases. First we allocate
// an array of the requested size, then get a memory after slicing to a target position, then cast
// and then slice again. We do so to ensure that pinning correctly tracks the correct index with
// respect to the original array through a number of internal offsets. As in, when pinning the
// final memory, our internal custom memory manager should be able to pin the item in the original
// array at offset preOffset + (postOffset * sizeof(float)), accounting for the cast as well.
byte[]? data = new byte[size];
Memory<byte> memoryOfBytes = data.AsMemory(preOffset);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>().Slice(postOffset);
using MemoryHandle handle = memoryOfFloats.Pin();
void* p1 = handle.Pointer;
void* p2 = Unsafe.AsPointer(ref data[preOffset + (postOffset * sizeof(float))]);
Assert.IsTrue(p1 == p2);
}
[TestMethod]
[DataRow(64, 0, 0)]
[DataRow(64, 4, 0)]
[DataRow(64, 0, 4)]
[DataRow(64, 4, 4)]
[DataRow(256, 16, 0)]
[DataRow(256, 4, 16)]
[DataRow(256, 64, 0)]
[DataRow(256, 64, 8)]
public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int size, int preOffset, int postOffset)
{
// Just like the test above, but this type the initial memory wraps a memory manager
ArrayMemoryManager<byte>? data = new(size);
Memory<byte> memoryOfBytes = data.Memory.Slice(preOffset);
Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>().Slice(postOffset);
using MemoryHandle handle = memoryOfFloats.Pin();
void* p1 = handle.Pointer;
void* p2 = Unsafe.AsPointer(ref data.GetSpan()[preOffset + (postOffset * sizeof(float))]);
Assert.IsTrue(p1 == p2);
}
[TestMethod]
public void Test_MemoryExtensions_FromString_CastFromByteAndBack()
{
// This is the same as the tests above, but here we're testing the
// other remaining case, that is when a memory is wrapping a string.
string? data = new('a', 128);
Memory<char> memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory());
Memory<float> memoryOfFloats = memoryOfChars.Cast<char, float>();
Memory<char> memoryBack = memoryOfFloats.Cast<float, char>();
Assert.AreEqual(memoryOfChars.Length, memoryBack.Length);
// Get the original string back (to validate the optimization too) and check the params
Assert.IsTrue(MemoryMarshal.TryGetString(memoryOfChars, out string? text, out int start, out int length));
Assert.AreSame(text!, data);
Assert.AreEqual(0, start);
Assert.AreEqual(length, data.Length);
Assert.IsTrue(memoryOfChars.Equals(memoryBack));
Span<char> span1 = memoryOfChars.Span;
Span<char> span2 = memoryBack.Span;
Assert.IsTrue(span1 == span2);
}
[TestMethod]
[DataRow(64, 0, 0)]
[DataRow(64, 4, 0)]
[DataRow(64, 0, 4)]
[DataRow(64, 4, 4)]
[DataRow(256, 16, 0)]
[DataRow(256, 4, 16)]
[DataRow(256, 64, 0)]
[DataRow(256, 64, 8)]
public unsafe void Test_MemoryExtensions_FromString_CastAndPin(int size, int preOffset, int postOffset)
{
// Same test as before to validate pinning, but starting from a string
string? data = new('a', size);
Memory<char> memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()).Slice(preOffset);
Memory<byte> memoryOfBytes = memoryOfChars.Cast<char, byte>().Slice(postOffset);
using (MemoryHandle handle1 = memoryOfBytes.Pin())
{
void* p1 = handle1.Pointer;
void* p2 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(byte) / sizeof(char))));
Assert.IsTrue(p1 == p2);
}
// Here we also add an extra test just like the one above, but casting to a type
// that is bigger in byte size than char. Just to double check the casting logic.
Memory<int> memoryOfInts = memoryOfChars.Cast<char, int>().Slice(postOffset);
using (MemoryHandle handle2 = memoryOfInts.Pin())
{
void* p3 = handle2.Pointer;
void* p4 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(int) / sizeof(char))));
Assert.IsTrue(p3 == p4);
}
}
[TestMethod]
public void Test_MemoryExtensions_EmptyMemoryStream()
{
Memory<byte> memory = default;
// Creating a stream from a default memory is valid, it's just empty
Stream stream = memory.AsStream();
Assert.IsNotNull(stream);
Assert.AreEqual(stream.Length, memory.Length);
Assert.IsTrue(stream.CanWrite);
}
[TestMethod]
public void Test_MemoryExtensions_MemoryStream()
{
Memory<byte> memory = new byte[1024];
Stream stream = memory.AsStream();
Assert.IsNotNull(stream);
Assert.AreEqual(stream.Length, memory.Length);
Assert.IsTrue(stream.CanWrite);
}
private sealed class ArrayMemoryManager<T> : MemoryManager<T>
where T : unmanaged
{
private readonly T[] array;
public ArrayMemoryManager(int size)
{
this.array = new T[size];
}
public override Span<T> GetSpan()
{
return this.array;
}
public override unsafe MemoryHandle Pin(int elementIndex = 0)
{
GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
ref T r0 = ref this.array[elementIndex];
void* p = Unsafe.AsPointer(ref r0);
return new(p, handle);
}
public override void Unpin()
{
}
protected override void Dispose(bool disposing)
{
}
public static implicit operator Memory<T>(ArrayMemoryManager<T> memoryManager)
{
return memoryManager.Memory;
}
}
}