// 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;
///
/// Tests for the Adaptive Radix Tree (ART) based SubjectTree.
/// Ported from Go: server/stree/stree_test.go (59 tests)
///
public class SubjectTreeTests
{
private static byte[] B(string s) => Encoding.UTF8.GetBytes(s);
private static void MatchCount(SubjectTree st, string filter, int expected)
{
var matches = new List();
st.Match(B(filter), (_, v) => matches.Add(v));
matches.Count.ShouldBe(expected, $"filter={filter}");
}
private static (int Count, bool Completed) MatchUntilCount(SubjectTree 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();
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();
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();
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();
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();
st.Size.ShouldBe(0);
}
// Go: TestFindEdgeCases server/stree/stree_test.go:1672
[Fact]
public void TestFindEdgeCases()
{
var st = new SubjectTree();
// 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();
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();
// 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();
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();
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();
// 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();
}
// Go: TestSubjectTreeNodePrefixMismatch server/stree/stree_test.go:127
[Fact]
public void TestSubjectTreeNodePrefixMismatch()
{
var st = new SubjectTree();
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(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(B("suffix2"), 43));
n.AddChild((byte)'c', new Leaf(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(() => 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([(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();
// 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([(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([(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();
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();
var (v7, found7) = st.Delete(B("foo.bar.A"));
found7.ShouldBeTrue();
v7.ShouldBe(22);
st.Root.ShouldBeOfType();
// 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();
var (v8, found8) = st.Delete(B("foo.bar.A"));
found8.ShouldBeTrue();
v8.ShouldBe(22);
st.Root.ShouldBeOfType();
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();
for (int i = 0; i < 17; i++)
{
var subj = $"foo.bar.{(char)('A' + i)}";
st.Insert(B(subj), 22);
}
st.Root.ShouldBeOfType();
var (v9, found9) = st.Delete(B("foo.bar.A"));
found9.ShouldBeTrue();
v9.ShouldBe(22);
st.Root.ShouldBeOfType();
st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
// Now pop up to node256
st = new SubjectTree();
for (int i = 0; i < 49; i++)
{
var subj = $"foo.bar.{(char)('A' + i)}";
st.Insert(B(subj), 22);
}
st.Root.ShouldBeOfType();
var (v10, found10) = st.Delete(B("foo.bar.A"));
found10.ShouldBeTrue();
v10.ShouldBe(22);
st.Root.ShouldBeOfType();
st.Find(B("foo.bar.B")).Found.ShouldBeTrue();
}
// Go: TestSubjectTreeNodesAndPaths server/stree/stree_test.go:243
[Fact]
public void TestSubjectTreeNodesAndPaths()
{
var st = new SubjectTree();
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();
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();
// 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();
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();
// 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(B("1"), 1));
n10.AddChild((byte)'b', new Leaf(B("2"), 2));
n10.AddChild((byte)'c', new Leaf(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(B("1"), 1));
n16.AddChild((byte)'b', new Leaf(B("2"), 2));
n16.AddChild((byte)'c', new Leaf(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();
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();
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();
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();
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();
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();
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
{
["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();
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();
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();
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();
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();
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();
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();
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();
// 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();
// 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.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();
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
{
["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();
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
{
["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 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();
// 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();
// 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();
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();
var subj = new List(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(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();
var subj = new List(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();
// 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();
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();
// 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();
var smap = new HashSet();
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();
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();
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(B("a"), 1);
var b = new Leaf(B("b"), 2);
var c = new Leaf(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([(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([(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([(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(B("test"), 42);
// Test setPrefix panic
Should.Throw(() => leaf.SetPrefix(B("prefix")))
.Message.ShouldBe("setPrefix called on leaf");
// Test addChild panic
Should.Throw(() => leaf.AddChild((byte)'a', null!))
.Message.ShouldBe("addChild called on leaf");
// Test findChild panic
Should.Throw(() => leaf.FindChild((byte)'a'))
.Message.ShouldBe("findChild called on leaf");
// Test grow panic
Should.Throw(() => leaf.Grow())
.Message.ShouldBe("grow called on leaf");
// Test deleteChild panic
Should.Throw(() => leaf.DeleteChild((byte)'a'))
.Message.ShouldBe("deleteChild called on leaf");
// Test shrink panic
Should.Throw(() => 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(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(B("1"), 1));
n4.AddChild((byte)'b', new Leaf(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([(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([(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(B("1"), 1));
n4.AddChild((byte)'b', new Leaf(B("2"), 2));
n4.AddChild((byte)'c', new Leaf(B("3"), 3));
n4.AddChild((byte)'d', new Leaf(B("4"), 4));
Should.Throw(() =>
n4.AddChild((byte)'e', new Leaf(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([(byte)('0' + i)], i));
}
Should.Throw(() =>
n10.AddChild((byte)'k', new Leaf(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([(byte)i], i));
}
Should.Throw(() =>
n16.AddChild(16, new Leaf(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([(byte)i], i));
}
Should.Throw(() =>
n48.AddChild(48, new Leaf(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(B("1"), 1));
n10.AddChild((byte)'b', new Leaf(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(B("1"), 1));
n16.AddChild((byte)'b', new Leaf(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(B("1"), 1));
n48.AddChild(1, new Leaf(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();
var st2 = new SubjectTree();
// 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();
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();
var st2 = new SubjectTree();
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();
st2 = new SubjectTree();
// 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();
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();
var small = new SubjectTree();
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();
var st4 = new SubjectTree();
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
}