First
This commit is contained in:
commit
79b5ab98aa
13 changed files with 2016 additions and 0 deletions
160
TestProject1/FuzzTest.cs
Normal file
160
TestProject1/FuzzTest.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
152
TestProject1/IteratorTests.cs
Normal file
152
TestProject1/IteratorTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
61
TestProject1/PersistenceTests.cs
Normal file
61
TestProject1/PersistenceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
87
TestProject1/StressTest.cs
Normal file
87
TestProject1/StressTest.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue