Did some code cleanup,

added some extra thingies.
switched to spans. Let google gemini do whatever it wanted..
This commit is contained in:
Linus Björnstam 2026-04-16 11:51:38 +02:00
parent 978d0873dc
commit 7bea233edc
11 changed files with 944 additions and 248 deletions

View file

@ -21,7 +21,7 @@ public class BTreeFuzzTests
// CONFIGURATION
const int Iterations = 100_000; // High enough to trigger all splits/merges
const int KeyRange = 5000; // Small enough to cause frequent collisions
const bool showOps = true;
const bool showOps = false;
int Seed = 2135974; // Environment.TickCount;
// ORACLES
@ -167,4 +167,4 @@ public class BTreeFuzzTests
throw new Exception("Enumerator has extra items!");
}
}
}
}

View file

@ -0,0 +1,181 @@
using System.Linq;
using Xunit;
using PersistentMap;
namespace PersistentMap.Tests
{
public class BTreeExtendedOperationsTests
{
// Helper to quickly spin up a populated map
private TransientMap<int, string, IntStrategy> CreateMap(params int[] keys)
{
var map = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
foreach (var key in keys)
{
map.Set(key, $"val_{key}");
}
return map;
}
[Fact]
public void MinMax_OnEmptyTree_ReturnsFalse()
{
var map = CreateMap();
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
Assert.False(hasMin);
Assert.False(hasMax);
Assert.Equal(default, minKey);
Assert.Equal(default, maxKey);
}
[Fact]
public void MinMax_OnPopulatedTree_ReturnsCorrectExtremes()
{
var map = CreateMap(50, 10, 40, 20, 30); // Insert out of order
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
Assert.True(hasMin);
Assert.Equal(10, minKey);
Assert.Equal("val_10", minVal);
Assert.True(hasMax);
Assert.Equal(50, maxKey);
Assert.Equal("val_50", maxVal);
}
[Theory]
[InlineData(20, true, 30)] // Exact match, get next
[InlineData(25, true, 30)] // Missing key, gets first greater
[InlineData(50, false, 0)] // Max element has no successor
[InlineData(60, false, 0)] // Out of bounds high
public void Successor_ReturnsCorrectNextKey(int searchKey, bool expectedSuccess, int expectedNextKey)
{
var map = CreateMap(10, 20, 30, 40, 50);
bool success = map.TryGetSuccessor(searchKey, out int nextKey, out string nextVal);
Assert.Equal(expectedSuccess, success);
if (expectedSuccess)
{
Assert.Equal(expectedNextKey, nextKey);
Assert.Equal($"val_{expectedNextKey}", nextVal);
}
}
[Theory]
[InlineData(40, true, 30)] // Exact match, get previous
[InlineData(35, true, 30)] // Missing key, gets largest smaller
[InlineData(10, false, 0)] // Min element has no predecessor
[InlineData(5, false, 0)] // Out of bounds low
public void Predecessor_ReturnsCorrectPreviousKey(int searchKey, bool expectedSuccess, int expectedPrevKey)
{
var map = CreateMap(10, 20, 30, 40, 50);
bool success = map.TryGetPredecessor(searchKey, out int prevKey, out string prevVal);
Assert.Equal(expectedSuccess, success);
if (expectedSuccess)
{
Assert.Equal(expectedPrevKey, prevKey);
Assert.Equal($"val_{expectedPrevKey}", prevVal);
}
}
[Fact]
public void SuccessorPredecessor_CrossNodeBoundaries_WorksCorrectly()
{
// Insert 200 elements to guarantee leaf node splits (Capacity is 64)
// and internal node creation.
var keys = Enumerable.Range(1, 200).ToArray();
var map = CreateMap(keys);
// Test boundaries between multiple leaves
for (int i = 1; i < 200; i++)
{
// Successor
bool sFound = map.TryGetSuccessor(i, out int next, out _);
Assert.True(sFound);
Assert.Equal(i + 1, next);
// Predecessor
bool pFound = map.TryGetPredecessor(i + 1, out int prev, out _);
Assert.True(pFound);
Assert.Equal(i, prev);
}
}
[Fact]
public void SetOperations_Intersect_ReturnsCommonElements()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var intersect = mapA.Intersect(mapB).Select(kvp => kvp.Key).ToArray();
Assert.Equal(new[] { 4, 5 }, intersect);
}
[Fact]
public void SetOperations_Except_ReturnsElementsOnlyInFirstMap()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var aExceptB = mapA.Except(mapB).Select(kvp => kvp.Key).ToArray();
var bExceptA = mapB.Except(mapA).Select(kvp => kvp.Key).ToArray();
Assert.Equal(new[] { 1, 2, 3 }, aExceptB);
Assert.Equal(new[] { 6, 7, 8 }, bExceptA);
}
[Fact]
public void SetOperations_SymmetricExcept_ReturnsNonOverlappingElements()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var symmetric = mapA.SymmetricExcept(mapB).Select(kvp => kvp.Key).ToArray();
// Should return elements exclusively in A or B, but not both.
// Expected sorted naturally: 1, 2, 3, 6, 7, 8
Assert.Equal(new[] { 1, 2, 3, 6, 7, 8 }, symmetric);
}
[Fact]
public void SetOperations_WithEmptyMaps_HandleGracefully()
{
var populatedMap = CreateMap(1, 2, 3);
var emptyMap = CreateMap();
// Intersect with empty is empty
Assert.Empty(populatedMap.Intersect(emptyMap));
Assert.Empty(emptyMap.Intersect(populatedMap));
// Populated Except empty is Populated
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.Except(emptyMap).Select(k => k.Key));
// Empty Except Populated is empty
Assert.Empty(emptyMap.Except(populatedMap));
// Symmetric Except with empty is just the populated map
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.SymmetricExcept(emptyMap).Select(k => k.Key));
Assert.Equal(new[] { 1, 2, 3 }, emptyMap.SymmetricExcept(populatedMap).Select(k => k.Key));
}
[Fact]
public void SetOperations_CompleteOverlap_HandlesCorrectly()
{
var mapA = CreateMap(1, 2, 3);
var mapB = CreateMap(1, 2, 3);
Assert.Equal(new[] { 1, 2, 3 }, mapA.Intersect(mapB).Select(k => k.Key));
Assert.Empty(mapA.Except(mapB));
Assert.Empty(mapA.SymmetricExcept(mapB));
}
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PersistentMap\PersistentMap.csproj" />
</ItemGroup>
</Project>

