- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify - config/config.go with WalConfig, Validate() with checked arithmetic - errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.) - wal/constants.go with all WAL format constants and enums - wal/header.go with WAL File Header encode/decode (CRC32 IEEE) - wal/record.go with Physical Record codec, block boundary, SplitIntoRecords - wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind) - wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe) - manifest/manifest.go with MANIFEST stub (Load/Save atomic) - manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent) - Comprehensive tests for all modules - .golangci.yml configuration
144 lines
3.9 KiB
Go
144 lines
3.9 KiB
Go
package wal
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestRecordRoundtrip(t *testing.T) {
|
|
payload := []byte("hello world")
|
|
encoded := EncodePhysicalRecord(RecFull, payload)
|
|
|
|
rec, consumed, err := DecodePhysicalRecord(encoded)
|
|
if err != nil {
|
|
t.Fatalf("DecodePhysicalRecord failed: %v", err)
|
|
}
|
|
|
|
if rec.Type != RecFull {
|
|
t.Errorf("expected type RecFull(%d), got %d", RecFull, rec.Type)
|
|
}
|
|
if string(rec.Payload) != "hello world" {
|
|
t.Errorf("expected payload 'hello world', got %q", string(rec.Payload))
|
|
}
|
|
if consumed != 7+len(payload) {
|
|
t.Errorf("expected consumed %d, got %d", 7+len(payload), consumed)
|
|
}
|
|
}
|
|
|
|
func TestSplitSmallPayload(t *testing.T) {
|
|
payload := make([]byte, 100)
|
|
for i := range payload {
|
|
payload[i] = byte(i)
|
|
}
|
|
|
|
records := SplitIntoRecords(payload)
|
|
if len(records) != 1 {
|
|
t.Fatalf("expected 1 record, got %d", len(records))
|
|
}
|
|
|
|
rec, _, err := DecodePhysicalRecord(records[0])
|
|
if err != nil {
|
|
t.Fatalf("DecodePhysicalRecord failed: %v", err)
|
|
}
|
|
if rec.Type != RecFull {
|
|
t.Errorf("expected RecFull, got %d", rec.Type)
|
|
}
|
|
if !bytes.Equal(rec.Payload, payload) {
|
|
t.Error("payload mismatch")
|
|
}
|
|
}
|
|
|
|
func TestSplitIntoRecords(t *testing.T) {
|
|
// 40 KB payload → needs to split across blocks.
|
|
payload := make([]byte, 40*1024)
|
|
for i := range payload {
|
|
payload[i] = byte(i % 256)
|
|
}
|
|
|
|
records := SplitIntoRecords(payload)
|
|
if len(records) < 2 {
|
|
t.Fatalf("expected at least 2 records, got %d", len(records))
|
|
}
|
|
|
|
// Verify fragment sequence.
|
|
types := make([]uint8, len(records))
|
|
var concatenated []byte
|
|
for i, enc := range records {
|
|
rec, _, err := DecodePhysicalRecord(enc)
|
|
if err != nil {
|
|
t.Fatalf("DecodePhysicalRecord record %d failed: %v", i, err)
|
|
}
|
|
types[i] = rec.Type
|
|
concatenated = append(concatenated, rec.Payload...)
|
|
}
|
|
|
|
// First record must be RecFirst.
|
|
if types[0] != RecFirst {
|
|
t.Errorf("first record type: expected RecFirst(%d), got %d", RecFirst, types[0])
|
|
}
|
|
// Last record must be RecLast.
|
|
if types[len(types)-1] != RecLast {
|
|
t.Errorf("last record type: expected RecLast(%d), got %d", RecLast, types[len(types)-1])
|
|
}
|
|
// Middle records must be RecMiddle.
|
|
for i := 1; i < len(types)-1; i++ {
|
|
if types[i] != RecMiddle {
|
|
t.Errorf("record %d type: expected RecMiddle(%d), got %d", i, RecMiddle, types[i])
|
|
}
|
|
}
|
|
|
|
// Concatenated payloads must equal original.
|
|
if !bytes.Equal(concatenated, payload) {
|
|
t.Error("concatenated payloads do not match original")
|
|
}
|
|
}
|
|
|
|
func TestBlockPadding(t *testing.T) {
|
|
// blockOffset = WalBlockSize - 5 → remaining = 5, which is <= 7 → padding needed = 5.
|
|
blockOffset := uint32(WalBlockSize - 5)
|
|
padding := PaddingNeeded(blockOffset)
|
|
if padding != 5 {
|
|
t.Errorf("PaddingNeeded(%d): expected 5, got %d", blockOffset, padding)
|
|
}
|
|
|
|
// Cannot fit a record.
|
|
if CanFitRecord(blockOffset, 1) {
|
|
t.Error("CanFitRecord should return false when remaining <= 7")
|
|
}
|
|
|
|
// blockOffset = WalBlockSize - 8 → remaining = 8, which is > 7 → no padding needed.
|
|
blockOffset2 := uint32(WalBlockSize - 8)
|
|
padding2 := PaddingNeeded(blockOffset2)
|
|
if padding2 != 0 {
|
|
t.Errorf("PaddingNeeded(%d): expected 0, got %d", blockOffset2, padding2)
|
|
}
|
|
|
|
// Can fit a 1-byte payload: remaining=8, header=7, payload=1 → 8 >= 8.
|
|
if !CanFitRecord(blockOffset2, 1) {
|
|
t.Error("CanFitRecord should return true when remaining=8 and payloadLen=1")
|
|
}
|
|
|
|
// Cannot fit a 2-byte payload: remaining=8, header=7, payload=2 → 8 < 9.
|
|
if CanFitRecord(blockOffset2, 2) {
|
|
t.Error("CanFitRecord should return false when remaining=8 and payloadLen=2")
|
|
}
|
|
}
|
|
|
|
func TestCRCMismatch(t *testing.T) {
|
|
encoded := EncodePhysicalRecord(RecFull, []byte("test"))
|
|
// Corrupt a payload byte.
|
|
encoded[8] ^= 0xFF
|
|
|
|
_, _, err := DecodePhysicalRecord(encoded)
|
|
if err == nil {
|
|
t.Error("expected CRC mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestDataTooShort(t *testing.T) {
|
|
_, _, err := DecodePhysicalRecord([]byte{1, 2, 3})
|
|
if err == nil {
|
|
t.Error("expected error for data too short")
|
|
}
|
|
}
|