using System;
using System.Runtime.InteropServices;
using UnmanagedMMU;
using UnmanagedMMU.Allocators;
using Xunit;
namespace UnmanagedMMUTests
{
///
/// UnmanagedAllocator that will fail
///
public sealed unsafe class FailingAllocator : IUnmanagedAllocator
{
private readonly int _failAfter;
private int _allocCount;
///
/// Initializes a new wich fails after bytes has been allocated
///
/// Indicates the number of allocations that are allowed to succssed, more allocations after this will fail
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(Span span, int nElements) where T : unmanaged
{
Assert.False(span.IsEmpty);
Assert.Equal(nElements, span.Length);
}
///
/// Test that an ArgumentException is raised if zero is given for SegmentSize
///
[Fact]
public void ConstructorSegmentSizeZeroThrowsArgumentException()
{
var ex = Assert.Throws(() => new SegmentedPool(segmentSize: 0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'segmentSize')", ex.Message);
}
///
/// Test that an ArgumentException is raised if initialSegment is 0
///
[Fact]
public void ConstructorInitialSegmentCountLessThanOneThrowsArgumentException()
{
var ex = Assert.Throws(() => new SegmentedPool(initialSegments: 0));
Assert.Equal("Initial segments count must be at least 1. (Parameter 'initialSegments')", ex.Message);
}
///
/// Test that valid arguments create valid object
///
[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);
}
}
///
/// Test that valid arguments but the allocation of the initial segments fails throws OutOfMemoryException
///
[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(() => new SegmentedPool(segmentSize, initialSegments, failingAllocator));
Assert.Equal("The allocator has failed!", ex.Message);
}
///
/// Test that SetSegmentSize with the new size set to Zero throws ArgumentOutOfRangeException
///
[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(() => pool.SetSegmentSize(0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'newSize')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that SetSegmentSize changes the allocated SegmentSize
///
[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);
}
}
///
/// Test that ResetSegmentSize changes the allocated SegmentSize to the default 4 MiB
///
[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);
}
}
///
/// Test that SetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
///
[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(() => pool.SetSegmentSize(4096));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that ResetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
///
[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(() => pool.ResetSegmentSize());
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate a valid count and unamanged T is successful
///
[Fact]
public void AllocateValidCountAndForGenericTSucceeds()
{
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
AllocateAndAssert();
}
private void AllocateAndAssert() where T : unmanaged
{
SegmentedPool pool = new SegmentedPool();
try
{
Span span = pool.Allocate(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate with an invalid count but valid unamanged T throws ArgumentOutOfRangeException
///
[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(() => pool.Allocate(0));
Assert.Equal("Allocation count must be greater than zero. (Parameter 'count')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate a valid count and unamanged T but the allocation is larger than the current segment is successful
///
[Fact]
public void AllocateValidCountButAllocationIsLargerThanCurrentSegmentSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128);
try
{
Span span = pool.Allocate(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate a valid count and unamanged T but there are no free segments
///
[Fact]
public void AllocateValidCountButNoFreeSegmentsSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128, initialSegments: 1);
try
{
Span spanb = pool.Allocate(128);
AssertSpanIsNotEmptyAndHasNElements(spanb, 128);
Assert.Equal(0, pool.FreeSegmentCount);
Span spanb2 = pool.Allocate(128);
AssertSpanIsNotEmptyAndHasNElements(spanb2, 128);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate a valid count and unamanged T but the allocation size exceeds the segment size then the segment is switched Segment
///
[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 span = pool.Allocate(count);
AssertSpanIsNotEmptyAndHasNElements(span, count);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate a valid count and unamanged T and the allocaiton
///
[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 span1 = pool.Allocate(sizeToFillCurrent - 1);
AssertSpanIsNotEmptyAndHasNElements(span1, sizeToFillCurrent - 1);
// triggers SwitchSegment
Span span2 = pool.Allocate(2);
AssertSpanIsNotEmptyAndHasNElements(span2, 2);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Allocate called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
///
[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(() => pool.Allocate(123));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Trim with the min Segment less than 0 throws ArgumentOutOfRangeException
///
[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(() => 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);
}
}
///
/// Test that Trim does not modify the total number of free segments if the minFreeSegment variable is greater than the number of free Segments
///
[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);
}
}
///
/// Test that Trim doesmodify the total number of free segments if the minFreeSegment variable is less than the number of free Segments
///
[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);
}
}
///
/// Test that Trim called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
///
[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(() => pool.Trim(16));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
///
/// Test that Reset with trim false clears all active Segments and leaves the SegmentPool with a single active page
///
[Fact]
public void ResetNoTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 300 bytes for the test
int initialSegments = 8;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span testSpan1 = pool.Allocate(256);
Span testSpan2 = pool.Allocate(256);
Span testSpan3 = pool.Allocate(256);
Span testSpan4 = pool.Allocate(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);
}
}
///
/// 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
///
[Fact]
public void ResetWithTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 300 bytes for the test
int initialSegments = 64;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span testSpan1 = pool.Allocate(256);
Span testSpan2 = pool.Allocate(256);
Span testSpan3 = pool.Allocate(256);
Span testSpan4 = pool.Allocate(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);
}
}
}
}