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,198 @@
// Go: consumer.go (trackPending ~line 5540, processNak, rdq/rdc map,
// addToRedeliverQueue, maxdeliver check)
using NATS.Server.JetStream.Consumers;
namespace NATS.Server.JetStream.Tests.JetStream.Consumers;
public class RedeliveryTrackerTests
{
// -------------------------------------------------------------------------
// Test 1 — Backoff array clamping at last entry for high delivery counts
//
// Go reference: consumer.go — backoff index = min(deliveries-1, len(backoff)-1)
// so that sequences with delivery counts past the array length use the last
// backoff value rather than going out of bounds.
// -------------------------------------------------------------------------
[Fact]
public async Task Schedule_clamps_backoff_at_last_entry_for_high_delivery_count()
{
var tracker = new RedeliveryTracker([1, 5000]);
// delivery 1 → backoff[0] = 1ms
tracker.Schedule(seq: 1, deliveryCount: 1);
await Task.Delay(10);
tracker.GetDue().ShouldContain(1UL);
tracker.Acknowledge(1);
// delivery 3 → index clamps to 1 → backoff[1] = 5000ms
tracker.Schedule(seq: 1, deliveryCount: 3);
tracker.GetDue().ShouldNotContain(1UL);
}
// -------------------------------------------------------------------------
// Test 2 — GetDue returns only entries whose deadline has passed
//
// Go reference: consumer.go — rdq items are eligible for redelivery only
// once their scheduled deadline has elapsed.
// -------------------------------------------------------------------------
[Fact]
public async Task GetDue_returns_only_expired_entries()
{
var tracker = new RedeliveryTracker([1, 5000]);
// 1ms backoff → will expire quickly
tracker.Schedule(seq: 10, deliveryCount: 1);
// 5000ms backoff → will not expire in test window
tracker.Schedule(seq: 20, deliveryCount: 2);
// Neither should be due yet immediately after scheduling
tracker.GetDue().ShouldNotContain(10UL);
await Task.Delay(15);
var due = tracker.GetDue();
due.ShouldContain(10UL);
due.ShouldNotContain(20UL);
}
// -------------------------------------------------------------------------
// Test 3 — Acknowledge removes the sequence from tracking
//
// Go reference: consumer.go — acking a sequence removes it from pending map
// so it is never surfaced by GetDue again.
// -------------------------------------------------------------------------
[Fact]
public async Task Acknowledge_removes_sequence_from_tracking()
{
var tracker = new RedeliveryTracker([1]);
tracker.Schedule(seq: 5, deliveryCount: 1);
await Task.Delay(10);
tracker.GetDue().ShouldContain(5UL);
tracker.Acknowledge(5);
tracker.IsTracking(5).ShouldBeFalse();
tracker.GetDue().ShouldNotContain(5UL);
tracker.TrackedCount.ShouldBe(0);
}
// -------------------------------------------------------------------------
// Test 4 — IsMaxDeliveries returns true when threshold is reached
//
// Go reference: consumer.go — when rdc[sseq] >= MaxDeliver the sequence is
// dropped from redelivery and never surfaced again.
// -------------------------------------------------------------------------
[Fact]
public void IsMaxDeliveries_returns_true_when_delivery_count_meets_threshold()
{
var tracker = new RedeliveryTracker([100]);
tracker.Schedule(seq: 7, deliveryCount: 3);
tracker.IsMaxDeliveries(7, maxDeliver: 3).ShouldBeTrue();
tracker.IsMaxDeliveries(7, maxDeliver: 4).ShouldBeFalse();
tracker.IsMaxDeliveries(7, maxDeliver: 2).ShouldBeTrue();
}
// -------------------------------------------------------------------------
// Test 5 — IsMaxDeliveries returns false when maxDeliver is 0 (unlimited)
//
// Go reference: consumer.go — MaxDeliver <= 0 means unlimited redeliveries.
// -------------------------------------------------------------------------
[Fact]
public void IsMaxDeliveries_returns_false_when_maxDeliver_is_zero()
{
var tracker = new RedeliveryTracker([100]);
tracker.Schedule(seq: 99, deliveryCount: 1000);
tracker.IsMaxDeliveries(99, maxDeliver: 0).ShouldBeFalse();
}
// -------------------------------------------------------------------------
// Test 6 — Empty backoff falls back to ackWait
//
// Go reference: consumer.go — when BackOff is empty the ack-wait duration is
// used as the redelivery delay.
// -------------------------------------------------------------------------
[Fact]
public async Task Schedule_with_empty_backoff_falls_back_to_ackWait()
{
// Empty backoff array → fall back to ackWaitMs
var tracker = new RedeliveryTracker([]);
tracker.Schedule(seq: 1, deliveryCount: 1, ackWaitMs: 1);
await Task.Delay(10);
tracker.GetDue().ShouldContain(1UL);
}
// -------------------------------------------------------------------------
// Test 7 — Empty backoff with large ackWait does not expire prematurely
// -------------------------------------------------------------------------
[Fact]
public void Schedule_with_empty_backoff_and_large_ackWait_does_not_expire()
{
var tracker = new RedeliveryTracker([]);
tracker.Schedule(seq: 2, deliveryCount: 1, ackWaitMs: 5000);
tracker.GetDue().ShouldNotContain(2UL);
}
// -------------------------------------------------------------------------
// Test 8 — Schedule returns the deadline UTC time
//
// Go reference: consumer.go:5540 — trackPending stores the computed deadline.
// -------------------------------------------------------------------------
[Fact]
public void Schedule_returns_deadline_in_the_future()
{
var tracker = new RedeliveryTracker([100]);
var before = DateTime.UtcNow;
var deadline = tracker.Schedule(seq: 3, deliveryCount: 1);
var after = DateTime.UtcNow;
deadline.ShouldBeGreaterThanOrEqualTo(before);
// Deadline should be ahead of scheduling time by at least the backoff value
(deadline - after).TotalMilliseconds.ShouldBeGreaterThan(0);
}
// -------------------------------------------------------------------------
// Test 9 — Multiple sequences tracked independently
// -------------------------------------------------------------------------
[Fact]
public async Task Multiple_sequences_are_tracked_independently()
{
var tracker = new RedeliveryTracker([1, 5000]);
tracker.Schedule(seq: 1, deliveryCount: 1); // 1ms → expires soon
tracker.Schedule(seq: 2, deliveryCount: 2); // 5000ms → won't expire
tracker.TrackedCount.ShouldBe(2);
await Task.Delay(15);
var due = tracker.GetDue();
due.ShouldContain(1UL);
due.ShouldNotContain(2UL);
tracker.Acknowledge(1);
tracker.TrackedCount.ShouldBe(1);
}
// -------------------------------------------------------------------------
// Test 10 — IsMaxDeliveries returns false for untracked sequence
// -------------------------------------------------------------------------
[Fact]
public void IsMaxDeliveries_returns_false_for_untracked_sequence()
{
var tracker = new RedeliveryTracker([100]);
tracker.IsMaxDeliveries(999, maxDeliver: 1).ShouldBeFalse();
}
}