store: edge-path tests for migrations and Open
Three gaps from the coverage plan, none of which were covered before. internal/store/migrations_test.go: TestRunMigrationsIgnoresUnknownAppliedIDs — simulates a DB written by a newer banger opened by an older one: schema_migrations carries an id (9001) the current binary doesn't know about. The runner must leave the alien row alone AND still apply its own known migrations. Without this, forward-then-backward upgrades or running two daemon versions against the same state dir would either fail or start destructively reinterpreting rows. TestDropColumnIfExistsIsIdempotent — pins the "run twice, no harm" property. A daemon restart after migration 2 succeeded on a fresh install must not fail because the column is already gone. dropColumnIfExists is what makes that idempotent. internal/store/store_test.go: TestOpenRejectsCorruptDB — writes garbage to state.db, Open must error cleanly (not panic, not silently overwrite). Also verifies the garbage bytes are untouched so the operator can hand the file to a recovery tool. TestOpenReadOnlyRejectsMissingDB — the doctor path must not silently create an empty DB when none exists; that would make "no VMs yet" and "your state is missing" indistinguishable. Package function coverage nudged 39.1% → 40.1%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f3db9b104
commit
7820960706
2 changed files with 171 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
|
@ -319,6 +320,58 @@ func TestStoreConfiguresSQLitePragmasOnPooledConnections(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestOpenRejectsCorruptDB pins the actionable-error contract when
|
||||
// state.db exists on disk but isn't a valid SQLite file. Users can
|
||||
// hit this after a disk-full crash mid-write, a copy that truncated,
|
||||
// or accidental manual editing. banger must surface the error
|
||||
// cleanly so the operator can delete-and-retry — never panic, never
|
||||
// silently overwrite, never leak a partially-opened sql.DB handle.
|
||||
func TestOpenRejectsCorruptDB(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "state.db")
|
||||
garbage := []byte("this is definitely not a sqlite database")
|
||||
if err := os.WriteFile(path, garbage, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
s, err := Open(path)
|
||||
if err == nil {
|
||||
_ = s.Close()
|
||||
t.Fatal("Open: want error on corrupt DB file")
|
||||
}
|
||||
|
||||
// The garbage bytes must still be there — Open must not have
|
||||
// overwritten the file mid-attempt. A user recovering from a
|
||||
// mid-write crash needs that invariant to hand the file to a
|
||||
// tool like sqlite3_analyzer.
|
||||
got, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
t.Fatalf("ReadFile: %v", readErr)
|
||||
}
|
||||
if string(got) != string(garbage) {
|
||||
t.Fatalf("Open touched the garbage file: got %q, want %q", string(got), string(garbage))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenReadOnlyRejectsMissingDB pins the "no silent creation"
|
||||
// contract for the doctor path: OpenReadOnly against a path that
|
||||
// doesn't exist must error, not create an empty DB that later reads
|
||||
// would mistake for "no VMs yet."
|
||||
func TestOpenReadOnlyRejectsMissingDB(t *testing.T) {
|
||||
t.Parallel()
|
||||
missing := filepath.Join(t.TempDir(), "never-existed.db")
|
||||
s, err := OpenReadOnly(missing)
|
||||
if err == nil {
|
||||
_ = s.Close()
|
||||
t.Fatal("OpenReadOnly: want error when the DB file doesn't exist")
|
||||
}
|
||||
if _, statErr := os.Stat(missing); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("OpenReadOnly silently created %q (stat err = %v)", missing, statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func openTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
store, err := Open(filepath.Join(t.TempDir(), "state.db"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue