// Go reference: server/gsl/gsl_test.go // Tests for GenericSubjectList trie-based subject matching. using NATS.Server.Internal.Gsl; namespace NATS.Server.Tests.Internal.Gsl; public class GenericSubjectListTests { /// /// Helper: count matches for a subject. /// private static int CountMatches(GenericSubjectList s, string subject) where T : IEquatable { var count = 0; s.Match(subject, _ => count++); return count; } // Go: TestGenericSublistInit server/gsl/gsl_test.go:23 [Fact] public void Init_EmptyList() { var s = new GenericSubjectList(); s.Count.ShouldBe(0u); } // Go: TestGenericSublistInsertCount server/gsl/gsl_test.go:29 [Fact] public void InsertCount_TracksCorrectly() { var s = new GenericSubjectList(); s.Insert("foo", 1); s.Insert("bar", 2); s.Insert("foo.bar", 3); s.Count.ShouldBe(3u); } // Go: TestGenericSublistSimple server/gsl/gsl_test.go:37 [Fact] public void Simple_ExactMatch() { var s = new GenericSubjectList(); s.Insert("foo", 1); CountMatches(s, "foo").ShouldBe(1); } // Go: TestGenericSublistSimpleMultiTokens server/gsl/gsl_test.go:43 [Fact] public void SimpleMultiTokens_Match() { var s = new GenericSubjectList(); s.Insert("foo.bar.baz", 1); CountMatches(s, "foo.bar.baz").ShouldBe(1); } // Go: TestGenericSublistPartialWildcard server/gsl/gsl_test.go:49 [Fact] public void PartialWildcard_StarMatches() { var s = new GenericSubjectList(); s.Insert("a.b.c", 1); s.Insert("a.*.c", 2); CountMatches(s, "a.b.c").ShouldBe(2); } // Go: TestGenericSublistPartialWildcardAtEnd server/gsl/gsl_test.go:56 [Fact] public void PartialWildcardAtEnd_StarMatches() { var s = new GenericSubjectList(); s.Insert("a.b.c", 1); s.Insert("a.b.*", 2); CountMatches(s, "a.b.c").ShouldBe(2); } // Go: TestGenericSublistFullWildcard server/gsl/gsl_test.go:63 [Fact] public void FullWildcard_GreaterThanMatches() { var s = new GenericSubjectList(); s.Insert("a.b.c", 1); s.Insert("a.>", 2); CountMatches(s, "a.b.c").ShouldBe(2); CountMatches(s, "a.>").ShouldBe(1); } // Go: TestGenericSublistRemove server/gsl/gsl_test.go:71 [Fact] public void Remove_DecreasesCount() { var s = new GenericSubjectList(); s.Insert("a.b.c.d", 1); s.Count.ShouldBe(1u); CountMatches(s, "a.b.c.d").ShouldBe(1); s.Remove("a.b.c.d", 1); s.Count.ShouldBe(0u); CountMatches(s, "a.b.c.d").ShouldBe(0); } // Go: TestGenericSublistRemoveWildcard server/gsl/gsl_test.go:83 [Fact] public void RemoveWildcard_CleansUp() { var s = new GenericSubjectList(); s.Insert("a.b.c.d", 11); s.Insert("a.b.*.d", 22); s.Insert("a.b.>", 33); s.Count.ShouldBe(3u); CountMatches(s, "a.b.c.d").ShouldBe(3); s.Remove("a.b.*.d", 22); s.Count.ShouldBe(2u); CountMatches(s, "a.b.c.d").ShouldBe(2); s.Remove("a.b.>", 33); s.Count.ShouldBe(1u); CountMatches(s, "a.b.c.d").ShouldBe(1); s.Remove("a.b.c.d", 11); s.Count.ShouldBe(0u); CountMatches(s, "a.b.c.d").ShouldBe(0); } // Go: TestGenericSublistRemoveCleanup server/gsl/gsl_test.go:105 [Fact] public void RemoveCleanup_PrunesEmptyNodes() { var s = new GenericSubjectList(); s.NumLevels().ShouldBe(0); s.Insert("a.b.c.d.e.f", 1); s.NumLevels().ShouldBe(6); s.Remove("a.b.c.d.e.f", 1); s.NumLevels().ShouldBe(0); } // Go: TestGenericSublistRemoveCleanupWildcards server/gsl/gsl_test.go:114 [Fact] public void RemoveCleanupWildcards_PrunesEmptyNodes() { var s = new GenericSubjectList(); s.NumLevels().ShouldBe(0); s.Insert("a.b.*.d.e.>", 1); s.NumLevels().ShouldBe(6); s.Remove("a.b.*.d.e.>", 1); s.NumLevels().ShouldBe(0); } // Go: TestGenericSublistInvalidSubjectsInsert server/gsl/gsl_test.go:123 [Fact] public void InvalidSubjectsInsert_RejectsInvalid() { var s = new GenericSubjectList(); // Empty tokens and FWC not terminal Should.Throw(() => s.Insert(".foo", 1)); Should.Throw(() => s.Insert("foo.", 1)); Should.Throw(() => s.Insert("foo..bar", 1)); Should.Throw(() => s.Insert("foo.bar..baz", 1)); Should.Throw(() => s.Insert("foo.>.baz", 1)); } // Go: TestGenericSublistBadSubjectOnRemove server/gsl/gsl_test.go:134 [Fact] public void BadSubjectOnRemove_RejectsInvalid() { var s = new GenericSubjectList(); Should.Throw(() => s.Insert("a.b..d", 1)); Should.Throw(() => s.Remove("a.b..d", 1)); Should.Throw(() => s.Remove("a.>.b", 1)); } // Go: TestGenericSublistTwoTokenPubMatchSingleTokenSub server/gsl/gsl_test.go:141 [Fact] public void TwoTokenPub_DoesNotMatchSingleTokenSub() { var s = new GenericSubjectList(); s.Insert("foo", 1); CountMatches(s, "foo").ShouldBe(1); CountMatches(s, "foo.bar").ShouldBe(0); } // Go: TestGenericSublistInsertWithWildcardsAsLiterals server/gsl/gsl_test.go:148 [Fact] public void InsertWithWildcardsAsLiterals_TreatsAsLiteral() { var s = new GenericSubjectList(); var subjects = new[] { "foo.*-", "foo.>-" }; for (var i = 0; i < subjects.Length; i++) { s.Insert(subjects[i], i); CountMatches(s, "foo.bar").ShouldBe(0); CountMatches(s, subjects[i]).ShouldBe(1); } } // Go: TestGenericSublistRemoveWithWildcardsAsLiterals server/gsl/gsl_test.go:157 [Fact] public void RemoveWithWildcardsAsLiterals_RemovesCorrectly() { var s = new GenericSubjectList(); var subjects = new[] { "foo.*-", "foo.>-" }; for (var i = 0; i < subjects.Length; i++) { s.Insert(subjects[i], i); CountMatches(s, "foo.bar").ShouldBe(0); CountMatches(s, subjects[i]).ShouldBe(1); Should.Throw(() => s.Remove("foo.bar", i)); s.Count.ShouldBe(1u); s.Remove(subjects[i], i); s.Count.ShouldBe(0u); } } // Go: TestGenericSublistMatchWithEmptyTokens server/gsl/gsl_test.go:170 [Theory] [InlineData(".foo")] [InlineData("..foo")] [InlineData("foo..")] [InlineData("foo.")] [InlineData("foo..bar")] [InlineData("foo...bar")] public void MatchWithEmptyTokens_HandlesEdgeCase(string subject) { var s = new GenericSubjectList(); s.Insert(">", 1); CountMatches(s, subject).ShouldBe(0); } // Go: TestGenericSublistHasInterest server/gsl/gsl_test.go:180 [Fact] public void HasInterest_ReturnsTrueForMatchingSubjects() { var s = new GenericSubjectList(); 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. CountMatches(s, "bar").ShouldBe(0); s.HasInterest("bar").ShouldBeFalse(); // Remove fooSub and check interest again s.Remove("foo", 11); s.HasInterest("foo").ShouldBeFalse(); // Try with partial wildcard * 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(); // Try with full wildcard > 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(); // Try with *.> s.Insert("*.>", 44); s.HasInterest("foo").ShouldBeFalse(); s.HasInterest("foo.bar").ShouldBeTrue(); s.HasInterest("foo.baz").ShouldBeTrue(); s.Remove("*.>", 44); // Try with *.bar s.Insert("*.bar", 55); s.HasInterest("foo").ShouldBeFalse(); s.HasInterest("foo.bar").ShouldBeTrue(); s.HasInterest("foo.baz").ShouldBeFalse(); s.Remove("*.bar", 55); // Try with * s.Insert("*", 66); s.HasInterest("foo").ShouldBeTrue(); s.HasInterest("foo.bar").ShouldBeFalse(); s.Remove("*", 66); } // Go: TestGenericSublistHasInterestOverlapping server/gsl/gsl_test.go:237 [Fact] public void HasInterestOverlapping_HandlesOverlap() { var s = new GenericSubjectList(); s.Insert("stream.A.child", 11); s.Insert("stream.*", 11); s.HasInterest("stream.A.child").ShouldBeTrue(); s.HasInterest("stream.A").ShouldBeTrue(); } // Go: TestGenericSublistHasInterestStartingInRace server/gsl/gsl_test.go:247 [Fact] public async Task HasInterestStartingIn_ThreadSafe() { var s = new GenericSubjectList(); // 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; var tasks = new List(); // Task 1: repeatedly call HasInterestStartingIn tasks.Add(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 tasks.Add(Task.Run(() => { for (var i = 0; i < iterations; i++) { var val = 1000 + i; var ch = (char)('a' + (i % 26)); s.Insert($"test.subject.{ch}", val); s.Insert("foo.*.test", val); s.Remove($"test.subject.{ch}", val); s.Remove("foo.*.test", val); } })); // Task 3: also call HasInterest (which does lock) tasks.Add(Task.Run(() => { for (var i = 0; i < iterations; i++) { s.HasInterest("foo.bar.baz"); s.HasInterest("foo.something.baz"); } })); // Wait for all tasks - should not throw (no deadlocks or data races) await Task.WhenAll(tasks); } // Go: TestGenericSublistNumInterest server/gsl/gsl_test.go:298 [Fact] public void NumInterest_CountsMatchingSubscriptions() { var s = new GenericSubjectList(); s.Insert("foo", 11); // Helper to check both Match count and NumInterest agree void RequireNumInterest(string subj, int expected) { CountMatches(s, subj).ShouldBe(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 partial wildcard * 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); // Full wildcard > 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); // *.bar 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); } }