// Copyright 2023-2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Shouldly; using ZB.MOM.NatsNet.Server.Internal.DataStructures; namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures; public class SubjectTreeTests { // Helper to convert string to byte array (Latin-1). private static byte[] B(string s) => System.Text.Encoding.Latin1.GetBytes(s); // Helper to count matches. private static int MatchCount(SubjectTree st, string filter) { var count = 0; st.Match(B(filter), (_, _) => { count++; return true; }); return count; } // ------------------------------------------------------------------------- // TestSubjectTreeBasics // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeBasics() { var st = new SubjectTree(); st.Size().ShouldBe(0); // Single leaf insert. var (old, updated) = st.Insert(B("foo.bar.baz"), 22); old.ShouldBe(default); updated.ShouldBeFalse(); st.Size().ShouldBe(1); // Find should not work with a wildcard. var (_, found) = st.Find(B("foo.bar.*")); found.ShouldBeFalse(); // Find with literal — single leaf. var (val, found2) = st.Find(B("foo.bar.baz")); found2.ShouldBeTrue(); val.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); // Find both entries after split. var (v1, f1) = st.Find(B("foo.bar")); f1.ShouldBeTrue(); v1.ShouldBe(22); var (v2, f2) = st.Find(B("foo.bar.baz")); f2.ShouldBeTrue(); v2.ShouldBe(33); } // ------------------------------------------------------------------------- // TestSubjectTreeConstruction // ------------------------------------------------------------------------- [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); // Validate structure. st._root.ShouldNotBeNull(); st._root!.Kind.ShouldBe("NODE4"); st._root.NumChildren.ShouldBe(2); // Now delete "foo.bar" and verify structure collapses correctly. var (v, found) = st.Delete(B("foo.bar")); found.ShouldBeTrue(); v.ShouldBe(42); // The remaining entries should still be findable. var (v1, f1) = st.Find(B("foo.bar.A")); f1.ShouldBeTrue(); v1.ShouldBe(1); var (v2, f2) = st.Find(B("foo.bar.B")); f2.ShouldBeTrue(); v2.ShouldBe(2); var (v3, f3) = st.Find(B("foo.bar.C")); f3.ShouldBeTrue(); v3.ShouldBe(3); var (v4, f4) = st.Find(B("foo.baz.A")); f4.ShouldBeTrue(); v4.ShouldBe(11); } // ------------------------------------------------------------------------- // TestSubjectTreeNodeGrow // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeNodeGrow() { var st = new SubjectTree(); // Fill a node4 (4 children). for (var i = 0; i < 4; i++) { var subj = B($"foo.bar.{(char)('A' + i)}"); var (old, upd) = st.Insert(subj, 22); old.ShouldBe(default); upd.ShouldBeFalse(); } st._root.ShouldBeOfType>(); // 5th child causes grow to node10. st.Insert(B("foo.bar.E"), 22); st._root.ShouldBeOfType>(); // Fill to 10. for (var i = 5; i < 10; i++) { st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); } // 11th child causes grow to node16. st.Insert(B("foo.bar.K"), 22); st._root.ShouldBeOfType>(); // Fill to 16. for (var i = 11; i < 16; i++) { st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); } // 17th child causes grow to node48. st.Insert(B("foo.bar.Q"), 22); st._root.ShouldBeOfType>(); // Fill the node48. for (var i = 17; i < 48; i++) { st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); } // 49th child causes grow to node256. var subjLast = B($"foo.bar.{(char)('A' + 49)}"); st.Insert(subjLast, 22); st._root.ShouldBeOfType>(); } // ------------------------------------------------------------------------- // TestSubjectTreeInsertSamePivot (same pivot bug) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeInsertSamePivot() { var testSubjects = new[] { 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, upd) = st.Insert(subj, 22); old.ShouldBe(default); upd.ShouldBeFalse(); var (_, found) = st.Find(subj); found.ShouldBeTrue($"Could not find subject '{System.Text.Encoding.Latin1.GetString(subj)}' after insert"); } } // ------------------------------------------------------------------------- // TestSubjectTreeInsertLonger // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeInsertLonger() { 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); var (v1, f1) = st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa0")); f1.ShouldBeTrue(); v1.ShouldBe(1); var (v2, f2) = st.Find(B("a1.aaaaaaaaaaaaaaaaaaaaaa1")); f2.ShouldBeTrue(); v2.ShouldBe(3); } // ------------------------------------------------------------------------- // TestInsertEdgeCases // ------------------------------------------------------------------------- [Fact] public void TestInsertEdgeCases() { var st = new SubjectTree(); // Reject subject with noPivot byte (127). var (old, upd) = st.Insert(new byte[] { (byte)'f', (byte)'o', (byte)'o', 127 }, 1); old.ShouldBe(default); upd.ShouldBeFalse(); st.Size().ShouldBe(0); // Empty-ish subjects. st.Insert(B("a"), 1); st.Insert(B("b"), 2); st.Size().ShouldBe(2); } // ------------------------------------------------------------------------- // TestFindEdgeCases // ------------------------------------------------------------------------- [Fact] public void TestFindEdgeCases() { var st = new SubjectTree(); var (_, found) = st.Find(B("anything")); found.ShouldBeFalse(); st.Insert(B("foo"), 42); var (v, f) = st.Find(B("foo")); f.ShouldBeTrue(); v.ShouldBe(42); var (_, f2) = st.Find(B("fo")); f2.ShouldBeFalse(); var (_, f3) = st.Find(B("foobar")); f3.ShouldBeFalse(); } // ------------------------------------------------------------------------- // TestSubjectTreeNodeDelete // ------------------------------------------------------------------------- [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(); // Delete non-existent. var (v2, found2) = st.Delete(B("foo.bar.A")); found2.ShouldBeFalse(); v2.ShouldBe(default); // Fill to node4 then shrink back through deletes. st.Insert(B("foo.bar.A"), 11); st.Insert(B("foo.bar.B"), 22); st.Insert(B("foo.bar.C"), 33); var (vC, fC) = st.Delete(B("foo.bar.C")); fC.ShouldBeTrue(); vC.ShouldBe(33); var (vB, fB) = st.Delete(B("foo.bar.B")); fB.ShouldBeTrue(); vB.ShouldBe(22); // Should have shrunk to a leaf. st._root.ShouldNotBeNull(); st._root!.IsLeaf.ShouldBeTrue(); var (vA, fA) = st.Delete(B("foo.bar.A")); fA.ShouldBeTrue(); vA.ShouldBe(11); st._root.ShouldBeNull(); // Pop up to node10 and shrink back. for (var i = 0; i < 5; i++) st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); st._root.ShouldBeOfType>(); var (vDel, fDel) = st.Delete(B("foo.bar.A")); fDel.ShouldBeTrue(); vDel.ShouldBe(22); st._root.ShouldBeOfType>(); // Pop up to node16 and shrink back. for (var i = 0; i < 11; i++) st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); st._root.ShouldBeOfType>(); var (vDel2, fDel2) = st.Delete(B("foo.bar.A")); fDel2.ShouldBeTrue(); vDel2.ShouldBe(22); st._root.ShouldBeOfType>(); // Pop up to node48 and shrink back. st = new SubjectTree(); for (var i = 0; i < 17; i++) st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); st._root.ShouldBeOfType>(); var (vDel3, fDel3) = st.Delete(B("foo.bar.A")); fDel3.ShouldBeTrue(); vDel3.ShouldBe(22); st._root.ShouldBeOfType>(); // Pop up to node256 and shrink back. st = new SubjectTree(); for (var i = 0; i < 49; i++) st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22); st._root.ShouldBeOfType>(); var (vDel4, fDel4) = st.Delete(B("foo.bar.A")); fDel4.ShouldBeTrue(); vDel4.ShouldBe(22); st._root.ShouldBeOfType>(); } // ------------------------------------------------------------------------- // TestDeleteEdgeCases // ------------------------------------------------------------------------- [Fact] public void TestDeleteEdgeCases() { var st = new SubjectTree(); // Delete from empty tree. var (v, f) = st.Delete(B("foo")); f.ShouldBeFalse(); v.ShouldBe(default); // Insert and delete the only item. st.Insert(B("foo"), 1); var (v2, f2) = st.Delete(B("foo")); f2.ShouldBeTrue(); v2.ShouldBe(1); st.Size().ShouldBe(0); st._root.ShouldBeNull(); // Delete a non-existent item in a non-empty tree. st.Insert(B("bar"), 2); var (v3, f3) = st.Delete(B("baz")); f3.ShouldBeFalse(); v3.ShouldBe(default); st.Size().ShouldBe(1); } // ------------------------------------------------------------------------- // TestSubjectTreeMatchLeafOnly // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeMatchLeafOnly() { var st = new SubjectTree(); st.Insert(B("foo.bar.baz.A"), 1); // All positions of pwc. MatchCount(st, "foo.bar.*.A").ShouldBe(1); MatchCount(st, "foo.*.baz.A").ShouldBe(1); MatchCount(st, "foo.*.*.A").ShouldBe(1); MatchCount(st, "foo.*.*.*").ShouldBe(1); MatchCount(st, "*.*.*.*").ShouldBe(1); // fwc tests. MatchCount(st, ">").ShouldBe(1); MatchCount(st, "foo.>").ShouldBe(1); MatchCount(st, "foo.*.>").ShouldBe(1); MatchCount(st, "foo.bar.>").ShouldBe(1); MatchCount(st, "foo.bar.*.>").ShouldBe(1); // Partial match should not trigger. MatchCount(st, "foo.bar.baz").ShouldBe(0); } // ------------------------------------------------------------------------- // TestSubjectTreeMatchNodes // ------------------------------------------------------------------------- [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); // Literals. MatchCount(st, "foo.bar.A").ShouldBe(1); MatchCount(st, "foo.baz.A").ShouldBe(1); MatchCount(st, "foo.bar").ShouldBe(0); // Internal pwc. MatchCount(st, "foo.*.A").ShouldBe(2); // Terminal pwc. MatchCount(st, "foo.bar.*").ShouldBe(3); MatchCount(st, "foo.baz.*").ShouldBe(3); // fwc. MatchCount(st, ">").ShouldBe(6); MatchCount(st, "foo.>").ShouldBe(6); MatchCount(st, "foo.bar.>").ShouldBe(3); MatchCount(st, "foo.baz.>").ShouldBe(3); // No false positives on prefix. MatchCount(st, "foo.ba").ShouldBe(0); // Add "foo.bar" and re-test. st.Insert(B("foo.bar"), 42); MatchCount(st, "foo.bar.A").ShouldBe(1); MatchCount(st, "foo.bar").ShouldBe(1); MatchCount(st, "foo.*.A").ShouldBe(2); MatchCount(st, "foo.bar.*").ShouldBe(3); MatchCount(st, ">").ShouldBe(7); MatchCount(st, "foo.>").ShouldBe(7); MatchCount(st, "foo.bar.>").ShouldBe(3); MatchCount(st, "foo.baz.>").ShouldBe(3); } // ------------------------------------------------------------------------- // TestSubjectTreePartialTermination (partial termination) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreePartialTermination() { 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.*").ShouldBe(3); } // ------------------------------------------------------------------------- // TestSubjectTreeMatchMultiple // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeMatchMultiple() { 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").ShouldBe(1); } // ------------------------------------------------------------------------- // TestSubjectTreeMatchSubject (verify correct subject bytes in callback) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeMatchSubject() { 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, }; st.Match(B(">"), (subject, val) => { var subjectStr = System.Text.Encoding.Latin1.GetString(subject); checkValMap.ShouldContainKey(subjectStr); val.ShouldBe(checkValMap[subjectStr]); return true; }); } // ------------------------------------------------------------------------- // TestMatchEdgeCases // ------------------------------------------------------------------------- [Fact] public void TestMatchEdgeCases() { var st = new SubjectTree(); st.Insert(B("foo.123"), 22); st.Insert(B("one.two.three.four.five"), 22); // Basic fwc. MatchCount(st, ">").ShouldBe(2); // No matches. MatchCount(st, "invalid.>").ShouldBe(0); // fwc after content is not terminal — should not match. MatchCount(st, "foo.>.bar").ShouldBe(0); } // ------------------------------------------------------------------------- // TestSubjectTreeIterOrdered // ------------------------------------------------------------------------- [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"] = 42, ["foo.bar.A"] = 1, ["foo.bar.B"] = 2, ["foo.bar.C"] = 3, ["foo.baz.A"] = 11, ["foo.baz.B"] = 22, ["foo.baz.C"] = 33, }; var checkOrder = new[] { "foo.bar", "foo.bar.A", "foo.bar.B", "foo.bar.C", "foo.baz.A", "foo.baz.B", "foo.baz.C", }; var received = new List(); st.IterOrdered((subject, val) => { var subjectStr = System.Text.Encoding.Latin1.GetString(subject); received.Add(subjectStr); val.ShouldBe(checkValMap[subjectStr]); return true; }); received.Count.ShouldBe(checkOrder.Length); for (var i = 0; i < checkOrder.Length; i++) received[i].ShouldBe(checkOrder[i]); // Make sure we can terminate early. var count = 0; st.IterOrdered((_, _) => { count++; return count != 4; }); count.ShouldBe(4); } // ------------------------------------------------------------------------- // TestSubjectTreeIterFast // ------------------------------------------------------------------------- [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, }; var received = 0; st.IterFast((subject, val) => { received++; var subjectStr = System.Text.Encoding.Latin1.GetString(subject); checkValMap.ShouldContainKey(subjectStr); val.ShouldBe(checkValMap[subjectStr]); return true; }); received.ShouldBe(checkValMap.Count); // Early termination. received = 0; st.IterFast((_, _) => { received++; return received != 4; }); received.ShouldBe(4); } // ------------------------------------------------------------------------- // TestSubjectTreeEmpty // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeEmpty() { var st = new SubjectTree(); st.Empty().ShouldBeTrue(); st.Insert(B("foo"), 1); st.Empty().ShouldBeFalse(); st.Delete(B("foo")); st.Empty().ShouldBeTrue(); } // ------------------------------------------------------------------------- // TestSizeOnEmptyTree // ------------------------------------------------------------------------- [Fact] public void TestSizeOnEmptyTree() { var st = new SubjectTree(); st.Size().ShouldBe(0); } // ------------------------------------------------------------------------- // TestSubjectTreeNilNoPanic (nil/null safety) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeNullNoPanic() { var st = new SubjectTree(); // Operations on empty tree should not throw. st.Size().ShouldBe(0); st.Empty().ShouldBeTrue(); var (_, f1) = st.Find(B("foo")); f1.ShouldBeFalse(); var (_, f2) = st.Delete(B("foo")); f2.ShouldBeFalse(); // Match on empty tree. var count = 0; st.Match(B(">"), (_, _) => { count++; return true; }); count.ShouldBe(0); // MatchUntil on empty tree. var completed = st.MatchUntil(B(">"), (_, _) => { count++; return true; }); completed.ShouldBeTrue(); // Iter on empty tree. st.IterOrdered((_, _) => { count++; return true; }); st.IterFast((_, _) => { count++; return true; }); count.ShouldBe(0); } // ------------------------------------------------------------------------- // TestSubjectTreeMatchUntil // ------------------------------------------------------------------------- [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); // Early stop terminates traversal. var n = 0; var completed = st.MatchUntil(B("foo.>"), (_, _) => { n++; return n < 3; }); n.ShouldBe(3); completed.ShouldBeFalse(); // Match that completes normally. n = 0; completed = st.MatchUntil(B("foo.bar"), (_, _) => { n++; return true; }); n.ShouldBe(1); completed.ShouldBeTrue(); // Stop after 4 (more than available in "foo.baz.*"). n = 0; completed = st.MatchUntil(B("foo.baz.*"), (_, _) => { n++; return n < 4; }); n.ShouldBe(3); completed.ShouldBeTrue(); } // ------------------------------------------------------------------------- // TestSubjectTreeGSLIntersect (basic lazy intersect equivalent) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeLazyIntersect() { // Build two trees and verify that inserting matching keys from both yields correct count. var tl = new SubjectTree(); var tr = new SubjectTree(); tl.Insert(B("foo.bar"), 1); tl.Insert(B("foo.baz"), 2); tl.Insert(B("other"), 3); tr.Insert(B("foo.bar"), 10); tr.Insert(B("foo.baz"), 20); // Manually intersect: iterate smaller tree, find in larger. var matches = new List<(string key, int vl, int vr)>(); tl.IterFast((key, vl) => { var (vr, found) = tr.Find(key); if (found) matches.Add((System.Text.Encoding.Latin1.GetString(key), vl, vr)); return true; }); matches.Count.ShouldBe(2); } // ------------------------------------------------------------------------- // TestSubjectTreePrefixMismatch // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreePrefixMismatch() { 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); // This will force a split. st.Insert(B("foo.foo.A"), 44); var (v1, f1) = st.Find(B("foo.bar.A")); f1.ShouldBeTrue(); v1.ShouldBe(11); var (v2, f2) = st.Find(B("foo.bar.B")); f2.ShouldBeTrue(); v2.ShouldBe(22); var (v3, f3) = st.Find(B("foo.bar.C")); f3.ShouldBeTrue(); v3.ShouldBe(33); var (v4, f4) = st.Find(B("foo.foo.A")); f4.ShouldBeTrue(); v4.ShouldBe(44); } // ------------------------------------------------------------------------- // TestSubjectTreeNodesAndPaths // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeNodesAndPaths() { var st = new SubjectTree(); void Check(string subj) { var (val, found) = st.Find(B(subj)); found.ShouldBeTrue(); val.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"); // Deletion that involves shrinking / prefix adjustment. st.Delete(B("foo.bar")); Check("foo.bar.A"); Check("foo.bar.B"); Check("foo.bar.C"); } // ------------------------------------------------------------------------- // TestSubjectTreeRandomTrack (basic random insert/find) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeRandomTrack() { var st = new SubjectTree(); var tracked = new Dictionary(); var rng = new Random(42); for (var i = 0; i < 200; i++) { var tokens = rng.Next(1, 5); var parts = new List(); for (var t = 0; t < tokens; t++) { var len = rng.Next(2, 7); var chars = new char[len]; for (var c = 0; c < len; c++) chars[c] = (char)('a' + rng.Next(26)); parts.Add(new string(chars)); } var subj = string.Join(".", parts); if (tracked.ContainsKey(subj)) continue; tracked[subj] = true; st.Insert(B(subj), 1); } foreach (var subj in tracked.Keys) { var (_, found) = st.Find(B(subj)); found.ShouldBeTrue($"Subject '{subj}' not found after insert"); } st.Size().ShouldBe(tracked.Count); } // ------------------------------------------------------------------------- // TestSubjectTreeNode48 (detailed node48 operations) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeNode48Operations() { var st = new SubjectTree(); // Insert 26 single-char subjects (no prefix — goes directly to node48). for (var i = 0; i < 26; i++) st.Insert(new[] { (byte)('A' + i) }, 22); st._root.ShouldBeOfType>(); st._root!.NumChildren.ShouldBe(26); st.Delete(new[] { (byte)'B' }); st._root.NumChildren.ShouldBe(25); st.Delete(new[] { (byte)'Z' }); st._root.NumChildren.ShouldBe(24); // Remaining subjects should still be findable. for (var i = 0; i < 26; i++) { var ch = (byte)('A' + i); if (ch == (byte)'B' || ch == (byte)'Z') continue; var (_, found) = st.Find(new[] { ch }); found.ShouldBeTrue(); } } // ------------------------------------------------------------------------- // TestSubjectTreeMatchTsepSecondThenPartial (bug regression) // ------------------------------------------------------------------------- [Fact] public void TestSubjectTreeMatchTsepSecondThenPartial() { 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.*").ShouldBe(1); MatchCount(st, "foo.*.*.zzz.foo.>").ShouldBe(0); } }