This commit is contained in:
Linus Björnstam 2026-02-01 20:52:23 +01:00
commit 79b5ab98aa
13 changed files with 2016 additions and 0 deletions

160
TestProject1/FuzzTest.cs Normal file
View file

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using PersistentMap;
public class BTreeFuzzTests
{
private readonly ITestOutputHelper _output;
private readonly IntStrategy _strategy = new();
public BTreeFuzzTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void Fuzz_Insert_And_Remove_consistency()
{
// CONFIGURATION
const int Iterations = 100_000; // High enough to trigger all splits/merges
const int KeyRange = 5000; // Small enough to cause frequent collisions
int Seed = Environment.TickCount;
// ORACLES
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
var random = new Random(Seed);
_output.WriteLine($"Starting Fuzz Test with Seed: {Seed}");
try
{
for (int i = 0; i < Iterations; i++)
{
// 1. Pick an Action: 70% Insert/Update, 30% Remove
bool isInsert = random.NextDouble() < 0.7;
int key = random.Next(KeyRange);
int val = key * 100;
if (isInsert)
{
// ACTION: INSERT
reference[key] = val;
subject.Set(key, val);
}
else
{
// ACTION: REMOVE
if (reference.ContainsKey(key))
{
reference.Remove(key);
subject.Remove(key);
}
else
{
// Try removing non-existent key (should be safe)
subject.Remove(key);
}
}
// 2. VERIFY CONSISTENCY (Expensive but necessary)
// We check consistency every 1000 ops or if the tree is small,
// to keep the test fast enough.
if (i % 1000 == 0 || reference.Count < 100)
{
AssertConsistency(reference, subject);
}
}
// Final check
AssertConsistency(reference, subject);
}
catch (Exception)
{
_output.WriteLine($"FAILED at iteration with SEED: {Seed}");
throw; // Re-throw to fail the test
}
}
[Fact]
public void Fuzz_Range_Queries()
{
// Validates that your Range Enumerator matches LINQ on the reference
const int Iterations = 1000;
const int KeyRange = 2000;
int Seed = Environment.TickCount;
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
var random = new Random(Seed);
// Fill Data
for(int i=0; i<KeyRange; i++)
{
int k = random.Next(KeyRange);
reference[k] = k;
subject.Set(k, k);
}
var persistent = subject.ToPersistent();
for (int i = 0; i < Iterations; i++)
{
int min = random.Next(KeyRange);
int max = min + random.Next(KeyRange - min); // Ensure max >= min
// 1. Reference Result (LINQ)
// Note: SortedDictionary doesn't have a direct Range query, so we filter memory.
var expected = reference
.Where(kv => kv.Key >= min && kv.Key <= max)
.Select(kv => kv.Key)
.ToList();
// 2. Subject Result
var actual = persistent.Range(min, max)
.Select(kv => kv.Key)
.ToList();
// 3. Compare
if (!expected.SequenceEqual(actual))
{
_output.WriteLine($"Range Mismatch! Range: [{min}, {max}]");
_output.WriteLine($"Expected: {string.Join(",", expected)}");
_output.WriteLine($"Actual: {string.Join(",", actual)}");
Assert.Fail("Range query results differ.");
}
}
}
private void AssertConsistency(SortedDictionary<int, int> expected, TransientMap<int, int, IntStrategy> actual)
{
// 1. Count
// if (expected.Count != actual.Count)
// {
// throw new Exception($"Count Mismatch! Expected {expected.Count}, Got {actual.Count}");
// }
// 2. Full Scan Verification
using var enumerator = actual.GetEnumerator();
foreach (var kvp in expected)
{
if (!enumerator.MoveNext())
{
throw new Exception("Enumerator ended too early!");
}
if (enumerator.Current.Key != kvp.Key || enumerator.Current.Value != kvp.Value)
{
throw new Exception($"Content Mismatch! Expected [{kvp.Key}:{kvp.Value}], Got [{enumerator.Current.Key}:{enumerator.Current.Value}]");
}
}
if (enumerator.MoveNext())
{
throw new Exception("Enumerator has extra items!");
}
}
}

