Files
handler-base/natsutil/natsutil_test.go
Billy D. 35912d5844 feat: add e2e tests, perf benchmarks, and infrastructure improvements
- messages/bench_test.go: serialization benchmarks (msgpack map vs struct vs protobuf)
- clients/clients_test.go: HTTP client tests with pooling verification (20 tests)
- natsutil/natsutil_test.go: encode/decode roundtrip + binary data tests
- handler/handler_test.go: handler dispatch tests + benchmark
- config/config.go: live reload via fsnotify + RWMutex getter methods
- clients/clients.go: SharedTransport + sync.Pool buffer pooling
- messages/messages.go: typed structs with msgpack+json tags
- messages/proto/: protobuf schema + generated code

Benchmark baseline (ChatRequest roundtrip):
  MsgpackMap:    2949 ns/op, 36 allocs
  MsgpackStruct: 2030 ns/op, 13 allocs (31% faster, 64% fewer allocs)
  Protobuf:       793 ns/op,  8 allocs (73% faster, 78% fewer allocs)
2026-02-20 06:44:37 -05:00

257 lines
7.3 KiB
Go

package natsutil
import (
"testing"
"github.com/vmihailenco/msgpack/v5"
)
// ────────────────────────────────────────────────────────────────────────────
// DecodeMsgpackMap tests
// ────────────────────────────────────────────────────────────────────────────
func TestDecodeMsgpackMap_Roundtrip(t *testing.T) {
orig := map[string]any{
"request_id": "req-001",
"user_id": "user-42",
"premium": true,
"top_k": int64(10), // msgpack decodes ints as int64
}
data, err := msgpack.Marshal(orig)
if err != nil {
t.Fatal(err)
}
decoded, err := DecodeMsgpackMap(data)
if err != nil {
t.Fatal(err)
}
if decoded["request_id"] != "req-001" {
t.Errorf("request_id = %v", decoded["request_id"])
}
if decoded["premium"] != true {
t.Errorf("premium = %v", decoded["premium"])
}
}
func TestDecodeMsgpackMap_Empty(t *testing.T) {
data, _ := msgpack.Marshal(map[string]any{})
m, err := DecodeMsgpackMap(data)
if err != nil {
t.Fatal(err)
}
if len(m) != 0 {
t.Errorf("expected empty map, got %v", m)
}
}
func TestDecodeMsgpackMap_InvalidData(t *testing.T) {
_, err := DecodeMsgpackMap([]byte{0xFF, 0xFE})
if err == nil {
t.Error("expected error for invalid msgpack data")
}
}
// ────────────────────────────────────────────────────────────────────────────
// DecodeMsgpack (typed struct) tests
// ────────────────────────────────────────────────────────────────────────────
type testMessage struct {
RequestID string `msgpack:"request_id"`
UserID string `msgpack:"user_id"`
Count int `msgpack:"count"`
Active bool `msgpack:"active"`
}
func TestDecodeMsgpackTyped_Roundtrip(t *testing.T) {
orig := testMessage{
RequestID: "req-typed-001",
UserID: "user-7",
Count: 42,
Active: true,
}
data, err := msgpack.Marshal(orig)
if err != nil {
t.Fatal(err)
}
// Simulate nats.Msg data decoding.
var decoded testMessage
if err := msgpack.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if decoded.RequestID != orig.RequestID {
t.Errorf("RequestID = %q, want %q", decoded.RequestID, orig.RequestID)
}
if decoded.Count != orig.Count {
t.Errorf("Count = %d, want %d", decoded.Count, orig.Count)
}
if decoded.Active != orig.Active {
t.Errorf("Active = %v, want %v", decoded.Active, orig.Active)
}
}
// TestTypedStructDecodesMapEncoding verifies that a typed struct can be
// decoded from data that was encoded as map[string]any (backwards compat).
func TestTypedStructDecodesMapEncoding(t *testing.T) {
// Encode as map (the old way).
mapData := map[string]any{
"request_id": "req-compat",
"user_id": "user-compat",
"count": int64(99),
"active": false,
}
data, err := msgpack.Marshal(mapData)
if err != nil {
t.Fatal(err)
}
// Decode into typed struct (the new way).
var msg testMessage
if err := msgpack.Unmarshal(data, &msg); err != nil {
t.Fatal(err)
}
if msg.RequestID != "req-compat" {
t.Errorf("RequestID = %q", msg.RequestID)
}
if msg.Count != 99 {
t.Errorf("Count = %d, want 99", msg.Count)
}
}
// ────────────────────────────────────────────────────────────────────────────
// Binary data tests (audio []byte in msgpack)
// ────────────────────────────────────────────────────────────────────────────
type audioMessage struct {
SessionID string `msgpack:"session_id"`
Audio []byte `msgpack:"audio"`
SampleRate int `msgpack:"sample_rate"`
}
func TestBinaryDataRoundtrip(t *testing.T) {
audio := make([]byte, 32768)
for i := range audio {
audio[i] = byte(i % 256)
}
orig := audioMessage{
SessionID: "sess-audio-001",
Audio: audio,
SampleRate: 24000,
}
data, err := msgpack.Marshal(orig)
if err != nil {
t.Fatal(err)
}
var decoded audioMessage
if err := msgpack.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if len(decoded.Audio) != len(orig.Audio) {
t.Fatalf("audio len = %d, want %d", len(decoded.Audio), len(orig.Audio))
}
for i := range decoded.Audio {
if decoded.Audio[i] != orig.Audio[i] {
t.Fatalf("audio[%d] = %d, want %d", i, decoded.Audio[i], orig.Audio[i])
}
}
}
// TestBinaryVsBase64Size shows the wire-size win of raw bytes vs base64 string.
func TestBinaryVsBase64Size(t *testing.T) {
audio := make([]byte, 16384)
// Old approach: base64 string in map.
import_b64 := make([]byte, (len(audio)*4+2)/3) // approximate base64 size
mapMsg := map[string]any{
"session_id": "sess-1",
"audio_b64": string(import_b64),
}
mapData, _ := msgpack.Marshal(mapMsg)
// New approach: raw bytes in struct.
structMsg := audioMessage{
SessionID: "sess-1",
Audio: audio,
}
structData, _ := msgpack.Marshal(structMsg)
t.Logf("base64-in-map: %d bytes, raw-bytes-in-struct: %d bytes (%.0f%% smaller)",
len(mapData), len(structData),
100*(1-float64(len(structData))/float64(len(mapData))))
}
// ────────────────────────────────────────────────────────────────────────────
// Benchmarks
// ────────────────────────────────────────────────────────────────────────────
func BenchmarkEncodeMap(b *testing.B) {
data := map[string]any{
"request_id": "req-bench",
"user_id": "user-bench",
"message": "What is the weather today?",
"premium": true,
"top_k": 10,
}
for b.Loop() {
msgpack.Marshal(data)
}
}
func BenchmarkEncodeStruct(b *testing.B) {
data := testMessage{
RequestID: "req-bench",
UserID: "user-bench",
Count: 10,
Active: true,
}
for b.Loop() {
msgpack.Marshal(data)
}
}
func BenchmarkDecodeMap(b *testing.B) {
raw, _ := msgpack.Marshal(map[string]any{
"request_id": "req-bench",
"user_id": "user-bench",
"message": "What is the weather today?",
"premium": true,
"top_k": 10,
})
for b.Loop() {
var m map[string]any
msgpack.Unmarshal(raw, &m)
}
}
func BenchmarkDecodeStruct(b *testing.B) {
raw, _ := msgpack.Marshal(testMessage{
RequestID: "req-bench",
UserID: "user-bench",
Count: 10,
Active: true,
})
for b.Loop() {
var m testMessage
msgpack.Unmarshal(raw, &m)
}
}
func BenchmarkDecodeAudio32KB(b *testing.B) {
raw, _ := msgpack.Marshal(audioMessage{
SessionID: "s1",
Audio: make([]byte, 32768),
SampleRate: 24000,
})
for b.Loop() {
var m audioMessage
msgpack.Unmarshal(raw, &m)
}
}