Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/DataStructures/SubscriptionIndexTests.cs
Joseph Doherty ed78a100e2 feat: port session 05 — Subscription Index (sublist)
Port trie-based subject matching engine (81 features, 74 tests).
Includes SubscriptionIndex with cache, wildcard matching (*/>),
queue subscription groups, reverse match, notifications, stats,
and subject validation utilities. Also adds minimal Subscription
and NatsClient stubs needed by the index.
2026-02-26 12:11:06 -05:00

1041 lines
38 KiB
C#

// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
namespace ZB.MOM.NatsNet.Server.Tests.Internal.DataStructures;
/// <summary>
/// Tests for SubscriptionIndex — mirrors tests from server/sublist_test.go.
/// Cache/no-cache pairs are collapsed into Theory with InlineData.
/// </summary>
public class SubscriptionIndexTests
{
// Helper factories matching Go's newSub/newQSub/newRemoteQSub.
private static Subscription NewSub(string subject) =>
new() { Subject = Encoding.ASCII.GetBytes(subject), Client = new NatsClient { Kind = ClientKind.Client } };
private static Subscription NewQSub(string subject, string queue) =>
queue.Length > 0
? new() { Subject = Encoding.ASCII.GetBytes(subject), Queue = Encoding.ASCII.GetBytes(queue) }
: NewSub(subject);
private static Subscription NewRemoteQSub(string subject, string queue, int weight) =>
queue.Length > 0
? new() { Subject = Encoding.ASCII.GetBytes(subject), Queue = Encoding.ASCII.GetBytes(queue), Qw = weight, Client = new NatsClient { Kind = ClientKind.Router } }
: NewSub(subject);
private static SubscriptionIndex MakeSl(bool cache) =>
cache ? SubscriptionIndex.NewSublistWithCache() : SubscriptionIndex.NewSublistNoCache();
private static void VerifyMember(List<Subscription> subs, Subscription expected)
{
subs.ShouldContain(expected, $"Subscription for [{Encoding.ASCII.GetString(expected.Subject)}] not found in results");
}
private static void VerifyQMember(List<List<Subscription>> qsubs, Subscription expected)
{
var idx = SubscriptionIndex.FindQSlot(expected.Queue, qsubs);
idx.ShouldBeGreaterThanOrEqualTo(0);
qsubs[idx].ShouldContain(expected);
}
// -------------------------------------------------------------------------
// Init & Count (T:2962-2964)
// -------------------------------------------------------------------------
[Fact] // T:2962
public void Init_StartsWithZeroCount()
{
var s = SubscriptionIndex.NewSublistWithCache();
s.Count().ShouldBe(0u);
}
[Theory] // T:2963, T:2964
[InlineData(true)]
[InlineData(false)]
public void InsertCount_ReturnsCorrectCount(bool cache)
{
var s = MakeSl(cache);
s.Insert(NewSub("foo"));
s.Insert(NewSub("bar"));
s.Insert(NewSub("foo.bar"));
s.Count().ShouldBe(3u);
}
// -------------------------------------------------------------------------
// Simple Match (T:2965-2968)
// -------------------------------------------------------------------------
[Theory] // T:2965, T:2966
[InlineData(true)]
[InlineData(false)]
public void Simple_SingleTokenMatch(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("foo");
s.Insert(sub);
var r = s.Match("foo");
r.PSubs.Count.ShouldBe(1);
VerifyMember(r.PSubs, sub);
}
[Theory] // T:2967, T:2968
[InlineData(true)]
[InlineData(false)]
public void Simple_MultiTokenMatch(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("foo.bar.baz");
s.Insert(sub);
var r = s.Match("foo.bar.baz");
r.PSubs.Count.ShouldBe(1);
VerifyMember(r.PSubs, sub);
}
// -------------------------------------------------------------------------
// Wildcard Match (T:2969-2974)
// -------------------------------------------------------------------------
[Theory] // T:2969, T:2970
[InlineData(true)]
[InlineData(false)]
public void PartialWildcard_MatchesCorrectly(bool cache)
{
var s = MakeSl(cache);
var lsub = NewSub("a.b.c");
var psub = NewSub("a.*.c");
s.Insert(lsub);
s.Insert(psub);
var r = s.Match("a.b.c");
r.PSubs.Count.ShouldBe(2);
VerifyMember(r.PSubs, lsub);
VerifyMember(r.PSubs, psub);
}
[Theory] // T:2971, T:2972
[InlineData(true)]
[InlineData(false)]
public void PartialWildcardAtEnd_MatchesCorrectly(bool cache)
{
var s = MakeSl(cache);
var lsub = NewSub("a.b.c");
var psub = NewSub("a.b.*");
s.Insert(lsub);
s.Insert(psub);
var r = s.Match("a.b.c");
r.PSubs.Count.ShouldBe(2);
VerifyMember(r.PSubs, lsub);
VerifyMember(r.PSubs, psub);
}
[Theory] // T:2973, T:2974
[InlineData(true)]
[InlineData(false)]
public void FullWildcard_MatchesAll(bool cache)
{
var s = MakeSl(cache);
var lsub = NewSub("a.b.c");
var fsub = NewSub("a.>");
s.Insert(lsub);
s.Insert(fsub);
var r = s.Match("a.b.c");
r.PSubs.Count.ShouldBe(2);
VerifyMember(r.PSubs, lsub);
VerifyMember(r.PSubs, fsub);
r = s.Match("a.>");
r.PSubs.Count.ShouldBe(1);
VerifyMember(r.PSubs, fsub);
}
// -------------------------------------------------------------------------
// Remove (T:2975-2984)
// -------------------------------------------------------------------------
[Theory] // T:2975, T:2976
[InlineData(true)]
[InlineData(false)]
public void Remove_BasicRemoval(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("a.b.c.d");
s.Insert(sub);
s.Count().ShouldBe(1u);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(1);
s.Remove(NewSub("a.b.c")); // wrong sub, no-op
s.Count().ShouldBe(1u);
s.Remove(sub);
s.Count().ShouldBe(0u);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(0);
}
[Theory] // T:2977, T:2978
[InlineData(true)]
[InlineData(false)]
public void RemoveWildcard_CorrectCountsAndMatch(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("a.b.c.d");
var psub = NewSub("a.b.*.d");
var fsub = NewSub("a.b.>");
s.Insert(sub); s.Insert(psub); s.Insert(fsub);
s.Count().ShouldBe(3u);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(3);
s.Remove(sub); s.Count().ShouldBe(2u);
s.Remove(fsub); s.Count().ShouldBe(1u);
s.Remove(psub); s.Count().ShouldBe(0u);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(0);
}
[Theory] // T:2979, T:2980
[InlineData(true)]
[InlineData(false)]
public void RemoveCleanup_LevelsReclaimed(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("a.b.c.d.e.f");
s.NumLevels().ShouldBe(0);
s.Insert(sub);
s.NumLevels().ShouldBe(6);
s.Remove(sub);
s.NumLevels().ShouldBe(0);
}
[Theory] // T:2981, T:2982
[InlineData(true)]
[InlineData(false)]
public void RemoveCleanupWildcards_LevelsReclaimed(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("a.b.*.d.e.>");
s.NumLevels().ShouldBe(0);
s.Insert(sub);
s.NumLevels().ShouldBe(6);
s.Remove(sub);
s.NumLevels().ShouldBe(0);
}
[Theory] // T:2983, T:2984
[InlineData(true)]
[InlineData(false)]
public void RemoveWithLargeSubs_CorrectAfterRemoval(bool cache)
{
var s = MakeSl(cache);
for (int i = 0; i < SubscriptionIndex.PlistMin * 2; i++)
s.Insert(NewSub("foo"));
var r = s.Match("foo");
r.PSubs.Count.ShouldBe(SubscriptionIndex.PlistMin * 2);
s.Remove(r.PSubs[SubscriptionIndex.PlistMin]);
s.Remove(r.PSubs[0]);
s.Remove(r.PSubs[^1]);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(SubscriptionIndex.PlistMin * 2 - 3);
}
// -------------------------------------------------------------------------
// Invalid Subjects (T:2985, T:2986)
// -------------------------------------------------------------------------
[Theory] // T:2985, T:2986
[InlineData(true)]
[InlineData(false)]
public void InvalidSubjects_InsertReturnsError(bool cache)
{
var s = MakeSl(cache);
s.Insert(NewSub(".foo")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Insert(NewSub("foo.")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Insert(NewSub("foo..bar")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Insert(NewSub("foo.bar..baz")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Insert(NewSub("foo.>.bar")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
}
// -------------------------------------------------------------------------
// RemoveBatch (T:2987, T:2988)
// -------------------------------------------------------------------------
[Fact] // T:2987
public void NoCacheRemoveBatch_DoesNotEnableCache()
{
var s = SubscriptionIndex.NewSublistNoCache();
s.Insert(NewSub("foo"));
var sub = NewSub("bar");
s.Insert(sub);
s.RemoveBatch(new[] { sub });
for (int i = 0; i < 10; i++) s.Match("foo");
s.CacheEnabled().ShouldBeFalse();
}
[Fact] // T:2988
public void RemoveBatchWithError_StillRemovesPresent()
{
var s = SubscriptionIndex.NewSublistNoCache();
var sub1 = NewSub("foo");
var sub2 = NewSub("bar");
var sub3 = NewSub("baz");
s.Insert(sub1); s.Insert(sub2); s.Insert(sub3);
var notPresent = NewSub("not.inserted");
var err = s.RemoveBatch(new[] { notPresent, sub1, sub3 });
err.ShouldBe(SubscriptionIndex.ErrNotFound);
s.Count().ShouldBe(1u);
s.Match("bar").PSubs.Count.ShouldBe(1);
VerifyMember(s.Match("bar").PSubs, sub2);
s.Match("foo").PSubs.Count.ShouldBe(0);
s.Match("baz").PSubs.Count.ShouldBe(0);
}
// -------------------------------------------------------------------------
// Cache behavior (T:2989)
// -------------------------------------------------------------------------
[Fact] // T:2989
public void Cache_BasicBehavior()
{
var s = SubscriptionIndex.NewSublistWithCache();
var sub = NewSub("a.b.c.d");
var psub = NewSub("a.b.*.d");
var fsub = NewSub("a.b.>");
s.Insert(sub);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(1);
s.Insert(psub); s.Insert(fsub);
s.Count().ShouldBe(3u);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(3);
s.Remove(sub); s.Count().ShouldBe(2u);
s.Remove(fsub); s.Count().ShouldBe(1u);
s.Remove(psub); s.Count().ShouldBe(0u);
s.CacheCount().ShouldBe(0);
s.Match("a.b.c.d").PSubs.Count.ShouldBe(0);
// Fill cache beyond max.
for (int i = 0; i < 2 * SubscriptionIndex.SlCacheMax; i++)
s.Match($"foo-{i}");
// Cache sweep runs async, wait briefly.
Thread.Sleep(200);
s.CacheCount().ShouldBeLessThanOrEqualTo(SubscriptionIndex.SlCacheMax);
// Test wildcard cache update.
s = SubscriptionIndex.NewSublistWithCache();
s.Insert(NewSub("foo.*"));
s.Insert(NewSub("foo.bar"));
s.Match("foo.baz").PSubs.Count.ShouldBe(1);
s.Match("foo.bar").PSubs.Count.ShouldBe(2);
s.Insert(NewSub("foo.>"));
s.Match("foo.bar").PSubs.Count.ShouldBe(3);
}
// -------------------------------------------------------------------------
// Queue Results (T:2990, T:2991)
// -------------------------------------------------------------------------
[Theory] // T:2990, T:2991
[InlineData(true)]
[InlineData(false)]
public void BasicQueueResults_CorrectGrouping(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("foo");
var sub1 = NewQSub("foo", "bar");
var sub2 = NewQSub("foo", "baz");
s.Insert(sub1);
var r = s.Match("foo");
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(1);
r.QSubs[0].Count.ShouldBe(1);
VerifyQMember(r.QSubs, sub1);
s.Insert(sub2);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(2);
VerifyQMember(r.QSubs, sub1);
VerifyQMember(r.QSubs, sub2);
s.Insert(sub);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(1);
r.QSubs.Count.ShouldBe(2);
var sub3 = NewQSub("foo", "bar");
var sub4 = NewQSub("foo", "baz");
s.Insert(sub3); s.Insert(sub4);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(1);
r.QSubs.Count.ShouldBe(2);
// Verify each group has 2 members.
var barIdx = SubscriptionIndex.FindQSlot(Encoding.ASCII.GetBytes("bar"), r.QSubs);
var bazIdx = SubscriptionIndex.FindQSlot(Encoding.ASCII.GetBytes("baz"), r.QSubs);
r.QSubs[barIdx].Count.ShouldBe(2);
r.QSubs[bazIdx].Count.ShouldBe(2);
// Removal.
s.Remove(sub);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(2);
s.Remove(sub1);
r = s.Match("foo");
barIdx = SubscriptionIndex.FindQSlot(Encoding.ASCII.GetBytes("bar"), r.QSubs);
bazIdx = SubscriptionIndex.FindQSlot(Encoding.ASCII.GetBytes("baz"), r.QSubs);
r.QSubs[barIdx].Count.ShouldBe(1);
r.QSubs[bazIdx].Count.ShouldBe(2);
s.Remove(sub3); // last bar
r = s.Match("foo");
r.QSubs.Count.ShouldBe(1); // only baz remains
s.Remove(sub2); s.Remove(sub4);
r = s.Match("foo");
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(0);
}
// -------------------------------------------------------------------------
// Subject Validation (T:2992-2997)
// -------------------------------------------------------------------------
[Fact] // T:2992
public void ValidLiteralSubjects_CorrectResults()
{
SubscriptionIndex.IsValidLiteralSubject("foo").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject(".foo").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject("foo.").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject("foo..bar").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject("foo.bar.*").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject("foo.bar.>").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject("*").ShouldBeFalse();
SubscriptionIndex.IsValidLiteralSubject(">").ShouldBeFalse();
// Wildcard chars that aren't standalone tokens.
SubscriptionIndex.IsValidLiteralSubject("foo*").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo**").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo.**").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo*bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo.*bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo*.bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("*bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo>").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo>>").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo.>>").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo>bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo.>bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject("foo>.bar").ShouldBeTrue();
SubscriptionIndex.IsValidLiteralSubject(">bar").ShouldBeTrue();
}
[Fact] // T:2993
public void ValidSubjects_CorrectResults()
{
SubscriptionIndex.IsValidSubject(".").ShouldBeFalse();
SubscriptionIndex.IsValidSubject(".foo").ShouldBeFalse();
SubscriptionIndex.IsValidSubject("foo.").ShouldBeFalse();
SubscriptionIndex.IsValidSubject("foo..bar").ShouldBeFalse();
SubscriptionIndex.IsValidSubject(">.bar").ShouldBeFalse();
SubscriptionIndex.IsValidSubject("foo.>.bar").ShouldBeFalse();
SubscriptionIndex.IsValidSubject("foo").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.bar.*").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.bar.>").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("*").ShouldBeTrue();
SubscriptionIndex.IsValidSubject(">").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo*").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo**").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.**").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo*bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.*bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo*.bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("*bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo>").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.>>").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo>bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo.>bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject("foo>.bar").ShouldBeTrue();
SubscriptionIndex.IsValidSubject(">bar").ShouldBeTrue();
}
[Fact] // T:2994
public void MatchLiterals_CorrectResults()
{
SubscriptionIndex.MatchLiteral("foo", "foo").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo", "bar").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("foo", "*").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo", ">").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.bar", ">").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.bar", "foo.>").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.bar", "bar.>").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("stats.test.22", "stats.>").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("stats.test.22", "stats.*.*").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.bar", "foo").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("stats.test.foos", "stats.test.foos").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("stats.test.foos", "stats.test.foo").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("stats.test", "stats.test.*").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("stats.test.foos", "stats.*").ShouldBeFalse();
SubscriptionIndex.MatchLiteral("stats.test.foos", "stats.*.*.foos").ShouldBeFalse();
// Wildcards as non-token literals.
SubscriptionIndex.MatchLiteral("*bar", "*bar").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo*", "foo*").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo*bar", "foo*bar").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.***.bar", "foo.***.bar").ShouldBeTrue();
SubscriptionIndex.MatchLiteral(">bar", ">bar").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo>", "foo>").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo>bar", "foo>bar").ShouldBeTrue();
SubscriptionIndex.MatchLiteral("foo.>>>.bar", "foo.>>>.bar").ShouldBeTrue();
}
[Fact] // T:2995
public void SubjectIsLiteral_CorrectResults()
{
SubscriptionIndex.SubjectIsLiteral("foo").ShouldBeTrue();
SubscriptionIndex.SubjectIsLiteral("foo.bar").ShouldBeTrue();
SubscriptionIndex.SubjectIsLiteral("foo*.bar").ShouldBeTrue();
SubscriptionIndex.SubjectIsLiteral("*").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral(">").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral("foo.*").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral("foo.>").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral("foo.*.>").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral("foo.*.bar").ShouldBeFalse();
SubscriptionIndex.SubjectIsLiteral("foo.bar.>").ShouldBeFalse();
}
[Fact] // T:2997
public void TokenAt_ReturnsCorrectTokens()
{
SubscriptionIndex.TokenAt("foo.bar.baz.*", 0).ShouldBe(string.Empty);
SubscriptionIndex.TokenAt("foo.bar.baz.*", 1).ShouldBe("foo");
SubscriptionIndex.TokenAt("foo.bar.baz.*", 2).ShouldBe("bar");
SubscriptionIndex.TokenAt("foo.bar.baz.*", 3).ShouldBe("baz");
SubscriptionIndex.TokenAt("foo.bar.baz.*", 4).ShouldBe("*");
SubscriptionIndex.TokenAt("foo.bar.baz.*", 5).ShouldBe(string.Empty);
}
// -------------------------------------------------------------------------
// Bad Subject on Remove (T:2998, T:2999)
// -------------------------------------------------------------------------
[Theory] // T:2998, T:2999
[InlineData(true)]
[InlineData(false)]
public void BadSubjectOnRemove_ReturnsError(bool cache)
{
var s = MakeSl(cache);
s.Insert(NewSub("a.b..d")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Remove(NewSub("a.b..d")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.Remove(NewSub("a.>.b")).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
}
// -------------------------------------------------------------------------
// Two-token pub vs single-token sub (T:3000, T:3001)
// -------------------------------------------------------------------------
[Theory] // T:3000, T:3001
[InlineData(true)]
[InlineData(false)]
public void TwoTokenPub_DoesNotMatchSingleTokenSub(bool cache)
{
var s = MakeSl(cache);
var sub = NewSub("foo");
s.Insert(sub);
s.Match("foo").PSubs.Count.ShouldBe(1);
s.Match("foo.bar").PSubs.Count.ShouldBe(0);
}
// -------------------------------------------------------------------------
// Wildcards as literals (T:3002-3005)
// -------------------------------------------------------------------------
[Theory] // T:3002, T:3003
[InlineData(true)]
[InlineData(false)]
public void InsertWithWildcardsAsLiterals_OnlyExactMatch(bool cache)
{
var s = MakeSl(cache);
foreach (var subject in new[] { "foo.*-", "foo.>-" })
{
var sub = NewSub(subject);
s.Insert(sub);
s.Match("foo.bar").PSubs.Count.ShouldBe(0);
s.Match(subject).PSubs.Count.ShouldBe(1);
}
}
[Theory] // T:3004, T:3005
[InlineData(true)]
[InlineData(false)]
public void RemoveWithWildcardsAsLiterals_CorrectRemoval(bool cache)
{
var s = MakeSl(cache);
foreach (var subject in new[] { "foo.*-", "foo.>-" })
{
var sub = NewSub(subject);
s.Insert(sub);
s.Remove(NewSub("foo.bar")); // wrong subject, no-op
s.Count().ShouldBe(1u);
s.Remove(sub);
s.Count().ShouldBe(0u);
}
}
// -------------------------------------------------------------------------
// Race tests (T:3006-3010)
// -------------------------------------------------------------------------
[Theory] // T:3006, T:3007
[InlineData(true)]
[InlineData(false)]
public void RaceOnRemove_NoCorruption(bool cache)
{
var s = MakeSl(cache);
var subs = new Subscription[100];
for (int i = 0; i < subs.Length; i++)
subs[i] = NewQSub("foo", "bar");
for (int iter = 0; iter < 2; iter++)
{
foreach (var sub in subs) s.Insert(sub);
if (iter == 1) s.Match("foo");
var r = s.Match("foo");
var task = Task.Run(() => { foreach (var sub in subs) s.Remove(sub); });
foreach (var qsub in r.QSubs)
for (int i = 0; i < qsub.Count; i++)
Encoding.ASCII.GetString(qsub[i].Queue!).ShouldBe("bar");
task.Wait();
}
}
[Theory] // T:3008, T:3009
[InlineData(true)]
[InlineData(false)]
public void RaceOnInsert_NoCorruption(bool cache)
{
var s = MakeSl(cache);
var subs = new Subscription[100];
for (int i = 0; i < subs.Length; i++)
subs[i] = NewQSub("foo", "bar");
var task = Task.Run(() => { foreach (var sub in subs) s.Insert(sub); });
for (int i = 0; i < 1000; i++)
{
var r = s.Match("foo");
foreach (var qsub in r.QSubs)
foreach (var sub in qsub)
Encoding.ASCII.GetString(sub.Queue!).ShouldBe("bar");
}
task.Wait();
}
[Fact] // T:3010
public void RaceOnMatch_NoCorruption()
{
var s = SubscriptionIndex.NewSublistNoCache();
s.Insert(NewQSub("foo.*", "workers"));
s.Insert(NewQSub("foo.bar", "workers"));
s.Insert(NewSub("foo.*"));
s.Insert(NewSub("foo.bar"));
Exception? error = null;
var tasks = Enumerable.Range(0, 2).Select(_ => Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
var r = s.Match("foo.bar");
foreach (var sub in r.PSubs)
if (!Encoding.ASCII.GetString(sub.Subject).StartsWith("foo."))
Interlocked.CompareExchange(ref error, new Exception($"Wrong subject: {Encoding.ASCII.GetString(sub.Subject)}"), null);
foreach (var qsub in r.QSubs)
foreach (var sub in qsub)
if (Encoding.ASCII.GetString(sub.Queue!) != "workers")
Interlocked.CompareExchange(ref error, new Exception($"Wrong queue: {Encoding.ASCII.GetString(sub.Queue!)}"), null);
}
})).ToArray();
Task.WaitAll(tasks);
error.ShouldBeNull();
}
// -------------------------------------------------------------------------
// Remote Queue Subscriptions (T:3011, T:3012)
// -------------------------------------------------------------------------
[Theory] // T:3011, T:3012
[InlineData(true)]
[InlineData(false)]
public void RemoteQueueSubscriptions_WeightExpansion(bool cache)
{
var s = MakeSl(cache);
var s1 = NewQSub("foo", "bar");
var s2 = NewQSub("foo", "bar");
s.Insert(s1); s.Insert(s2);
var rs1 = NewRemoteQSub("foo", "bar", 10);
var rs2 = NewRemoteQSub("foo", "bar", 10);
s.Insert(rs1); s.Insert(rs2);
s.Count().ShouldBe(4u);
var r = s.Match("foo");
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(1);
r.QSubs[0].Count.ShouldBe(22); // 2 normal + 10 + 10 remote
s.Remove(s1); s.Remove(rs1);
s.Count().ShouldBe(2u);
r = s.Match("foo");
r.QSubs[0].Count.ShouldBe(11); // 1 normal + 10 remote
Interlocked.Exchange(ref rs2.Qw, 1);
s.UpdateRemoteQSub(rs2);
r = s.Match("foo");
r.QSubs[0].Count.ShouldBe(2); // 1 normal + 1 remote
}
// -------------------------------------------------------------------------
// Shared Empty Result (T:3013)
// -------------------------------------------------------------------------
[Fact] // T:3013
public void SharedEmptyResult_SameReference()
{
var s = SubscriptionIndex.NewSublistWithCache();
var r1 = s.Match("foo");
var r2 = s.Match("bar");
r1.PSubs.Count.ShouldBe(0);
r2.QSubs.Count.ShouldBe(0);
ReferenceEquals(r1, r2).ShouldBeTrue();
}
// -------------------------------------------------------------------------
// No-cache Stats (T:3014)
// -------------------------------------------------------------------------
[Fact] // T:3014
public void NoCacheStats_CacheCountIsZero()
{
var s = SubscriptionIndex.NewSublistNoCache();
s.Insert(NewSub("foo")); s.Insert(NewSub("bar")); s.Insert(NewSub("baz")); s.Insert(NewSub("foo.bar.baz"));
s.Match("a.b.c"); s.Match("bar");
s.Stats().NumCache.ShouldBe(0u);
}
// -------------------------------------------------------------------------
// All (T:3015)
// -------------------------------------------------------------------------
[Fact] // T:3015
public void All_CollectsAllSubscriptions()
{
var s = SubscriptionIndex.NewSublistNoCache();
var subs = new[]
{
NewSub("foo.bar.baz"),
NewSub("foo"),
NewSub("baz"),
};
subs[0].Client!.Kind = ClientKind.Leaf;
foreach (var sub in subs) s.Insert(sub);
var output = new List<Subscription>();
s.All(output);
output.Count.ShouldBe(3);
}
// -------------------------------------------------------------------------
// IsSubsetMatch (T:3016)
// -------------------------------------------------------------------------
[Theory] // T:3016
[InlineData("foo.bar", "foo.bar", true)]
[InlineData("foo.*", ">", true)]
[InlineData("foo.*", "*.*", true)]
[InlineData("foo.*", "foo.*", true)]
[InlineData("foo.*", "foo.bar", false)]
[InlineData("foo.>", ">", true)]
[InlineData("foo.>", "*.>", true)]
[InlineData("foo.>", "foo.>", true)]
[InlineData("foo.>", "foo.bar", false)]
[InlineData("foo..bar", "foo.*", false)]
[InlineData("foo.*", "foo..bar", false)]
public void IsSubsetMatch_CorrectResults(string subject, string test, bool expected)
{
SubscriptionIndex.SubjectIsSubsetMatch(subject, test).ShouldBe(expected);
}
// -------------------------------------------------------------------------
// ReverseMatch (T:3018, T:3019)
// -------------------------------------------------------------------------
[Fact] // T:3018
public void ReverseMatch_FindsMatchingSubscriptions()
{
var s = SubscriptionIndex.NewSublistWithCache();
var fooSub = NewSub("foo");
var barSub = NewSub("bar");
var fooBarSub = NewSub("foo.bar");
var fooBazSub = NewSub("foo.baz");
var fooBarBazSub = NewSub("foo.bar.baz");
s.Insert(fooSub); s.Insert(barSub); s.Insert(fooBarSub); s.Insert(fooBazSub); s.Insert(fooBarBazSub);
var r = s.ReverseMatch("foo");
r.PSubs.Count.ShouldBe(1); VerifyMember(r.PSubs, fooSub);
r = s.ReverseMatch("bar");
r.PSubs.Count.ShouldBe(1); VerifyMember(r.PSubs, barSub);
r = s.ReverseMatch("*");
r.PSubs.Count.ShouldBe(2);
VerifyMember(r.PSubs, fooSub); VerifyMember(r.PSubs, barSub);
r = s.ReverseMatch("baz");
r.PSubs.Count.ShouldBe(0);
r = s.ReverseMatch("foo.*");
r.PSubs.Count.ShouldBe(2);
VerifyMember(r.PSubs, fooBarSub); VerifyMember(r.PSubs, fooBazSub);
r = s.ReverseMatch("*.*");
r.PSubs.Count.ShouldBe(2);
r = s.ReverseMatch("*.bar");
r.PSubs.Count.ShouldBe(1); VerifyMember(r.PSubs, fooBarSub);
r = s.ReverseMatch("bar.*");
r.PSubs.Count.ShouldBe(0);
r = s.ReverseMatch("foo.>");
r.PSubs.Count.ShouldBe(3);
r = s.ReverseMatch(">");
r.PSubs.Count.ShouldBe(5);
}
[Fact] // T:3019
public void ReverseMatchWider_MatchesWiderFilter()
{
var s = SubscriptionIndex.NewSublistWithCache();
var sub = NewSub("uplink.*.*.>");
s.Insert(sub);
var r = s.ReverseMatch("uplink.1.*.*.>");
r.PSubs.Count.ShouldBe(1);
VerifyMember(r.PSubs, sub);
r = s.ReverseMatch("uplink.1.2.3.>");
r.PSubs.Count.ShouldBe(1);
VerifyMember(r.PSubs, sub);
}
// -------------------------------------------------------------------------
// Match with empty tokens (T:3020)
// -------------------------------------------------------------------------
[Fact] // T:3020
public void MatchWithEmptyTokens_ReturnsNoResults()
{
var sl = SubscriptionIndex.NewSublistWithCache();
sl.Insert(NewSub(">"));
sl.Insert(NewQSub(">", "queue"));
foreach (var subj in new[] { ".foo", "..foo", "foo..", "foo.", "foo..bar", "foo...bar" })
{
var r = sl.Match(subj);
r.PSubs.Count.ShouldBe(0);
r.QSubs.Count.ShouldBe(0);
}
}
// -------------------------------------------------------------------------
// SubjectsCollide (T:3021)
// -------------------------------------------------------------------------
[Fact] // T:3021
public void SubjectsCollide_CorrectResults()
{
SubscriptionIndex.SubjectsCollide("foo.*", "foo.*.bar.>").ShouldBeFalse();
SubscriptionIndex.SubjectsCollide("foo.*.bar.>", "foo.*").ShouldBeFalse();
SubscriptionIndex.SubjectsCollide("foo.*", "foo.foo").ShouldBeTrue();
SubscriptionIndex.SubjectsCollide("foo.*", "*.foo").ShouldBeTrue();
SubscriptionIndex.SubjectsCollide("foo.bar.>", "*.bar.foo").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// Cache hit rate (T:3022)
// -------------------------------------------------------------------------
[Fact] // T:3022
public void AddCacheHitRate_CorrectAggregation()
{
var sl1 = SubscriptionIndex.NewSublistWithCache();
sl1.Insert(NewSub("foo"));
for (int i = 0; i < 4; i++) sl1.Match("foo");
var stats1 = sl1.Stats();
stats1.CacheHitRate.ShouldBe(0.75);
var sl2 = SubscriptionIndex.NewSublistWithCache();
sl2.Insert(NewSub("bar"));
for (int i = 0; i < 4; i++) sl2.Match("bar");
var stats2 = sl2.Stats();
stats2.CacheHitRate.ShouldBe(0.75);
var ts = new SublistStats();
ts.Add(stats1);
ts.Add(stats2);
ts.CacheHitRate.ShouldBe(0.75);
}
// -------------------------------------------------------------------------
// HasInterest (T:3024)
// -------------------------------------------------------------------------
[Fact] // T:3024
public void HasInterest_CorrectForLiteralsAndWildcards()
{
var sl = SubscriptionIndex.NewSublistWithCache();
var fooSub = NewSub("foo");
sl.Insert(fooSub);
sl.HasInterest("foo").ShouldBeTrue();
sl.HasInterest("bar").ShouldBeFalse();
sl.Remove(fooSub);
sl.HasInterest("foo").ShouldBeFalse();
// Partial wildcard.
var sub = NewSub("foo.*");
sl.Insert(sub);
sl.HasInterest("foo").ShouldBeFalse();
sl.HasInterest("foo.bar").ShouldBeTrue();
sl.HasInterest("foo.bar.baz").ShouldBeFalse();
sl.Remove(sub);
// Full wildcard.
sub = NewSub("foo.>");
sl.Insert(sub);
sl.HasInterest("foo").ShouldBeFalse();
sl.HasInterest("foo.bar").ShouldBeTrue();
sl.HasInterest("foo.bar.baz").ShouldBeTrue();
sl.Remove(sub);
// Queue subs.
var qsub = NewQSub("foo", "bar");
sl.Insert(qsub);
sl.HasInterest("foo").ShouldBeTrue();
sl.HasInterest("foo.bar").ShouldBeFalse();
sl.Remove(qsub);
sl.HasInterest("foo").ShouldBeFalse();
}
// -------------------------------------------------------------------------
// HasInterest Overlapping (T:3025)
// -------------------------------------------------------------------------
[Fact] // T:3025
public void HasInterestOverlapping_BothMatch()
{
var sl = SubscriptionIndex.NewSublistWithCache();
sl.Insert(NewSub("stream.A.child"));
sl.Insert(NewSub("stream.*"));
sl.HasInterest("stream.A.child").ShouldBeTrue();
sl.HasInterest("stream.A").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// NumInterest (T:3026)
// -------------------------------------------------------------------------
[Fact] // T:3026
public void NumInterest_CorrectCounts()
{
var sl = SubscriptionIndex.NewSublistWithCache();
sl.Insert(NewSub("foo"));
var (np, nq) = sl.NumInterest("foo");
np.ShouldBe(1);
nq.ShouldBe(0);
(np, nq) = sl.NumInterest("bar");
np.ShouldBe(0);
nq.ShouldBe(0);
// Add queue sub.
sl.Insert(NewQSub("foo", "q1"));
(np, nq) = sl.NumInterest("foo");
np.ShouldBe(1);
nq.ShouldBe(1);
}
// -------------------------------------------------------------------------
// Notifications (T:3017) — simplified from the large Go test
// -------------------------------------------------------------------------
[Fact] // T:3017
public void RegisterInterestNotification_BasicFlow()
{
var s = SubscriptionIndex.NewSublistWithCache();
var notifications = new List<bool>();
Action<bool> notify = b => notifications.Add(b);
// Wildcards should be rejected.
s.RegisterNotification("foo.*", notify).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
s.RegisterNotification(">", notify).ShouldBe(SubscriptionIndex.ErrInvalidSubject);
// Register for "foo" — no existing interest, should get false.
s.RegisterNotification("foo", notify).ShouldBeNull();
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeFalse();
notifications.Clear();
// Insert interest — should get true.
var sub = NewSub("foo");
s.Insert(sub);
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeTrue();
notifications.Clear();
// Insert a second — no notification (already have interest).
var sub2 = NewSub("foo");
s.Insert(sub2);
notifications.Count.ShouldBe(0);
// Remove all interest — should get false.
s.Remove(sub);
s.Remove(sub2);
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeFalse();
notifications.Clear();
// Clear should return true.
s.ClearNotification("foo", notify).ShouldBeTrue();
// Clear again should return false (already cleared).
s.ClearNotification("foo", notify).ShouldBeFalse();
}
[Fact] // T:3017 (queue notification portion)
public void RegisterQueueNotification_BasicFlow()
{
var s = SubscriptionIndex.NewSublistWithCache();
var notifications = new List<bool>();
Action<bool> notify = b => notifications.Add(b);
s.RegisterQueueNotification("foo.bar", "q1", notify).ShouldBeNull();
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeFalse();
notifications.Clear();
var qsub = NewQSub("foo.bar", "q1");
s.Insert(qsub);
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeTrue();
notifications.Clear();
s.Remove(qsub);
notifications.Count.ShouldBe(1);
notifications[0].ShouldBeFalse();
notifications.Clear();
s.ClearQueueNotification("foo.bar", "q1", notify).ShouldBeTrue();
}
}