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,39 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Auth.CertificateIdentityProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the certidp module, mirroring certidp_test.go and ocsp_responder_test.go.
|
||||
/// </summary>
|
||||
public sealed class CertificateIdentityProviderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, "good")]
|
||||
[InlineData(1, "revoked")]
|
||||
[InlineData(2, "unknown")]
|
||||
[InlineData(42, "unknown")] // Invalid → defaults to unknown (never good)
|
||||
public void GetStatusAssertionStr_ShouldMapCorrectly(int input, string expected)
|
||||
{
|
||||
// Mirror: TestGetStatusAssertionStr
|
||||
OcspStatusAssertionExtensions.GetStatusAssertionStr(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeOCSPRequest_ShouldProduceUrlSafeBase64()
|
||||
{
|
||||
// Mirror: TestEncodeOCSPRequest
|
||||
var data = "test data for OCSP request"u8.ToArray();
|
||||
var encoded = OcspResponder.EncodeOCSPRequest(data);
|
||||
|
||||
// Should not contain unescaped base64 chars that are URL-unsafe.
|
||||
encoded.ShouldNotContain("+");
|
||||
encoded.ShouldNotContain("/");
|
||||
encoded.ShouldNotContain("=");
|
||||
|
||||
// Should round-trip: URL-unescape → base64-decode → original bytes.
|
||||
var unescaped = Uri.UnescapeDataString(encoded);
|
||||
var decoded = Convert.FromBase64String(unescaped);
|
||||
decoded.ShouldBe(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
||||
|
||||
public sealed class TpmKeyProviderTests
|
||||
{
|
||||
private static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
[Fact]
|
||||
public void LoadJetStreamEncryptionKeyFromTpm_NonWindows_ThrowsPlatformNotSupportedException()
|
||||
{
|
||||
if (IsWindows)
|
||||
return; // This test is for non-Windows only
|
||||
|
||||
var ex = Should.Throw<PlatformNotSupportedException>(() =>
|
||||
TpmKeyProvider.LoadJetStreamEncryptionKeyFromTpm("", "keys.json", "password", 22));
|
||||
|
||||
ex.Message.ShouldContain("TPM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadJetStreamEncryptionKeyFromTpm_Create_ShouldSucceed()
|
||||
{
|
||||
if (!IsWindows)
|
||||
return; // Requires real TPM hardware on Windows
|
||||
|
||||
var tempFile = Path.Combine(Path.GetTempPath(), $"jskeys_{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempFile)) File.Delete(tempFile);
|
||||
|
||||
var key = TpmKeyProvider.LoadJetStreamEncryptionKeyFromTpm("", tempFile, "password", 22);
|
||||
key.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile)) File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AccessTimeService"/>, mirroring ats_test.go.
|
||||
/// </summary>
|
||||
[Collection("AccessTimeService")]
|
||||
public sealed class AccessTimeServiceTests : IDisposable
|
||||
{
|
||||
public AccessTimeServiceTests()
|
||||
{
|
||||
AccessTimeService.Reset();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AccessTimeService.Reset();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotRunningValue_ShouldReturnNonZero()
|
||||
{
|
||||
// Mirror: TestNotRunningValue
|
||||
// No registrants; AccessTime() must still return a non-zero value.
|
||||
var at = AccessTimeService.AccessTime();
|
||||
at.ShouldBeGreaterThan(0);
|
||||
|
||||
// Should be stable (no background timer updating it).
|
||||
var atn = AccessTimeService.AccessTime();
|
||||
atn.ShouldBe(at);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAndUnregister_ShouldManageLifetime()
|
||||
{
|
||||
// Mirror: TestRegisterAndUnregister
|
||||
|
||||
AccessTimeService.Register();
|
||||
|
||||
var at = AccessTimeService.AccessTime();
|
||||
at.ShouldBeGreaterThan(0);
|
||||
|
||||
// Background timer should update the time.
|
||||
await Task.Delay(AccessTimeService.TickInterval * 3);
|
||||
var atn = AccessTimeService.AccessTime();
|
||||
atn.ShouldBeGreaterThan(at);
|
||||
|
||||
// Unregister; timer should stop.
|
||||
AccessTimeService.Unregister();
|
||||
await Task.Delay(AccessTimeService.TickInterval);
|
||||
|
||||
at = AccessTimeService.AccessTime();
|
||||
await Task.Delay(AccessTimeService.TickInterval * 3);
|
||||
atn = AccessTimeService.AccessTime();
|
||||
atn.ShouldBe(at);
|
||||
|
||||
// Re-register should restart the timer.
|
||||
AccessTimeService.Register();
|
||||
try
|
||||
{
|
||||
at = AccessTimeService.AccessTime();
|
||||
await Task.Delay(AccessTimeService.TickInterval * 3);
|
||||
atn = AccessTimeService.AccessTime();
|
||||
atn.ShouldBeGreaterThan(at);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AccessTimeService.Unregister();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnbalancedUnregister_ShouldThrow()
|
||||
{
|
||||
// Mirror: TestUnbalancedUnregister
|
||||
Should.Throw<InvalidOperationException>(() => AccessTimeService.Unregister());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ProcessStatsProvider"/>, mirroring pse_test.go.
|
||||
/// The Go tests compare against `ps` command output — the .NET tests verify
|
||||
/// that values are within reasonable bounds since Process gives us the same data
|
||||
/// through a managed API without needing external command comparison.
|
||||
/// </summary>
|
||||
public sealed class ProcessStatsProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task PSEmulationCPU_ShouldReturnReasonableValue()
|
||||
{
|
||||
// Mirror: TestPSEmulationCPU
|
||||
// Allow one sampling cycle to complete.
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
|
||||
ProcessStatsProvider.ProcUsage(out var pcpu, out _, out _);
|
||||
|
||||
// CPU % should be non-negative and at most 100% × processor count.
|
||||
pcpu.ShouldBeGreaterThanOrEqualTo(0);
|
||||
pcpu.ShouldBeLessThanOrEqualTo(100.0 * Environment.ProcessorCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PSEmulationMem_ShouldReturnReasonableValue()
|
||||
{
|
||||
// Mirror: TestPSEmulationMem
|
||||
ProcessStatsProvider.ProcUsage(out _, out var rss, out var vss);
|
||||
|
||||
// RSS should be at least 1 MB (any .NET process uses far more).
|
||||
rss.ShouldBeGreaterThan(1024L * 1024L);
|
||||
|
||||
// VSS should be at least as large as RSS.
|
||||
vss.ShouldBeGreaterThanOrEqualTo(rss);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PSEmulationWin_ShouldCacheAndRefresh()
|
||||
{
|
||||
// Mirror: TestPSEmulationWin (caching behaviour validation)
|
||||
ProcessStatsProvider.ProcUsage(out _, out var rss1, out _);
|
||||
ProcessStatsProvider.ProcUsage(out _, out var rss2, out _);
|
||||
|
||||
// Two immediate calls should return the same cached value.
|
||||
rss1.ShouldBe(rss2);
|
||||
|
||||
// After a sampling interval, values should still be valid.
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
ProcessStatsProvider.ProcUsage(out _, out var rssAfter, out _);
|
||||
rssAfter.ShouldBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
// Copyright 2012-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 System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Protocol;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the NATS protocol parser.
|
||||
/// Mirrors Go parser_test.go — 17 test functions.
|
||||
/// </summary>
|
||||
public class ProtocolParserTests
|
||||
{
|
||||
// =====================================================================
|
||||
// Test helpers — mirrors Go dummyClient/dummyRouteClient
|
||||
// =====================================================================
|
||||
|
||||
private static ParseContext DummyClient() => new()
|
||||
{
|
||||
Kind = ClientKind.Client,
|
||||
MaxControlLine = ServerConstants.MaxControlLineSize,
|
||||
MaxPayload = -1,
|
||||
HasHeaders = false,
|
||||
};
|
||||
|
||||
private static ParseContext DummyRouteClient() => new()
|
||||
{
|
||||
Kind = ClientKind.Router,
|
||||
MaxControlLine = ServerConstants.MaxControlLineSize,
|
||||
MaxPayload = -1,
|
||||
};
|
||||
|
||||
private static TestProtocolHandler DummyHandler() => new();
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePing — Go test ID 2598
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParsePing_ByteByByte()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
|
||||
var ping = "PING\r\n"u8.ToArray();
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[..1]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpP);
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[1..2]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPi);
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[2..3]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPin);
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[3..4]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPing);
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[4..5]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPing);
|
||||
|
||||
ProtocolParser.Parse(c, h, ping[5..6]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
h.PingCount.ShouldBe(1);
|
||||
|
||||
// Full message
|
||||
ProtocolParser.Parse(c, h, ping).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
h.PingCount.ShouldBe(2);
|
||||
|
||||
// Should tolerate spaces
|
||||
var pingSpaces = "PING \r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pingSpaces).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPing);
|
||||
|
||||
c.State = ParserState.OpStart;
|
||||
var pingSpaces2 = "PING \r \n"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pingSpaces2).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePong — Go test ID 2599
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParsePong_ByteByByte()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
|
||||
var pong = "PONG\r\n"u8.ToArray();
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[..1]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpP);
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[1..2]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPo);
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[2..3]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPon);
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[3..4]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPong);
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[4..5]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPong);
|
||||
|
||||
ProtocolParser.Parse(c, h, pong[5..6]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
h.PongCount.ShouldBe(1);
|
||||
|
||||
// Full message
|
||||
ProtocolParser.Parse(c, h, pong).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
h.PongCount.ShouldBe(2);
|
||||
|
||||
// Should tolerate spaces
|
||||
var pongSpaces = "PONG \r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pongSpaces).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPong);
|
||||
|
||||
c.State = ParserState.OpStart;
|
||||
var pongSpaces2 = "PONG \r \n"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pongSpaces2).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseConnect — Go test ID 2600
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseConnect_ParsesCorrectly()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
var connect = Encoding.ASCII.GetBytes(
|
||||
"CONNECT {\"verbose\":false,\"pedantic\":true,\"tls_required\":false}\r\n");
|
||||
ProtocolParser.Parse(c, h, connect).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
h.ConnectArgs.ShouldNotBeNull();
|
||||
|
||||
// Check saved state: arg start should be 8 (after "CONNECT ")
|
||||
c.ArgStart.ShouldBe(connect.Length); // After full parse, ArgStart is past the end
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseSub — Go test ID 2601
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseSub_SetsState()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
var sub = "SUB foo 1\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, sub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.SubArg);
|
||||
|
||||
// The arg buffer should have been set up for split buffer
|
||||
c.ArgBuf.ShouldNotBeNull();
|
||||
Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 1");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePub — Go test ID 2602
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParsePub_ParsesSubjectReplySize()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
// Simple PUB
|
||||
var pub = "PUB foo 5\r\nhello\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
c.Pa.Size.ShouldBe(5);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// PUB with reply
|
||||
pub = "PUB foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
|
||||
c.Pa.Size.ShouldBe(11);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// Data larger than expected size
|
||||
pub = "PUB foo.bar 11\r\nhello world hello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
|
||||
c.MsgBuf.ShouldBeNull();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePubSizeOverflow — Go test ID 2603
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParsePubSizeOverflow_ReturnsError()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
var pub = Encoding.ASCII.GetBytes(
|
||||
"PUB foo 3333333333333333333333333333333333333333333333333333333333333333\r\n");
|
||||
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePubArg — Go test ID 2604
|
||||
// =====================================================================
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(PubArgTestCases))]
|
||||
public void ProcessPub_ParsesArgsCorrectly(string arg, string subject, string reply, int size, string szb)
|
||||
{
|
||||
var c = DummyClient();
|
||||
var err = ProtocolParser.ProcessPub(c, Encoding.ASCII.GetBytes(arg));
|
||||
err.ShouldBeNull();
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
|
||||
if (string.IsNullOrEmpty(reply))
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
else
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
|
||||
Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
|
||||
c.Pa.Size.ShouldBe(size);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, int, string> PubArgTestCases => new()
|
||||
{
|
||||
{ "a 2", "a", "", 2, "2" },
|
||||
{ "a 222", "a", "", 222, "222" },
|
||||
{ "foo 22", "foo", "", 22, "22" },
|
||||
{ " foo 22", "foo", "", 22, "22" },
|
||||
{ "foo 22 ", "foo", "", 22, "22" },
|
||||
{ "foo 22", "foo", "", 22, "22" },
|
||||
{ " foo 22 ", "foo", "", 22, "22" },
|
||||
{ " foo 22 ", "foo", "", 22, "22" },
|
||||
{ "foo bar 22", "foo", "bar", 22, "22" },
|
||||
{ " foo bar 22", "foo", "bar", 22, "22" },
|
||||
{ "foo bar 22 ", "foo", "bar", 22, "22" },
|
||||
{ "foo bar 22", "foo", "bar", 22, "22" },
|
||||
{ " foo bar 22 ", "foo", "bar", 22, "22" },
|
||||
{ " foo bar 22 ", "foo", "bar", 22, "22" },
|
||||
{ " foo bar 2222 ", "foo", "bar", 2222, "2222" },
|
||||
{ " foo 2222 ", "foo", "", 2222, "2222" },
|
||||
{ "a\t2", "a", "", 2, "2" },
|
||||
{ "a\t222", "a", "", 222, "222" },
|
||||
{ "foo\t22", "foo", "", 22, "22" },
|
||||
{ "\tfoo\t22", "foo", "", 22, "22" },
|
||||
{ "foo\t22\t", "foo", "", 22, "22" },
|
||||
{ "foo\t\t\t22", "foo", "", 22, "22" },
|
||||
{ "\tfoo\t22\t", "foo", "", 22, "22" },
|
||||
{ "\tfoo\t\t\t22\t", "foo", "", 22, "22" },
|
||||
{ "foo\tbar\t22", "foo", "bar", 22, "22" },
|
||||
{ "\tfoo\tbar\t22", "foo", "bar", 22, "22" },
|
||||
{ "foo\tbar\t22\t", "foo", "bar", 22, "22" },
|
||||
{ "foo\t\tbar\t\t22", "foo", "bar", 22, "22" },
|
||||
{ "\tfoo\tbar\t22\t", "foo", "bar", 22, "22" },
|
||||
{ "\t \tfoo\t \t \tbar\t \t22\t \t", "foo", "bar", 22, "22" },
|
||||
{ "\t\tfoo\t\t\tbar\t\t2222\t\t", "foo", "bar", 2222, "2222" },
|
||||
{ "\t \tfoo\t \t \t\t\t2222\t \t", "foo", "", 2222, "2222" },
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// TestParsePubBadSize — Go test ID 2605
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ProcessPub_BadSize_ReturnsError()
|
||||
{
|
||||
var c = DummyClient();
|
||||
c.MaxPayload = 32768;
|
||||
var err = ProtocolParser.ProcessPub(c, "foo 2222222222222222"u8.ToArray());
|
||||
err.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseHeaderPub — Go test ID 2606
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseHeaderPub_ParsesSubjectReplyHdrSize()
|
||||
{
|
||||
var c = DummyClient();
|
||||
c.HasHeaders = true;
|
||||
var h = DummyHandler();
|
||||
|
||||
// Simple HPUB
|
||||
var hpub = "HPUB foo 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
c.Pa.HeaderSize.ShouldBe(12);
|
||||
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
|
||||
c.Pa.Size.ShouldBe(17);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// HPUB with reply
|
||||
hpub = "HPUB foo INBOX.22 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
|
||||
c.Pa.HeaderSize.ShouldBe(12);
|
||||
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
|
||||
c.Pa.Size.ShouldBe(17);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// HPUB with hdr=0
|
||||
hpub = "HPUB foo INBOX.22 0 5\r\nHELLO\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
|
||||
c.Pa.HeaderSize.ShouldBe(0);
|
||||
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("0");
|
||||
c.Pa.Size.ShouldBe(5);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseHeaderPubArg — Go test ID 2607
|
||||
// =====================================================================
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HeaderPubArgTestCases))]
|
||||
public void ProcessHeaderPub_ParsesArgsCorrectly(
|
||||
string arg, string subject, string reply, int hdr, int size, string szb)
|
||||
{
|
||||
var c = DummyClient();
|
||||
c.HasHeaders = true;
|
||||
var err = ProtocolParser.ProcessHeaderPub(c, Encoding.ASCII.GetBytes(arg), null);
|
||||
err.ShouldBeNull();
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
|
||||
if (string.IsNullOrEmpty(reply))
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
else
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
|
||||
Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
|
||||
c.Pa.HeaderSize.ShouldBe(hdr);
|
||||
c.Pa.Size.ShouldBe(size);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, string, int, int, string> HeaderPubArgTestCases => new()
|
||||
{
|
||||
{ "a 2 4", "a", "", 2, 4, "4" },
|
||||
{ "a 22 222", "a", "", 22, 222, "222" },
|
||||
{ "foo 3 22", "foo", "", 3, 22, "22" },
|
||||
{ " foo 1 22", "foo", "", 1, 22, "22" },
|
||||
{ "foo 0 22 ", "foo", "", 0, 22, "22" },
|
||||
{ "foo 0 22", "foo", "", 0, 22, "22" },
|
||||
{ " foo 1 22 ", "foo", "", 1, 22, "22" },
|
||||
{ " foo 3 22 ", "foo", "", 3, 22, "22" },
|
||||
{ "foo bar 1 22", "foo", "bar", 1, 22, "22" },
|
||||
{ " foo bar 11 22", "foo", "bar", 11, 22, "22" },
|
||||
{ "foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
|
||||
{ "foo bar 11 22", "foo", "bar", 11, 22, "22" },
|
||||
{ " foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
|
||||
{ " foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
|
||||
{ " foo bar 22 2222 ", "foo", "bar", 22, 2222, "2222" },
|
||||
{ " foo 1 2222 ", "foo", "", 1, 2222, "2222" },
|
||||
{ "a\t2\t22", "a", "", 2, 22, "22" },
|
||||
{ "a\t2\t\t222", "a", "", 2, 222, "222" },
|
||||
{ "foo\t2 22", "foo", "", 2, 22, "22" },
|
||||
{ "\tfoo\t11\t 22", "foo", "", 11, 22, "22" },
|
||||
{ "foo\t11\t22\t", "foo", "", 11, 22, "22" },
|
||||
{ "foo\t\t\t11 22", "foo", "", 11, 22, "22" },
|
||||
{ "\tfoo\t11\t \t 22\t", "foo", "", 11, 22, "22" },
|
||||
{ "\tfoo\t\t\t11 22\t", "foo", "", 11, 22, "22" },
|
||||
{ "foo\tbar\t2 22", "foo", "bar", 2, 22, "22" },
|
||||
{ "\tfoo\tbar\t11\t22", "foo", "bar", 11, 22, "22" },
|
||||
{ "foo\tbar\t11\t\t22\t ", "foo", "bar", 11, 22, "22" },
|
||||
{ "foo\t\tbar\t\t11\t\t\t22", "foo", "bar", 11, 22, "22" },
|
||||
{ "\tfoo\tbar\t11\t22\t", "foo", "bar", 11, 22, "22" },
|
||||
{ "\t \tfoo\t \t \tbar\t \t11\t 22\t \t", "foo", "bar", 11, 22, "22" },
|
||||
{ "\t\tfoo\t\t\tbar\t\t22\t\t\t2222\t\t", "foo", "bar", 22, 2222, "2222" },
|
||||
{ "\t \tfoo\t \t \t\t\t11\t\t 2222\t \t", "foo", "", 11, 2222, "2222" },
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// TestParseRoutedHeaderMsg — Go test ID 2608
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseRoutedHeaderMsg_ParsesCorrectly()
|
||||
{
|
||||
var c = DummyRouteClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
// hdr > size should error
|
||||
var pub = "HMSG $foo foo 10 8\r\nXXXhello\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// Simple HMSG
|
||||
pub = "HMSG $foo foo 3 8\r\nXXXhello\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
c.Pa.HeaderSize.ShouldBe(3);
|
||||
c.Pa.Size.ShouldBe(8);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// HMSG with reply
|
||||
pub = "HMSG $G foo.bar INBOX.22 3 14\r\nOK:hello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
|
||||
c.Pa.HeaderSize.ShouldBe(3);
|
||||
c.Pa.Size.ShouldBe(14);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// HMSG with + reply and queue
|
||||
pub = "HMSG $G foo.bar + reply baz 3 14\r\nOK:hello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
|
||||
c.Pa.Queues.ShouldNotBeNull();
|
||||
c.Pa.Queues!.Count.ShouldBe(1);
|
||||
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
|
||||
c.Pa.HeaderSize.ShouldBe(3);
|
||||
c.Pa.Size.ShouldBe(14);
|
||||
|
||||
// Clear snapshots
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// HMSG with | queue (no reply)
|
||||
pub = "HMSG $G foo.bar | baz 3 14\r\nOK:hello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
|
||||
c.Pa.Queues.ShouldNotBeNull();
|
||||
c.Pa.Queues!.Count.ShouldBe(1);
|
||||
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
|
||||
c.Pa.HeaderSize.ShouldBe(3);
|
||||
c.Pa.Size.ShouldBe(14);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseRouteMsg — Go test ID 2609
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseRouteMsg_ParsesCorrectly()
|
||||
{
|
||||
var c = DummyRouteClient();
|
||||
var h = DummyHandler();
|
||||
|
||||
// MSG from route should error (must use RMSG)
|
||||
var pub = "MSG $foo foo 5\r\nhello\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
|
||||
|
||||
// Reset
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// RMSG simple
|
||||
pub = "RMSG $foo foo 5\r\nhello\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
|
||||
c.Pa.Reply.ShouldBeNull();
|
||||
c.Pa.Size.ShouldBe(5);
|
||||
|
||||
// Clear
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// RMSG with reply
|
||||
pub = "RMSG $G foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
|
||||
c.Pa.Size.ShouldBe(11);
|
||||
|
||||
// Clear
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// RMSG with + reply and queue
|
||||
pub = "RMSG $G foo.bar + reply baz 11\r\nhello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
|
||||
c.Pa.Queues.ShouldNotBeNull();
|
||||
c.Pa.Queues!.Count.ShouldBe(1);
|
||||
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
|
||||
|
||||
// Clear
|
||||
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
|
||||
|
||||
// RMSG with | queue (no reply)
|
||||
pub = "RMSG $G foo.bar | baz 11\r\nhello world\r"u8.ToArray();
|
||||
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.MsgEndN);
|
||||
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
|
||||
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
|
||||
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
|
||||
c.Pa.Queues.ShouldNotBeNull();
|
||||
c.Pa.Queues!.Count.ShouldBe(1);
|
||||
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseMsgSpace — Go test ID 2610
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseMsgSpace_ErrorsCorrectly()
|
||||
{
|
||||
// MSG <SPC> from route should error
|
||||
var c = DummyRouteClient();
|
||||
var h = DummyHandler();
|
||||
ProtocolParser.Parse(c, h, "MSG \r\n"u8.ToArray()).ShouldNotBeNull();
|
||||
|
||||
// M from client should error
|
||||
c = DummyClient();
|
||||
ProtocolParser.Parse(c, h, "M"u8.ToArray()).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestShouldFail — Go test ID 2611
|
||||
// =====================================================================
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ShouldFailClientProtos))]
|
||||
public void ShouldFail_ClientProtos(string proto)
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
public static TheoryData<string> ShouldFailClientProtos => new()
|
||||
{
|
||||
"xxx",
|
||||
"Px", "PIx", "PINx", " PING",
|
||||
"POx", "PONx",
|
||||
"+x", "+Ox",
|
||||
"-x", "-Ex", "-ERx", "-ERRx",
|
||||
"Cx", "COx", "CONx", "CONNx", "CONNEx", "CONNECx", "CONNECT \r\n",
|
||||
"PUx", "PUB foo\r\n", "PUB \r\n", "PUB foo bar \r\n",
|
||||
"PUB foo 2\r\nok \r\n", "PUB foo 2\r\nok\r \n",
|
||||
"Sx", "SUx", "SUB\r\n", "SUB \r\n", "SUB foo\r\n",
|
||||
"SUB foo bar baz 22\r\n",
|
||||
"Ux", "UNx", "UNSx", "UNSUx", "UNSUBx", "UNSUBUNSUB 1\r\n", "UNSUB_2\r\n",
|
||||
"UNSUB_UNSUB_UNSUB 2\r\n", "UNSUB_\t2\r\n", "UNSUB\r\n", "UNSUB \r\n",
|
||||
"UNSUB \t \r\n",
|
||||
"Ix", "INx", "INFx", "INFO \r\n",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ShouldFailRouterProtos))]
|
||||
public void ShouldFail_RouterProtos(string proto)
|
||||
{
|
||||
var c = DummyClient();
|
||||
c.Kind = ClientKind.Router;
|
||||
var h = DummyHandler();
|
||||
ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
public static TheoryData<string> ShouldFailRouterProtos => new()
|
||||
{
|
||||
"Mx", "MSx", "MSGx", "MSG \r\n",
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// TestProtoSnippet — Go test ID 2612
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ProtoSnippet_MatchesGoOutput()
|
||||
{
|
||||
var sample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"u8.ToArray();
|
||||
|
||||
var tests = new (int Start, string Expected)[]
|
||||
{
|
||||
(0, "\"abcdefghijklmnopqrstuvwxyzABCDEF\""),
|
||||
(1, "\"bcdefghijklmnopqrstuvwxyzABCDEFG\""),
|
||||
(2, "\"cdefghijklmnopqrstuvwxyzABCDEFGH\""),
|
||||
(3, "\"defghijklmnopqrstuvwxyzABCDEFGHI\""),
|
||||
(4, "\"efghijklmnopqrstuvwxyzABCDEFGHIJ\""),
|
||||
(5, "\"fghijklmnopqrstuvwxyzABCDEFGHIJK\""),
|
||||
(6, "\"ghijklmnopqrstuvwxyzABCDEFGHIJKL\""),
|
||||
(7, "\"hijklmnopqrstuvwxyzABCDEFGHIJKLM\""),
|
||||
(8, "\"ijklmnopqrstuvwxyzABCDEFGHIJKLMN\""),
|
||||
(9, "\"jklmnopqrstuvwxyzABCDEFGHIJKLMNO\""),
|
||||
(10, "\"klmnopqrstuvwxyzABCDEFGHIJKLMNOP\""),
|
||||
(11, "\"lmnopqrstuvwxyzABCDEFGHIJKLMNOPQ\""),
|
||||
(12, "\"mnopqrstuvwxyzABCDEFGHIJKLMNOPQR\""),
|
||||
(13, "\"nopqrstuvwxyzABCDEFGHIJKLMNOPQRS\""),
|
||||
(14, "\"opqrstuvwxyzABCDEFGHIJKLMNOPQRST\""),
|
||||
(15, "\"pqrstuvwxyzABCDEFGHIJKLMNOPQRSTU\""),
|
||||
(16, "\"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUV\""),
|
||||
(17, "\"rstuvwxyzABCDEFGHIJKLMNOPQRSTUVW\""),
|
||||
(18, "\"stuvwxyzABCDEFGHIJKLMNOPQRSTUVWX\""),
|
||||
(19, "\"tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(20, "\"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\""),
|
||||
(21, "\"vwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(22, "\"wxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(23, "\"xyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(24, "\"yzABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(25, "\"zABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(26, "\"ABCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(27, "\"BCDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(28, "\"CDEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(29, "\"DEFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(30, "\"EFGHIJKLMNOPQRSTUVWXY\""),
|
||||
(31, "\"FGHIJKLMNOPQRSTUVWXY\""),
|
||||
(32, "\"GHIJKLMNOPQRSTUVWXY\""),
|
||||
(33, "\"HIJKLMNOPQRSTUVWXY\""),
|
||||
(34, "\"IJKLMNOPQRSTUVWXY\""),
|
||||
(35, "\"JKLMNOPQRSTUVWXY\""),
|
||||
(36, "\"KLMNOPQRSTUVWXY\""),
|
||||
(37, "\"LMNOPQRSTUVWXY\""),
|
||||
(38, "\"MNOPQRSTUVWXY\""),
|
||||
(39, "\"NOPQRSTUVWXY\""),
|
||||
(40, "\"OPQRSTUVWXY\""),
|
||||
(41, "\"PQRSTUVWXY\""),
|
||||
(42, "\"QRSTUVWXY\""),
|
||||
(43, "\"RSTUVWXY\""),
|
||||
(44, "\"STUVWXY\""),
|
||||
(45, "\"TUVWXY\""),
|
||||
(46, "\"UVWXY\""),
|
||||
(47, "\"VWXY\""),
|
||||
(48, "\"WXY\""),
|
||||
(49, "\"XY\""),
|
||||
(50, "\"Y\""),
|
||||
(51, "\"\""),
|
||||
(52, "\"\""),
|
||||
(53, "\"\""),
|
||||
(54, "\"\""),
|
||||
};
|
||||
|
||||
foreach (var (start, expected) in tests)
|
||||
{
|
||||
var got = ProtocolParser.ProtoSnippet(start, ServerConstants.ProtoSnippetSize, sample);
|
||||
got.ShouldBe(expected, $"start={start}");
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestParseOK — Go test ID 2613 (mapped from Go TestParseOK)
|
||||
// =====================================================================
|
||||
|
||||
[Fact]
|
||||
public void ParseOK_ByteByByte()
|
||||
{
|
||||
var c = DummyClient();
|
||||
var h = DummyHandler();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
|
||||
var ok = "+OK\r\n"u8.ToArray();
|
||||
|
||||
ProtocolParser.Parse(c, h, ok[..1]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPlus);
|
||||
|
||||
ProtocolParser.Parse(c, h, ok[1..2]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPlusO);
|
||||
|
||||
ProtocolParser.Parse(c, h, ok[2..3]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPlusOk);
|
||||
|
||||
ProtocolParser.Parse(c, h, ok[3..4]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpPlusOk);
|
||||
|
||||
ProtocolParser.Parse(c, h, ok[4..5]).ShouldBeNull();
|
||||
c.State.ShouldBe(ParserState.OpStart);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestMaxControlLine — Go test ID 2614
|
||||
// =====================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(ClientKind.Client, true)]
|
||||
[InlineData(ClientKind.Leaf, false)]
|
||||
[InlineData(ClientKind.Router, false)]
|
||||
[InlineData(ClientKind.Gateway, false)]
|
||||
public void MaxControlLine_EnforcedForClientOnly(ClientKind kind, bool shouldFail)
|
||||
{
|
||||
var pub = "PUB foo.bar.baz 2\r\nok\r\n"u8.ToArray();
|
||||
|
||||
var c = new ParseContext
|
||||
{
|
||||
Kind = kind,
|
||||
MaxControlLine = 8, // Very small limit
|
||||
MaxPayload = -1,
|
||||
};
|
||||
var h = DummyHandler();
|
||||
|
||||
// For non-client kinds, we need to set up the OP appropriately
|
||||
// Routes use RMSG not PUB, but PUB is fine for testing mcl enforcement
|
||||
// since the state machine handles it the same way.
|
||||
|
||||
var err = ProtocolParser.Parse(c, h, pub);
|
||||
if (shouldFail)
|
||||
{
|
||||
err.ShouldNotBeNull();
|
||||
ErrorContextHelper.ErrorIs(err, ServerErrors.ErrMaxControlLine).ShouldBeTrue();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-client kinds don't enforce max control line
|
||||
err.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestProtocolHandler — stub handler for tests
|
||||
// =====================================================================
|
||||
|
||||
private sealed class TestProtocolHandler : IProtocolHandler
|
||||
{
|
||||
public bool IsMqtt => false;
|
||||
public bool Trace => false;
|
||||
public bool HasMappings => false;
|
||||
public bool IsAwaitingAuth => false;
|
||||
public bool TryRegisterNoAuthUser() => true; // Allow all
|
||||
public bool IsGatewayInboundNotConnected => false;
|
||||
|
||||
public int PingCount { get; private set; }
|
||||
public int PongCount { get; private set; }
|
||||
public byte[]? ConnectArgs { get; private set; }
|
||||
|
||||
public Exception? ProcessConnect(byte[] arg) { ConnectArgs = arg; return null; }
|
||||
public Exception? ProcessInfo(byte[] arg) => null;
|
||||
public void ProcessPing() => PingCount++;
|
||||
public void ProcessPong() => PongCount++;
|
||||
public void ProcessErr(string arg) { }
|
||||
public Exception? ProcessClientSub(byte[] arg) => null;
|
||||
public Exception? ProcessClientUnsub(byte[] arg) => null;
|
||||
public Exception? ProcessRemoteSub(byte[] arg, bool isLeaf) => null;
|
||||
public Exception? ProcessRemoteUnsub(byte[] arg, bool isLeafUnsub) => null;
|
||||
public Exception? ProcessGatewayRSub(byte[] arg) => null;
|
||||
public Exception? ProcessGatewayRUnsub(byte[] arg) => null;
|
||||
public Exception? ProcessLeafSub(byte[] arg) => null;
|
||||
public Exception? ProcessLeafUnsub(byte[] arg) => null;
|
||||
public Exception? ProcessAccountSub(byte[] arg) => null;
|
||||
public void ProcessAccountUnsub(byte[] arg) { }
|
||||
public void ProcessInboundMsg(byte[] msg) { }
|
||||
public bool SelectMappedSubject() => false;
|
||||
public void TraceInOp(string name, byte[]? arg) { }
|
||||
public void TraceMsg(byte[] msg) { }
|
||||
public void SendErr(string msg) { }
|
||||
public void AuthViolation() { }
|
||||
public void CloseConnection(int reason) { }
|
||||
public string KindString() => "CLIENT";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user