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:
Joseph Doherty
2026-02-26 13:16:56 -05:00
parent 0a54d342ba
commit 88b1391ef0
56 changed files with 9006 additions and 6 deletions

View File

@@ -0,0 +1,511 @@
// Copyright 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;
/// <summary>
/// Ports all 21 tests from Go's gsl/gsl_test.go.
/// </summary>
public sealed class GenericSublistTests
{
// -------------------------------------------------------------------------
// Helpers (mirror Go's require_* functions)
// -------------------------------------------------------------------------
/// <summary>
/// Counts how many values the sublist matches for <paramref name="subject"/>
/// and asserts that count equals <paramref name="expected"/>.
/// Mirrors Go's <c>require_Matches</c>.
/// </summary>
private static void RequireMatches<T>(GenericSublist<T> s, string subject, int expected)
where T : notnull
{
var matches = 0;
s.Match(subject, _ => matches++);
matches.ShouldBe(expected);
}
// -------------------------------------------------------------------------
// TestGenericSublistInit
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistInit()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Count.ShouldBe(0u);
}
// -------------------------------------------------------------------------
// TestGenericSublistInsertCount
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistInsertCount()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("foo", EmptyStruct.Value);
s.Insert("bar", EmptyStruct.Value);
s.Insert("foo.bar", EmptyStruct.Value);
s.Count.ShouldBe(3u);
}
// -------------------------------------------------------------------------
// TestGenericSublistSimple
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistSimple()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("foo", EmptyStruct.Value);
RequireMatches(s, "foo", 1);
}
// -------------------------------------------------------------------------
// TestGenericSublistSimpleMultiTokens
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistSimpleMultiTokens()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("foo.bar.baz", EmptyStruct.Value);
RequireMatches(s, "foo.bar.baz", 1);
}
// -------------------------------------------------------------------------
// TestGenericSublistPartialWildcard
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistPartialWildcard()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("a.b.c", EmptyStruct.Value);
s.Insert("a.*.c", EmptyStruct.Value);
RequireMatches(s, "a.b.c", 2);
}
// -------------------------------------------------------------------------
// TestGenericSublistPartialWildcardAtEnd
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistPartialWildcardAtEnd()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("a.b.c", EmptyStruct.Value);
s.Insert("a.b.*", EmptyStruct.Value);
RequireMatches(s, "a.b.c", 2);
}
// -------------------------------------------------------------------------
// TestGenericSublistFullWildcard
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistFullWildcard()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("a.b.c", EmptyStruct.Value);
s.Insert("a.>", EmptyStruct.Value);
RequireMatches(s, "a.b.c", 2);
RequireMatches(s, "a.>", 1);
}
// -------------------------------------------------------------------------
// TestGenericSublistRemove
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistRemove()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("a.b.c.d", EmptyStruct.Value);
s.Count.ShouldBe(1u);
RequireMatches(s, "a.b.c.d", 1);
s.Remove("a.b.c.d", EmptyStruct.Value);
s.Count.ShouldBe(0u);
RequireMatches(s, "a.b.c.d", 0);
}
// -------------------------------------------------------------------------
// TestGenericSublistRemoveWildcard
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistRemoveWildcard()
{
var s = GenericSublist<int>.NewSublist();
s.Insert("a.b.c.d", 11);
s.Insert("a.b.*.d", 22);
s.Insert("a.b.>", 33);
s.Count.ShouldBe(3u);
RequireMatches(s, "a.b.c.d", 3);
s.Remove("a.b.*.d", 22);
s.Count.ShouldBe(2u);
RequireMatches(s, "a.b.c.d", 2);
s.Remove("a.b.>", 33);
s.Count.ShouldBe(1u);
RequireMatches(s, "a.b.c.d", 1);
s.Remove("a.b.c.d", 11);
s.Count.ShouldBe(0u);
RequireMatches(s, "a.b.c.d", 0);
}
// -------------------------------------------------------------------------
// TestGenericSublistRemoveCleanup
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistRemoveCleanup()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.NumLevels().ShouldBe(0);
s.Insert("a.b.c.d.e.f", EmptyStruct.Value);
s.NumLevels().ShouldBe(6);
s.Remove("a.b.c.d.e.f", EmptyStruct.Value);
s.NumLevels().ShouldBe(0);
}
// -------------------------------------------------------------------------
// TestGenericSublistRemoveCleanupWildcards
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistRemoveCleanupWildcards()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.NumLevels().ShouldBe(0);
s.Insert("a.b.*.d.e.>", EmptyStruct.Value);
s.NumLevels().ShouldBe(6);
s.Remove("a.b.*.d.e.>", EmptyStruct.Value);
s.NumLevels().ShouldBe(0);
}
// -------------------------------------------------------------------------
// TestGenericSublistInvalidSubjectsInsert
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistInvalidSubjectsInsert()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
// Insert, or subscriptions, can have wildcards, but not empty tokens,
// and can not have a FWC that is not the terminal token.
Should.Throw<ArgumentException>(() => s.Insert(".foo", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Insert("foo.", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Insert("foo..bar", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Insert("foo.bar..baz", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Insert("foo.>.baz", EmptyStruct.Value));
}
// -------------------------------------------------------------------------
// TestGenericSublistBadSubjectOnRemove
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistBadSubjectOnRemove()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
Should.Throw<ArgumentException>(() => s.Insert("a.b..d", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Remove("a.b..d", EmptyStruct.Value));
Should.Throw<ArgumentException>(() => s.Remove("a.>.b", EmptyStruct.Value));
}
// -------------------------------------------------------------------------
// TestGenericSublistTwoTokenPubMatchSingleTokenSub
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistTwoTokenPubMatchSingleTokenSub()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert("foo", EmptyStruct.Value);
RequireMatches(s, "foo", 1);
RequireMatches(s, "foo.bar", 0);
}
// -------------------------------------------------------------------------
// TestGenericSublistInsertWithWildcardsAsLiterals
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistInsertWithWildcardsAsLiterals()
{
var s = GenericSublist<int>.NewSublist();
var subjects = new[] { "foo.*-", "foo.>-" };
for (var i = 0; i < subjects.Length; i++)
{
var subject = subjects[i];
s.Insert(subject, i);
RequireMatches(s, "foo.bar", 0);
RequireMatches(s, subject, 1);
}
}
// -------------------------------------------------------------------------
// TestGenericSublistRemoveWithWildcardsAsLiterals
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistRemoveWithWildcardsAsLiterals()
{
var s = GenericSublist<int>.NewSublist();
var subjects = new[] { "foo.*-", "foo.>-" };
for (var i = 0; i < subjects.Length; i++)
{
var subject = subjects[i];
s.Insert(subject, i);
RequireMatches(s, "foo.bar", 0);
RequireMatches(s, subject, 1);
Should.Throw<KeyNotFoundException>(() => s.Remove("foo.bar", i));
s.Count.ShouldBe(1u);
s.Remove(subject, i);
s.Count.ShouldBe(0u);
}
}
// -------------------------------------------------------------------------
// TestGenericSublistMatchWithEmptyTokens
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistMatchWithEmptyTokens()
{
var s = GenericSublist<EmptyStruct>.NewSublist();
s.Insert(">", EmptyStruct.Value);
var subjects = new[]
{
".foo", "..foo", "foo..", "foo.", "foo..bar", "foo...bar"
};
foreach (var subject in subjects)
{
RequireMatches(s, subject, 0);
}
}
// -------------------------------------------------------------------------
// TestGenericSublistHasInterest
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistHasInterest()
{
var s = GenericSublist<int>.NewSublist();
s.Insert("foo", 11);
// Expect to find that "foo" matches but "bar" doesn't.
s.HasInterest("foo").ShouldBeTrue();
s.HasInterest("bar").ShouldBeFalse();
// Call Match on a subject we know there is no match.
RequireMatches(s, "bar", 0);
s.HasInterest("bar").ShouldBeFalse();
// Remove fooSub and check interest again.
s.Remove("foo", 11);
s.HasInterest("foo").ShouldBeFalse();
// Try with some wildcards.
s.Insert("foo.*", 22);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeTrue();
s.HasInterest("foo.bar.baz").ShouldBeFalse();
// Remove sub, there should be no interest.
s.Remove("foo.*", 22);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeFalse();
s.HasInterest("foo.bar.baz").ShouldBeFalse();
s.Insert("foo.>", 33);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeTrue();
s.HasInterest("foo.bar.baz").ShouldBeTrue();
s.Remove("foo.>", 33);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeFalse();
s.HasInterest("foo.bar.baz").ShouldBeFalse();
s.Insert("*.>", 44);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeTrue();
s.HasInterest("foo.baz").ShouldBeTrue();
s.Remove("*.>", 44);
s.Insert("*.bar", 55);
s.HasInterest("foo").ShouldBeFalse();
s.HasInterest("foo.bar").ShouldBeTrue();
s.HasInterest("foo.baz").ShouldBeFalse();
s.Remove("*.bar", 55);
s.Insert("*", 66);
s.HasInterest("foo").ShouldBeTrue();
s.HasInterest("foo.bar").ShouldBeFalse();
s.Remove("*", 66);
}
// -------------------------------------------------------------------------
// TestGenericSublistHasInterestOverlapping
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistHasInterestOverlapping()
{
var s = GenericSublist<int>.NewSublist();
s.Insert("stream.A.child", 11);
s.Insert("stream.*", 11);
s.HasInterest("stream.A.child").ShouldBeTrue();
s.HasInterest("stream.A").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// TestGenericSublistHasInterestStartingInRace
// Tests that HasInterestStartingIn is safe to call concurrently with
// modifications to the sublist. Mirrors Go's goroutine test using Tasks.
// -------------------------------------------------------------------------
[Fact]
public async Task TestGenericSublistHasInterestStartingInRace()
{
var s = GenericSublist<int>.NewSublist();
// Pre-populate with some patterns.
for (var i = 0; i < 10; i++)
{
s.Insert("foo.bar.baz", i);
s.Insert("foo.*.baz", i + 10);
s.Insert("foo.>", i + 20);
}
const int iterations = 1000;
// Task 1: repeatedly call HasInterestStartingIn.
var task1 = Task.Run(() =>
{
for (var i = 0; i < iterations; i++)
{
s.HasInterestStartingIn("foo");
s.HasInterestStartingIn("foo.bar");
s.HasInterestStartingIn("foo.bar.baz");
s.HasInterestStartingIn("other.subject");
}
});
// Task 2: repeatedly modify the sublist.
var task2 = Task.Run(() =>
{
for (var i = 0; i < iterations; i++)
{
var val = 1000 + i;
var dynSubject = "test.subject." + (char)('a' + i % 26);
s.Insert(dynSubject, val);
s.Insert("foo.*.test", val);
// Remove may fail if not found (concurrent), so swallow KeyNotFoundException.
try { s.Remove(dynSubject, val); } catch (KeyNotFoundException) { }
try { s.Remove("foo.*.test", val); } catch (KeyNotFoundException) { }
}
});
// Task 3: also call HasInterest (which does lock).
var task3 = Task.Run(() =>
{
for (var i = 0; i < iterations; i++)
{
s.HasInterest("foo.bar.baz");
s.HasInterest("foo.something.baz");
}
});
await Task.WhenAll(task1, task2, task3);
}
// -------------------------------------------------------------------------
// TestGenericSublistNumInterest
// -------------------------------------------------------------------------
[Fact]
public void TestGenericSublistNumInterest()
{
var s = GenericSublist<int>.NewSublist();
s.Insert("foo", 11);
void RequireNumInterest(string subj, int expected)
{
RequireMatches(s, subj, expected);
s.NumInterest(subj).ShouldBe(expected);
}
// Expect to find that "foo" matches but "bar" doesn't.
RequireNumInterest("foo", 1);
RequireNumInterest("bar", 0);
// Remove fooSub and check interest again.
s.Remove("foo", 11);
RequireNumInterest("foo", 0);
// Try with some wildcards.
s.Insert("foo.*", 22);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 1);
RequireNumInterest("foo.bar.baz", 0);
// Remove sub, there should be no interest.
s.Remove("foo.*", 22);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 0);
RequireNumInterest("foo.bar.baz", 0);
s.Insert("foo.>", 33);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 1);
RequireNumInterest("foo.bar.baz", 1);
s.Remove("foo.>", 33);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 0);
RequireNumInterest("foo.bar.baz", 0);
s.Insert("*.>", 44);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 1);
RequireNumInterest("foo.bar.baz", 1);
s.Remove("*.>", 44);
s.Insert("*.bar", 55);
RequireNumInterest("foo", 0);
RequireNumInterest("foo.bar", 1);
RequireNumInterest("foo.bar.baz", 0);
s.Remove("*.bar", 55);
s.Insert("*", 66);
RequireNumInterest("foo", 1);
RequireNumInterest("foo.bar", 0);
s.Remove("*", 66);
}
}