70
TestProject1/UnitTest1.cs Normal file
View file

@ -0,0 +1,70 @@
namespace TestProject1;
using Xunit;
using PersistentMap;
public class BasicTests
{
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
[Fact]
public void Transient_InsertAndGet_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Apple", 1);
map.Set("Banana", 2);
map.Set("Cherry", 3);
Assert.True(map.TryGetValue("Apple", out int v1));
Assert.Equal(1, v1);
Assert.True(map.TryGetValue("Banana", out int v2));
Assert.Equal(2, v2);
Assert.False(map.TryGetValue("Date", out _));
}
[Fact]
public void Transient_Update_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Key", 100);
map.Set("Key", 200); // Overwrite
map.TryGetValue("Key", out int val);
Assert.Equal(200, val);
}
[Fact]
public void Transient_Remove_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("A", 1);
map.Set("B", 2);
map.Set("C", 3);
map.Remove("B");
Assert.True(map.ContainsKey("A"));
Assert.False(map.ContainsKey("B"));
Assert.True(map.ContainsKey("C"));
}
[Fact]
public void Transient_PrefixCollision_HandlesCollision()
{
// UnicodeStrategy only packs the first 4 chars.
// "Test1" and "Test2" have the same prefix "Test".
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Test1", 1);
map.Set("Test2", 2);
Assert.True(map.TryGetValue("Test1", out var v1));
Assert.Equal(1, v1);
Assert.True(map.TryGetValue("Test2", out var v2));
Assert.Equal(2, v2);
}
}