// 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; /// /// Ports all 21 tests from Go's gsl/gsl_test.go. /// public sealed class GenericSublistTests { // ------------------------------------------------------------------------- // Helpers (mirror Go's require_* functions) // ------------------------------------------------------------------------- /// /// Counts how many values the sublist matches for /// and asserts that count equals . /// Mirrors Go's require_Matches. /// private static void RequireMatches(GenericSublist 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.NewSublist(); s.Count.ShouldBe(0u); } // ------------------------------------------------------------------------- // TestGenericSublistInsertCount // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistInsertCount() { var s = GenericSublist.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.NewSublist(); s.Insert("foo", EmptyStruct.Value); RequireMatches(s, "foo", 1); } // ------------------------------------------------------------------------- // TestGenericSublistSimpleMultiTokens // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistSimpleMultiTokens() { var s = GenericSublist.NewSublist(); s.Insert("foo.bar.baz", EmptyStruct.Value); RequireMatches(s, "foo.bar.baz", 1); } // ------------------------------------------------------------------------- // TestGenericSublistPartialWildcard // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistPartialWildcard() { var s = GenericSublist.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.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.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.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.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.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.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.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(() => s.Insert(".foo", EmptyStruct.Value)); Should.Throw(() => s.Insert("foo.", EmptyStruct.Value)); Should.Throw(() => s.Insert("foo..bar", EmptyStruct.Value)); Should.Throw(() => s.Insert("foo.bar..baz", EmptyStruct.Value)); Should.Throw(() => s.Insert("foo.>.baz", EmptyStruct.Value)); } // ------------------------------------------------------------------------- // TestGenericSublistBadSubjectOnRemove // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistBadSubjectOnRemove() { var s = GenericSublist.NewSublist(); Should.Throw(() => s.Insert("a.b..d", EmptyStruct.Value)); Should.Throw(() => s.Remove("a.b..d", EmptyStruct.Value)); Should.Throw(() => s.Remove("a.>.b", EmptyStruct.Value)); } // ------------------------------------------------------------------------- // TestGenericSublistTwoTokenPubMatchSingleTokenSub // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistTwoTokenPubMatchSingleTokenSub() { var s = GenericSublist.NewSublist(); s.Insert("foo", EmptyStruct.Value); RequireMatches(s, "foo", 1); RequireMatches(s, "foo.bar", 0); } // ------------------------------------------------------------------------- // TestGenericSublistInsertWithWildcardsAsLiterals // ------------------------------------------------------------------------- [Fact] public void TestGenericSublistInsertWithWildcardsAsLiterals() { var s = GenericSublist.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.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(() => 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.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.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.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.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.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); } }