- Fix pull consumer fetch: send original stream subject in HMSG (not inbox) so NATS client distinguishes data messages from control messages - Fix MaxAge expiry: add background timer in StreamManager for periodic pruning - Fix JetStream wire format: Go-compatible anonymous objects with string enums, proper offset-based pagination for stream/consumer list APIs - Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream) - Add ~1000 parity tests across all subsystems (gaps closure) - Update gap inventory docs to reflect implementation status
199 lines
8.3 KiB
C#
199 lines
8.3 KiB
C#
// Go reference: server/events.go:2082-2090 — compressionType / snappyCompression,
|
|
// and events.go:578-598 — internalSendLoop optional compression branch.
|
|
|
|
using System.Text;
|
|
using NATS.Server.Events;
|
|
|
|
namespace NATS.Server.Tests.Events;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="EventCompressor"/> — S2/Snappy compression for system event payloads.
|
|
/// Go reference: server/events.go — compressed system events via snappyCompression.
|
|
/// </summary>
|
|
public class EventCompressionTests : IDisposable
|
|
{
|
|
public EventCompressionTests()
|
|
{
|
|
// Ensure a clean statistics baseline for every test.
|
|
EventCompressor.ResetStats();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
EventCompressor.ResetStats();
|
|
}
|
|
|
|
// ── 1 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Compress_ValidPayload_ReturnsCompressed()
|
|
{
|
|
// Arrange
|
|
var json = """{"server":{"name":"s1","id":"ABCDEF"},"data":{"conns":42,"bytes":1024}}""";
|
|
var payload = Encoding.UTF8.GetBytes(json);
|
|
|
|
// Act
|
|
var compressed = EventCompressor.Compress(payload);
|
|
|
|
// Assert
|
|
compressed.ShouldNotBeNull();
|
|
compressed.Length.ShouldBeGreaterThan(0);
|
|
// Snappy output begins with a varint for the original length — not the same raw bytes.
|
|
compressed.ShouldNotBe(payload);
|
|
}
|
|
|
|
// ── 2 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Decompress_RoundTrip_MatchesOriginal()
|
|
{
|
|
// Arrange
|
|
var original = Encoding.UTF8.GetBytes(
|
|
"""{"server":{"name":"test","id":"XYZ"},"stats":{"cpu":0.5,"mem":1048576}}""");
|
|
|
|
// Act
|
|
var compressed = EventCompressor.Compress(original);
|
|
var decompressed = EventCompressor.Decompress(compressed);
|
|
|
|
// Assert
|
|
decompressed.ShouldBe(original);
|
|
}
|
|
|
|
// ── 3 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void ShouldCompress_BelowThreshold_ReturnsFalse()
|
|
{
|
|
// 100 bytes is well below the default 256-byte threshold.
|
|
EventCompressor.ShouldCompress(100).ShouldBeFalse();
|
|
}
|
|
|
|
// ── 4 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void ShouldCompress_AboveThreshold_ReturnsTrue()
|
|
{
|
|
// 500 bytes exceeds the default 256-byte threshold.
|
|
EventCompressor.ShouldCompress(500).ShouldBeTrue();
|
|
}
|
|
|
|
// ── 5 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void CompressIfBeneficial_SmallPayload_NotCompressed()
|
|
{
|
|
// Arrange — 50 bytes is below the 256-byte threshold.
|
|
var payload = Encoding.UTF8.GetBytes("small");
|
|
|
|
// Act
|
|
var (data, compressed) = EventCompressor.CompressIfBeneficial(payload);
|
|
|
|
// Assert
|
|
compressed.ShouldBeFalse();
|
|
data.ShouldBe(payload);
|
|
}
|
|
|
|
// ── 6 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void CompressIfBeneficial_LargePayload_Compressed()
|
|
{
|
|
// Arrange — build a payload well above the 256-byte threshold.
|
|
var largeJson = """{"server":{"name":"s1"},"data":""" + new string('x', 500) + "}";
|
|
var payload = Encoding.UTF8.GetBytes(largeJson);
|
|
payload.Length.ShouldBeGreaterThan(256);
|
|
|
|
// Act
|
|
var (data, isCompressed) = EventCompressor.CompressIfBeneficial(payload);
|
|
|
|
// Assert
|
|
isCompressed.ShouldBeTrue();
|
|
// The returned bytes should decompress back to the original.
|
|
var restored = EventCompressor.Decompress(data);
|
|
restored.ShouldBe(payload);
|
|
}
|
|
|
|
// ── 7 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void GetCompressionRatio_Calculates()
|
|
{
|
|
// 100 / 200 = 0.5
|
|
var ratio = EventCompressor.GetCompressionRatio(originalSize: 200, compressedSize: 100);
|
|
|
|
ratio.ShouldBe(0.5, tolerance: 0.001);
|
|
}
|
|
|
|
// ── 8 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void TotalCompressed_IncrementedOnCompress()
|
|
{
|
|
// Arrange — stats were reset in constructor.
|
|
EventCompressor.TotalCompressed.ShouldBe(0L);
|
|
|
|
var largePayload = Encoding.UTF8.GetBytes(new string('a', 512));
|
|
|
|
// Act — two calls that exceed threshold.
|
|
EventCompressor.CompressIfBeneficial(largePayload);
|
|
EventCompressor.CompressIfBeneficial(largePayload);
|
|
|
|
// Assert
|
|
EventCompressor.TotalCompressed.ShouldBe(2L);
|
|
}
|
|
|
|
// ── 9 ──────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void BytesSaved_TracksCorrectly()
|
|
{
|
|
// Arrange
|
|
// Use a highly-compressible payload so savings are guaranteed.
|
|
var payload = Encoding.UTF8.GetBytes(new string('z', 1024));
|
|
|
|
// Act
|
|
EventCompressor.CompressIfBeneficial(payload);
|
|
|
|
// Assert — compressed version of 1 024 repeated bytes should be much smaller.
|
|
EventCompressor.BytesSaved.ShouldBeGreaterThan(0L);
|
|
// BytesSaved = original - compressed; should be less than original size.
|
|
EventCompressor.BytesSaved.ShouldBeLessThan(payload.Length);
|
|
}
|
|
|
|
// ── 10 ─────────────────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void ResetStats_ClearsAll()
|
|
{
|
|
// Arrange — produce some stats first.
|
|
var largePayload = Encoding.UTF8.GetBytes(new string('b', 512));
|
|
EventCompressor.CompressIfBeneficial(largePayload);
|
|
EventCompressor.TotalCompressed.ShouldBeGreaterThan(0L);
|
|
|
|
// Act
|
|
EventCompressor.ResetStats();
|
|
|
|
// Assert
|
|
EventCompressor.TotalCompressed.ShouldBe(0L);
|
|
EventCompressor.TotalUncompressed.ShouldBe(0L);
|
|
EventCompressor.BytesSaved.ShouldBe(0L);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetAcceptEncoding_ParsesSnappyAndGzip()
|
|
{
|
|
EventCompressor.GetAcceptEncoding("gzip, snappy").ShouldBe(EventCompressionType.Snappy);
|
|
EventCompressor.GetAcceptEncoding("gzip").ShouldBe(EventCompressionType.Gzip);
|
|
EventCompressor.GetAcceptEncoding("br").ShouldBe(EventCompressionType.Unsupported);
|
|
EventCompressor.GetAcceptEncoding(null).ShouldBe(EventCompressionType.None);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompressionHeaderConstants_MatchGo()
|
|
{
|
|
EventCompressor.AcceptEncodingHeader.ShouldBe("Accept-Encoding");
|
|
EventCompressor.ContentEncodingHeader.ShouldBe("Content-Encoding");
|
|
}
|
|
|
|
[Fact]
|
|
public void CompressAndDecompress_Gzip_RoundTrip_MatchesOriginal()
|
|
{
|
|
var payload = Encoding.UTF8.GetBytes("""{"server":"s1","data":"gzip-payload"}""");
|
|
|
|
var compressed = EventCompressor.Compress(payload, EventCompressionType.Gzip);
|
|
var restored = EventCompressor.Decompress(compressed, EventCompressionType.Gzip);
|
|
|
|
restored.ShouldBe(payload);
|
|
}
|
|
}
|