// 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 }