View File

@@ -0,0 +1,238 @@
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures;
/// <summary>
/// Tests for <see cref="HashWheel"/>, mirroring thw_test.go (functional tests only;
/// benchmarks are omitted as they require BenchmarkDotNet).
/// </summary>
public sealed class HashWheelTests
{
private static readonly long Second = 1_000_000_000L; // nanoseconds
[Fact]
public void HashWheelBasics_ShouldSucceed()
{
// Mirror: TestHashWheelBasics
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var seq = 1UL;
var expires = now + 5 * Second;
hw.Add(seq, expires);
hw.Count.ShouldBe(1UL);
// Remove non-existent sequence.
Should.Throw<InvalidOperationException>(() => hw.Remove(999, expires));
hw.Count.ShouldBe(1UL);
// Remove properly.
hw.Remove(seq, expires);
hw.Count.ShouldBe(0UL);
// Already gone.
Should.Throw<InvalidOperationException>(() => hw.Remove(seq, expires));
hw.Count.ShouldBe(0UL);
}
[Fact]
public void HashWheelUpdate_ShouldSucceed()
{
// Mirror: TestHashWheelUpdate
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var oldExpires = now + 5 * Second;
var newExpires = now + 10 * Second;
hw.Add(1, oldExpires);
hw.Count.ShouldBe(1UL);
hw.Update(1, oldExpires, newExpires);
hw.Count.ShouldBe(1UL);
// Old position gone.
Should.Throw<InvalidOperationException>(() => hw.Remove(1, oldExpires));
hw.Count.ShouldBe(1UL);
// New position exists.
hw.Remove(1, newExpires);
hw.Count.ShouldBe(0UL);
}
[Fact]
public void HashWheelExpiration_ShouldExpireOnly_AlreadyExpired()
{
// Mirror: TestHashWheelExpiration
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var seqs = new Dictionary<ulong, long>
{
[1] = now - 1 * Second, // already expired
[2] = now + 1 * Second,
[3] = now + 10 * Second,
[4] = now + 60 * Second,
};
foreach (var (s, exp) in seqs)
hw.Add(s, exp);
hw.Count.ShouldBe((ulong)seqs.Count);
var expired = new HashSet<ulong>();
hw.ExpireTasksInternal(now, (s, _) => { expired.Add(s); return true; });
expired.Count.ShouldBe(1);
expired.ShouldContain(1UL);
hw.Count.ShouldBe(3UL);
}
[Fact]
public void HashWheelManualExpiration_ShouldRespectCallbackReturn()
{
// Mirror: TestHashWheelManualExpiration
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
for (var s = 1UL; s <= 4; s++)
hw.Add(s, now);
hw.Count.ShouldBe(4UL);
// Iterate without removing.
var expired = new Dictionary<ulong, ulong>();
for (var i = 0UL; i <= 1; i++)
{
hw.ExpireTasksInternal(now, (s, _) => { expired.TryAdd(s, 0); expired[s]++; return false; });
expired.Count.ShouldBe(4);
expired[1].ShouldBe(1 + i);
expired[2].ShouldBe(1 + i);
expired[3].ShouldBe(1 + i);
expired[4].ShouldBe(1 + i);
hw.Count.ShouldBe(4UL);
}
// Remove only even sequences.
for (var i = 0UL; i <= 1; i++)
{
hw.ExpireTasksInternal(now, (s, _) => { expired.TryAdd(s, 0); expired[s]++; return s % 2 == 0; });
expired[1].ShouldBe(3 + i);
expired[2].ShouldBe(3UL);
expired[3].ShouldBe(3 + i);
expired[4].ShouldBe(3UL);
hw.Count.ShouldBe(2UL);
}
// Manually remove remaining.
hw.Remove(1, now);
hw.Remove(3, now);
hw.Count.ShouldBe(0UL);
}
[Fact]
public void HashWheelExpirationLargerThanWheel_ShouldExpireAll()
{
// Mirror: TestHashWheelExpirationLargerThanWheel
const int WheelMask = (1 << 12) - 1;
var hw = HashWheel.NewHashWheel();
hw.Add(1, 0);
hw.Add(2, Second);
hw.Count.ShouldBe(2UL);
// Timestamp large enough to wrap the entire wheel.
var nowWrapped = Second * WheelMask;
var expired = new HashSet<ulong>();
hw.ExpireTasksInternal(nowWrapped, (s, _) => { expired.Add(s); return true; });
expired.Count.ShouldBe(2);
hw.Count.ShouldBe(0UL);
}
[Fact]
public void HashWheelNextExpiration_ShouldReturnEarliest()
{
// Mirror: TestHashWheelNextExpiration
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var seqs = new Dictionary<ulong, long>
{
[1] = now + 5 * Second,
[2] = now + 3 * Second, // earliest
[3] = now + 10 * Second,
};
foreach (var (s, exp) in seqs)
hw.Add(s, exp);
var tick = now + 6 * Second;
hw.GetNextExpiration(tick).ShouldBe(seqs[2]);
var empty = HashWheel.NewHashWheel();
empty.GetNextExpiration(now + Second).ShouldBe(long.MaxValue);
}
[Fact]
public void HashWheelStress_ShouldHandleLargeScale()
{
// Mirror: TestHashWheelStress
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
const int numSeqs = 100_000;
for (var seq = 0; seq < numSeqs; seq++)
{
var exp = now + (long)seq * Second;
hw.Add((ulong)seq, exp);
}
// Update even sequences.
for (var seq = 0; seq < numSeqs; seq += 2)
{
var oldExp = now + (long)seq * Second;
var newExp = now + (long)(seq + numSeqs) * Second;
hw.Update((ulong)seq, oldExp, newExp);
}
// Remove odd sequences.
for (var seq = 1; seq < numSeqs; seq += 2)
{
var exp = now + (long)seq * Second;
hw.Remove((ulong)seq, exp);
}
}
[Fact]
public void HashWheelEncodeDecode_ShouldRoundTrip()
{
// Mirror: TestHashWheelEncodeDecode
var hw = HashWheel.NewHashWheel();
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
const int numSeqs = 100_000;
for (var seq = 0; seq < numSeqs; seq++)
{
var exp = now + (long)seq * Second;
hw.Add((ulong)seq, exp);
}
var b = hw.Encode(12345);
b.Length.ShouldBeGreaterThan(17);
var nhw = HashWheel.NewHashWheel();
var stamp = nhw.Decode(b);
stamp.ShouldBe(12345UL);
// Lowest expiry should match.
hw.GetNextExpiration(long.MaxValue).ShouldBe(nhw.GetNextExpiration(long.MaxValue));
// Verify all entries transferred by removing them from nhw.
for (var seq = 0; seq < numSeqs; seq++)
{
var exp = now + (long)seq * Second;
nhw.Remove((ulong)seq, exp); // throws if missing
}
nhw.Count.ShouldBe(0UL);
}
}

View File

@@ -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);
}
}