package wal import ( "os" "path/filepath" "reflect" "testing" "github.com/dailz/go-kv/manifest" ) func TestRecoverFromEmptyDir(t *testing.T) { dir := t.TempDir() replayer := &mockReplayer{} result, err := Recover(dir, replayer) if err != nil { t.Fatalf("Recover empty dir: %v", err) } if result.NextSequence != 0 { t.Errorf("NextSequence = %d, want 0", result.NextSequence) } if result.Truncated { t.Error("Truncated = true, want false") } if result.TruncateError != nil { t.Errorf("TruncateError = %v, want nil", result.TruncateError) } } func TestRecoverFullFlow(t *testing.T) { dir := t.TempDir() // Write test data: 2 batches across 1 segment. writeTestSegment(t, dir, 0, 0, [][]*WalEntry{ {makePutEntry("key1", "val1"), makeDeleteEntry("key2")}, {makePutEntry("key3", "val3")}, }) replayer := &mockReplayer{} result, err := Recover(dir, replayer) if err != nil { t.Fatalf("Recover: %v", err) } if result.NextSequence != 3 { t.Errorf("NextSequence = %d, want 3", result.NextSequence) } if result.NextSegmentID != 1 { t.Errorf("NextSegmentID = %d, want 1", result.NextSegmentID) } if result.Truncated { t.Error("Truncated = true, want false") } wantPuts := []replayPut{ {key: "key1", value: "val1", seq: 0}, {key: "key3", value: "val3", seq: 2}, } if !reflect.DeepEqual(replayer.puts, wantPuts) { t.Errorf("puts = %#v, want %#v", replayer.puts, wantPuts) } wantDeletes := []replayDelete{ {key: "key2", seq: 1}, } if !reflect.DeepEqual(replayer.deletes, wantDeletes) { t.Errorf("deletes = %#v, want %#v", replayer.deletes, wantDeletes) } } func TestRecoverWithTailCorruption(t *testing.T) { dir := t.TempDir() // Write valid batches, then corrupt the tail. filePath := writeTestSegment(t, dir, 0, 100, [][]*WalEntry{ {makePutEntry("good1", "before-corruption")}, {makePutEntry("good2", "also-before")}, }) appendFileBytes(t, filePath, []byte{0xDE, 0xAD, 0xBE, 0xEF}) replayer := &mockReplayer{} result, err := Recover(dir, replayer) if err != nil { t.Fatalf("Recover with tail corruption: %v", err) } if !result.Truncated { t.Fatal("Truncated = false, want true") } if result.TruncateError == nil { t.Fatal("TruncateError = nil, want non-nil") } if result.NextSequence != 102 { t.Errorf("NextSequence = %d, want 102", result.NextSequence) } if result.NextSegmentID != 1 { t.Errorf("NextSegmentID = %d, want 1", result.NextSegmentID) } // Verify only the valid entries were replayed. wantPuts := []replayPut{ {key: "good1", value: "before-corruption", seq: 100}, {key: "good2", value: "also-before", seq: 101}, } if !reflect.DeepEqual(replayer.puts, wantPuts) { t.Errorf("puts = %#v, want %#v", replayer.puts, wantPuts) } // Verify the file was truncated — it should be smaller than before. fi, fiErr := os.Stat(filePath) if fiErr != nil { t.Fatalf("Stat truncated file: %v", fiErr) } if fi.Size() == 0 { t.Error("truncated file is empty") } // Verify the truncated file still parses cleanly. replayer2 := &mockReplayer{} _, parseErr := ReplaySegmentFile(filePath, 100, replayer2) if parseErr != nil { t.Fatalf("replay after truncation: %v", parseErr) } if !reflect.DeepEqual(replayer2.puts, wantPuts) { t.Errorf("replay after truncation puts = %#v, want %#v", replayer2.puts, wantPuts) } } func TestRecoverUpdatesManifest(t *testing.T) { dir := t.TempDir() // Write test data. writeTestSegment(t, dir, 0, 50, [][]*WalEntry{ {makePutEntry("a", "b")}, {makePutEntry("c", "d")}, }) replayer := &mockReplayer{} result, err := Recover(dir, replayer) if err != nil { t.Fatalf("Recover: %v", err) } // Verify MANIFEST file exists and has correct content. mf, err := manifest.Load(dir) if err != nil { t.Fatalf("manifest.Load after recover: %v", err) } if mf.RecoverySegmentID != result.NextSegmentID { t.Errorf("MANIFEST RecoverySegmentID = %d, want %d", mf.RecoverySegmentID, result.NextSegmentID) } // Verify MANIFEST file path. manifestPath := filepath.Join(dir, "MANIFEST") if _, err := os.Stat(manifestPath); os.IsNotExist(err) { t.Error("MANIFEST file should exist after Recover") } }