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:
@@ -0,0 +1,200 @@
|
||||
// Reference: golang/nats-server/server/filestore.go
|
||||
// Go wire format: filestore.go:6720-6724 (writeMsgRecordLocked)
|
||||
// Go decode: filestore.go:8180-8250 (msgFromBufEx)
|
||||
// Go size calc: filestore.go:8770-8777 (fileStoreMsgSizeRaw)
|
||||
//
|
||||
// These tests verify the .NET MessageRecord binary encoder/decoder that matches
|
||||
// the Go message block record format:
|
||||
// [1:flags][varint:subj_len][N:subject][varint:hdr_len][M:headers][varint:payload_len][P:payload][8:sequence_LE][8:checksum]
|
||||
|
||||
using NATS.Server.JetStream.Storage;
|
||||
using System.Text;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
|
||||
|
||||
public sealed class MessageRecordTests
|
||||
{
|
||||
// Go: writeMsgRecordLocked / msgFromBufEx — basic round-trip
|
||||
[Fact]
|
||||
public void RoundTrip_SimpleMessage()
|
||||
{
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 42,
|
||||
Subject = "foo.bar",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = Encoding.UTF8.GetBytes("hello world"),
|
||||
Timestamp = 1_700_000_000_000_000_000L,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
var decoded = MessageRecord.Decode(encoded);
|
||||
|
||||
decoded.Sequence.ShouldBe(record.Sequence);
|
||||
decoded.Subject.ShouldBe(record.Subject);
|
||||
decoded.Headers.Length.ShouldBe(0);
|
||||
decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("hello world"));
|
||||
decoded.Timestamp.ShouldBe(record.Timestamp);
|
||||
decoded.Deleted.ShouldBe(false);
|
||||
}
|
||||
|
||||
// Go: writeMsgRecordLocked with headers — hdr_len(4) hdr present in record
|
||||
[Fact]
|
||||
public void RoundTrip_WithHeaders()
|
||||
{
|
||||
var headers = "NATS/1.0\r\nX-Test: value\r\n\r\n"u8.ToArray();
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 99,
|
||||
Subject = "test.headers",
|
||||
Headers = headers,
|
||||
Payload = Encoding.UTF8.GetBytes("payload with headers"),
|
||||
Timestamp = 1_700_000_000_000_000_000L,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
var decoded = MessageRecord.Decode(encoded);
|
||||
|
||||
decoded.Sequence.ShouldBe(99UL);
|
||||
decoded.Subject.ShouldBe("test.headers");
|
||||
decoded.Headers.ToArray().ShouldBe(headers);
|
||||
decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("payload with headers"));
|
||||
decoded.Timestamp.ShouldBe(record.Timestamp);
|
||||
}
|
||||
|
||||
// Verify that the last 8 bytes of the encoded record contain a nonzero XxHash64 checksum.
|
||||
[Fact]
|
||||
public void Encode_SetsChecksumInTrailer()
|
||||
{
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 1,
|
||||
Subject = "checksum.test",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = Encoding.UTF8.GetBytes("data"),
|
||||
Timestamp = 0,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
|
||||
// Last 8 bytes are the checksum — should be nonzero for any non-trivial message.
|
||||
var checksumBytes = encoded.AsSpan(encoded.Length - 8);
|
||||
var checksum = BitConverter.ToUInt64(checksumBytes);
|
||||
checksum.ShouldNotBe(0UL);
|
||||
}
|
||||
|
||||
// Flip a byte in the encoded data and verify decode throws InvalidDataException.
|
||||
[Fact]
|
||||
public void Decode_DetectsCorruptChecksum()
|
||||
{
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 7,
|
||||
Subject = "corrupt",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = Encoding.UTF8.GetBytes("will be corrupted"),
|
||||
Timestamp = 0,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
|
||||
// Flip a byte in the payload area (not the checksum itself).
|
||||
var corrupted = encoded.ToArray();
|
||||
corrupted[corrupted.Length / 2] ^= 0xFF;
|
||||
|
||||
Should.Throw<InvalidDataException>(() => MessageRecord.Decode(corrupted));
|
||||
}
|
||||
|
||||
// Go: varint encoding matches protobuf convention — high-bit continuation.
|
||||
[Theory]
|
||||
[InlineData(0UL)]
|
||||
[InlineData(1UL)]
|
||||
[InlineData(127UL)]
|
||||
[InlineData(128UL)]
|
||||
[InlineData(16383UL)]
|
||||
[InlineData(16384UL)]
|
||||
public void Varint_RoundTrip(ulong value)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[10];
|
||||
var written = MessageRecord.WriteVarint(buf, value);
|
||||
written.ShouldBeGreaterThan(0);
|
||||
|
||||
var (decoded, bytesRead) = MessageRecord.ReadVarint(buf);
|
||||
decoded.ShouldBe(value);
|
||||
bytesRead.ShouldBe(written);
|
||||
}
|
||||
|
||||
// Go: ebit (1 << 63) marks deleted/erased messages in the sequence field.
|
||||
[Fact]
|
||||
public void RoundTrip_DeletedFlag()
|
||||
{
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 100,
|
||||
Subject = "deleted.msg",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = ReadOnlyMemory<byte>.Empty,
|
||||
Timestamp = 0,
|
||||
Deleted = true,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
var decoded = MessageRecord.Decode(encoded);
|
||||
|
||||
decoded.Deleted.ShouldBe(true);
|
||||
decoded.Sequence.ShouldBe(100UL);
|
||||
decoded.Subject.ShouldBe("deleted.msg");
|
||||
}
|
||||
|
||||
// Edge case: empty payload should encode and decode cleanly.
|
||||
[Fact]
|
||||
public void RoundTrip_EmptyPayload()
|
||||
{
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 1,
|
||||
Subject = "empty",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = ReadOnlyMemory<byte>.Empty,
|
||||
Timestamp = 0,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
var decoded = MessageRecord.Decode(encoded);
|
||||
|
||||
decoded.Subject.ShouldBe("empty");
|
||||
decoded.Payload.Length.ShouldBe(0);
|
||||
decoded.Headers.Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
// Verify 64KB payload works (large payload stress test).
|
||||
[Fact]
|
||||
public void RoundTrip_LargePayload()
|
||||
{
|
||||
var payload = new byte[64 * 1024];
|
||||
Random.Shared.NextBytes(payload);
|
||||
|
||||
var record = new MessageRecord
|
||||
{
|
||||
Sequence = 999_999,
|
||||
Subject = "large.payload.test",
|
||||
Headers = ReadOnlyMemory<byte>.Empty,
|
||||
Payload = payload,
|
||||
Timestamp = long.MaxValue,
|
||||
Deleted = false,
|
||||
};
|
||||
|
||||
var encoded = MessageRecord.Encode(record);
|
||||
var decoded = MessageRecord.Decode(encoded);
|
||||
|
||||
decoded.Sequence.ShouldBe(999_999UL);
|
||||
decoded.Subject.ShouldBe("large.payload.test");
|
||||
decoded.Payload.ToArray().ShouldBe(payload);
|
||||
decoded.Timestamp.ShouldBe(long.MaxValue);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user