feat: port session 07 — Protocol Parser, Auth extras (TPM/certidp/certstore), Internal utilities & data structures
Session 07 scope (5 features, 17 tests, ~1165 Go LOC): - Protocol/ParserTypes.cs: ParserState enum (79 states), PublishArgument, ParseContext - Protocol/IProtocolHandler.cs: handler interface decoupling parser from client - Protocol/ProtocolParser.cs: Parse(), ProtoSnippet(), OverMaxControlLineLimit(), ProcessPub/HeaderPub/RoutedMsgArgs/RoutedHeaderMsgArgs, ClonePubArg(), GetHeader() - tests/Protocol/ProtocolParserTests.cs: 17 tests via TestProtocolHandler stub Auth extras from session 06 (committed separately): - Auth/TpmKeyProvider.cs, Auth/CertificateIdentityProvider/, Auth/CertificateStore/ Internal utilities & data structures (session 06 overflow): - Internal/AccessTimeService.cs, ElasticPointer.cs, SystemMemory.cs, ProcessStatsProvider.cs - Internal/DataStructures/GenericSublist.cs, HashWheel.cs - Internal/DataStructures/SubjectTree.cs, SubjectTreeNode.cs, SubjectTreeParts.cs All 461 tests pass (460 unit + 1 integration). DB updated for features 2588-2592 and tests 2598-2614.
This commit is contained in:
@@ -0,0 +1,948 @@
|
||||
// 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<int> st, string filter)
|
||||
{
|
||||
var count = 0;
|
||||
st.Match(B(filter), (_, _) =>
|
||||
{
|
||||
count++;
|
||||
return true;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TestSubjectTreeBasics
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TestSubjectTreeBasics()
|
||||
{
|
||||
var st = new SubjectTree<int>();
|
||||
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<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
st.Insert(B("foo.bar"), 42);
|
||||
|
||||
// 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<int>();
|
||||
|
||||
// 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<SubjectTreeNode4<int>>();
|
||||
|
||||
// 5th child causes grow to node10.
|
||||
st.Insert(B("foo.bar.E"), 22);
|
||||
st._root.ShouldBeOfType<SubjectTreeNode10<int>>();
|
||||
|
||||
// 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<SubjectTreeNode16<int>>();
|
||||
|
||||
// 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<SubjectTreeNode48<int>>();
|
||||
|
||||
// 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<SubjectTreeNode256<int>>();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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<int>();
|
||||
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<int>();
|
||||
st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa0"), 1);
|
||||
st.Insert(B("a2.0"), 2);
|
||||
st.Insert(B("a1.aaaaaaaaaaaaaaaaaaaaaa1"), 3);
|
||||
st.Insert(B("a2.1"), 4);
|
||||
|
||||
// Simulate purge of a2.>
|
||||
st.Delete(B("a2.0"));
|
||||
st.Delete(B("a2.1"));
|
||||
|
||||
st.Size().ShouldBe(2);
|
||||
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<int>();
|
||||
|
||||
// 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<int>();
|
||||
|
||||
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<int>();
|
||||
st.Insert(B("foo.bar.A"), 22);
|
||||
|
||||
var (v, found) = st.Delete(B("foo.bar.A"));
|
||||
found.ShouldBeTrue();
|
||||
v.ShouldBe(22);
|
||||
st._root.ShouldBeNull();
|
||||
|
||||
// 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<SubjectTreeNode10<int>>();
|
||||
|
||||
var (vDel, fDel) = st.Delete(B("foo.bar.A"));
|
||||
fDel.ShouldBeTrue();
|
||||
vDel.ShouldBe(22);
|
||||
st._root.ShouldBeOfType<SubjectTreeNode4<int>>();
|
||||
|
||||
// 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<SubjectTreeNode16<int>>();
|
||||
var (vDel2, fDel2) = st.Delete(B("foo.bar.A"));
|
||||
fDel2.ShouldBeTrue();
|
||||
vDel2.ShouldBe(22);
|
||||
st._root.ShouldBeOfType<SubjectTreeNode10<int>>();
|
||||
|
||||
// Pop up to node48 and shrink back.
|
||||
st = new SubjectTree<int>();
|
||||
for (var i = 0; i < 17; i++)
|
||||
st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);
|
||||
|
||||
st._root.ShouldBeOfType<SubjectTreeNode48<int>>();
|
||||
var (vDel3, fDel3) = st.Delete(B("foo.bar.A"));
|
||||
fDel3.ShouldBeTrue();
|
||||
vDel3.ShouldBe(22);
|
||||
st._root.ShouldBeOfType<SubjectTreeNode16<int>>();
|
||||
|
||||
// Pop up to node256 and shrink back.
|
||||
st = new SubjectTree<int>();
|
||||
for (var i = 0; i < 49; i++)
|
||||
st.Insert(B($"foo.bar.{(char)('A' + i)}"), 22);
|
||||
|
||||
st._root.ShouldBeOfType<SubjectTreeNode256<int>>();
|
||||
var (vDel4, fDel4) = st.Delete(B("foo.bar.A"));
|
||||
fDel4.ShouldBeTrue();
|
||||
vDel4.ShouldBe(22);
|
||||
st._root.ShouldBeOfType<SubjectTreeNode48<int>>();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TestDeleteEdgeCases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TestDeleteEdgeCases()
|
||||
{
|
||||
var st = new SubjectTree<int>();
|
||||
|
||||
// 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<int>();
|
||||
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<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
|
||||
// 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<int>();
|
||||
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-A"), 5);
|
||||
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-B"), 1);
|
||||
st.Insert(B("STATE.GLOBAL.CELL1.7PDSGAALXNN000010.PROPERTY-C"), 2);
|
||||
MatchCount(st, "STATE.GLOBAL.CELL1.7PDSGAALXNN000010.*").ShouldBe(3);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TestSubjectTreeMatchMultiple
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TestSubjectTreeMatchMultiple()
|
||||
{
|
||||
var st = new SubjectTree<int>();
|
||||
st.Insert(B("A.B.C.D.0.G.H.I.0"), 22);
|
||||
st.Insert(B("A.B.C.D.1.G.H.I.0"), 22);
|
||||
MatchCount(st, "A.B.*.D.1.*.*.I.0").ShouldBe(1);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TestSubjectTreeMatchSubject (verify correct subject bytes in callback)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TestSubjectTreeMatchSubject()
|
||||
{
|
||||
var st = new SubjectTree<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
st.Insert(B("foo.bar"), 42);
|
||||
|
||||
var checkValMap = new Dictionary<string, int>
|
||||
{
|
||||
["foo.bar.A"] = 1,
|
||||
["foo.bar.B"] = 2,
|
||||
["foo.bar.C"] = 3,
|
||||
["foo.baz.A"] = 11,
|
||||
["foo.baz.B"] = 22,
|
||||
["foo.baz.C"] = 33,
|
||||
["foo.bar"] = 42,
|
||||
};
|
||||
|
||||
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<int>();
|
||||
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<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
st.Insert(B("foo.bar"), 42);
|
||||
|
||||
var checkValMap = new Dictionary<string, int>
|
||||
{
|
||||
["foo.bar"] = 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<string>();
|
||||
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<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
st.Insert(B("foo.bar"), 42);
|
||||
|
||||
var checkValMap = new Dictionary<string, int>
|
||||
{
|
||||
["foo.bar.A"] = 1,
|
||||
["foo.bar.B"] = 2,
|
||||
["foo.bar.C"] = 3,
|
||||
["foo.baz.A"] = 11,
|
||||
["foo.baz.B"] = 22,
|
||||
["foo.baz.C"] = 33,
|
||||
["foo.bar"] = 42,
|
||||
};
|
||||
|
||||
var 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<int>();
|
||||
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<int>();
|
||||
st.Size().ShouldBe(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TestSubjectTreeNilNoPanic (nil/null safety)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void TestSubjectTreeNullNoPanic()
|
||||
{
|
||||
var st = new SubjectTree<int>();
|
||||
|
||||
// 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<int>();
|
||||
st.Insert(B("foo.bar.A"), 1);
|
||||
st.Insert(B("foo.bar.B"), 2);
|
||||
st.Insert(B("foo.bar.C"), 3);
|
||||
st.Insert(B("foo.baz.A"), 11);
|
||||
st.Insert(B("foo.baz.B"), 22);
|
||||
st.Insert(B("foo.baz.C"), 33);
|
||||
st.Insert(B("foo.bar"), 42);
|
||||
|
||||
// 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<int>();
|
||||
var tr = new SubjectTree<int>();
|
||||
|
||||
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<int>();
|
||||
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<int>();
|
||||
|
||||
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<int>();
|
||||
var tracked = new Dictionary<string, bool>();
|
||||
var rng = new Random(42);
|
||||
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
var tokens = rng.Next(1, 5);
|
||||
var parts = new List<string>();
|
||||
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<int>();
|
||||
|
||||
// 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<SubjectTreeNode48<int>>();
|
||||
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<int>();
|
||||
st.Insert(B("foo.xxxxx.foo1234.zz"), 22);
|
||||
st.Insert(B("foo.yyy.foo123.zz"), 22);
|
||||
st.Insert(B("foo.yyybar789.zz"), 22);
|
||||
st.Insert(B("foo.yyy.foo12345.zz"), 22);
|
||||
st.Insert(B("foo.yyy.foo12345.yy"), 22);
|
||||
st.Insert(B("foo.yyy.foo123456789.zz"), 22);
|
||||
|
||||
MatchCount(st, "foo.*.foo123456789.*").ShouldBe(1);
|
||||
MatchCount(st, "foo.*.*.zzz.foo.>").ShouldBe(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user