View file

@ -0,0 +1,152 @@
using Xunit;
using PersistentMap;
using System.Linq;
using System.Collections.Generic;
public class EnumeratorTests
{
// Use IntStrategy for simple numeric testing
private readonly IntStrategy _strategy = new();
// Helper to create a populated PersistentMap quickly
private PersistentMap<int, int, IntStrategy> CreateMap(params int[] keys)
{
var map = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
foreach (var k in keys)
{
map.Set(k, k * 10); // Value is key * 10 (e.g., 5 -> 50)
}
return map.ToPersistent();
}
[Fact]
public void EmptyMap_EnumeratesNothing()
{
var map = PersistentMap<int, int, IntStrategy>.Empty(_strategy);
var list = new List<int>();
foreach(var kv in map) list.Add(kv.Key);
Assert.Empty(list);
}
[Fact]
public void FullScan_ReturnsSortedItems()
{
// Insert in random order to prove sorting works
var map = CreateMap(5, 1, 3, 2, 4);
var keys = map.AsEnumerable().Select(x => x.Key).ToList();
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, keys);
}
[Fact]
public void Range_Inclusive_ExactMatches()
{
// Tree: 10, 20, 30, 40, 50
var map = CreateMap(10, 20, 30, 40, 50);
// Range 20..40 should give 20, 30, 40
var result = map.Range(20, 40).Select(x => x.Key).ToList();
Assert.Equal(new[] { 20, 30, 40 }, result);
}
[Fact]
public void Range_Inclusive_WithGaps()
{
// Tree: 10, 20, 30, 40, 50
var map = CreateMap(10, 20, 30, 40, 50);
// Range 15..45:
// Start: 15 -> Seeks to first key >= 15 (which is 20)
// End: 45 -> Stops after 40 (next key 50 is > 45)
var result = map.Range(15, 45).Select(x => x.Key).ToList();
Assert.Equal(new[] { 20, 30, 40 }, result);
}
[Fact]
public void Range_SingleItem_Exact()
{
var map = CreateMap(1, 2, 3);
// Range 2..2 should just return 2
var result = map.Range(2, 2).Select(x => x.Key).ToList();
Assert.Single(result);
Assert.Equal(2, result[0]);
}
[Fact]
public void From_StartOpen_ToEnd()
{
// Tree: 10, 20, 30, 40, 50
var map = CreateMap(10, 20, 30, 40, 50);
// From 35 -> Should be 40, 50 (everything >= 35)
var result = map.From(35).Select(x => x.Key).ToList();
Assert.Equal(new[] { 40, 50 }, result);
}
[Fact]
public void Until_StartTo_EndOpen()
{
// Tree: 10, 20, 30, 40, 50
var map = CreateMap(10, 20, 30, 40, 50);
// Until 25 -> Should be 10, 20 (everything <= 25)
var result = map.Until(25).Select(x => x.Key).ToList();
Assert.Equal(new[] { 10, 20 }, result);
}
[Fact]
public void Range_OutOfBounds_ReturnsEmpty()
{
var map = CreateMap(10, 20, 30);
// Range 40..50 (Past end)
Assert.Empty(map.Range(40, 50));
// Range 0..5 (Before start)
Assert.Empty(map.Range(0, 5));
// Range 15..16 (Gap between keys)
Assert.Empty(map.Range(15, 16));
}
[Fact]
public void Range_LargeDataset_CrossesLeafBoundaries()
{
// Force splits. Leaf Capacity is 32, so 100 items ensures ~3-4 leaves.
var tMap = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
for(int i=0; i<100; i++) tMap.Set(i, i);
var map = tMap.ToPersistent();
// Range spanning multiple leaves (e.g. 20 to 80)
var list = map.Range(20, 80).Select(x => x.Key).ToList();
Assert.Equal(61, list.Count); // 20 through 80 inclusive is 61 items
Assert.Equal(20, list.First());
Assert.Equal(80, list.Last());
}
[Fact]
public void Enumerator_Reset_Works()
{
// Verify that Reset() logic (re-seek/re-dive) works
var map = CreateMap(1, 2, 3);
using var enumerator = map.GetEnumerator();
enumerator.MoveNext(); // 1
enumerator.MoveNext(); // 2
enumerator.Reset(); // Should go back to start
Assert.True(enumerator.MoveNext());
Assert.Equal(1, enumerator.Current.Key);
}
}

