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