Files
go-kv/wal/scanner_test.go
T
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

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)
}
}