feat: phase B distributed substrate test parity — 39 new tests across 5 subsystems
FileStore basics (4), MemStore/retention (10), RAFT election/append (16), config reload parity (3), monitoring endpoints varz/connz/healthz (6). 972 total tests passing, 0 failures.
This commit is contained in:
165
tests/NATS.Server.Tests/JetStream/Storage/FileStoreBasicTests.cs
Normal file
165
tests/NATS.Server.Tests/JetStream/Storage/FileStoreBasicTests.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
// Reference: golang/nats-server/server/filestore_test.go
|
||||
// Tests ported: TestFileStoreBasics, TestFileStoreMsgHeaders,
|
||||
// TestFileStoreBasicWriteMsgsAndRestore, TestFileStoreRemove
|
||||
|
||||
using NATS.Server.JetStream.Storage;
|
||||
|
||||
namespace NATS.Server.Tests.JetStream.Storage;
|
||||
|
||||
public sealed class FileStoreBasicTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public FileStoreBasicTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"nats-js-fs-basic-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_dir))
|
||||
Directory.Delete(_dir, recursive: true);
|
||||
}
|
||||
|
||||
private FileStore CreateStore(string? subdirectory = null)
|
||||
{
|
||||
var dir = subdirectory is null ? _dir : Path.Combine(_dir, subdirectory);
|
||||
return new FileStore(new FileStoreOptions { Directory = dir });
|
||||
}
|
||||
|
||||
// Ref: TestFileStoreBasics — stores 5 msgs, checks sequence numbers,
|
||||
// checks State().Msgs, loads msg by sequence and verifies subject/payload.
|
||||
[Fact]
|
||||
public async Task Store_and_load_messages()
|
||||
{
|
||||
await using var store = CreateStore();
|
||||
|
||||
const string subject = "foo";
|
||||
var payload = "Hello World"u8.ToArray();
|
||||
|
||||
for (var i = 1; i <= 5; i++)
|
||||
{
|
||||
var seq = await store.AppendAsync(subject, payload, default);
|
||||
seq.ShouldBe((ulong)i);
|
||||
}
|
||||
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)5);
|
||||
|
||||
var msg2 = await store.LoadAsync(2, default);
|
||||
msg2.ShouldNotBeNull();
|
||||
msg2!.Subject.ShouldBe(subject);
|
||||
msg2.Payload.ToArray().ShouldBe(payload);
|
||||
|
||||
var msg3 = await store.LoadAsync(3, default);
|
||||
msg3.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// Ref: TestFileStoreMsgHeaders — stores a message whose payload carries raw
|
||||
// NATS header bytes, then loads it back and verifies the bytes are intact.
|
||||
//
|
||||
// The .NET FileStore keeps headers as part of the payload bytes (callers
|
||||
// embed the NATS wire header in the payload slice they pass in). We
|
||||
// verify round-trip fidelity for a payload that happens to look like a
|
||||
// NATS header line.
|
||||
[Fact]
|
||||
public async Task Store_message_with_headers()
|
||||
{
|
||||
await using var store = CreateStore();
|
||||
|
||||
// Simulate a NATS header embedded in the payload, e.g. "name:derek\r\n\r\nHello World"
|
||||
var headerBytes = "NATS/1.0\r\nname:derek\r\n\r\n"u8.ToArray();
|
||||
var bodyBytes = "Hello World"u8.ToArray();
|
||||
var fullPayload = headerBytes.Concat(bodyBytes).ToArray();
|
||||
|
||||
await store.AppendAsync("foo", fullPayload, default);
|
||||
|
||||
var msg = await store.LoadAsync(1, default);
|
||||
msg.ShouldNotBeNull();
|
||||
msg!.Payload.ToArray().ShouldBe(fullPayload);
|
||||
}
|
||||
|
||||
// Ref: TestFileStoreBasicWriteMsgsAndRestore — stores 100 msgs, disposes
|
||||
// the store, recreates from the same directory, verifies message count
|
||||
// is preserved, stores 100 more, verifies total of 200.
|
||||
[Fact]
|
||||
public async Task Stop_and_restart_preserves_messages()
|
||||
{
|
||||
const int firstBatch = 100;
|
||||
const int secondBatch = 100;
|
||||
|
||||
await using (var store = CreateStore())
|
||||
{
|
||||
for (var i = 1; i <= firstBatch; i++)
|
||||
{
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!");
|
||||
var seq = await store.AppendAsync("foo", payload, default);
|
||||
seq.ShouldBe((ulong)i);
|
||||
}
|
||||
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)firstBatch);
|
||||
}
|
||||
|
||||
// Reopen the same directory.
|
||||
await using (var store = CreateStore())
|
||||
{
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)firstBatch);
|
||||
|
||||
for (var i = firstBatch + 1; i <= firstBatch + secondBatch; i++)
|
||||
{
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!");
|
||||
var seq = await store.AppendAsync("foo", payload, default);
|
||||
seq.ShouldBe((ulong)i);
|
||||
}
|
||||
|
||||
state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)(firstBatch + secondBatch));
|
||||
}
|
||||
|
||||
// Reopen again to confirm the second batch survived.
|
||||
await using (var store = CreateStore())
|
||||
{
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)(firstBatch + secondBatch));
|
||||
}
|
||||
}
|
||||
|
||||
// Ref: TestFileStoreBasics (remove section) and Go TestFileStoreRemove
|
||||
// pattern — stores 5 msgs, removes first, last, and a middle message,
|
||||
// verifies State().Msgs decrements correctly after each removal.
|
||||
[Fact]
|
||||
public async Task Remove_messages_updates_state()
|
||||
{
|
||||
await using var store = CreateStore();
|
||||
|
||||
const string subject = "foo";
|
||||
var payload = "Hello World"u8.ToArray();
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
await store.AppendAsync(subject, payload, default);
|
||||
|
||||
// Remove first (seq 1) — expect 4 remaining.
|
||||
(await store.RemoveAsync(1, default)).ShouldBeTrue();
|
||||
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)4);
|
||||
|
||||
// Remove last (seq 5) — expect 3 remaining.
|
||||
(await store.RemoveAsync(5, default)).ShouldBeTrue();
|
||||
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)3);
|
||||
|
||||
// Remove a middle message (seq 3) — expect 2 remaining.
|
||||
(await store.RemoveAsync(3, default)).ShouldBeTrue();
|
||||
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)2);
|
||||
|
||||
// Sequences 2 and 4 should still be loadable.
|
||||
(await store.LoadAsync(2, default)).ShouldNotBeNull();
|
||||
(await store.LoadAsync(4, default)).ShouldNotBeNull();
|
||||
|
||||
// Removed sequences must return null.
|
||||
(await store.LoadAsync(1, default)).ShouldBeNull();
|
||||
(await store.LoadAsync(3, default)).ShouldBeNull();
|
||||
(await store.LoadAsync(5, default)).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user