Files
UnmanagedMMU/UnmanagedMMUTests/SegmentedPoolTests.cs

797 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Threading;
using UnmanagedMMU;
using UnmanagedMMU.Allocators;
using UnmanagedMMU.Handles;
using UnmanagedMMU.Handles.Internal;
using Xunit;
namespace UnmanagedMMUTests
{
/// <summary>
/// <see cref="IUnmanagedAllocator"/> that will fail after the specified number allocations have occurred
/// </summary>
public sealed unsafe class FailingAllocator : IUnmanagedAllocator
{
private readonly int _failAfter;
private int _allocCount;
public FailingAllocator(int failAfterNAllocations = 0)
{
// Multiply to account for both base memory and metadata allocation in Segment creation
_failAfter = 2 * failAfterNAllocations;
}
public void* Alloc(nuint size)
{
Interlocked.Increment(ref _allocCount);
if (_allocCount > _failAfter)
{
throw new OutOfMemoryException("The allocator has failed!");
}
return NativeMemory.Alloc(size);
}
public unsafe void* AllocAligned(nuint size, nuint alignment)
{
Interlocked.Increment(ref _allocCount);
if (_allocCount > _failAfter)
{
throw new OutOfMemoryException("The allocator has failed!");
}
// For test simplicity, we return aligned memory but rely on underlying native alloc logic
// or just raw alloc if it's a simulation.
// Since we use NativeMemory.AlignedAlloc in DefaultUnmanagedAllocator,
// we should ideally mimic that for segment alignment checks.
// However, for failure testing, any valid pointer works.
// To ensure alignment consistency with SegmentedPool expectations:
if (size > 0 && (alignment & (alignment - 1)) == 0)
{
// Basic aligned alloc implementation for test purposes if we want strict behavior
// For failure testing, just allocating unaligned is usually enough to trigger the check later,
// but let's stick to the simpler implementation used in the prompt to keep tests stable.
return NativeMemory.Alloc(size);
}
return NativeMemory.Alloc(size);
}
public void Free(void* ptr)
{
if (ptr == null)
{
return;
}
NativeMemory.Free(ptr);
}
public unsafe void FreeAligned(void* ptr, nuint alignment)
{
if (ptr == null)
{
return;
}
NativeMemory.AlignedFree(ptr);
}
}
/// <summary>
/// Helper class for providing useful assertions
/// </summary>
public unsafe static class AssertEx
{
public static void ThrowsArgumentOutOfRangeException(Action action, string? paramName = null)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(action);
if (paramName != null)
{
Assert.Equal(paramName, ex.ParamName);
}
}
public static void ThrowsObjectDisposedException(Action action, string? messageContains = null)
{
var ex = Assert.Throws<ObjectDisposedException>(action);
if (messageContains != null)
{
Assert.Contains(messageContains, ex.Message);
}
}
/// <summary>
/// Asserts the given <typeparamref name="T"/>* is not null
/// </summary>
/// <typeparam name="T">Unmanged type of the pointer</typeparam>
/// <param name="pointer"></param>
public static void AssertNotNullPointer<T>(T* pointer) where T : unmanaged
{
Assert.True((nuint)pointer != 0, "Pointer should not be null");
}
}
public unsafe class SegmentedPoolTests
{
/// <summary>
/// Helper method to create a SegmentedPool for testing.
/// Ensures consistent test setup across all tests.
/// </summary>
private SegmentedPool CreateTestPool(nuint segmentSize = 1024, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 2, bool zeroMemory = false)
{
return new SegmentedPool(segmentSize, segmentAlignment, initialSegments, zeroMemory);
}
/// <summary>
/// Helper method to create a SegmentedPool with a custom allocator for testing.
/// </summary>
private SegmentedPool CreateTestPoolWithAllocator(IUnmanagedAllocator allocator, int initialSegments = 2)
{
return new SegmentedPool(segmentSize: 1024, segmentAlignment: SegmentAlignment.Aligned32, initialSegments: initialSegments, zeroMemory: false, allocator: allocator);
}
private void AssertPoolDisposed(SegmentedPool pool, Action? verifyDisposedAfter = null)
{
pool.Dispose();
Assert.True(pool.IsDisposed);
verifyDisposedAfter?.Invoke();
}
#region Constructor Tests
[Fact]
public void ConstructorSegmentSizeZeroThrowsArgumentException()
{
AssertEx.ThrowsArgumentOutOfRangeException(() => new SegmentedPool(segmentSize: 0), "segmentSize");
}
[Fact]
public void ConstructorInitialSegmentCountLessThanOneThrowsArgumentException()
{
AssertEx.ThrowsArgumentOutOfRangeException(() => new SegmentedPool(initialSegments: 0), "initialSegments");
}
[Fact]
public void ConstructorValidArgumentsIsValidObject()
{
nuint segmentSize = 1024;
int initialSegments = 2;
using var pool = new SegmentedPool(segmentSize: segmentSize, initialSegments: initialSegments);
Assert.False(pool.IsDisposed);
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
// Total allocated is segmentSize * initialSegments
Assert.Equal(segmentSize * (nuint)initialSegments, pool.TotalAllocatedBytes);
// 1 active, (initial-1) free
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(initialSegments - 1, pool.FreeSegmentCount);
Assert.Equal(0u, pool.TotalUsedBytes);
}
[Fact]
public void ConstructorValidArgumentsButAllocationOfInitialSegmentsFailsThrowsOutOfMemoryException()
{
int initialSegments = 2;
FailingAllocator failingAllocator = new FailingAllocator(failAfterNAllocations: 1);
var ex = Assert.Throws<OutOfMemoryException>(() => CreateTestPoolWithAllocator(failingAllocator, initialSegments));
Assert.Equal("The allocator has failed!", ex.Message);
}
[Fact]
public void ConstructorWithZeroMemoryInitializationZeroesMemory()
{
using var pool = new SegmentedPool(segmentSize: 256, initialSegments: 1, zeroMemory: true);
using var handle = pool.Allocate<byte>(8);
// Memory should be zeroed
for (int i = 0; i < 8; i++)
{
Assert.Equal(0, handle.Pointer[i]);
}
}
#endregion
#region Property Access Tests
[Fact]
public void SetSegmentSizeWithSizeOfZeroThrowsArgumentOutOfRangeException()
{
using var pool = CreateTestPool();
AssertEx.ThrowsArgumentOutOfRangeException(() => pool.SetSegmentSize(0), "newSize");
}
[Fact]
public void SetSegmentSizeChangesTheSegmentSize()
{
using var pool = CreateTestPool();
Assert.Equal(1024u, pool.CurrentSegmentSize);
pool.SetSegmentSize(4096);
Assert.Equal(4096u, pool.CurrentSegmentSize);
}
[Fact]
public void ResetSegmentSizeChangesTheSegmentSizeToDefault()
{
using var pool = CreateTestPool();
Assert.Equal(1024u, pool.CurrentSegmentSize);
pool.ResetSegmentSize();
Assert.Equal(4194304u, pool.CurrentSegmentSize);
}
[Fact]
public void SetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.SetSegmentSize(4096));
}
[Fact]
public void ResetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.ResetSegmentSize());
}
#endregion
#region Allocation Tests
[Fact]
public void AllocateValidCountAndForGenericTSucceeds()
{
using var pool = CreateTestPool();
using IMemoryHandle<byte> handle = pool.Allocate<byte>(100);
Assert.True(handle.Length > 0);
Assert.Equal(100, (int)handle.Length);
AssertEx.AssertNotNullPointer(handle.Pointer);
}
[Fact]
public void AllocateInvalidCountThrowsArgumentOutOfRangeException()
{
using var pool = CreateTestPool();
AssertEx.ThrowsArgumentOutOfRangeException(() => pool.Allocate<int>(0));
}
[Fact]
public void AllocateValidCountButAllocationIsLargerThanCurrentSegmentSucceeds()
{
// Force segment size to be small to ensure a switch happens or check behavior
using var pool = CreateTestPool(segmentSize: 128);
using var handle = pool.Allocate<int>(100);
Assert.Equal(100, (int)handle.Length);
}
[Fact]
public void AllocateValidCountWhenAllocationExceedsSegmentSizeSucceedsSwitchesSegment()
{
using var pool = CreateTestPool(segmentSize: 1024, initialSegments: 2);
// int * 300 = 1200 bytes > 1024 segment
using var handle = pool.Allocate<int>(300);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
}
[Fact]
public void AllocateValidCountWhenFreeSegmentAvailableReusesSegment()
{
using var pool = CreateTestPool(segmentSize: 1024, initialSegments: 2);
// Fill current segment roughly
int sizeToFillCurrent = (int)(1024 / sizeof(int));
using var handle1 = pool.Allocate<int>(sizeToFillCurrent - 1);
using var handle2 = pool.Allocate<int>(2);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
[Fact]
public void AllocateValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.Allocate<int>(123));
}
[Fact]
public void AllocateHandlesOwnership()
{
using var pool = CreateTestPool();
using var handle = pool.Allocate<byte>(50);
// Cast to internal interface to access ownership method
AssertEx.AssertNotNullPointer(handle.Pointer);
var ownedHandle = handle as UnmanagedMMU.Handles.Internal.IOwnedHandle;
Assert.NotNull(ownedHandle);
var owner = ownedHandle.GetOwner();
Assert.NotNull(owner);
Assert.Same(pool, owner);
}
[Fact]
public void AllocateHandlesValidity()
{
using var pool = CreateTestPool();
using var handle = pool.Allocate<byte>(100);
AssertEx.AssertNotNullPointer(handle.Pointer);
Assert.Equal((nuint)100, handle.Length);
Assert.Equal((nuint)100, handle.ByteCount);
}
[Fact]
public void AllocateHandlesMemoryIsZeroedWhenZeroMemoryFlagIsSet()
{
using var pool = new SegmentedPool(segmentSize: 512, initialSegments: 2, zeroMemory: true);
using var handle = pool.Allocate<byte>(64);
for (int i = 0; i < 64; i++)
{
Assert.Equal(0, handle.Pointer[i]);
}
}
#endregion
#region Aligned Allocation Tests (New Coverage)
[Fact]
public void AllocateAlignedValidCountSucceeds()
{
using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned16);
using var handle = pool.AllocateAligned<int>(10, SegmentAlignment.Aligned16);
Assert.Equal(10, (int)handle.Length);
AssertEx.AssertNotNullPointer(handle.Pointer);
}
[Fact]
public void AllocateAlignedReturnsAlignedMemory()
{
using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned32);
using var handle = pool.AllocateAligned<int>(5, SegmentAlignment.Aligned32);
// Verify address alignment
nuint address = (nuint)handle.Pointer;
Assert.True(address % 32 == 0, "Memory address should be aligned to 32 bytes.");
}
[Fact]
public void AllocateAlignedFailsOnDisposedPool()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.AllocateAligned<int>(10, SegmentAlignment.Aligned16));
}
[Fact]
public void AllocateAlignedHandlesMaxAlignment()
{
using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned128);
using var handle = pool.AllocateAligned<byte>(128, SegmentAlignment.Aligned128);
Assert.Equal(128, (int)handle.Length); // 128 elements of byte type
Assert.Equal(128, (int)handle.ByteCount); // 128 bytes total
Assert.True((nuint)handle.Pointer % 128 == 0);
}
#endregion
#region Diagnostics Tests
[Fact]
public void GetDiagnosticReportReturnsValidString()
{
using var pool = CreateTestPool(segmentSize: 1024, initialSegments: 2);
string report = pool.GetDiagnosticReport();
Assert.NotNull(report);
Assert.NotEmpty(report);
Assert.Contains("SegmentedPool Diagnostics", report);
Assert.Contains("Configuration", report);
Assert.Contains("Segment Summary", report);
Assert.Contains("Current Segment", report);
Assert.Contains("Memory Statistics", report);
var state = pool.GetPoolState();
Assert.Contains($"Total Segments: {state.TotalSegmentCount}", report);
Assert.Matches(@"Total Reserved:\s+\d+\s+(KiB|MiB|GiB|B)", report);
Assert.Matches(@"Total Used:\s+\d+\s+(KiB|MiB|GiB|B)", report);
Assert.Contains("KiB", report);
Assert.Contains("bytes", report);
Assert.Contains("OK", report);
Assert.Contains("Action Required", report);
string report2 = pool.GetDiagnosticReport();
Assert.Equal(report, report2);
}
[Fact]
public void GetDiagnosticReportNoCrashOnFreshPool()
{
using var pool = CreateTestPool(segmentSize: 512, initialSegments: 1);
using var handle = pool.Allocate<byte>(100);
string report1 = pool.GetDiagnosticReport();
string report2 = pool.GetDiagnosticReport();
Assert.Equal(report1, report2);
}
[Fact]
public void GetPoolStateReturnsValidState()
{
using var pool = CreateTestPool();
var state = pool.GetPoolState();
Assert.Equal(1, pool.FreeSegmentCount);
Assert.Equal(pool.FreeSegmentCount, state.FreeSegmentCount); // Check consistency
Assert.Equal(1, state.ActiveSegmentCount);
Assert.Equal(1024u, state.SegmentSize);
Assert.Equal(0u, state.PaddingBytes);
}
[Fact]
public void GetPoolStateReflectsPadding()
{
using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned64);
using var handle = pool.Allocate<int>(1); // 4 bytes.
// Offset will be 4, TotalUsed 4.
// But alignment padding might exist if base not aligned perfectly or internal logic.
// This test verifies state struct is populated.
var state = pool.GetPoolState();
Assert.True(state.PaddingBytes <= state.SegmentSize);
Assert.True(state.TotalUsed >= 0);
}
[Fact]
public void GetDiagnosticReportGeneratesSuggestions()
{
using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned32);
// Trigger a condition for "High Overhead" or similar
// Default config generates "Pool operating normally" or similar if usage is low.
// Let's force an alignment issue check or similar.
// However, base alignment is checked in constructor.
string report = pool.GetDiagnosticReport();
// Ensure suggestion field exists in output
Assert.Contains("Action Required", report);
}
[Fact]
public void GetCurrentSegmentInfoReturnsActiveSegment()
{
using var pool = CreateTestPool(segmentSize: 512, initialSegments: 2);
var current = pool.GetCurrentSegmentInfo();
Assert.True(current.IsActive, "Current segment should always be active");
}
[Fact]
public void GetAllSegmentInfosContainsFreeSegments()
{
using var pool = CreateTestPool(segmentSize: 512, initialSegments: 2);
var all = pool.GetAllSegmentInfos();
Assert.NotNull(all);
bool hasFreeSegment = false;
foreach (var segment in all)
{
if (!segment.IsActive)
{
hasFreeSegment = true;
break;
}
}
Assert.True(hasFreeSegment, "Should contain at least one free segment");
}
#endregion
#region Trim Tests
[Fact]
public void TrimInvalidArgumentThrowsArgumentOutOfRangeException()
{
using var pool = CreateTestPool();
AssertEx.ThrowsArgumentOutOfRangeException(() => pool.Trim(-123), "minFreeSegments");
}
[Fact]
public void TrimValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.Trim(16));
}
[Fact]
public void TrimWhenFreeSegmentCountLessThanMinSegmentsDoesNothing()
{
using var pool = CreateTestPool(initialSegments: 32);
int currentFree = pool.FreeSegmentCount;
Assert.Equal(31, currentFree);
nuint allocBefore = pool.TotalAllocatedBytes;
pool.Trim(128);
Assert.Equal(31, pool.FreeSegmentCount);
Assert.Equal(allocBefore, pool.TotalAllocatedBytes); // Should not change
}
[Fact]
public void TrimWhenFreeSegmentGreaterThanMinSegmentsTrimsFreeSegmentsToNewSize()
{
using var pool = CreateTestPool(initialSegments: 32);
int currentFree = pool.FreeSegmentCount;
Assert.Equal(31, currentFree);
nuint allocBefore = pool.TotalAllocatedBytes;
nuint segmentSize = pool.CurrentSegmentSize;
pool.Trim(16);
Assert.Equal(16, pool.FreeSegmentCount);
// Verify bytes freed
nuint allocAfter = pool.TotalAllocatedBytes;
nuint segmentsFreed = 31 - 16;
Assert.Equal(allocBefore - (segmentsFreed * segmentSize), allocAfter);
}
#endregion
#region Reset Tests
[Fact]
public void ResetNoTrimClearsAllActivateSegments()
{
using var pool = CreateTestPool(segmentSize: 300, initialSegments: 8);
using var h1 = pool.Allocate<byte>(256);
using var h2 = pool.Allocate<byte>(256);
using var h3 = pool.Allocate<byte>(256);
using var h4 = pool.Allocate<byte>(256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(4, pool.FreeSegmentCount);
Assert.Equal(1024, (int)pool.TotalUsedBytes);
pool.Reset();
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(7, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
Assert.Equal(2400, (int)pool.TotalAllocatedBytes);
}
[Fact]
public void ResetWithZeroMemoryTrueZeroesMemory()
{
using var pool = CreateTestPool(initialSegments: 1, zeroMemory: true);
using var handle = pool.Allocate<byte>(8);
for (int i = 0; i < 8; i++)
{
handle.Pointer[i] = (byte)i;
}
pool.Reset();
Assert.Equal(0, (int)pool.TotalUsedBytes);
using var handle2 = pool.Allocate<byte>(8);
for (int i = 0; i < 8; i++)
{
Assert.Equal(0, handle2.Pointer[i]);
}
}
[Fact]
public void ResetWithTrimClearsAllActivateSegments()
{
using var pool = CreateTestPool(segmentSize: 300, initialSegments: 64);
using var h1 = pool.Allocate<byte>(256);
using var h2 = pool.Allocate<byte>(256);
using var h3 = pool.Allocate<byte>(256);
using var h4 = pool.Allocate<byte>(256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(60, pool.FreeSegmentCount);
nuint bytesBefore = pool.TotalAllocatedBytes;
pool.Reset(trim: true);
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(16, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
// Total allocated should drop due to trim
Assert.True(pool.TotalAllocatedBytes < bytesBefore);
}
#endregion
#region Concurrency Tests (New Coverage)
[Fact]
public void ConcurrentAllocationsDoNotCorruptState()
{
using var pool = new SegmentedPool(segmentSize: 1024 * 1024, initialSegments: 10);
const int threads = 4;
const int allocsPerThread = 1000;
const int bytesPerAlloc = 100;
int successCount = 0;
int exceptionCount = 0;
Parallel.For(0, threads, i =>
{
try
{
for (int j = 0; j < allocsPerThread; j++)
{
using var handle = pool.Allocate<byte>(bytesPerAlloc);
if (handle.Pointer != null)
{
Interlocked.Increment(ref successCount);
}
}
}
catch
{
Interlocked.Increment(ref exceptionCount);
}
});
Assert.Equal(0, exceptionCount);
Assert.Equal(threads * allocsPerThread, successCount);
Assert.False(pool.IsDisposed);
nuint expectedTotalUsed = (nuint)(threads * allocsPerThread * bytesPerAlloc);
Assert.Equal(expectedTotalUsed, pool.TotalUsedBytes);
var report = pool.GetDiagnosticReport();
Assert.NotNull(report);
Assert.NotEmpty(report);
Assert.Contains("SegmentedPool Diagnostics", report);
}
[Fact]
public void ConcurrentResetAndAllocationsDoNotCorruptState()
{
using var pool = new SegmentedPool(segmentSize: 1024 * 1024, initialSegments: 10);
const int iterations = 10;
int successCount = 0;
Parallel.For(0, iterations, i =>
{
try
{
for (int j = 0; j < 5; j++)
{
pool.Reset();
using var handle = pool.Allocate<byte>(100);
if (handle.Pointer != null) Interlocked.Increment(ref successCount);
}
}
catch (Exception ex)
{
Assert.Fail($"Exception in concurrent reset: {ex.Message}");
}
});
Assert.True(successCount > 0);
}
[Fact]
public void ConcurrentDisposeIsSafe()
{
using var pool = CreateTestPool();
Parallel.For(0, 10, i =>
{
pool.Dispose();
});
Assert.True(pool.IsDisposed);
}
#endregion
#region Disposal Tests
[Fact]
public void DisposeIsIdempotent()
{
using var pool = CreateTestPool();
pool.Dispose();
pool.Dispose(); // Should not throw
Assert.True(pool.IsDisposed);
}
[Fact]
public void AllocatedHandlesBecomeInvalidAfterDispose()
{
using var pool = CreateTestPool();
using var handle = pool.Allocate<byte>(100);
pool.Dispose();
// The handle pointer should still exist but pool state is invalid.
// Accessing memory might crash if used by another thread, but here we just check handle state.
// Note: SegmentedMemoryHandle does not clear pointer.
// The important check is that pool is disposed.
Assert.True(pool.IsDisposed);
}
[Fact]
public void GetPoolStateThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.GetPoolState());
}
[Fact]
public void GetDiagnosticReportThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.GetDiagnosticReport());
}
[Fact]
public void GetCurrentSegmentInfoThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.GetCurrentSegmentInfo());
}
[Fact]
public void GetAllSegmentInfosThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.GetAllSegmentInfos());
}
[Fact]
public void ResetThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.Reset());
}
[Fact]
public void TrimThrowsObjectDisposedException()
{
using var pool = CreateTestPool();
pool.Dispose();
AssertEx.ThrowsObjectDisposedException(() => pool.Trim(0));
}
#endregion
}
}