Add comprehensive Go-parity test coverage across 3 subsystems: - FileStore: basic CRUD, limits, purge, recovery, subjects, encryption, compression, MemStore (161 tests, 24 skipped for not-yet-implemented) - RAFT: core types, wire format, election, log replication, snapshots (95 tests) - JetStream Clustering: meta controller, stream/consumer replica groups, concurrency stress tests (90 tests) Total: ~346 new test annotations across 17 files (+7,557 lines) Full suite: 2,606 passing, 0 failures, 27 skipped
307 lines
11 KiB
C#
307 lines
11 KiB
C#
// Reference: golang/nats-server/server/filestore_test.go
|
|
// Tests ported from: TestFileStoreNoFSSWhenNoSubjects,
|
|
// TestFileStoreNoFSSBugAfterRemoveFirst,
|
|
// TestFileStoreNoFSSAfterRecover,
|
|
// TestFileStoreSubjectStateCacheExpiration,
|
|
// TestFileStoreSubjectsTotals,
|
|
// TestFileStoreSubjectCorruption,
|
|
// TestFileStoreFilteredPendingBug,
|
|
// TestFileStoreFilteredFirstMatchingBug,
|
|
// TestFileStoreExpireSubjectMeta,
|
|
// TestFileStoreAllFilteredStateWithDeleted
|
|
|
|
using System.Text;
|
|
using NATS.Server.JetStream.Storage;
|
|
|
|
namespace NATS.Server.Tests.JetStream.Storage;
|
|
|
|
public sealed class FileStoreSubjectTests : IDisposable
|
|
{
|
|
private readonly string _dir;
|
|
|
|
public FileStoreSubjectTests()
|
|
{
|
|
_dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-subject-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_dir);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_dir))
|
|
Directory.Delete(_dir, recursive: true);
|
|
}
|
|
|
|
private FileStore CreateStore(string subdirectory, FileStoreOptions? options = null)
|
|
{
|
|
var dir = Path.Combine(_dir, subdirectory);
|
|
var opts = options ?? new FileStoreOptions();
|
|
opts.Directory = dir;
|
|
return new FileStore(opts);
|
|
}
|
|
|
|
// Go: TestFileStoreNoFSSWhenNoSubjects server/filestore_test.go:4251
|
|
[Fact]
|
|
public async Task Store_with_empty_subject()
|
|
{
|
|
await using var store = CreateStore("empty-subj");
|
|
|
|
// Store messages with empty subject (like raft state).
|
|
for (var i = 0; i < 10; i++)
|
|
await store.AppendAsync(string.Empty, "raft state"u8.ToArray(), default);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)10);
|
|
|
|
// Should be loadable.
|
|
var msg = await store.LoadAsync(1, default);
|
|
msg.ShouldNotBeNull();
|
|
msg!.Subject.ShouldBe(string.Empty);
|
|
}
|
|
|
|
// Go: TestFileStoreNoFSSBugAfterRemoveFirst server/filestore_test.go:4289
|
|
[Fact]
|
|
public async Task Remove_first_with_different_subjects()
|
|
{
|
|
await using var store = CreateStore("rm-first-subj");
|
|
|
|
await store.AppendAsync("foo", "first"u8.ToArray(), default);
|
|
await store.AppendAsync("bar", "second"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "third"u8.ToArray(), default);
|
|
|
|
// Remove first message.
|
|
(await store.RemoveAsync(1, default)).ShouldBeTrue();
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)2);
|
|
state.FirstSeq.ShouldBe((ulong)2);
|
|
|
|
// LoadLastBySubject should still work for "foo".
|
|
var lastFoo = await store.LoadLastBySubjectAsync("foo", default);
|
|
lastFoo.ShouldNotBeNull();
|
|
lastFoo!.Sequence.ShouldBe((ulong)3);
|
|
}
|
|
|
|
// Go: TestFileStoreNoFSSAfterRecover server/filestore_test.go:4333
|
|
[Fact]
|
|
public async Task Subject_filtering_after_recovery()
|
|
{
|
|
var subDir = "subj-after-recover";
|
|
|
|
await using (var store = CreateStore(subDir))
|
|
{
|
|
await store.AppendAsync("foo.1", "a"u8.ToArray(), default);
|
|
await store.AppendAsync("foo.2", "b"u8.ToArray(), default);
|
|
await store.AppendAsync("bar.1", "c"u8.ToArray(), default);
|
|
await store.AppendAsync("foo.1", "d"u8.ToArray(), default);
|
|
}
|
|
|
|
// Recover.
|
|
await using (var store = CreateStore(subDir))
|
|
{
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)4);
|
|
|
|
// LoadLastBySubject should work after recovery.
|
|
var lastFoo1 = await store.LoadLastBySubjectAsync("foo.1", default);
|
|
lastFoo1.ShouldNotBeNull();
|
|
lastFoo1!.Sequence.ShouldBe((ulong)4);
|
|
lastFoo1.Payload.ToArray().ShouldBe("d"u8.ToArray());
|
|
|
|
var lastBar1 = await store.LoadLastBySubjectAsync("bar.1", default);
|
|
lastBar1.ShouldNotBeNull();
|
|
lastBar1!.Sequence.ShouldBe((ulong)3);
|
|
}
|
|
}
|
|
|
|
// Go: TestFileStoreSubjectStateCacheExpiration server/filestore_test.go:4143
|
|
[Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")]
|
|
public async Task Subject_state_cache_expiration()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreSubjectsTotals server/filestore_test.go:4948
|
|
[Fact(Skip = "SubjectsTotals not yet implemented in .NET FileStore")]
|
|
public async Task Subjects_totals_with_wildcards()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreSubjectCorruption server/filestore_test.go:6466
|
|
[Fact(Skip = "SubjectForSeq not yet implemented in .NET FileStore")]
|
|
public async Task Subject_corruption_detection()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreFilteredPendingBug server/filestore_test.go:3414
|
|
[Fact(Skip = "FilteredState not yet implemented in .NET FileStore")]
|
|
public async Task Filtered_pending_no_match_returns_zero()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreFilteredFirstMatchingBug server/filestore_test.go:4448
|
|
[Fact(Skip = "LoadNextMsg not yet implemented in .NET FileStore")]
|
|
public async Task Filtered_first_matching_finds_correct_sequence()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreExpireSubjectMeta server/filestore_test.go:4014
|
|
[Fact(Skip = "SubjectsState not yet implemented in .NET FileStore")]
|
|
public async Task Expired_subject_metadata_cleans_up()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Go: TestFileStoreAllFilteredStateWithDeleted server/filestore_test.go:4827
|
|
[Fact(Skip = "FilteredState not yet implemented in .NET FileStore")]
|
|
public async Task Filtered_state_with_deleted_messages()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Test LoadLastBySubject with multiple subjects and removes.
|
|
[Fact]
|
|
public async Task LoadLastBySubject_after_removes()
|
|
{
|
|
await using var store = CreateStore("last-after-rm");
|
|
|
|
await store.AppendAsync("foo", "a"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "b"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "c"u8.ToArray(), default);
|
|
|
|
// Remove the last message on "foo" (seq 3).
|
|
await store.RemoveAsync(3, default);
|
|
|
|
var last = await store.LoadLastBySubjectAsync("foo", default);
|
|
last.ShouldNotBeNull();
|
|
last!.Sequence.ShouldBe((ulong)2);
|
|
last.Payload.ToArray().ShouldBe("b"u8.ToArray());
|
|
}
|
|
|
|
// Test LoadLastBySubject when all messages on that subject are removed.
|
|
[Fact]
|
|
public async Task LoadLastBySubject_all_removed_returns_null()
|
|
{
|
|
await using var store = CreateStore("last-all-rm");
|
|
|
|
await store.AppendAsync("foo", "a"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "b"u8.ToArray(), default);
|
|
await store.AppendAsync("bar", "c"u8.ToArray(), default);
|
|
|
|
await store.RemoveAsync(1, default);
|
|
await store.RemoveAsync(2, default);
|
|
|
|
var last = await store.LoadLastBySubjectAsync("foo", default);
|
|
last.ShouldBeNull();
|
|
|
|
// "bar" should still be present.
|
|
var lastBar = await store.LoadLastBySubjectAsync("bar", default);
|
|
lastBar.ShouldNotBeNull();
|
|
lastBar!.Sequence.ShouldBe((ulong)3);
|
|
}
|
|
|
|
// Test multiple subjects interleaved.
|
|
[Fact]
|
|
public async Task Multiple_subjects_interleaved()
|
|
{
|
|
await using var store = CreateStore("interleaved");
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var subject = i % 3 == 0 ? "alpha" : (i % 3 == 1 ? "beta" : "gamma");
|
|
await store.AppendAsync(subject, Encoding.UTF8.GetBytes($"msg-{i}"), default);
|
|
}
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)20);
|
|
|
|
// Verify all subjects are loadable and correct.
|
|
for (ulong i = 1; i <= 20; i++)
|
|
{
|
|
var msg = await store.LoadAsync(i, default);
|
|
msg.ShouldNotBeNull();
|
|
var idx = (int)(i - 1);
|
|
var expectedSubj = idx % 3 == 0 ? "alpha" : (idx % 3 == 1 ? "beta" : "gamma");
|
|
msg!.Subject.ShouldBe(expectedSubj);
|
|
}
|
|
}
|
|
|
|
// Test LoadLastBySubject with case-sensitive subjects.
|
|
[Fact]
|
|
public async Task LoadLastBySubject_is_case_sensitive()
|
|
{
|
|
await using var store = CreateStore("case-sensitive");
|
|
|
|
await store.AppendAsync("Foo", "upper"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "lower"u8.ToArray(), default);
|
|
|
|
var lastUpper = await store.LoadLastBySubjectAsync("Foo", default);
|
|
lastUpper.ShouldNotBeNull();
|
|
lastUpper!.Payload.ToArray().ShouldBe("upper"u8.ToArray());
|
|
|
|
var lastLower = await store.LoadLastBySubjectAsync("foo", default);
|
|
lastLower.ShouldNotBeNull();
|
|
lastLower!.Payload.ToArray().ShouldBe("lower"u8.ToArray());
|
|
}
|
|
|
|
// Test subject preservation across restarts.
|
|
[Fact]
|
|
public async Task Subject_preserved_across_restart()
|
|
{
|
|
var subDir = "subj-restart";
|
|
|
|
await using (var store = CreateStore(subDir))
|
|
{
|
|
await store.AppendAsync("topic.a", "one"u8.ToArray(), default);
|
|
await store.AppendAsync("topic.b", "two"u8.ToArray(), default);
|
|
await store.AppendAsync("topic.c", "three"u8.ToArray(), default);
|
|
}
|
|
|
|
await using (var store = CreateStore(subDir))
|
|
{
|
|
var msg1 = await store.LoadAsync(1, default);
|
|
msg1.ShouldNotBeNull();
|
|
msg1!.Subject.ShouldBe("topic.a");
|
|
|
|
var msg2 = await store.LoadAsync(2, default);
|
|
msg2.ShouldNotBeNull();
|
|
msg2!.Subject.ShouldBe("topic.b");
|
|
|
|
var msg3 = await store.LoadAsync(3, default);
|
|
msg3.ShouldNotBeNull();
|
|
msg3!.Subject.ShouldBe("topic.c");
|
|
}
|
|
}
|
|
|
|
// Go: TestFileStoreNumPendingLastBySubject server/filestore_test.go:6501
|
|
[Fact(Skip = "NumPending not yet implemented in .NET FileStore")]
|
|
public async Task NumPending_last_per_subject()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// Test many distinct subjects.
|
|
[Fact]
|
|
public async Task Many_distinct_subjects()
|
|
{
|
|
await using var store = CreateStore("many-subjects");
|
|
|
|
for (var i = 0; i < 100; i++)
|
|
await store.AppendAsync($"kv.{i}", Encoding.UTF8.GetBytes($"value-{i}"), default);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)100);
|
|
|
|
// Each subject should have exactly one message.
|
|
for (var i = 0; i < 100; i++)
|
|
{
|
|
var last = await store.LoadLastBySubjectAsync($"kv.{i}", default);
|
|
last.ShouldNotBeNull();
|
|
last!.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes($"value-{i}"));
|
|
}
|
|
}
|
|
}
|