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.
949 lines
30 KiB
C#
949 lines
30 KiB
C#
// 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);
|
|
}
|
|
}
|