package hostbox import ( "database/sql" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/lease" ) // walLTXFixture builds a snapshot whose database is in WAL mode, as every // database restored from a real litestream replica is (litestream requires // it). With tables false the schema is created and dropped again, so the // file is a valid WAL-mode database that the restore check refuses ("no // tables") after it has opened it. func walLTXFixture(t *testing.T, value string, tables bool) (string, []byte, uint64) { t.Helper() path := filepath.Join(t.TempDir(), "fixture.db") db, err := sql.Open("sqlite", path) if err != nil { t.Fatal(err) } db.SetMaxOpenConns(1) stmts := []string{`PRAGMA journal_mode=WAL`, `CREATE TABLE fixture(value TEXT)`} if tables { stmts = append(stmts, `INSERT INTO fixture VALUES ('`+value+`')`) } else { stmts = append(stmts, `DROP TABLE fixture`) } for _, s := range stmts { if _, err := db.Exec(s); err != nil { t.Fatalf("%s: %v", s, err) } } if err := db.Close(); err != nil { t.Fatal(err) } raw := mustRead(t, path) if len(raw) < 20 || raw[18] != 2 || raw[19] != 2 { t.Fatalf("fixture header says journal mode bytes %d,%d; want 2,2 (WAL)", raw[18], raw[19]) } return ltxFromDBFile(t, path, 9, 1, 1) } func restoreLeftovers(t *testing.T, dir string) []string { t.Helper() es, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } var out []string for _, e := range es { if strings.HasPrefix(e.Name(), ".restore-") { out = append(out, e.Name()) } } return out } // A restored database is in WAL mode, so the read-only check attaches a -wal // and a -shm it cannot delete. Install must leave none of them behind, on the // way to success and on the way to a failed check, and must never touch the // .restore-* files of a concurrent restore. func TestColdRestoreLeavesNoRestoreTempFilesBehindWALDatabase(t *testing.T) { cases := []struct { name string tables bool wantErr string }{ {name: "success", tables: true}, {name: "failed check", tables: false, wantErr: "check restored database"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() db := filepath.Join(dir, "shop.db") key, snapshot, crc := walLTXFixture(t, "selected", tc.tables) rd := &restoreReader{data: map[string][]byte{key + "@v1": snapshot}} // Another restore's files, in flight beside ours. others := []string{".restore-other.db", ".restore-other.db-shm", ".restore-other.db-wal"} for _, o := range others { if err := os.WriteFile(filepath.Join(dir, o), []byte("not ours"), 0o600); err != nil { t.Fatal(err) } } err := (ColdRestore{Reader: rd, Bucket: "bucket", Generation: "one", Work: &restoreWork{}}).Install(t.Context(), db, &genselect.Plan{Generation: "one", MaxTXID: 1, Pin: &genselect.Pin{SnapshotCRC64: crc}, Files: []genselect.File{{Key: key, VersionID: "v1", Level: 9, MinTXID: 1, MaxTXID: 1, Size: int64(len(snapshot))}}}) if tc.wantErr == "" { if err != nil { t.Fatalf("Install: %v", err) } if err := lease.CheckDB(db); err != nil { t.Fatalf("installed database: %v", err) } // The query above may attach its own sidecars to the installed // database; they are named for it, not for the restore temp. } else { if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("Install error = %v, want %q", err, tc.wantErr) } if _, err := os.Stat(db); !os.IsNotExist(err) { t.Fatalf("a refused restore left a database: %v", err) } } got := restoreLeftovers(t, dir) if strings.Join(got, ",") != strings.Join(others, ",") { t.Fatalf(".restore-* files = %v, want only the concurrent restore's %v", got, others) } }) } }