Files
go-kv/wal/record_parser_test.go
dailz 08960a9bcf feat(wal): implement segment rotation, recovery scanner, and record parser
- wal/segment_manager.go: segment lifecycle with rotation at batch boundaries
- wal/scanner.go: segment discovery, ordering, and continuity validation
- wal/record_parser.go: block-level physical record parsing with tail corruption detection
- Comprehensive tests for all modules, all pass with -race
2026-06-12 13:50:03 +08:00

270 lines
6.5 KiB
Go

package wal
import (
"os"
"path/filepath"
"testing"
)
func TestParseBlockSingleRecord(t *testing.T) {
payload := []byte("hello world")
encoded := EncodePhysicalRecord(RecFull, payload)
block := make([]byte, WalBlockSize)
copy(block, encoded)
recs, err := ParseBlock(block)
if err != nil {
t.Fatalf("ParseBlock: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 record, got %d", len(recs))
}
if recs[0].Type != RecFull {
t.Errorf("Type = %d, want RecFull(%d)", recs[0].Type, RecFull)
}
if string(recs[0].Payload) != string(payload) {
t.Errorf("Payload = %q, want %q", recs[0].Payload, payload)
}
}
func TestParseBlockMultipleRecords(t *testing.T) {
payloads := [][]byte{
[]byte("first"),
[]byte("second"),
[]byte("third"),
}
block := make([]byte, WalBlockSize)
offset := 0
for i, p := range payloads {
rec := EncodePhysicalRecord(RecFull, p)
copy(block[offset:], rec)
offset += len(rec)
if offset > WalBlockSize {
t.Fatalf("record %d overflows block", i)
}
}
recs, err := ParseBlock(block)
if err != nil {
t.Fatalf("ParseBlock: %v", err)
}
if len(recs) != 3 {
t.Fatalf("expected 3 records, got %d", len(recs))
}
for i, want := range payloads {
if string(recs[i].Payload) != string(want) {
t.Errorf("rec[%d].Payload = %q, want %q", i, recs[i].Payload, want)
}
}
}
func TestParseBlockWithPadding(t *testing.T) {
payload := []byte("data")
encoded := EncodePhysicalRecord(RecFull, payload)
// Place record at offset 0, then fill rest with zeros.
block := make([]byte, WalBlockSize)
copy(block, encoded)
recs, err := ParseBlock(block)
if err != nil {
t.Fatalf("ParseBlock: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 record, got %d", len(recs))
}
if string(recs[0].Payload) != string(payload) {
t.Errorf("Payload = %q, want %q", recs[0].Payload, payload)
}
}
func TestParseBlockCorruptPadding(t *testing.T) {
payload := []byte("data")
encoded := EncodePhysicalRecord(RecFull, payload)
block := make([]byte, WalBlockSize)
copy(block, encoded)
// Write non-zero byte in the trailing padding area.
block[len(encoded)+3] = 0xFF
recs, err := ParseBlock(block)
if err == nil {
t.Fatal("expected tail corruption error")
}
if !IsTailCorruption(err) {
t.Errorf("expected TailCorruptionError, got: %v", err)
}
// Should still return records parsed before the corruption.
if len(recs) != 1 {
t.Errorf("expected 1 record before corruption, got %d", len(recs))
}
}
func TestParseBlockBadCRC(t *testing.T) {
payload := []byte("data")
encoded := EncodePhysicalRecord(RecFull, payload)
block := make([]byte, WalBlockSize)
copy(block, encoded)
// Corrupt a payload byte.
block[PhysicalRecordHeaderSize+1] ^= 0xFF
_, err := ParseBlock(block)
if err == nil {
t.Fatal("expected CRC mismatch error")
}
if !IsTailCorruption(err) {
t.Errorf("expected TailCorruptionError, got: %v", err)
}
}
func TestParseBlockShortBlock(t *testing.T) {
payload := []byte("short block data")
encoded := EncodePhysicalRecord(RecFull, payload)
// Simulate a short last block (less than WalBlockSize).
block := make([]byte, len(encoded)+10) // extra trailing zeros
copy(block, encoded)
recs, err := ParseBlock(block)
if err != nil {
t.Fatalf("ParseBlock short block: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 record, got %d", len(recs))
}
if string(recs[0].Payload) != string(payload) {
t.Errorf("Payload = %q, want %q", recs[0].Payload, payload)
}
}
func TestParseBlockEmpty(t *testing.T) {
block := make([]byte, 64) // all zeros
recs, err := ParseBlock(block)
if err != nil {
t.Fatalf("ParseBlock empty: %v", err)
}
if len(recs) != 0 {
t.Fatalf("expected 0 records, got %d", len(recs))
}
}
func TestParseBlockZeroHeaderWithNonZeroTail(t *testing.T) {
block := make([]byte, WalBlockSize)
// First 7 bytes are zero (valid zero header), but byte at offset 8 is non-zero.
block[8] = 0x42
_, err := ParseBlock(block)
if err == nil {
t.Fatal("expected tail corruption error for zero header + non-zero tail")
}
if !IsTailCorruption(err) {
t.Errorf("expected TailCorruptionError, got: %v", err)
}
}
func TestIsTailCorruptionFalse(t *testing.T) {
if IsTailCorruption(nil) {
t.Error("IsTailCorruption(nil) = true, want false")
}
if IsTailCorruption(os.ErrNotExist) {
t.Error("IsTailCorruption(ErrNotExist) = true, want false")
}
}
func TestParseRecordsFromFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "segment-0.wal")
// Create a segment file with header + one full block containing 2 records.
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
hdr := &WalFileHeader{
SegmentID: 0,
StartSequence: 0,
BlockSize: WalBlockSize,
}
encoded := EncodeWalHeader(hdr)
if _, err := f.Write(encoded[:]); err != nil {
t.Fatal(err)
}
block := make([]byte, WalBlockSize)
payload1 := []byte("record-one")
payload2 := []byte("record-two")
offset := 0
rec1 := EncodePhysicalRecord(RecFull, payload1)
copy(block[offset:], rec1)
offset += len(rec1)
rec2 := EncodePhysicalRecord(RecFull, payload2)
copy(block[offset:], rec2)
if _, err := f.Write(block); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
recs, err := ParseRecordsFromFile(path)
if err != nil {
t.Fatalf("ParseRecordsFromFile: %v", err)
}
if len(recs) != 2 {
t.Fatalf("expected 2 records, got %d", len(recs))
}
if string(recs[0].Payload) != string(payload1) {
t.Errorf("rec[0].Payload = %q, want %q", recs[0].Payload, payload1)
}
if string(recs[1].Payload) != string(payload2) {
t.Errorf("rec[1].Payload = %q, want %q", recs[1].Payload, payload2)
}
}
func TestParseRecordsFromFileShortLastBlock(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "segment-0.wal")
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
hdr := &WalFileHeader{
SegmentID: 0,
StartSequence: 0,
BlockSize: WalBlockSize,
}
encoded := EncodeWalHeader(hdr)
if _, err := f.Write(encoded[:]); err != nil {
t.Fatal(err)
}
// Write a partial block (just one record, no full 32KB).
payload := []byte("short")
rec := EncodePhysicalRecord(RecFull, payload)
if _, err := f.Write(rec); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
recs, err := ParseRecordsFromFile(path)
if err != nil {
t.Fatalf("ParseRecordsFromFile: %v", err)
}
if len(recs) != 1 {
t.Fatalf("expected 1 record, got %d", len(recs))
}
if string(recs[0].Payload) != string(payload) {
t.Errorf("Payload = %q, want %q", recs[0].Payload, payload)
}
}