refactor: extract NATS.Server.JetStream.Tests project

Move 225 JetStream-related test files from NATS.Server.Tests into a
dedicated NATS.Server.JetStream.Tests project. This includes root-level
JetStream*.cs files, storage test files (FileStore, MemStore,
StreamStoreContract), and the full JetStream/ subfolder tree (Api,
Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams).

Updated all namespaces, added InternalsVisibleTo, registered in the
solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
Joseph Doherty
2026-03-12 15:58:10 -04:00
parent 36b9dfa654
commit 78b4bc2486
228 changed files with 253 additions and 227 deletions

View File

@@ -0,0 +1,196 @@
// Go: consumer.go — Pull consumer timeout enforcement and compiled filter tests.
// ExpiresMs support per consumer.go pull request handling.
// CompiledFilter optimizes multi-subject filter matching for consumers.
using System.Text;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Consumers;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
namespace NATS.Server.JetStream.Tests.JetStream.Consumers;
public class PullConsumerTimeoutTests
{
private static StreamHandle MakeStream(MemStore store)
=> new(new StreamConfig { Name = "TEST", Subjects = ["test.>"] }, store);
private static ConsumerHandle MakeConsumer(ConsumerConfig? config = null)
=> new("TEST", config ?? new ConsumerConfig { DurableName = "C1" });
// -------------------------------------------------------------------------
// Test 1 — ExpiresMs returns partial batch when timeout fires
//
// Go reference: consumer.go — pull fetch with expires returns whatever
// messages are available when the timeout fires, even if batch is not full.
// -------------------------------------------------------------------------
[Fact]
public async Task FetchAsync_ExpiresMs_ReturnsPartialBatch()
{
var store = new MemStore();
var stream = MakeStream(store);
// Store only 2 messages, but request a batch of 10
await store.AppendAsync("test.a", Encoding.UTF8.GetBytes("msg1"), CancellationToken.None);
await store.AppendAsync("test.b", Encoding.UTF8.GetBytes("msg2"), CancellationToken.None);
var consumer = MakeConsumer();
var engine = new PullConsumerEngine();
var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
{
Batch = 10,
ExpiresMs = 100,
}, CancellationToken.None);
// Should get the 2 available messages (partial batch)
result.Messages.Count.ShouldBe(2);
result.Messages[0].Subject.ShouldBe("test.a");
result.Messages[1].Subject.ShouldBe("test.b");
}
// -------------------------------------------------------------------------
// Test 2 — ExpiresMs sets TimedOut = true on partial result
//
// Go reference: consumer.go — when a pull request expires and the batch
// is not fully filled, the response indicates a timeout occurred.
// -------------------------------------------------------------------------
[Fact]
public async Task FetchAsync_ExpiresMs_ReturnsTimedOutTrue()
{
var store = new MemStore();
var stream = MakeStream(store);
// Store no messages — the fetch should time out with empty results
var consumer = MakeConsumer();
var engine = new PullConsumerEngine();
var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
{
Batch = 5,
ExpiresMs = 50,
}, CancellationToken.None);
result.TimedOut.ShouldBeTrue();
result.Messages.Count.ShouldBe(0);
}
// -------------------------------------------------------------------------
// Test 3 — No ExpiresMs waits for full batch (returns what's available)
//
// Go reference: consumer.go — without expires, the fetch returns available
// messages up to batch size without a timeout constraint.
// -------------------------------------------------------------------------
[Fact]
public async Task FetchAsync_NoExpires_WaitsForFullBatch()
{
var store = new MemStore();
var stream = MakeStream(store);
await store.AppendAsync("test.a", Encoding.UTF8.GetBytes("msg1"), CancellationToken.None);
await store.AppendAsync("test.b", Encoding.UTF8.GetBytes("msg2"), CancellationToken.None);
await store.AppendAsync("test.c", Encoding.UTF8.GetBytes("msg3"), CancellationToken.None);
var consumer = MakeConsumer();
var engine = new PullConsumerEngine();
var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
{
Batch = 3,
ExpiresMs = 0, // No timeout
}, CancellationToken.None);
result.Messages.Count.ShouldBe(3);
result.TimedOut.ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Test 4 — CompiledFilter with no filters matches everything
//
// Go reference: consumer.go — a consumer with no filter subjects receives
// all messages from the stream.
// -------------------------------------------------------------------------
[Fact]
public void CompiledFilter_NoFilters_MatchesEverything()
{
var filter = new CompiledFilter([]);
filter.Matches("test.a").ShouldBeTrue();
filter.Matches("foo.bar.baz").ShouldBeTrue();
filter.Matches("anything").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// Test 5 — CompiledFilter with single exact filter matches only that subject
//
// Go reference: consumer.go — single filter_subject matches via MatchLiteral.
// -------------------------------------------------------------------------
[Fact]
public void CompiledFilter_SingleFilter_MatchesExact()
{
var filter = new CompiledFilter(["test.specific"]);
filter.Matches("test.specific").ShouldBeTrue();
filter.Matches("test.other").ShouldBeFalse();
filter.Matches("test").ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Test 6 — CompiledFilter with single wildcard filter
//
// Go reference: consumer.go — wildcard filter_subject uses MatchLiteral
// which supports * (single token) and > (multi-token) wildcards.
// -------------------------------------------------------------------------
[Fact]
public void CompiledFilter_SingleWildcard_MatchesPattern()
{
var starFilter = new CompiledFilter(["test.*"]);
starFilter.Matches("test.a").ShouldBeTrue();
starFilter.Matches("test.b").ShouldBeTrue();
starFilter.Matches("test.a.b").ShouldBeFalse();
starFilter.Matches("other.a").ShouldBeFalse();
var fwcFilter = new CompiledFilter(["test.>"]);
fwcFilter.Matches("test.a").ShouldBeTrue();
fwcFilter.Matches("test.a.b").ShouldBeTrue();
fwcFilter.Matches("test.a.b.c").ShouldBeTrue();
fwcFilter.Matches("other.a").ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Test 7 — CompiledFilter with multiple filters matches any
//
// Go reference: consumer.go — filter_subjects (plural) matches if ANY of
// the patterns match. Uses HashSet for exact subjects + MatchLiteral for
// wildcard patterns.
// -------------------------------------------------------------------------
[Fact]
public void CompiledFilter_MultipleFilters_MatchesAny()
{
var filter = new CompiledFilter(["orders.us", "orders.eu", "events.>"]);
// Exact matches
filter.Matches("orders.us").ShouldBeTrue();
filter.Matches("orders.eu").ShouldBeTrue();
// Wildcard match
filter.Matches("events.created").ShouldBeTrue();
filter.Matches("events.updated.v2").ShouldBeTrue();
}
// -------------------------------------------------------------------------
// Test 8 — CompiledFilter with multiple filters rejects non-matching
//
// Go reference: consumer.go — subjects that match none of the filter
// patterns are excluded from delivery.
// -------------------------------------------------------------------------
[Fact]
public void CompiledFilter_MultipleFilters_RejectsNonMatching()
{
var filter = new CompiledFilter(["orders.us", "orders.eu", "events.>"]);
filter.Matches("orders.jp").ShouldBeFalse();
filter.Matches("billing.us").ShouldBeFalse();
filter.Matches("events").ShouldBeFalse(); // ">" requires at least one token after
filter.Matches("random.subject").ShouldBeFalse();
}
}