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
358 lines
11 KiB
C#
358 lines
11 KiB
C#
// Reference: golang/nats-server/server/memstore_test.go and filestore_test.go
|
|
// Tests ported from: TestMemStoreBasics, TestMemStorePurge, TestMemStoreMsgHeaders,
|
|
// TestMemStoreTimeStamps, TestMemStoreEraseMsg,
|
|
// TestMemStoreMsgLimit, TestMemStoreBytesLimit,
|
|
// TestMemStoreAgeLimit, plus parity tests matching
|
|
// filestore behavior in MemStore.
|
|
|
|
using System.Text;
|
|
using NATS.Server.JetStream.Storage;
|
|
|
|
namespace NATS.Server.Tests.JetStream.Storage;
|
|
|
|
public sealed class MemStoreTests
|
|
{
|
|
// Go: TestMemStoreBasics server/memstore_test.go
|
|
[Fact]
|
|
public async Task Store_and_load_messages()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
var seq1 = await store.AppendAsync("foo", "Hello World"u8.ToArray(), default);
|
|
var seq2 = await store.AppendAsync("bar", "Second"u8.ToArray(), default);
|
|
|
|
seq1.ShouldBe((ulong)1);
|
|
seq2.ShouldBe((ulong)2);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)2);
|
|
state.FirstSeq.ShouldBe((ulong)1);
|
|
state.LastSeq.ShouldBe((ulong)2);
|
|
|
|
var msg1 = await store.LoadAsync(1, default);
|
|
msg1.ShouldNotBeNull();
|
|
msg1!.Subject.ShouldBe("foo");
|
|
msg1.Payload.ToArray().ShouldBe("Hello World"u8.ToArray());
|
|
|
|
var msg2 = await store.LoadAsync(2, default);
|
|
msg2.ShouldNotBeNull();
|
|
msg2!.Subject.ShouldBe("bar");
|
|
}
|
|
|
|
// Go: TestMemStoreBasics server/memstore_test.go
|
|
[Fact]
|
|
public async Task Load_non_existent_returns_null()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
(await store.LoadAsync(99, default)).ShouldBeNull();
|
|
(await store.LoadAsync(0, default)).ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestMemStoreEraseMsg server/memstore_test.go
|
|
[Fact]
|
|
public async Task Remove_messages()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
|
|
|
|
(await store.RemoveAsync(2, default)).ShouldBeTrue();
|
|
(await store.RemoveAsync(4, default)).ShouldBeTrue();
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)3);
|
|
|
|
(await store.LoadAsync(2, default)).ShouldBeNull();
|
|
(await store.LoadAsync(4, default)).ShouldBeNull();
|
|
(await store.LoadAsync(1, default)).ShouldNotBeNull();
|
|
(await store.LoadAsync(3, default)).ShouldNotBeNull();
|
|
(await store.LoadAsync(5, default)).ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestMemStoreEraseMsg server/memstore_test.go
|
|
[Fact]
|
|
public async Task Remove_non_existent_returns_false()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
(await store.RemoveAsync(99, default)).ShouldBeFalse();
|
|
}
|
|
|
|
// Go: TestMemStorePurge server/memstore_test.go
|
|
[Fact]
|
|
public async Task Purge_clears_all()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)10);
|
|
|
|
await store.PurgeAsync(default);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)0);
|
|
state.Bytes.ShouldBe((ulong)0);
|
|
}
|
|
|
|
// Go: TestMemStorePurge server/memstore_test.go
|
|
[Fact]
|
|
public async Task Purge_empty_store_is_safe()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.PurgeAsync(default);
|
|
|
|
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)0);
|
|
}
|
|
|
|
// Go: TestMemStoreTimeStamps server/memstore_test.go
|
|
[Fact]
|
|
public async Task Timestamps_non_decreasing()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
var messages = await store.ListAsync(default);
|
|
messages.Count.ShouldBe(10);
|
|
|
|
DateTime? prev = null;
|
|
foreach (var msg in messages)
|
|
{
|
|
if (prev.HasValue)
|
|
msg.TimestampUtc.ShouldBeGreaterThanOrEqualTo(prev.Value);
|
|
prev = msg.TimestampUtc;
|
|
}
|
|
}
|
|
|
|
// Go: TestMemStoreMsgHeaders (adapted) server/memstore_test.go
|
|
[Fact]
|
|
public async Task Payload_with_header_bytes_round_trips()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
var headerBytes = "NATS/1.0\r\nName: derek\r\n\r\n"u8.ToArray();
|
|
var bodyBytes = "Hello World"u8.ToArray();
|
|
byte[] combined = [.. headerBytes, .. bodyBytes];
|
|
|
|
await store.AppendAsync("foo", combined, default);
|
|
|
|
var msg = await store.LoadAsync(1, default);
|
|
msg.ShouldNotBeNull();
|
|
msg!.Payload.ToArray().ShouldBe(combined);
|
|
}
|
|
|
|
// Go: TestMemStoreBasics server/memstore_test.go
|
|
[Fact]
|
|
public async Task LoadLastBySubject_returns_most_recent()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.AppendAsync("foo", "first"u8.ToArray(), default);
|
|
await store.AppendAsync("bar", "other"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "second"u8.ToArray(), default);
|
|
await store.AppendAsync("foo", "third"u8.ToArray(), default);
|
|
|
|
var last = await store.LoadLastBySubjectAsync("foo", default);
|
|
last.ShouldNotBeNull();
|
|
last!.Payload.ToArray().ShouldBe("third"u8.ToArray());
|
|
last.Sequence.ShouldBe((ulong)4);
|
|
|
|
(await store.LoadLastBySubjectAsync("does.not.exist", default)).ShouldBeNull();
|
|
}
|
|
|
|
// Go: TestMemStoreMsgLimit server/memstore_test.go
|
|
[Fact]
|
|
public async Task TrimToMaxMessages_evicts_oldest()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
|
|
|
|
store.TrimToMaxMessages(10);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)10);
|
|
state.FirstSeq.ShouldBe((ulong)11);
|
|
state.LastSeq.ShouldBe((ulong)20);
|
|
|
|
(await store.LoadAsync(1, default)).ShouldBeNull();
|
|
(await store.LoadAsync(10, default)).ShouldBeNull();
|
|
(await store.LoadAsync(11, default)).ShouldNotBeNull();
|
|
}
|
|
|
|
// Go: TestMemStoreMsgLimit server/memstore_test.go
|
|
[Fact]
|
|
public async Task TrimToMaxMessages_to_zero()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
store.TrimToMaxMessages(0);
|
|
|
|
(await store.GetStateAsync(default)).Messages.ShouldBe((ulong)0);
|
|
}
|
|
|
|
// Go: TestMemStoreBytesLimit server/memstore_test.go
|
|
[Fact]
|
|
public async Task Bytes_tracks_payload_sizes()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
var payload = new byte[100];
|
|
for (var i = 0; i < 5; i++)
|
|
await store.AppendAsync("foo", payload, default);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Bytes.ShouldBe((ulong)500);
|
|
}
|
|
|
|
// Go: TestMemStoreBytesLimit server/memstore_test.go
|
|
[Fact]
|
|
public async Task Bytes_decrease_after_remove()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
var payload = new byte[100];
|
|
for (var i = 0; i < 5; i++)
|
|
await store.AppendAsync("foo", payload, default);
|
|
|
|
await store.RemoveAsync(1, default);
|
|
await store.RemoveAsync(3, default);
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Bytes.ShouldBe((ulong)300);
|
|
}
|
|
|
|
// Snapshot and restore.
|
|
[Fact]
|
|
public async Task Snapshot_and_restore()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
|
|
|
|
var snap = await store.CreateSnapshotAsync(default);
|
|
snap.Length.ShouldBeGreaterThan(0);
|
|
|
|
var restored = new MemStore();
|
|
await restored.RestoreSnapshotAsync(snap, default);
|
|
|
|
var srcState = await store.GetStateAsync(default);
|
|
var dstState = await restored.GetStateAsync(default);
|
|
dstState.Messages.ShouldBe(srcState.Messages);
|
|
dstState.FirstSeq.ShouldBe(srcState.FirstSeq);
|
|
dstState.LastSeq.ShouldBe(srcState.LastSeq);
|
|
|
|
for (ulong i = 1; i <= srcState.Messages; i++)
|
|
{
|
|
var original = await store.LoadAsync(i, default);
|
|
var copy = await restored.LoadAsync(i, default);
|
|
copy.ShouldNotBeNull();
|
|
copy!.Payload.ToArray().ShouldBe(original!.Payload.ToArray());
|
|
}
|
|
}
|
|
|
|
// Snapshot after removes.
|
|
[Fact]
|
|
public async Task Snapshot_after_removes()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
await store.AppendAsync("foo", Encoding.UTF8.GetBytes($"msg-{i}"), default);
|
|
|
|
await store.RemoveAsync(2, default);
|
|
await store.RemoveAsync(5, default);
|
|
await store.RemoveAsync(8, default);
|
|
|
|
var snap = await store.CreateSnapshotAsync(default);
|
|
|
|
var restored = new MemStore();
|
|
await restored.RestoreSnapshotAsync(snap, default);
|
|
|
|
var dstState = await restored.GetStateAsync(default);
|
|
dstState.Messages.ShouldBe((ulong)7);
|
|
|
|
(await restored.LoadAsync(2, default)).ShouldBeNull();
|
|
(await restored.LoadAsync(5, default)).ShouldBeNull();
|
|
(await restored.LoadAsync(8, default)).ShouldBeNull();
|
|
(await restored.LoadAsync(1, default)).ShouldNotBeNull();
|
|
}
|
|
|
|
// ListAsync ordered.
|
|
[Fact]
|
|
public async Task ListAsync_returns_ordered()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.AppendAsync("c", "three"u8.ToArray(), default);
|
|
await store.AppendAsync("a", "one"u8.ToArray(), default);
|
|
await store.AppendAsync("b", "two"u8.ToArray(), default);
|
|
|
|
var messages = await store.ListAsync(default);
|
|
messages.Count.ShouldBe(3);
|
|
messages[0].Sequence.ShouldBe((ulong)1);
|
|
messages[1].Sequence.ShouldBe((ulong)2);
|
|
messages[2].Sequence.ShouldBe((ulong)3);
|
|
}
|
|
|
|
// Purge then append.
|
|
[Fact]
|
|
public async Task Purge_then_append()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await store.AppendAsync("foo", "data"u8.ToArray(), default);
|
|
|
|
await store.PurgeAsync(default);
|
|
|
|
var seq = await store.AppendAsync("foo", "after purge"u8.ToArray(), default);
|
|
seq.ShouldBeGreaterThan((ulong)0);
|
|
|
|
var msg = await store.LoadAsync(seq, default);
|
|
msg.ShouldNotBeNull();
|
|
msg!.Payload.ToArray().ShouldBe("after purge"u8.ToArray());
|
|
}
|
|
|
|
// Empty payload.
|
|
[Fact]
|
|
public async Task Empty_payload_round_trips()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
await store.AppendAsync("foo", ReadOnlyMemory<byte>.Empty, default);
|
|
|
|
var msg = await store.LoadAsync(1, default);
|
|
msg.ShouldNotBeNull();
|
|
msg!.Payload.Length.ShouldBe(0);
|
|
}
|
|
|
|
// State on empty store.
|
|
[Fact]
|
|
public async Task Empty_store_state()
|
|
{
|
|
var store = new MemStore();
|
|
|
|
var state = await store.GetStateAsync(default);
|
|
state.Messages.ShouldBe((ulong)0);
|
|
state.Bytes.ShouldBe((ulong)0);
|
|
state.FirstSeq.ShouldBe((ulong)0);
|
|
state.LastSeq.ShouldBe((ulong)0);
|
|
}
|
|
}
|