- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests - Update solution file, InternalsVisibleTo, and csproj references - Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests) - Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.* - Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls - Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
1784 lines
56 KiB
C#
1784 lines
56 KiB
C#
// Go reference: server/stree/stree_test.go
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using NATS.Server.Internal.SubjectTree;
|
|
|
|
namespace NATS.Server.Core.Tests.Internal.SubjectTree;
|
|
|
|
/// <summary>
|
|
/// Tests for the Adaptive Radix Tree (ART) based SubjectTree.
|
|
/// Ported from Go: server/stree/stree_test.go (59 tests)
|
|
/// </summary>
|
|
public class SubjectTreeTests
|
|
{
|
|
private static byte[] B(string s) => Encoding.UTF8.GetBytes(s);
|
|
|
|
private static void MatchCount(SubjectTree<int> st, string filter, int expected)
|
|
{
|
|
var matches = new List<int>();
|
|
st.Match(B(filter), (_, v) => matches.Add(v));
|
|
matches.Count.ShouldBe(expected, $"filter={filter}");
|
|
}
|
|
|
|
private static (int Count, bool Completed) MatchUntilCount<T>(SubjectTree<T> st, string filter, int stopAfter)
|
|
{
|
|
int n = 0;
|
|
var completed = st.MatchUntil(B(filter), (_, _) =>
|
|
{
|
|
n++;
|
|
return n < stopAfter;
|
|
});
|
|
return (n, completed);
|
|
}
|
|
|
|
#region Basic CRUD
|
|
|
|
// Go: TestSubjectTreeBasics server/stree/stree_test.go:33
|
|
[Fact]
|
|
public void TestSubjectTreeBasics()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Size.ShouldBe(0);
|
|
|
|
// Single leaf
|
|
var (old, updated) = st.Insert(B("foo.bar.baz"), 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Size.ShouldBe(1);
|
|
|
|
// Find shouldn't work with a wildcard.
|
|
var (_, found) = st.Find(B("foo.bar.*"));
|
|
found.ShouldBeFalse();
|
|
|
|
// But it should with a literal. Find with single leaf.
|
|
var (v, found2) = st.Find(B("foo.bar.baz"));
|
|
found2.ShouldBeTrue();
|
|
v.ShouldBe(22);
|
|
|
|
// Update single leaf
|
|
var (old2, updated2) = st.Insert(B("foo.bar.baz"), 33);
|
|
old2.ShouldBe(22);
|
|
updated2.ShouldBeTrue();
|
|
st.Size.ShouldBe(1);
|
|
|
|
// Split the tree
|
|
var (old3, updated3) = st.Insert(B("foo.bar"), 22);
|
|
old3.ShouldBe(default);
|
|
updated3.ShouldBeFalse();
|
|
st.Size.ShouldBe(2);
|
|
|
|
// Now we have node4 -> leaf*2
|
|
var (v2, found3) = st.Find(B("foo.bar"));
|
|
found3.ShouldBeTrue();
|
|
v2.ShouldBe(22);
|
|
|
|
// Make sure we can still retrieve the original after the split.
|
|
var (v3, found4) = st.Find(B("foo.bar.baz"));
|
|
found4.ShouldBeTrue();
|
|
v3.ShouldBe(33);
|
|
}
|
|
|
|
// Go: TestSubjectTreeNoPrefix server/stree/stree_test.go:432
|
|
[Fact]
|
|
public void TestSubjectTreeNoPrefix()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
for (int i = 0; i < 26; i++)
|
|
{
|
|
var subj = B($"{(char)('A' + i)}");
|
|
var (old, updated) = st.Insert(subj, 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
}
|
|
|
|
st.Root.ShouldBeOfType<Node48>();
|
|
var n = (Node48)st.Root!;
|
|
n.NumChildren.ShouldBe((ushort)26);
|
|
|
|
var (v, found) = st.Delete(B("B"));
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(22);
|
|
n.NumChildren.ShouldBe((ushort)25);
|
|
|
|
var (v2, found2) = st.Delete(B("Z"));
|
|
found2.ShouldBeTrue();
|
|
v2.ShouldBe(22);
|
|
n.NumChildren.ShouldBe((ushort)24);
|
|
}
|
|
|
|
// Go: TestSubjectTreeEmpty server/stree/stree_test.go:1330
|
|
[Fact]
|
|
public void TestSubjectTreeEmpty()
|
|
{
|
|
// Test Empty on new tree
|
|
var st = new SubjectTree<int>();
|
|
st.Size.ShouldBe(0);
|
|
var st2 = st.Empty();
|
|
st2.ShouldBeSameAs(st); // Should return same instance
|
|
st2.Size.ShouldBe(0);
|
|
|
|
// Test Empty on tree with data
|
|
st.Insert(B("foo.bar"), 1);
|
|
st.Insert(B("foo.baz"), 2);
|
|
st.Insert(B("bar.baz"), 3);
|
|
st.Size.ShouldBe(3);
|
|
|
|
// Empty should clear everything
|
|
st2 = st.Empty();
|
|
st2.ShouldBeSameAs(st); // Should return same instance
|
|
st.Size.ShouldBe(0);
|
|
st.Root.ShouldBeNull();
|
|
|
|
// Verify we can't find old entries
|
|
st.Find(B("foo.bar")).Found.ShouldBeFalse();
|
|
st.Find(B("foo.baz")).Found.ShouldBeFalse();
|
|
st.Find(B("bar.baz")).Found.ShouldBeFalse();
|
|
|
|
// Verify we can insert new entries after Empty
|
|
var (old, updated) = st.Insert(B("new.entry"), 42);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Size.ShouldBe(1);
|
|
|
|
var (v, found) = st.Find(B("new.entry"));
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(42);
|
|
}
|
|
|
|
// Go: TestSizeOnNilTree server/stree/stree_test.go:1667
|
|
[Fact]
|
|
public void TestSizeOnNilTree()
|
|
{
|
|
// In C# we can't have a null reference call Size, but we test a new tree
|
|
var st = new SubjectTree<int>();
|
|
st.Size.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestFindEdgeCases server/stree/stree_test.go:1672
|
|
[Fact]
|
|
public void TestFindEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Test Find with empty subject at root level
|
|
st.Insert(B("foo.bar.baz"), 1);
|
|
st.Insert(B("foo"), 2);
|
|
|
|
// This should create a tree structure, now test finding with edge cases
|
|
var (v, found) = st.Find(B(""));
|
|
found.ShouldBeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Node Growth/Shrink
|
|
|
|
// Go: TestSubjectTreeNodeGrow server/stree/stree_test.go:69
|
|
[Fact]
|
|
public void TestSubjectTreeNodeGrow()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
var subj = B($"foo.bar.{(char)('A' + i)}");
|
|
var (old, updated) = st.Insert(subj, 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
}
|
|
|
|
// We have filled a node4.
|
|
st.Root.ShouldBeOfType<Node4>();
|
|
|
|
// This one will trigger us to grow.
|
|
var (old2, updated2) = st.Insert(B("foo.bar.E"), 22);
|
|
old2.ShouldBe(default);
|
|
updated2.ShouldBeFalse();
|
|
st.Root.ShouldBeOfType<Node10>();
|
|
|
|
for (int i = 5; i < 10; i++)
|
|
{
|
|
var subj = B($"foo.bar.{(char)('A' + i)}");
|
|
var (old3, updated3) = st.Insert(subj, 22);
|
|
old3.ShouldBe(default);
|
|
updated3.ShouldBeFalse();
|
|
}
|
|
|
|
// This one will trigger us to grow.
|
|
var (old4, updated4) = st.Insert(B("foo.bar.K"), 22);
|
|
old4.ShouldBe(default);
|
|
updated4.ShouldBeFalse();
|
|
// We have filled a node10.
|
|
st.Root.ShouldBeOfType<Node16>();
|
|
|
|
for (int i = 11; i < 16; i++)
|
|
{
|
|
var subj = B($"foo.bar.{(char)('A' + i)}");
|
|
var (old5, updated5) = st.Insert(subj, 22);
|
|
old5.ShouldBe(default);
|
|
updated5.ShouldBeFalse();
|
|
}
|
|
|
|
// This one will trigger us to grow.
|
|
var (old6, updated6) = st.Insert(B("foo.bar.Q"), 22);
|
|
old6.ShouldBe(default);
|
|
updated6.ShouldBeFalse();
|
|
st.Root.ShouldBeOfType<Node48>();
|
|
|
|
// Fill the node48.
|
|
for (int i = 17; i < 48; i++)
|
|
{
|
|
var subj = B($"foo.bar.{(char)('A' + i)}");
|
|
var (old7, updated7) = st.Insert(subj, 22);
|
|
old7.ShouldBe(default);
|
|
updated7.ShouldBeFalse();
|
|
}
|
|
|
|
// This one will trigger us to grow.
|
|
var subj8 = B($"foo.bar.{(char)('A' + 49)}");
|
|
var (old8, updated8) = st.Insert(subj8, 22);
|
|
old8.ShouldBe(default);
|
|
updated8.ShouldBeFalse();
|
|
st.Root.ShouldBeOfType<Node256>();
|
|
}
|
|
|
|
// Go: TestSubjectTreeNodePrefixMismatch server/stree/stree_test.go:127
|
|
[Fact]
|
|
public void TestSubjectTreeNodePrefixMismatch()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 11);
|
|
st.Insert(B("foo.bar.B"), 22);
|
|
st.Insert(B("foo.bar.C"), 33);
|
|
// Grab current root. Split below will cause update.
|
|
var or = st.Root;
|
|
// This one will force a split of the node
|
|
st.Insert(B("foo.foo.A"), 44);
|
|
st.Root.ShouldNotBeSameAs(or);
|
|
|
|
// Now make sure we can retrieve correctly.
|
|
st.Find(B("foo.bar.A")).Value.ShouldBe(11);
|
|
st.Find(B("foo.bar.B")).Value.ShouldBe(22);
|
|
st.Find(B("foo.bar.C")).Value.ShouldBe(33);
|
|
st.Find(B("foo.foo.A")).Value.ShouldBe(44);
|
|
}
|
|
|
|
// Go: TestNode256Operations server/stree/stree_test.go:1493
|
|
[Fact]
|
|
public void TestNode256Operations()
|
|
{
|
|
// Test node256 creation and basic operations
|
|
var n = new Node256(B("prefix"));
|
|
n.IsFull.ShouldBeFalse(); // node256 is never full
|
|
|
|
// Test findChild when child doesn't exist
|
|
var child = n.FindChild((byte)'a');
|
|
child.ShouldBeNull();
|
|
|
|
// Add a child and find it
|
|
var leaf = new Leaf<int>(B("suffix"), 42);
|
|
n.AddChild((byte)'a', leaf);
|
|
child = n.FindChild((byte)'a');
|
|
child.ShouldNotBeNull();
|
|
n.Meta.Size.ShouldBe((ushort)1);
|
|
|
|
// Test iter function
|
|
int iterCount = 0;
|
|
n.Iter((_) => { iterCount++; return true; });
|
|
iterCount.ShouldBe(1);
|
|
|
|
// Test iter with early termination
|
|
n.AddChild((byte)'b', new Leaf<int>(B("suffix2"), 43));
|
|
n.AddChild((byte)'c', new Leaf<int>(B("suffix3"), 44));
|
|
iterCount = 0;
|
|
n.Iter((_) => { iterCount++; return false; });
|
|
iterCount.ShouldBe(1);
|
|
|
|
// Test children() method
|
|
var children = n.Children();
|
|
children.Length.ShouldBe(256);
|
|
|
|
// Test that grow() panics
|
|
Should.Throw<InvalidOperationException>(() => n.Grow())
|
|
.Message.ShouldBe("grow can not be called on node256");
|
|
}
|
|
|
|
// Go: TestNode256Shrink server/stree/stree_test.go:1542
|
|
[Fact]
|
|
public void TestNode256Shrink()
|
|
{
|
|
var n256 = new Node256(B("prefix"));
|
|
|
|
// Add 49 children
|
|
for (int i = 0; i < 49; i++)
|
|
{
|
|
n256.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
n256.Meta.Size.ShouldBe((ushort)49);
|
|
|
|
// Shrink should not happen yet (> 48 children)
|
|
var shrunk = n256.Shrink();
|
|
shrunk.ShouldBeNull();
|
|
|
|
// Delete one to get to 48 children
|
|
n256.DeleteChild(0);
|
|
n256.Meta.Size.ShouldBe((ushort)48);
|
|
|
|
// Now shrink should return a node48
|
|
shrunk = n256.Shrink();
|
|
shrunk.ShouldNotBeNull();
|
|
shrunk.ShouldBeOfType<Node48>();
|
|
|
|
// Verify the shrunk node has all remaining children
|
|
for (int i = 1; i < 49; i++)
|
|
{
|
|
var child = shrunk.FindChild((byte)i);
|
|
child.ShouldNotBeNull();
|
|
}
|
|
}
|
|
|
|
// Go: TestNodeShrinkNotNeeded server/stree/stree_test.go:1850
|
|
[Fact]
|
|
public void TestNodeShrinkNotNeeded()
|
|
{
|
|
// Test node10 shrink when not needed (has more than 4 children)
|
|
var n10 = new Node10(B("prefix"));
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
|
|
}
|
|
var shrunk = n10.Shrink();
|
|
shrunk.ShouldBeNull(); // Should not shrink
|
|
|
|
// Test node16 shrink when not needed (has more than 10 children)
|
|
var n16 = new Node16(B("prefix"));
|
|
for (int i = 0; i < 11; i++)
|
|
{
|
|
n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
shrunk = n16.Shrink();
|
|
shrunk.ShouldBeNull(); // Should not shrink
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Delete
|
|
|
|
// Go: TestSubjectTreeNodeDelete server/stree/stree_test.go:152
|
|
[Fact]
|
|
public void TestSubjectTreeNodeDelete()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 22);
|
|
var (v, found) = st.Delete(B("foo.bar.A"));
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(22);
|
|
st.Root.ShouldBeNull();
|
|
|
|
var (v2, found2) = st.Delete(B("foo.bar.A"));
|
|
found2.ShouldBeFalse();
|
|
v2.ShouldBe(default);
|
|
|
|
var (v3, found3) = st.Find(B("foo.foo.A"));
|
|
found3.ShouldBeFalse();
|
|
v3.ShouldBe(default);
|
|
|
|
// Kick to a node4.
|
|
st.Insert(B("foo.bar.A"), 11);
|
|
st.Insert(B("foo.bar.B"), 22);
|
|
st.Insert(B("foo.bar.C"), 33);
|
|
|
|
// Make sure we can delete and that we shrink back to leaf.
|
|
var (v4, found4) = st.Delete(B("foo.bar.C"));
|
|
found4.ShouldBeTrue();
|
|
v4.ShouldBe(33);
|
|
var (v5, found5) = st.Delete(B("foo.bar.B"));
|
|
found5.ShouldBeTrue();
|
|
v5.ShouldBe(22);
|
|
// We should have shrunk here.
|
|
st.Root!.IsLeaf.ShouldBeTrue();
|
|
var (v6, found6) = st.Delete(B("foo.bar.A"));
|
|
found6.ShouldBeTrue();
|
|
v6.ShouldBe(11);
|
|
st.Root.ShouldBeNull();
|
|
|
|
// Now pop up to a node10 and make sure we can shrink back down.
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var subj = $"foo.bar.{(char)('A' + i)}";
|
|
st.Insert(B(subj), 22);
|
|
}
|
|
st.Root.ShouldBeOfType<Node10>();
|
|
var (v7, found7) = st.Delete(B("foo.bar.A"));
|
|
found7.ShouldBeTrue();
|
|
v7.ShouldBe(22);
|
|
st.Root.ShouldBeOfType<Node4>();
|
|
|
|
// Now pop up to node16
|
|
for (int i = 0; i < 11; i++)
|
|
{
|
|
var subj = $"foo.bar.{(char)('A' + i)}";
|
|
st.Insert(B(subj), 22);
|
|
}
|
|
st.Root.ShouldBeOfType<Node16>();
|
|
var (v8, found8) = st.Delete(B("foo.bar.A"));
|
|
found8.ShouldBeTrue();
|
|
v8.ShouldBe(22);
|
|
st.Root.ShouldBeOfType<Node10>();
|
|
st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
|
|
st.Find(B("foo.bar.B")).Value.ShouldBe(22);
|
|
|
|
// Now pop up to node48
|
|
st = new SubjectTree<int>();
|
|
for (int i = 0; i < 17; i++)
|
|
{
|
|
var subj = $"foo.bar.{(char)('A' + i)}";
|
|
st.Insert(B(subj), 22);
|
|
}
|
|
st.Root.ShouldBeOfType<Node48>();
|
|
var (v9, found9) = st.Delete(B("foo.bar.A"));
|
|
found9.ShouldBeTrue();
|
|
v9.ShouldBe(22);
|
|
st.Root.ShouldBeOfType<Node16>();
|
|
st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
|
|
|
|
// Now pop up to node256
|
|
st = new SubjectTree<int>();
|
|
for (int i = 0; i < 49; i++)
|
|
{
|
|
var subj = $"foo.bar.{(char)('A' + i)}";
|
|
st.Insert(B(subj), 22);
|
|
}
|
|
st.Root.ShouldBeOfType<Node256>();
|
|
var (v10, found10) = st.Delete(B("foo.bar.A"));
|
|
found10.ShouldBeTrue();
|
|
v10.ShouldBe(22);
|
|
st.Root.ShouldBeOfType<Node48>();
|
|
st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestSubjectTreeNodesAndPaths server/stree/stree_test.go:243
|
|
[Fact]
|
|
public void TestSubjectTreeNodesAndPaths()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
void Check(string subj)
|
|
{
|
|
var (v, found) = st.Find(B(subj));
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(22);
|
|
}
|
|
|
|
st.Insert(B("foo.bar.A"), 22);
|
|
st.Insert(B("foo.bar.B"), 22);
|
|
st.Insert(B("foo.bar.C"), 22);
|
|
st.Insert(B("foo.bar"), 22);
|
|
Check("foo.bar.A");
|
|
Check("foo.bar.B");
|
|
Check("foo.bar.C");
|
|
Check("foo.bar");
|
|
|
|
// This will do several things in terms of shrinking and pruning
|
|
st.Delete(B("foo.bar"));
|
|
Check("foo.bar.A");
|
|
Check("foo.bar.B");
|
|
Check("foo.bar.C");
|
|
}
|
|
|
|
// Go: TestSubjectTreeDeleteShortSubjectNoPanic server/stree/stree_test.go:1308
|
|
[Fact]
|
|
public void TestSubjectTreeDeleteShortSubjectNoPanic()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.baz"), 1);
|
|
st.Insert(B("foo.bar.qux"), 2);
|
|
|
|
var (v, found) = st.Delete(B("foo.bar"));
|
|
found.ShouldBeFalse();
|
|
v.ShouldBe(default);
|
|
|
|
st.Find(B("foo.bar.baz")).Value.ShouldBe(1);
|
|
st.Find(B("foo.bar.qux")).Value.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestDeleteEdgeCases server/stree/stree_test.go:1947
|
|
[Fact]
|
|
public void TestDeleteEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Test delete on empty tree
|
|
var (val, deleted) = st.Delete(B("foo"));
|
|
deleted.ShouldBeFalse();
|
|
val.ShouldBe(default);
|
|
|
|
// Test delete with empty subject
|
|
st.Insert(B("foo"), 1);
|
|
var (val2, deleted2) = st.Delete(B(""));
|
|
deleted2.ShouldBeFalse();
|
|
val2.ShouldBe(default);
|
|
|
|
// Test delete with subject shorter than prefix
|
|
st = new SubjectTree<int>();
|
|
st.Insert(B("verylongprefix.suffix"), 1);
|
|
st.Insert(B("verylongprefix.suffix2"), 2);
|
|
var (val3, deleted3) = st.Delete(B("very"));
|
|
deleted3.ShouldBeFalse();
|
|
val3.ShouldBe(default);
|
|
}
|
|
|
|
// Go: TestDeleteNilNodePointer server/stree/stree_test.go:2095
|
|
[Fact]
|
|
public void TestDeleteNilNodePointer()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
// Test delete on empty tree (no root)
|
|
var (val, deleted) = st.Delete(B("foo"));
|
|
deleted.ShouldBeFalse();
|
|
val.ShouldBe(default);
|
|
}
|
|
|
|
// Go: TestDeleteChildEdgeCasesMore server/stree/stree_test.go:2036
|
|
[Fact]
|
|
public void TestDeleteChildEdgeCasesMore()
|
|
{
|
|
// Test the edge case in node10 deleteChild where we don't swap (last element)
|
|
var n10 = new Node10(B("prefix"));
|
|
n10.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n10.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
n10.AddChild((byte)'c', new Leaf<int>(B("3"), 3));
|
|
|
|
// Delete the last child
|
|
n10.DeleteChild((byte)'c');
|
|
n10.Meta.Size.ShouldBe((ushort)2);
|
|
|
|
// Test the edge case in node16 deleteChild where we don't swap (last element)
|
|
var n16 = new Node16(B("prefix"));
|
|
n16.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n16.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
n16.AddChild((byte)'c', new Leaf<int>(B("3"), 3));
|
|
|
|
// Delete the last child
|
|
n16.DeleteChild((byte)'c');
|
|
n16.Meta.Size.ShouldBe((ushort)2);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Construction/Structure
|
|
|
|
// Go: TestSubjectTreeConstruction server/stree/stree_test.go:268
|
|
[Fact]
|
|
public void TestSubjectTreeConstruction()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
void CheckNode(INode? n, string kind, string pathStr, ushort numChildren)
|
|
{
|
|
n.ShouldNotBeNull();
|
|
n.Kind.ShouldBe(kind);
|
|
Encoding.UTF8.GetString(n.Path()).ShouldBe(pathStr);
|
|
n.NumChildren.ShouldBe(numChildren);
|
|
}
|
|
|
|
CheckNode(st.Root, "NODE4", "foo.ba", 2);
|
|
var nn = st.Root!.FindChild((byte)'r');
|
|
CheckNode(nn!.Node, "NODE4", "r", 2);
|
|
CheckNode(nn.Node!.FindChild(Parts.NoPivot)!.Node, "LEAF", "", 0);
|
|
var rnn = nn.Node!.FindChild((byte)'.');
|
|
CheckNode(rnn!.Node, "NODE4", ".", 3);
|
|
CheckNode(rnn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
|
|
CheckNode(rnn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
|
|
CheckNode(rnn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
|
|
var znn = st.Root!.FindChild((byte)'z');
|
|
CheckNode(znn!.Node, "NODE4", "z.", 3);
|
|
CheckNode(znn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
|
|
CheckNode(znn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
|
|
CheckNode(znn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
|
|
|
|
// Now delete "foo.bar" and make sure put ourselves back together properly.
|
|
var (v, found) = st.Delete(B("foo.bar"));
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(42);
|
|
|
|
CheckNode(st.Root, "NODE4", "foo.ba", 2);
|
|
nn = st.Root!.FindChild((byte)'r');
|
|
CheckNode(nn!.Node, "NODE4", "r.", 3);
|
|
CheckNode(nn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
|
|
CheckNode(nn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
|
|
CheckNode(nn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
|
|
znn = st.Root!.FindChild((byte)'z');
|
|
CheckNode(znn!.Node, "NODE4", "z.", 3);
|
|
CheckNode(znn.Node!.FindChild((byte)'A')!.Node, "LEAF", "A", 0);
|
|
CheckNode(znn.Node!.FindChild((byte)'B')!.Node, "LEAF", "B", 0);
|
|
CheckNode(znn.Node!.FindChild((byte)'C')!.Node, "LEAF", "C", 0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Matching
|
|
|
|
// Go: TestSubjectTreeMatchLeafOnly server/stree/stree_test.go:331
|
|
[Fact]
|
|
public void TestSubjectTreeMatchLeafOnly()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.baz.A"), 1);
|
|
|
|
// Check all placements of pwc in token space.
|
|
MatchCount(st, "foo.bar.*.A", 1);
|
|
MatchCount(st, "foo.*.baz.A", 1);
|
|
MatchCount(st, "foo.*.*.A", 1);
|
|
MatchCount(st, "foo.*.*.*", 1);
|
|
MatchCount(st, "*.*.*.*", 1);
|
|
|
|
// Now check fwc.
|
|
MatchCount(st, ">", 1);
|
|
MatchCount(st, "foo.>", 1);
|
|
MatchCount(st, "foo.*.>", 1);
|
|
MatchCount(st, "foo.bar.>", 1);
|
|
MatchCount(st, "foo.bar.*.>", 1);
|
|
|
|
// Check partials so they do not trigger on leafs.
|
|
MatchCount(st, "foo.bar.baz", 0);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchNodes server/stree/stree_test.go:352
|
|
[Fact]
|
|
public void TestSubjectTreeMatchNodes()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
|
|
// Test literals.
|
|
MatchCount(st, "foo.bar.A", 1);
|
|
MatchCount(st, "foo.baz.A", 1);
|
|
MatchCount(st, "foo.bar", 0);
|
|
// Test internal pwc
|
|
MatchCount(st, "foo.*.A", 2);
|
|
// Test terminal pwc
|
|
MatchCount(st, "foo.bar.*", 3);
|
|
MatchCount(st, "foo.baz.*", 3);
|
|
// Check fwc
|
|
MatchCount(st, ">", 6);
|
|
MatchCount(st, "foo.>", 6);
|
|
MatchCount(st, "foo.bar.>", 3);
|
|
MatchCount(st, "foo.baz.>", 3);
|
|
// Make sure we do not have false positives on prefix matches.
|
|
MatchCount(st, "foo.ba", 0);
|
|
|
|
// Now add in "foo.bar" to make a more complex tree construction and re-test.
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
// Test literals.
|
|
MatchCount(st, "foo.bar.A", 1);
|
|
MatchCount(st, "foo.baz.A", 1);
|
|
MatchCount(st, "foo.bar", 1);
|
|
// Test internal pwc
|
|
MatchCount(st, "foo.*.A", 2);
|
|
// Test terminal pwc
|
|
MatchCount(st, "foo.bar.*", 3);
|
|
MatchCount(st, "foo.baz.*", 3);
|
|
// Check fwc
|
|
MatchCount(st, ">", 7);
|
|
MatchCount(st, "foo.>", 7);
|
|
MatchCount(st, "foo.bar.>", 3);
|
|
MatchCount(st, "foo.baz.>", 3);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchUntil server/stree/stree_test.go:407
|
|
[Fact]
|
|
public void TestSubjectTreeMatchUntil()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
// Ensure early stop terminates traversal.
|
|
var (count, completed) = MatchUntilCount(st, "foo.>", 3);
|
|
count.ShouldBe(3);
|
|
completed.ShouldBeFalse();
|
|
|
|
// Match completes
|
|
(count, completed) = MatchUntilCount(st, "foo.bar", 3);
|
|
count.ShouldBe(1);
|
|
completed.ShouldBeTrue();
|
|
|
|
(count, completed) = MatchUntilCount(st, "foo.baz.*", 4);
|
|
count.ShouldBe(3);
|
|
completed.ShouldBeTrue();
|
|
}
|
|
|
|
// Go: TestSubjectTreePartialTerminalWildcardBugMatch server/stree/stree_test.go:453
|
|
[Fact]
|
|
public void TestSubjectTreePartialTerminalWildcardBugMatch()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-A"), 5);
|
|
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-B"), 1);
|
|
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-C"), 2);
|
|
MatchCount(st, "STATE.GLOBAL.CELL1.7PDSGAALXNN000010.*", 3);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchSubjectParam server/stree/stree_test.go:461
|
|
[Fact]
|
|
public void TestSubjectTreeMatchSubjectParam()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
var checkValMap = new Dictionary<string, int>
|
|
{
|
|
["foo.bar.A"] = 1,
|
|
["foo.bar.B"] = 2,
|
|
["foo.bar.C"] = 3,
|
|
["foo.baz.A"] = 11,
|
|
["foo.baz.B"] = 22,
|
|
["foo.baz.C"] = 33,
|
|
["foo.bar"] = 42,
|
|
};
|
|
|
|
// Make sure we get a proper subject parameter and it matches our value properly.
|
|
st.Match(B(">"), (subject, v) =>
|
|
{
|
|
var key = Encoding.UTF8.GetString(subject);
|
|
checkValMap.ShouldContainKey(key);
|
|
v.ShouldBe(checkValMap[key]);
|
|
});
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchRandomDoublePWC server/stree/stree_test.go:490
|
|
[Fact]
|
|
public void TestSubjectTreeMatchRandomDoublePWC()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
var rng = new Random(42);
|
|
for (int i = 1; i <= 10_000; i++)
|
|
{
|
|
var subj = $"foo.{rng.Next(20) + 1}.{i}";
|
|
st.Insert(B(subj), 42);
|
|
}
|
|
MatchCount(st, "foo.*.*", 10_000);
|
|
|
|
// Check with pwc and short interior token.
|
|
int seen = 0;
|
|
st.Match(B("*.2.*"), (_, _) => seen++);
|
|
|
|
// Now check via walk to make sure we are right.
|
|
int verified = 0;
|
|
st.IterOrdered((subject, _) =>
|
|
{
|
|
var tokens = Encoding.UTF8.GetString(subject).Split('.');
|
|
tokens.Length.ShouldBe(3);
|
|
if (tokens[1] == "2") verified++;
|
|
return true;
|
|
});
|
|
seen.ShouldBe(verified);
|
|
|
|
seen = 0;
|
|
verified = 0;
|
|
st.Match(B("*.*.222"), (_, _) => seen++);
|
|
st.IterOrdered((subject, _) =>
|
|
{
|
|
var tokens = Encoding.UTF8.GetString(subject).Split('.');
|
|
tokens.Length.ShouldBe(3);
|
|
if (tokens[2] == "222") verified++;
|
|
return true;
|
|
});
|
|
seen.ShouldBe(verified);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchTsepSecondThenPartialPartBug server/stree/stree_test.go:643
|
|
[Fact]
|
|
public void TestSubjectTreeMatchTsepSecondThenPartialPartBug()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.xxxxx.foo1234.zz"), 22);
|
|
st.Insert(B("foo.yyy.foo123.zz"), 22);
|
|
st.Insert(B("foo.yyybar789.zz"), 22);
|
|
st.Insert(B("foo.yyy.foo12345.zz"), 22);
|
|
st.Insert(B("foo.yyy.foo12345.yy"), 22);
|
|
st.Insert(B("foo.yyy.foo123456789.zz"), 22);
|
|
MatchCount(st, "foo.*.foo123456789.*", 1);
|
|
MatchCount(st, "foo.*.*.zzz.foo.>", 0);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchMultipleWildcardBasic server/stree/stree_test.go:655
|
|
[Fact]
|
|
public void TestSubjectTreeMatchMultipleWildcardBasic()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("A.B.C.D.0.G.H.I.0"), 22);
|
|
st.Insert(B("A.B.C.D.1.G.H.I.0"), 22);
|
|
MatchCount(st, "A.B.*.D.1.*.*.I.0", 1);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchInvalidWildcard server/stree/stree_test.go:662
|
|
[Fact]
|
|
public void TestSubjectTreeMatchInvalidWildcard()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.123"), 22);
|
|
st.Insert(B("one.two.three.four.five"), 22);
|
|
st.Insert(B("'*.123"), 22);
|
|
st.Insert(B("bar"), 22);
|
|
MatchCount(st, "invalid.>", 0);
|
|
MatchCount(st, "foo.>.bar", 0);
|
|
MatchCount(st, ">", 4);
|
|
MatchCount(st, "'*.*", 1);
|
|
MatchCount(st, "'*.*.*'", 0);
|
|
// None of these should match.
|
|
MatchCount(st, "`>`", 0);
|
|
MatchCount(st, "\">\u0022", 0);
|
|
MatchCount(st, "'>'", 0);
|
|
MatchCount(st, "'*.>'", 0);
|
|
MatchCount(st, "'*.>.", 0);
|
|
MatchCount(st, "`invalid.>`", 0);
|
|
MatchCount(st, "'*.*'", 0);
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchNoCallbackDupe server/stree/stree_test.go:881
|
|
[Fact]
|
|
public void TestSubjectTreeMatchNoCallbackDupe()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 1);
|
|
st.Insert(B("foo.bar.C"), 1);
|
|
st.Insert(B("foo.bar.>"), 1);
|
|
|
|
foreach (var f in new[] { ">", "foo.>", "foo.bar.>" })
|
|
{
|
|
var seen = new HashSet<string>();
|
|
st.Match(B(f), (bsubj, _) =>
|
|
{
|
|
var subj = Encoding.UTF8.GetString(bsubj);
|
|
seen.Contains(subj).ShouldBeFalse($"Match callback was called twice for {subj}");
|
|
seen.Add(subj);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Go: TestSubjectTreeMatchHasFWCNoPanic server/stree/stree_test.go:954
|
|
[Fact]
|
|
public void TestSubjectTreeMatchHasFWCNoPanic()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
var subj = B("foo");
|
|
st.Insert(subj, 1);
|
|
// Should not throw
|
|
st.Match(B("."), (_, _) => { });
|
|
}
|
|
|
|
// Go: TestMatchEdgeCases server/stree/stree_test.go:1970
|
|
[Fact]
|
|
public void TestMatchEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Test match with null callback
|
|
st.Insert(B("foo.bar"), 1);
|
|
st.Match(B("foo.*"), null); // Should not panic
|
|
|
|
// Test match with empty filter
|
|
int count = 0;
|
|
st.Match(B(""), (_, _) => count++);
|
|
count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestMatchComplexEdgeCases server/stree/stree_test.go:2104
|
|
[Fact]
|
|
public void TestMatchComplexEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Build a complex tree to test match coverage
|
|
st.Insert(B("foo.bar.baz"), 1);
|
|
st.Insert(B("foo.bar.qux"), 2);
|
|
st.Insert(B("foo.baz.bar"), 3);
|
|
st.Insert(B("bar.foo.baz"), 4);
|
|
|
|
// Test with terminal fwc but no remaining parts
|
|
int count = 0;
|
|
st.Match(B("foo.bar.>"), (_, _) => count++);
|
|
count.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestMatchPartsEdgeCases server/stree/stree_test.go:1912
|
|
[Fact]
|
|
public void TestMatchPartsEdgeCases()
|
|
{
|
|
// Test the edge case in matchParts
|
|
var filter = B("foo.*.bar.>");
|
|
var parts = Parts.GenParts(filter);
|
|
|
|
// Test with a fragment that will cause partial matching
|
|
var frag = B("foo.test");
|
|
var (remaining, matched) = Parts.MatchPartsAgainstFragment(parts, frag);
|
|
matched.ShouldBeTrue();
|
|
remaining.Length.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
// Go: TestMatchPartsMoreEdgeCases server/stree/stree_test.go:2058
|
|
[Fact]
|
|
public void TestMatchPartsMoreEdgeCases()
|
|
{
|
|
// Case where frag is empty
|
|
var parts = Parts.GenParts(B("foo.*"));
|
|
var (remaining, matched) = Parts.MatchPartsAgainstFragment(parts, ReadOnlySpan<byte>.Empty);
|
|
matched.ShouldBeTrue();
|
|
remaining.Length.ShouldBe(parts.Length);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Iteration
|
|
|
|
// Go: TestSubjectTreeIterOrdered server/stree/stree_test.go:529
|
|
[Fact]
|
|
public void TestSubjectTreeIterOrdered()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
var checkValMap = new Dictionary<string, int>
|
|
{
|
|
["foo.bar.A"] = 1,
|
|
["foo.bar.B"] = 2,
|
|
["foo.bar.C"] = 3,
|
|
["foo.baz.A"] = 11,
|
|
["foo.baz.B"] = 22,
|
|
["foo.baz.C"] = 33,
|
|
["foo.bar"] = 42,
|
|
};
|
|
var checkOrder = new[]
|
|
{
|
|
"foo.bar",
|
|
"foo.bar.A",
|
|
"foo.bar.B",
|
|
"foo.bar.C",
|
|
"foo.baz.A",
|
|
"foo.baz.B",
|
|
"foo.baz.C",
|
|
};
|
|
|
|
int received = 0;
|
|
st.IterOrdered((subject, v) =>
|
|
{
|
|
var subj = Encoding.UTF8.GetString(subject);
|
|
subj.ShouldBe(checkOrder[received]);
|
|
received++;
|
|
v.ShouldBe(checkValMap[subj]);
|
|
return true;
|
|
});
|
|
received.ShouldBe(checkOrder.Length);
|
|
|
|
// Make sure we can terminate properly.
|
|
received = 0;
|
|
st.IterOrdered((_, _) =>
|
|
{
|
|
received++;
|
|
return received != 4;
|
|
});
|
|
received.ShouldBe(4);
|
|
}
|
|
|
|
// Go: TestSubjectTreeIterFast server/stree/stree_test.go:582
|
|
[Fact]
|
|
public void TestSubjectTreeIterFast()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("foo.bar.A"), 1);
|
|
st.Insert(B("foo.bar.B"), 2);
|
|
st.Insert(B("foo.bar.C"), 3);
|
|
st.Insert(B("foo.baz.A"), 11);
|
|
st.Insert(B("foo.baz.B"), 22);
|
|
st.Insert(B("foo.baz.C"), 33);
|
|
st.Insert(B("foo.bar"), 42);
|
|
|
|
var checkValMap = new Dictionary<string, int>
|
|
{
|
|
["foo.bar.A"] = 1,
|
|
["foo.bar.B"] = 2,
|
|
["foo.bar.C"] = 3,
|
|
["foo.baz.A"] = 11,
|
|
["foo.baz.B"] = 22,
|
|
["foo.baz.C"] = 33,
|
|
["foo.bar"] = 42,
|
|
};
|
|
|
|
int received = 0;
|
|
st.IterFast((subject, v) =>
|
|
{
|
|
received++;
|
|
var subj = Encoding.UTF8.GetString(subject);
|
|
v.ShouldBe(checkValMap[subj]);
|
|
return true;
|
|
});
|
|
received.ShouldBe(checkValMap.Count);
|
|
|
|
// Make sure we can terminate properly.
|
|
received = 0;
|
|
st.IterFast((_, _) =>
|
|
{
|
|
received++;
|
|
return received != 4;
|
|
});
|
|
received.ShouldBe(4);
|
|
}
|
|
|
|
// Go: TestIterOrderedAndIterFastNilRoot server/stree/stree_test.go:1733
|
|
[Fact]
|
|
public void TestIterOrderedAndIterFastNilRoot()
|
|
{
|
|
// Test IterOrdered with nil root
|
|
var st = new SubjectTree<int>();
|
|
int count = 0;
|
|
st.IterOrdered((_, _) => { count++; return true; });
|
|
count.ShouldBe(0);
|
|
|
|
// Test IterFast with nil root
|
|
count = 0;
|
|
st.IterFast((_, _) => { count++; return true; });
|
|
count.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestIterEdgeCases server/stree/stree_test.go:1985
|
|
[Fact]
|
|
public void TestIterEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Add multiple subjects to create a complex tree
|
|
st.Insert(B("a.b.c"), 1);
|
|
st.Insert(B("a.b.d"), 2);
|
|
st.Insert(B("a.c.d"), 3);
|
|
st.Insert(B("b.c.d"), 4);
|
|
|
|
// Test iter with early termination at different points
|
|
int count = 0;
|
|
st.IterInternal(st.Root!, [], false, (_, _) =>
|
|
{
|
|
count++;
|
|
return count < 2;
|
|
});
|
|
count.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestIterComplexTree server/stree/stree_test.go:2121
|
|
[Fact]
|
|
public void TestIterComplexTree()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Build a deeper tree to test the remaining iter cases
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
st.Insert(B($"level1.level2.level3.item{i}"), i);
|
|
}
|
|
|
|
// This should create multiple node types and test more paths
|
|
int count = 0;
|
|
st.IterOrdered((_, _) => { count++; return true; });
|
|
count.ShouldBe(20);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Insert Edge Cases
|
|
|
|
// Go: TestSubjectTreeInsertSamePivotBug server/stree/stree_test.go:623
|
|
[Fact]
|
|
public void TestSubjectTreeInsertSamePivotBug()
|
|
{
|
|
byte[][] testSubjects =
|
|
[
|
|
B("0d00.2abbb82c1d.6e16.fa7f85470e.3e46"),
|
|
B("534b12.3486c17249.4dde0666"),
|
|
B("6f26aabd.920ee3.d4d3.5ffc69f6"),
|
|
B("8850.ade3b74c31.aa533f77.9f59.a4bd8415.b3ed7b4111"),
|
|
B("5a75047dcb.5548e845b6.76024a34.14d5b3.80c426.51db871c3a"),
|
|
B("825fa8acfc.5331.00caf8bbbd.107c4b.c291.126d1d010e"),
|
|
];
|
|
var st = new SubjectTree<int>();
|
|
foreach (var subj in testSubjects)
|
|
{
|
|
var (old, updated) = st.Insert(subj, 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Find(subj).Found.ShouldBeTrue($"Could not find subject which should be findable");
|
|
}
|
|
}
|
|
|
|
// Go: TestSubjectTreeInsertLongerLeafSuffixWithTrailingNulls server/stree/stree_test.go:917
|
|
[Fact]
|
|
public void TestSubjectTreeInsertLongerLeafSuffixWithTrailingNulls()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
var subj = new List<byte>(B("foo.bar.baz_"));
|
|
// add in 10 nulls.
|
|
for (int i = 0; i < 10; i++) subj.Add(0);
|
|
var subjArr = subj.ToArray();
|
|
|
|
st.Insert(subjArr, 1);
|
|
// add in 10 more nulls.
|
|
var subj2 = new List<byte>(subjArr);
|
|
for (int i = 0; i < 10; i++) subj2.Add(0);
|
|
var subj2Arr = subj2.ToArray();
|
|
st.Insert(subj2Arr, 2);
|
|
|
|
// Make sure we can look them up.
|
|
var (v, found) = st.Find(subjArr);
|
|
found.ShouldBeTrue();
|
|
v.ShouldBe(1);
|
|
var (v2, found2) = st.Find(subj2Arr);
|
|
found2.ShouldBeTrue();
|
|
v2.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestSubjectTreeInsertWithNoPivot server/stree/stree_test.go:943
|
|
[Fact]
|
|
public void TestSubjectTreeInsertWithNoPivot()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
var subj = new List<byte>(B("foo.bar.baz."));
|
|
subj.Add(Parts.NoPivot);
|
|
var (old, updated) = st.Insert(subj.ToArray(), 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Size.ShouldBe(0);
|
|
}
|
|
|
|
// Go: TestInsertEdgeCases server/stree/stree_test.go:1927
|
|
[Fact]
|
|
public void TestInsertEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Test inserting with noPivot byte (should fail)
|
|
var noPivotSubj = new byte[] { (byte)'f', (byte)'o', (byte)'o', 0x7F, (byte)'b', (byte)'a', (byte)'r' };
|
|
var (old, updated) = st.Insert(noPivotSubj, 1);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Size.ShouldBe(0); // Should not insert
|
|
|
|
// Test the edge case where we need to split with same pivot
|
|
st = new SubjectTree<int>();
|
|
st.Insert(B("a.b"), 1);
|
|
st.Insert(B("a.c"), 2);
|
|
st.Size.ShouldBe(2);
|
|
}
|
|
|
|
// Go: TestInsertComplexEdgeCases server/stree/stree_test.go:2067
|
|
[Fact]
|
|
public void TestInsertComplexEdgeCases()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
|
|
// Test the recursive insert case with same pivot
|
|
st.Insert(B("a"), 1);
|
|
st.Insert(B("aa"), 2); // This will create a split
|
|
|
|
// Now insert something that has the same pivot after split
|
|
st.Insert(B("aaa"), 3); // This should trigger the recursive insert path
|
|
|
|
st.Size.ShouldBe(3);
|
|
|
|
// Verify all values can be found
|
|
st.Find(B("a")).Value.ShouldBe(1);
|
|
st.Find(B("aa")).Value.ShouldBe(2);
|
|
st.Find(B("aaa")).Value.ShouldBe(3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Random/Stress Tests
|
|
|
|
// Go: TestSubjectTreeRandomTrackEntries server/stree/stree_test.go:683
|
|
[Fact]
|
|
public void TestSubjectTreeRandomTrackEntries()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
var smap = new HashSet<string>();
|
|
var rng = new Random(42);
|
|
var buf = new byte[10];
|
|
|
|
for (int i = 0; i < 1000; i++)
|
|
{
|
|
var sb = new StringBuilder();
|
|
int numTokens = rng.Next(6) + 1;
|
|
for (int t = 0; t < numTokens; t++)
|
|
{
|
|
int tlen = rng.Next(4) + 2;
|
|
var tok = new byte[tlen];
|
|
RandomNumberGenerator.Fill(tok);
|
|
sb.Append(Convert.ToHexString(tok).ToLowerInvariant());
|
|
if (t != numTokens - 1) sb.Append('.');
|
|
}
|
|
var subj = sb.ToString();
|
|
// Avoid dupes
|
|
if (smap.Contains(subj)) continue;
|
|
smap.Add(subj);
|
|
|
|
var (old, updated) = st.Insert(B(subj), 22);
|
|
old.ShouldBe(default);
|
|
updated.ShouldBeFalse();
|
|
st.Size.ShouldBe(smap.Count);
|
|
|
|
// Make sure all added items can be found.
|
|
foreach (var s in smap)
|
|
{
|
|
st.Find(B(s)).Found.ShouldBeTrue($"Could not find subject {s} which should be findable");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go: TestSubjectTreeLongTokens server/stree/stree_test.go:726
|
|
[Fact]
|
|
public void TestSubjectTreeLongTokens()
|
|
{
|
|
var st = new SubjectTree<int>();
|
|
st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa0"), 1);
|
|
st.Insert(B("a2.0"), 2);
|
|
st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa1"), 3);
|
|
st.Insert(B("a2.1"), 4);
|
|
// Simulate purge of a2.>
|
|
st.Delete(B("a2.0"));
|
|
st.Delete(B("a2.1"));
|
|
st.Size.ShouldBe(2);
|
|
st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa0")).Value.ShouldBe(1);
|
|
st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa1")).Value.ShouldBe(3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nil/Panic Safety
|
|
|
|
// Go: TestSubjectTreeNilNoPanic server/stree/stree_test.go:904
|
|
[Fact]
|
|
public void TestSubjectTreeNilNoPanic()
|
|
{
|
|
// In C# we use a fresh empty tree instead of null
|
|
var st = new SubjectTree<int>();
|
|
st.Match(B("foo"), (_, _) => { });
|
|
st.Find(B("foo")).Found.ShouldBeFalse();
|
|
st.Delete(B("foo")).Found.ShouldBeFalse();
|
|
// Insert on a fresh tree should work
|
|
st.Insert(B("foo"), 22);
|
|
st.Size.ShouldBe(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Node-specific Tests
|
|
|
|
// Go: TestSubjectTreeNode48 server/stree/stree_test.go:799
|
|
[Fact]
|
|
public void TestSubjectTreeNode48()
|
|
{
|
|
var a = new Leaf<int>(B("a"), 1);
|
|
var b = new Leaf<int>(B("b"), 2);
|
|
var c = new Leaf<int>(B("c"), 3);
|
|
var n = new Node48([]);
|
|
|
|
n.AddChild((byte)'A', a);
|
|
n.Key[(byte)'A'].ShouldBe((byte)1);
|
|
n.Child[0].ShouldNotBeNull();
|
|
n.Child[0].ShouldBeSameAs(a);
|
|
n.Children().Length.ShouldBe(1);
|
|
|
|
var child = n.FindChild((byte)'A');
|
|
child.ShouldNotBeNull();
|
|
child!.Node.ShouldBeSameAs(a);
|
|
|
|
n.AddChild((byte)'B', b);
|
|
n.Key[(byte)'B'].ShouldBe((byte)2);
|
|
n.Child[1].ShouldNotBeNull();
|
|
n.Child[1].ShouldBeSameAs(b);
|
|
n.Children().Length.ShouldBe(2);
|
|
|
|
child = n.FindChild((byte)'B');
|
|
child.ShouldNotBeNull();
|
|
child!.Node.ShouldBeSameAs(b);
|
|
|
|
n.AddChild((byte)'C', c);
|
|
n.Key[(byte)'C'].ShouldBe((byte)3);
|
|
n.Child[2].ShouldNotBeNull();
|
|
n.Child[2].ShouldBeSameAs(c);
|
|
n.Children().Length.ShouldBe(3);
|
|
|
|
child = n.FindChild((byte)'C');
|
|
child.ShouldNotBeNull();
|
|
child!.Node.ShouldBeSameAs(c);
|
|
|
|
n.DeleteChild((byte)'A');
|
|
n.Children().Length.ShouldBe(2);
|
|
n.Key[(byte)'A'].ShouldBe((byte)0); // Now deleted
|
|
n.Key[(byte)'B'].ShouldBe((byte)2); // Untouched
|
|
n.Key[(byte)'C'].ShouldBe((byte)1); // Where A was
|
|
|
|
child = n.FindChild((byte)'A');
|
|
child.ShouldBeNull();
|
|
n.Child[0].ShouldNotBeNull();
|
|
n.Child[0].ShouldBeSameAs(c);
|
|
|
|
child = n.FindChild((byte)'B');
|
|
child.ShouldNotBeNull();
|
|
child!.Node.ShouldBeSameAs(b);
|
|
n.Child[1].ShouldNotBeNull();
|
|
n.Child[1].ShouldBeSameAs(b);
|
|
|
|
child = n.FindChild((byte)'C');
|
|
child.ShouldNotBeNull();
|
|
child!.Node.ShouldBeSameAs(c);
|
|
n.Child[2].ShouldBeNull();
|
|
|
|
bool gotB = false, gotC = false;
|
|
int iterations = 0;
|
|
n.Iter(nd =>
|
|
{
|
|
iterations++;
|
|
if (ReferenceEquals(nd, b)) gotB = true;
|
|
if (ReferenceEquals(nd, c)) gotC = true;
|
|
return true;
|
|
});
|
|
iterations.ShouldBe(2);
|
|
gotB.ShouldBeTrue();
|
|
gotC.ShouldBeTrue();
|
|
|
|
// Check for off-by-one on byte 255
|
|
n.AddChild(255, c);
|
|
n.Key[255].ShouldBe((byte)3);
|
|
var grown = (Node256)n.Grow();
|
|
grown.FindChild(255).ShouldNotBeNull();
|
|
var shrunk = (Node16)n.Shrink()!;
|
|
shrunk.FindChild(255).ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestNode48IterEarlyTermination server/stree/stree_test.go:1870
|
|
[Fact]
|
|
public void TestNode48IterEarlyTermination()
|
|
{
|
|
var n48 = new Node48(B("prefix"));
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
n48.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
|
|
int count = 0;
|
|
n48.Iter(_ => { count++; return false; }); // Stop immediately
|
|
count.ShouldBe(1);
|
|
}
|
|
|
|
// Go: TestNode10And16IterEarlyTermination server/stree/stree_test.go:1884
|
|
[Fact]
|
|
public void TestNode10And16IterEarlyTermination()
|
|
{
|
|
// Test node10 early termination
|
|
var n10 = new Node10(B("prefix"));
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
|
|
}
|
|
|
|
int count = 0;
|
|
n10.Iter(_ => { count++; return count < 2; }); // Stop after 2
|
|
count.ShouldBe(2);
|
|
|
|
// Test node16 early termination
|
|
var n16 = new Node16(B("prefix"));
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
|
|
count = 0;
|
|
n16.Iter(_ => { count++; return count < 3; }); // Stop after 3
|
|
count.ShouldBe(3);
|
|
}
|
|
|
|
// Go: TestLeafPanicMethods server/stree/stree_test.go:1577
|
|
[Fact]
|
|
public void TestLeafPanicMethods()
|
|
{
|
|
var leaf = new Leaf<int>(B("test"), 42);
|
|
|
|
// Test setPrefix panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.SetPrefix(B("prefix")))
|
|
.Message.ShouldBe("setPrefix called on leaf");
|
|
|
|
// Test addChild panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.AddChild((byte)'a', null!))
|
|
.Message.ShouldBe("addChild called on leaf");
|
|
|
|
// Test findChild panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.FindChild((byte)'a'))
|
|
.Message.ShouldBe("findChild called on leaf");
|
|
|
|
// Test grow panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.Grow())
|
|
.Message.ShouldBe("grow called on leaf");
|
|
|
|
// Test deleteChild panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.DeleteChild((byte)'a'))
|
|
.Message.ShouldBe("deleteChild called on leaf");
|
|
|
|
// Test shrink panic
|
|
Should.Throw<InvalidOperationException>(() => leaf.Shrink())
|
|
.Message.ShouldBe("shrink called on leaf");
|
|
|
|
// Test other leaf methods that should work
|
|
leaf.IsFull.ShouldBeTrue();
|
|
leaf.Base.ShouldBeNull();
|
|
leaf.NumChildren.ShouldBe((ushort)0);
|
|
leaf.Children().ShouldBeEmpty();
|
|
|
|
// Test iter (should do nothing)
|
|
bool called = false;
|
|
leaf.Iter(n => { called = true; return true; });
|
|
called.ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestLeafIter server/stree/stree_test.go:2003
|
|
[Fact]
|
|
public void TestLeafIter()
|
|
{
|
|
// Test that leaf iter does nothing (it's a no-op)
|
|
var leaf = new Leaf<int>(B("test"), 42);
|
|
bool called = false;
|
|
|
|
leaf.Iter(n => { called = true; return true; });
|
|
called.ShouldBeFalse();
|
|
|
|
leaf.Iter(n => { called = true; return false; });
|
|
called.ShouldBeFalse();
|
|
|
|
// Verify the leaf itself is not affected
|
|
leaf.Match(B("test")).ShouldBeTrue();
|
|
leaf.Value.ShouldBe(42);
|
|
|
|
// Also test through the node interface
|
|
INode n2 = leaf;
|
|
called = false;
|
|
n2.Iter(child => { called = true; return true; });
|
|
called.ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestNodeIterMethods server/stree/stree_test.go:1685
|
|
[Fact]
|
|
public void TestNodeIterMethods()
|
|
{
|
|
// Test node4 iter
|
|
var n4 = new Node4(B("prefix"));
|
|
n4.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n4.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
|
|
int count = 0;
|
|
n4.Iter(n => { count++; return true; });
|
|
count.ShouldBe(2);
|
|
|
|
// Test early termination
|
|
count = 0;
|
|
n4.Iter(n => { count++; return false; });
|
|
count.ShouldBe(1);
|
|
|
|
// Test node10 iter
|
|
var n10 = new Node10(B("prefix"));
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
|
|
}
|
|
|
|
count = 0;
|
|
n10.Iter(n => { count++; return true; });
|
|
count.ShouldBe(5);
|
|
|
|
// Test node16 iter
|
|
var n16 = new Node16(B("prefix"));
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
n16.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
|
|
}
|
|
|
|
count = 0;
|
|
n16.Iter(n => { count++; return true; });
|
|
count.ShouldBe(8);
|
|
}
|
|
|
|
// Go: TestNodeAddChildPanic server/stree/stree_test.go:1752
|
|
[Fact]
|
|
public void TestNodeAddChildPanic()
|
|
{
|
|
// Test node4 addChild panic when full
|
|
var n4 = new Node4(B("prefix"));
|
|
n4.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n4.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
n4.AddChild((byte)'c', new Leaf<int>(B("3"), 3));
|
|
n4.AddChild((byte)'d', new Leaf<int>(B("4"), 4));
|
|
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
n4.AddChild((byte)'e', new Leaf<int>(B("5"), 5)))
|
|
.Message.ShouldBe("node4 full!");
|
|
}
|
|
|
|
// Go: TestNodeAddChildPanicOthers server/stree/stree_test.go:1770
|
|
[Fact]
|
|
public void TestNodeAddChildPanicOthers()
|
|
{
|
|
// Test node10 addChild panic when full
|
|
var n10 = new Node10(B("prefix"));
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
n10.AddChild((byte)('a' + i), new Leaf<int>([(byte)('0' + i)], i));
|
|
}
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
n10.AddChild((byte)'k', new Leaf<int>(B("11"), 11)))
|
|
.Message.ShouldBe("node10 full!");
|
|
|
|
// Test node16 addChild panic when full
|
|
var n16 = new Node16(B("prefix"));
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
n16.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
n16.AddChild(16, new Leaf<int>(B("16"), 16)))
|
|
.Message.ShouldBe("node16 full!");
|
|
|
|
// Test node48 addChild panic when full
|
|
var n48 = new Node48(B("prefix"));
|
|
for (int i = 0; i < 48; i++)
|
|
{
|
|
n48.AddChild((byte)i, new Leaf<int>([(byte)i], i));
|
|
}
|
|
Should.Throw<InvalidOperationException>(() =>
|
|
n48.AddChild(48, new Leaf<int>(B("48"), 48)))
|
|
.Message.ShouldBe("node48 full!");
|
|
}
|
|
|
|
// Go: TestNodeDeleteChildNotFound server/stree/stree_test.go:1823
|
|
[Fact]
|
|
public void TestNodeDeleteChildNotFound()
|
|
{
|
|
// Test node10 deleteChild when child doesn't exist
|
|
var n10 = new Node10(B("prefix"));
|
|
n10.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n10.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
n10.DeleteChild((byte)'z');
|
|
n10.Meta.Size.ShouldBe((ushort)2);
|
|
|
|
// Test node16 deleteChild when child doesn't exist
|
|
var n16 = new Node16(B("prefix"));
|
|
n16.AddChild((byte)'a', new Leaf<int>(B("1"), 1));
|
|
n16.AddChild((byte)'b', new Leaf<int>(B("2"), 2));
|
|
n16.DeleteChild((byte)'z');
|
|
n16.Meta.Size.ShouldBe((ushort)2);
|
|
|
|
// Test node48 deleteChild when child doesn't exist
|
|
var n48 = new Node48(B("prefix"));
|
|
n48.AddChild(0, new Leaf<int>(B("1"), 1));
|
|
n48.AddChild(1, new Leaf<int>(B("2"), 2));
|
|
n48.DeleteChild(255);
|
|
n48.Meta.Size.ShouldBe((ushort)2);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region LazyIntersect
|
|
|
|
// Go: TestSubjectTreeLazyIntersect server/stree/stree_test.go:965
|
|
[Fact]
|
|
public void TestSubjectTreeLazyIntersect()
|
|
{
|
|
var st1 = new SubjectTree<int>();
|
|
var st2 = new SubjectTree<int>();
|
|
|
|
// Should cause an intersection.
|
|
st1.Insert(B("foo.bar"), 1);
|
|
st2.Insert(B("foo.bar"), 1);
|
|
|
|
// Should cause an intersection.
|
|
st1.Insert(B("foo.bar.baz.qux"), 1);
|
|
st2.Insert(B("foo.bar.baz.qux"), 1);
|
|
|
|
// Should not cause any intersections.
|
|
st1.Insert(B("bar"), 1);
|
|
st2.Insert(B("baz"), 1);
|
|
st1.Insert(B("a.b.c"), 1);
|
|
st2.Insert(B("a.b.d"), 1);
|
|
st1.Insert(B("a.b.ee"), 1);
|
|
st2.Insert(B("a.b.e"), 1);
|
|
st1.Insert(B("bb.c.d"), 1);
|
|
st2.Insert(B("b.c.d"), 1);
|
|
st2.Insert(B("foo.bar.baz.qux.alice"), 1);
|
|
st2.Insert(B("foo.bar.baz.qux.bob"), 1);
|
|
|
|
var intersected = new Dictionary<string, int>();
|
|
SubjectTreeHelper.LazyIntersect(st1, st2, (key, _, _) =>
|
|
{
|
|
var k = Encoding.UTF8.GetString(key);
|
|
intersected.TryGetValue(k, out var c);
|
|
intersected[k] = c + 1;
|
|
});
|
|
intersected.Count.ShouldBe(2);
|
|
intersected["foo.bar"].ShouldBe(1);
|
|
intersected["foo.bar.baz.qux"].ShouldBe(1);
|
|
}
|
|
|
|
// Go: TestSubjectTreeLazyIntersectComprehensive server/stree/stree_test.go:1375
|
|
[Fact]
|
|
public void TestSubjectTreeLazyIntersectComprehensive()
|
|
{
|
|
// Test with empty trees
|
|
var st1 = new SubjectTree<int>();
|
|
var st2 = new SubjectTree<string>();
|
|
int count = 0;
|
|
SubjectTreeHelper.LazyIntersect(st1, st2, (_, _, _) => count++);
|
|
count.ShouldBe(0);
|
|
|
|
// Test with one having data and one empty
|
|
st1.Insert(B("foo"), 1);
|
|
SubjectTreeHelper.LazyIntersect(st1, st2, (_, _, _) => count++);
|
|
count.ShouldBe(0);
|
|
|
|
// Test with different value types
|
|
st1 = new SubjectTree<int>();
|
|
st2 = new SubjectTree<string>();
|
|
|
|
// Add some intersecting keys
|
|
st1.Insert(B("foo.bar"), 42);
|
|
st2.Insert(B("foo.bar"), "hello");
|
|
st1.Insert(B("baz.qux"), 100);
|
|
st2.Insert(B("baz.qux"), "world");
|
|
|
|
// Add non-intersecting keys
|
|
st1.Insert(B("only.in.st1"), 1);
|
|
st2.Insert(B("only.in.st2"), "two");
|
|
|
|
var results = new Dictionary<string, (int V1, string V2)>();
|
|
SubjectTreeHelper.LazyIntersect(st1, st2, (key, v1, v2) =>
|
|
{
|
|
results[Encoding.UTF8.GetString(key)] = (v1, v2);
|
|
});
|
|
|
|
results.Count.ShouldBe(2);
|
|
results["foo.bar"].V1.ShouldBe(42);
|
|
results["foo.bar"].V2.ShouldBe("hello");
|
|
results["baz.qux"].V1.ShouldBe(100);
|
|
results["baz.qux"].V2.ShouldBe("world");
|
|
|
|
// Test that it iterates over smaller tree
|
|
var large = new SubjectTree<int>();
|
|
var small = new SubjectTree<int>();
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
large.Insert(B($"large.{i}"), i);
|
|
}
|
|
small.Insert(B("large.5"), 500);
|
|
small.Insert(B("large.10"), 1000);
|
|
small.Insert(B("large.50"), 5000);
|
|
small.Insert(B("small.only"), 999);
|
|
|
|
int intersectCount = 0;
|
|
SubjectTreeHelper.LazyIntersect(large, small, (key, v1, v2) =>
|
|
{
|
|
intersectCount++;
|
|
var k = Encoding.UTF8.GetString(key);
|
|
switch (k)
|
|
{
|
|
case "large.5": v1.ShouldBe(5); v2.ShouldBe(500); break;
|
|
case "large.10": v1.ShouldBe(10); v2.ShouldBe(1000); break;
|
|
case "large.50": v1.ShouldBe(50); v2.ShouldBe(5000); break;
|
|
default: throw new Exception($"Unexpected key: {k}");
|
|
}
|
|
});
|
|
intersectCount.ShouldBe(3);
|
|
|
|
// Test with complex subjects (multiple levels)
|
|
var st3 = new SubjectTree<int>();
|
|
var st4 = new SubjectTree<int>();
|
|
|
|
st3.Insert(B("a.b.c.d.e.f.g"), 1);
|
|
st4.Insert(B("a.b.c.d.e.f.g"), 2);
|
|
|
|
// Partial matches (should not intersect)
|
|
st3.Insert(B("a.b.c.d"), 3);
|
|
st4.Insert(B("a.b.c.d.e"), 4);
|
|
|
|
// Same prefix different suffix
|
|
st3.Insert(B("prefix.suffix1"), 5);
|
|
st4.Insert(B("prefix.suffix2"), 6);
|
|
|
|
int intersections = 0;
|
|
SubjectTreeHelper.LazyIntersect(st3, st4, (key, v1, v2) =>
|
|
{
|
|
intersections++;
|
|
Encoding.UTF8.GetString(key).ShouldBe("a.b.c.d.e.f.g");
|
|
v1.ShouldBe(1);
|
|
v2.ShouldBe(2);
|
|
});
|
|
intersections.ShouldBe(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GSL Intersection (Skipped - GSL not yet implemented)
|
|
|
|
// Go: TestSubjectTreeGSLIntersection server/stree/stree_test.go:998
|
|
[Fact(Skip = "GSL (GenericSubjectList) not yet implemented")]
|
|
public void TestSubjectTreeGSLIntersection()
|
|
{
|
|
// This test requires the GSL (GenericSubjectList) which is not yet ported.
|
|
// The test has 20+ subtests covering literals, PWC, FWC, and overlapping patterns.
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Performance Tests (Skipped)
|
|
|
|
// Go: TestSubjectTreeMatchAllPerf server/stree/stree_test.go:749
|
|
[Fact(Skip = "Performance test - enable with --results flag")]
|
|
public void TestSubjectTreeMatchAllPerf()
|
|
{
|
|
// Performance test - skipped by default, same as Go version
|
|
}
|
|
|
|
// Go: TestSubjectTreeIterPerf server/stree/stree_test.go:779
|
|
[Fact(Skip = "Performance test - enable with --results flag")]
|
|
public void TestSubjectTreeIterPerf()
|
|
{
|
|
// Performance test - skipped by default, same as Go version
|
|
}
|
|
|
|
#endregion
|
|
}
|