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 { /// /// that will fail after the specified number allocations have occurred /// 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); } } /// /// Helper class for providing useful assertions /// public unsafe static class AssertEx { public static void ThrowsArgumentOutOfRangeException(Action action, string? paramName = null) { var ex = Assert.Throws(action); if (paramName != null) { Assert.Equal(paramName, ex.ParamName); } } public static void ThrowsObjectDisposedException(Action action, string? messageContains = null) { var ex = Assert.Throws(action); if (messageContains != null) { Assert.Contains(messageContains, ex.Message); } } /// /// Asserts the given * is not null /// /// Unmanged type of the pointer /// public static void AssertNotNullPointer(T* pointer) where T : unmanaged { Assert.True((nuint)pointer != 0, "Pointer should not be null"); } } public unsafe class SegmentedPoolTests { /// /// Helper method to create a SegmentedPool for testing. /// Ensures consistent test setup across all tests. /// private SegmentedPool CreateTestPool(nuint segmentSize = 1024, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 2, bool zeroMemory = false) { return new SegmentedPool(segmentSize, segmentAlignment, initialSegments, zeroMemory); } /// /// Helper method to create a SegmentedPool with a custom allocator for testing. /// 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(() => 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(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 handle = pool.Allocate(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(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(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(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(sizeToFillCurrent - 1); using var handle2 = pool.Allocate(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(123)); } [Fact] public void AllocateHandlesOwnership() { using var pool = CreateTestPool(); using var handle = pool.Allocate(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(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(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(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(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(10, SegmentAlignment.Aligned16)); } [Fact] public void AllocateAlignedHandlesMaxAlignment() { using var pool = CreateTestPool(segmentAlignment: SegmentAlignment.Aligned128); using var handle = pool.AllocateAligned(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(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(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(256); using var h2 = pool.Allocate(256); using var h3 = pool.Allocate(256); using var h4 = pool.Allocate(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(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(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(256); using var h2 = pool.Allocate(256); using var h3 = pool.Allocate(256); using var h4 = pool.Allocate(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(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(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(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 } }