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); } } } }