View file

@ -0,0 +1,61 @@
namespace TestProject1;
using PersistentMap;
public class PersistenceTests
{
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
[Fact]
public void PersistentMap_IsTrulyImmutable()
{
var v0 = BaseOrderedMap<string, int, UnicodeStrategy>.Create(_strategy);
var v1 = v0.Set("A", 1);
var v2 = v1.Set("B", 2);
// v0 should be empty
Assert.False(v0.ContainsKey("A"));
// v1 should have A but not B
Assert.True(v1.ContainsKey("A"));
Assert.False(v1.ContainsKey("B"));
// v2 should have both
Assert.True(v2.ContainsKey("A"));
Assert.True(v2.ContainsKey("B"));
}
[Fact]
public void TransientToPersistent_CreatesSnapshot()
{
var tMap = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
tMap.Set("A", 1);
// Create Snapshot
var pMap = tMap.ToPersistent();
// Mutate Transient Map further
tMap.Set("B", 2);
tMap.Remove("A");
// Assert Transient State
Assert.False(tMap.ContainsKey("A"));
Assert.True(tMap.ContainsKey("B"));
// Assert Persistent Snapshot (Should be isolated)
Assert.True(pMap.ContainsKey("A")); // A should still be here
Assert.False(pMap.ContainsKey("B")); // B should not exist
}
[Fact]
public void PersistentToTransient_DoesNotCorruptSource()
{
var pMap = BaseOrderedMap<string, int, UnicodeStrategy>.Create(_strategy);
pMap = pMap.Set("Fixed", 1);
var tMap = pMap.ToTransient();
tMap.Set("Fixed", 999); // Modify shared key
// pMap should remain 1
Assert.True(pMap.TryGetValue("Fixed", out int val));
Assert.Equal(1, val);
}
}

View file

@ -0,0 +1,87 @@
namespace TestProject1;
using PersistentMap;
public class StressTests
{
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
[Fact]
public void LargeInsert_SplitsCorrectly()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
int count = 10_000;
// 1. Insert 10k items
for (int i = 0; i < count; i++)
{
// Pad with 0s to ensure consistent length sorting for simple debugging
map.Set($"Key_{i:D6}", i);
}
// 2. Read back all items
for (int i = 0; i < count; i++)
{
bool found = map.TryGetValue($"Key_{i:D6}", out int val);
Assert.True(found, $"Failed to find Key_{i:D6}");
Assert.Equal(i, val);
}
// 3. Verify Non-existent
Assert.False(map.ContainsKey("Key_999999"));
}
[Fact]
public void ReverseInsert_HandlesPrependSplits()
{
// Inserting in reverse order triggers the "Left/Right 90/10" split heuristic specific to prepends
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
int count = 5000;
for (int i = count; i > 0; i--)
{
map.Set(i.ToString("D6"), i);
}
for (int i = 1; i <= count; i++)
{
Assert.True(map.ContainsKey(i.ToString("D6")));
}
}
[Fact]
public void Random_InsertDelete_Churn()
{
// Fuzzing test to catch edge cases in Rebalance/Merge
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
var rng = new Random(12345);
var reference = new Dictionary<string, int>();
for (int i = 0; i < 5000; i++)
{
string key = rng.Next(0, 1000).ToString(); // High collision chance
int op = rng.Next(0, 3); // 0=Set, 1=Remove, 2=Check
if (op == 0)
{
map.Set(key, i);
reference[key] = i;
}
else if (op == 1)
{
map.Remove(key);
reference.Remove(key);
}
else
{
bool mapHas = map.TryGetValue(key, out int v1);
bool refHas = reference.TryGetValue(key, out int v2);
Assert.Equal(refHas, mapHas);
if (mapHas) Assert.Equal(v2, v1);
}
}
Console.WriteLine("bp");
}
}