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