- 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
270 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|