Files
UnmanagedMMU/UnmanagedMMUTests/SegmentedPoolTests.cs
stdranges dfbdf905fe Update SegmentedPoolTests.cs
fix: updated test comments
2025-11-24 20:51:32 +00:00

637 lines
22 KiB
C#

using System;
using System.Runtime.InteropServices;
using UnmanagedMMU;
using UnmanagedMMU.Allocators;
using Xunit;
namespace UnmanagedMMUTests
{
/// <summary>
/// UnmanagedAllocator that will fail
/// </summary>
public sealed unsafe class FailingAllocator : IUnmanagedAllocator
{
private readonly int _failAfter;
private int _allocCount;
/// <summary>
/// Initializes a new <see cref="SegmentedPool"/> wich fails after <paramref name="failAfterNSegmentsAllocated"/> bytes has been allocated
/// </summary>
/// <param name="failAfterNSegmentsAllocated">Indicates the number of allocations that are allowed to succssed, more allocations after this will fail </param>
public FailingAllocator(int failAfterNSegmentsAllocated = 0)
{
// each segment has two unmanaged allocs!
_failAfter = 2 * failAfterNSegmentsAllocated;
}
public void* Alloc(nuint size)
{
_allocCount++;
if (_allocCount > _failAfter)
{
throw new OutOfMemoryException("The allocator has failed!");
}
return NativeMemory.Alloc(size);
}
public void Free(void* ptr)
{
NativeMemory.Free(ptr);
}
}
public class SegmentedPoolTests
{
#region TestData
public struct TestMyStruct
{
public int A;
public double B;
}
// Example enum
public enum TestMyEnum : int
{
First,
Second
}
#endregion
private void AssertSpanIsNotEmptyAndHasNElements<T>(Span<T> span, int nElements) where T : unmanaged
{
Assert.False(span.IsEmpty);
Assert.Equal(nElements, span.Length);
}
/// <summary>
/// Test that an ArgumentException is raised if zero is given for SegmentSize
/// </summary>
[Fact]
public void ConstructorSegmentSizeZeroThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new SegmentedPool(segmentSize: 0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'segmentSize')", ex.Message);
}
/// <summary>
/// Test that an ArgumentException is raised if initialSegment is 0
/// </summary>
[Fact]
public void ConstructorInitialSegmentCountLessThanOneThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new SegmentedPool(initialSegments: 0));
Assert.Equal("Initial segments count must be at least 1. (Parameter 'initialSegments')", ex.Message);
}
/// <summary>
/// Test that valid arguments create valid object
/// </summary>
[Fact]
public void ConstructorValidArgumentsIsValidObject()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.False(pool.IsDisposed);
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(initialSegments, (int)(pool.TotalAllocatedBytes / segmentSize));
Assert.Equal(1, pool.ActiveSegmentCount); // one is active
Assert.True(pool.TotalAllocatedBytes >= segmentSize * (nuint)initialSegments);
Assert.Equal(0u, pool.TotalUsedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that valid arguments but the allocation of the initial segments fails throws OutOfMemoryException
/// </summary>
[Fact]
public void ConstructorValidArgumentsButAllocationOfinitialSegmentsFailsThrowsOutOfMemoryException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
FailingAllocator failingAllocator = new FailingAllocator(failAfterNSegmentsAllocated: 1);
var ex = Assert.Throws<OutOfMemoryException>(() => new SegmentedPool(segmentSize, initialSegments, failingAllocator));
Assert.Equal("The allocator has failed!", ex.Message);
}
/// <summary>
/// Test that SetSegmentSize with the new size set to Zero throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void SetSegmentSizeWithSizeOfZeroThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.SetSegmentSize(0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'newSize')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that SetSegmentSize changes the allocated SegmentSize
/// </summary>
[Fact]
public void SetSegmentSizeChangesTheSegmentSize()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
nuint newSegmentSize = 4096;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
pool.SetSegmentSize(newSegmentSize);
Assert.NotEqual(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(newSegmentSize, pool.CurrentSegmentSize);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that ResetSegmentSize changes the allocated SegmentSize to the default 4 MiB
/// </summary>
[Fact]
public void ReetSegmentSizeChangesTheSegmentSizeToDefault()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
nuint defaultSize = 4194304; // 4 MiB
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
pool.ResetSegmentSize();
Assert.NotEqual(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(defaultSize, pool.CurrentSegmentSize);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that SetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void SetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.SetSegmentSize(4096));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that ResetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void ResetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.ResetSegmentSize());
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T is successful
/// </summary>
[Fact]
public void AllocateValidCountAndForGenericTSucceeds()
{
AllocateAndAssert<sbyte>();
AllocateAndAssert<byte>();
AllocateAndAssert<short>();
AllocateAndAssert<ushort>();
AllocateAndAssert<int>();
AllocateAndAssert<uint>();
AllocateAndAssert<long>();
AllocateAndAssert<ulong>();
AllocateAndAssert<nint>();
AllocateAndAssert<nuint>();
AllocateAndAssert<char>();
AllocateAndAssert<float>();
AllocateAndAssert<double>();
AllocateAndAssert<decimal>();
AllocateAndAssert<bool>();
}
private void AllocateAndAssert<T>() where T : unmanaged
{
SegmentedPool pool = new SegmentedPool();
try
{
Span<T> span = pool.Allocate<T>(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate with an invalid count but valid unamanged T throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void AllocateInvalidCountThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.Allocate<int>(0));
Assert.Equal("Allocation count must be greater than zero. (Parameter 'count')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but the allocation is larger than the current segment is successful
/// </summary>
[Fact]
public void AllocateValidCountButAllocationIsLargerThanCurrentSegmentSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128);
try
{
Span<int> span = pool.Allocate<int>(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but there are no free segments
/// </summary>
[Fact]
public void AllocateValidCountButNoFreeSegmentsSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128, initialSegments: 1);
try
{
Span<byte> spanb = pool.Allocate<byte>(128);
AssertSpanIsNotEmptyAndHasNElements(spanb, 128);
Assert.Equal(0, pool.FreeSegmentCount);
Span<byte> spanb2 = pool.Allocate<byte>(128);
AssertSpanIsNotEmptyAndHasNElements(spanb2, 128);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but the allocation size exceeds the segment size then the segment is switched Segment
/// </summary>
[Fact]
public void AllocateValidCountWhenAllocationExceedsSegmentSizeSucceedsSwitchesSegment()
{
nuint segmentSize = 1024;
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
int count = 300;
Assert.Equal(1, pool.FreeSegmentCount);
Span<int> span = pool.Allocate<int>(count);
AssertSpanIsNotEmptyAndHasNElements(span, count);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T and the allocaiton
/// </summary>
[Fact]
public void AllocateValidCountWhenFreeSegmentAvailableReusesSegment()
{
nuint segmentSize = 1024;
int initialSegments = 2; // one current + one free
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
// Fill _current segment almost completely
int sizeToFillCurrent = (int)(segmentSize / sizeof(int));
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
Span<int> span1 = pool.Allocate<int>(sizeToFillCurrent - 1);
AssertSpanIsNotEmptyAndHasNElements(span1, sizeToFillCurrent - 1);
// triggers SwitchSegment
Span<int> span2 = pool.Allocate<int>(2);
AssertSpanIsNotEmptyAndHasNElements(span2, 2);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void AllocateValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.Allocate<int>(123));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim with the min Segment less than 0 throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void TrimInvalidArgumentThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.Trim(-123));
Assert.Equal("minFreeSegments ('-123') must be a non-negative value. (Parameter 'minFreeSegments')\r\nActual value was -123.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim does not modify the total number of free segments if the minFreeSegment variable is greater than the number of free Segments
/// </summary>
[Fact]
public void TrimWhenFreeSegmentCountLessThanMinSegmentsDoesNothing()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 32;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(31, pool.FreeSegmentCount);
pool.Trim(128);
Assert.Equal(31, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim doesmodify the total number of free segments if the minFreeSegment variable is less than the number of free Segments
/// </summary>
[Fact]
public void TrimWhenFreeSegmentGreaterThanMinSegmentsTrimsFreeSegmentsToNewSize()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 32;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(31, pool.FreeSegmentCount);
pool.Trim(16);
Assert.Equal(16, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void TrimValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.Trim(16));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Reset with trim false clears all active Segments and leaves the SegmentPool with a single active page
/// </summary>
[Fact]
public void ResetNoTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 300 bytes for the test
int initialSegments = 8;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span<byte> testSpan1 = pool.Allocate<byte>(256);
Span<byte> testSpan2 = pool.Allocate<byte>(256);
Span<byte> testSpan3 = pool.Allocate<byte>(256);
Span<byte> testSpan4 = pool.Allocate<byte>(256);
AssertSpanIsNotEmptyAndHasNElements(testSpan1, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan2, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan3, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan4, 256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(4, pool.FreeSegmentCount);
Assert.Equal(4 * 256, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(8*300, (int)pool.TotalAllocatedBytes);
pool.Reset();
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(7, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(8 * 300, (int)pool.TotalAllocatedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Reset with trim true clears all active Segments and leaves the SegmentPool with a single active page and trims the free sements to 16
/// </summary>
[Fact]
public void ResetWithTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 300 bytes for the test
int initialSegments = 64;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span<byte> testSpan1 = pool.Allocate<byte>(256);
Span<byte> testSpan2 = pool.Allocate<byte>(256);
Span<byte> testSpan3 = pool.Allocate<byte>(256);
Span<byte> testSpan4 = pool.Allocate<byte>(256);
AssertSpanIsNotEmptyAndHasNElements(testSpan1, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan2, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan3, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan4, 256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(60, pool.FreeSegmentCount);
Assert.Equal(4 * 256, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(64 * 300, (int)pool.TotalAllocatedBytes);
pool.Reset(true);
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(16, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(17 * 300, (int)pool.TotalAllocatedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
}
}