- 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
170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
package wal
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// writeSegmentFile creates a minimal WAL segment file in dir with the given
|
|
// segment ID and start sequence.
|
|
func writeSegmentFile(t *testing.T, dir string, segmentID, startSequence uint64) {
|
|
t.Helper()
|
|
path := filepath.Join(dir, fmt.Sprintf("segment-%d.wal", segmentID))
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create segment file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
hdr := &WalFileHeader{
|
|
SegmentID: segmentID,
|
|
StartSequence: startSequence,
|
|
BlockSize: WalBlockSize,
|
|
}
|
|
encoded := EncodeWalHeader(hdr)
|
|
if _, err := f.Write(encoded[:]); err != nil {
|
|
t.Fatalf("write header: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseSegmentFilename(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wantID uint64
|
|
wantOK bool
|
|
}{
|
|
{"segment-0.wal", 0, true},
|
|
{"segment-1.wal", 1, true},
|
|
{"segment-42.wal", 42, true},
|
|
{"segment-18446744073709551615.wal", 18446744073709551615, true},
|
|
{"segment-.wal", 0, false}, // no digits
|
|
{"segment-abc.wal", 0, false}, // non-numeric
|
|
{"segment-1.wal.bak", 0, false}, // extra suffix
|
|
{"foo-segment-1.wal", 0, false}, // extra prefix
|
|
{" Segment-1.wal", 0, false}, // space prefix
|
|
{"segment--1.wal", 0, false}, // negative sign
|
|
{"", 0, false},
|
|
{"readme.md", 0, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotID, gotOK := ParseSegmentFilename(tt.name)
|
|
if gotOK != tt.wantOK {
|
|
t.Errorf("ParseSegmentFilename(%q) ok = %v, want %v", tt.name, gotOK, tt.wantOK)
|
|
}
|
|
if gotOK && gotID != tt.wantID {
|
|
t.Errorf("ParseSegmentFilename(%q) id = %d, want %d", tt.name, gotID, tt.wantID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanSegmentsOrdered(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeSegmentFile(t, dir, 0, 100)
|
|
writeSegmentFile(t, dir, 1, 200)
|
|
writeSegmentFile(t, dir, 2, 300)
|
|
|
|
segs, err := ScanSegments(dir, 0)
|
|
if err != nil {
|
|
t.Fatalf("ScanSegments: %v", err)
|
|
}
|
|
if len(segs) != 3 {
|
|
t.Fatalf("expected 3 segments, got %d", len(segs))
|
|
}
|
|
|
|
for i, want := range []struct {
|
|
id uint64
|
|
seq uint64
|
|
}{
|
|
{0, 100}, {1, 200}, {2, 300},
|
|
} {
|
|
if segs[i].SegmentID != want.id {
|
|
t.Errorf("seg[%d].SegmentID = %d, want %d", i, segs[i].SegmentID, want.id)
|
|
}
|
|
if segs[i].StartSequence != want.seq {
|
|
t.Errorf("seg[%d].StartSequence = %d, want %d", i, segs[i].StartSequence, want.seq)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanSegmentsFiltersBelowRecoveryID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for i := uint64(0); i < 5; i++ {
|
|
writeSegmentFile(t, dir, i, i*100)
|
|
}
|
|
|
|
segs, err := ScanSegments(dir, 2)
|
|
if err != nil {
|
|
t.Fatalf("ScanSegments: %v", err)
|
|
}
|
|
if len(segs) != 3 {
|
|
t.Fatalf("expected 3 segments, got %d", len(segs))
|
|
}
|
|
|
|
wantIDs := []uint64{2, 3, 4}
|
|
for i, s := range segs {
|
|
if s.SegmentID != wantIDs[i] {
|
|
t.Errorf("seg[%d].SegmentID = %d, want %d", i, s.SegmentID, wantIDs[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanSegmentsGapDetection(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeSegmentFile(t, dir, 0, 0)
|
|
writeSegmentFile(t, dir, 1, 100)
|
|
// segment 2 is missing
|
|
writeSegmentFile(t, dir, 3, 300)
|
|
|
|
_, err := ScanSegments(dir, 0)
|
|
if err == nil {
|
|
t.Fatal("expected error for gap in segment sequence")
|
|
}
|
|
if !errors.Is(err, ErrSegmentGap) {
|
|
t.Errorf("expected ErrSegmentGap, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestScanSegmentsEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
segs, err := ScanSegments(dir, 0)
|
|
if err != nil {
|
|
t.Fatalf("ScanSegments empty dir: %v", err)
|
|
}
|
|
if len(segs) != 0 {
|
|
t.Fatalf("expected 0 segments, got %d", len(segs))
|
|
}
|
|
}
|
|
|
|
func TestScanSegmentsIgnoresNonWalFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeSegmentFile(t, dir, 0, 0)
|
|
// Create non-WAL files that should be ignored.
|
|
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "segment-abc.wal"), []byte("nope"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
segs, err := ScanSegments(dir, 0)
|
|
if err != nil {
|
|
t.Fatalf("ScanSegments: %v", err)
|
|
}
|
|
if len(segs) != 1 {
|
|
t.Fatalf("expected 1 segment, got %d", len(segs))
|
|
}
|
|
if segs[0].SegmentID != 0 {
|
|
t.Errorf("SegmentID = %d, want 0", segs[0].SegmentID)
|
|
}
|
|
}
|
|
|
|
|
|
|