test: cover absolutizePaths, lastID, runCheckMigrations
Adds focused unit tests for previously-uncovered cli helpers: - TestAbsolutizePaths covers the path-vararg helper's empty, absolute, and relative branches; complements the existing TestAbsolutizeImageRegisterPaths. - TestLastID is table-driven across nil/empty/sorted/unsorted/ duplicates/negative inputs. - TestRunCheckMigrations* exercises every Compatibility branch (compatible / migrations needed / incompatible / inspect error) by stubbing bangerdExit and pointing the layout at a temp-dir DB seeded directly with the schema_migrations table. - TestNewBangerdCommandSubcommands pins the flag set against accidental drift. Lifts internal/cli coverage from 71% to 76% combined. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2606bfbabb
commit
02a1472dd4
2 changed files with 238 additions and 0 deletions
194
internal/cli/bangerd_test.go
Normal file
194
internal/cli/bangerd_test.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"banger/internal/store"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestNewBangerdCommandSubcommands(t *testing.T) {
|
||||
cmd := NewBangerdCommand()
|
||||
if cmd.Use != "bangerd" {
|
||||
t.Errorf("Use = %q, want bangerd", cmd.Use)
|
||||
}
|
||||
for _, flag := range []string{"system", "root-helper", "check-migrations"} {
|
||||
if cmd.Flag(flag) == nil {
|
||||
t.Errorf("flag %q missing", flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []int
|
||||
want int
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []int{}, 0},
|
||||
{"single", []int{7}, 7},
|
||||
{"sorted ascending", []int{1, 2, 3}, 3},
|
||||
{"unsorted, max in middle", []int{1, 99, 5}, 99},
|
||||
{"duplicates", []int{4, 4, 2, 4}, 4},
|
||||
{"negative ignored", []int{-3, -1, 0}, 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := lastID(tc.in); got != tc.want {
|
||||
t.Fatalf("lastID(%v) = %d, want %d", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// stubExit replaces bangerdExit for the test and returns a pointer to
|
||||
// the captured exit code (-1 = not called) and a restore func.
|
||||
func stubExit(t *testing.T) *int {
|
||||
t.Helper()
|
||||
called := -1
|
||||
prev := bangerdExit
|
||||
bangerdExit = func(code int) { called = code }
|
||||
t.Cleanup(func() { bangerdExit = prev })
|
||||
return &called
|
||||
}
|
||||
|
||||
// pointHomeAtTempDB sets XDG_STATE_HOME (and HOME, which Resolve falls
|
||||
// back to) so that paths.Resolve().DBPath lands at <tmp>/banger/state.db.
|
||||
// Returns the DB path.
|
||||
func pointHomeAtTempDB(t *testing.T) string {
|
||||
t.Helper()
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("HOME", tmp)
|
||||
t.Setenv("XDG_STATE_HOME", tmp)
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
t.Setenv("XDG_CACHE_HOME", tmp)
|
||||
t.Setenv("XDG_RUNTIME_DIR", tmp)
|
||||
dir := filepath.Join(tmp, "banger")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir state dir: %v", err)
|
||||
}
|
||||
return filepath.Join(dir, "state.db")
|
||||
}
|
||||
|
||||
func TestRunCheckMigrationsCompatible(t *testing.T) {
|
||||
dbPath := pointHomeAtTempDB(t)
|
||||
s, err := store.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("store.Open: %v", err)
|
||||
}
|
||||
_ = s.Close()
|
||||
|
||||
exit := stubExit(t)
|
||||
cmd := &cobra.Command{}
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
|
||||
if err := runCheckMigrations(cmd, false); err != nil {
|
||||
t.Fatalf("runCheckMigrations: %v", err)
|
||||
}
|
||||
if *exit != -1 {
|
||||
t.Errorf("bangerdExit called with %d, want no call", *exit)
|
||||
}
|
||||
if !strings.HasPrefix(out.String(), "compatible:") {
|
||||
t.Errorf("stdout = %q, want prefix \"compatible:\"", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckMigrationsMigrationsNeeded(t *testing.T) {
|
||||
dbPath := pointHomeAtTempDB(t)
|
||||
// Hand-craft a DB that has schema_migrations with only the baseline
|
||||
// row — InspectSchemaState classifies this as "migrations needed".
|
||||
dsn := "file:" + dbPath + "?_pragma=foreign_keys(1)"
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("sql.Open: %v", err)
|
||||
}
|
||||
if _, err := db.Exec(`CREATE TABLE schema_migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`); err != nil {
|
||||
t.Fatalf("create table: %v", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (1, 'baseline', '2026-01-01T00:00:00Z')`); err != nil {
|
||||
t.Fatalf("insert baseline: %v", err)
|
||||
}
|
||||
_ = db.Close()
|
||||
|
||||
exit := stubExit(t)
|
||||
cmd := &cobra.Command{}
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
|
||||
if err := runCheckMigrations(cmd, false); err != nil {
|
||||
t.Fatalf("runCheckMigrations: %v", err)
|
||||
}
|
||||
if *exit != 1 {
|
||||
t.Errorf("bangerdExit called with %d, want 1", *exit)
|
||||
}
|
||||
if !strings.HasPrefix(out.String(), "migrations needed:") {
|
||||
t.Errorf("stdout = %q, want prefix \"migrations needed:\"", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckMigrationsIncompatible(t *testing.T) {
|
||||
dbPath := pointHomeAtTempDB(t)
|
||||
s, err := store.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("store.Open: %v", err)
|
||||
}
|
||||
_ = s.Close()
|
||||
|
||||
// Inject an unknown migration id directly so the binary's known set
|
||||
// is a strict subset — InspectSchemaState classifies as incompatible.
|
||||
dsn := "file:" + dbPath
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("sql.Open: %v", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations VALUES (9999, 'from_the_future', '2030-01-01T00:00:00Z')`); err != nil {
|
||||
t.Fatalf("insert future row: %v", err)
|
||||
}
|
||||
_ = db.Close()
|
||||
|
||||
exit := stubExit(t)
|
||||
cmd := &cobra.Command{}
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
|
||||
if err := runCheckMigrations(cmd, false); err != nil {
|
||||
t.Fatalf("runCheckMigrations: %v", err)
|
||||
}
|
||||
if *exit != 2 {
|
||||
t.Errorf("bangerdExit called with %d, want 2", *exit)
|
||||
}
|
||||
if !strings.HasPrefix(out.String(), "incompatible:") {
|
||||
t.Errorf("stdout = %q, want prefix \"incompatible:\"", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckMigrationsInspectError(t *testing.T) {
|
||||
// Point at a state dir with a non-DB file at state.db so Inspect
|
||||
// fails to open it. The function should wrap the error with the path.
|
||||
dbPath := pointHomeAtTempDB(t)
|
||||
if err := os.WriteFile(dbPath, []byte("not a sqlite file"), 0o600); err != nil {
|
||||
t.Fatalf("write garbage: %v", err)
|
||||
}
|
||||
|
||||
stubExit(t)
|
||||
cmd := &cobra.Command{}
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
|
||||
err := runCheckMigrations(cmd, false)
|
||||
if err == nil {
|
||||
t.Fatal("runCheckMigrations: nil error, want wrapped inspect error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), dbPath) {
|
||||
t.Errorf("error %q does not mention DB path %q", err.Error(), dbPath)
|
||||
}
|
||||
}
|
||||
|
|
@ -737,6 +737,50 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAbsolutizePaths(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Getwd: %v", err)
|
||||
}
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatalf("Chdir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(wd) })
|
||||
|
||||
empty := ""
|
||||
abs := "/already/absolute/path"
|
||||
rel1 := filepath.Join("a", "b")
|
||||
rel2 := "./c/d"
|
||||
|
||||
if err := absolutizePaths(&empty, &abs, &rel1, &rel2); err != nil {
|
||||
t.Fatalf("absolutizePaths: %v", err)
|
||||
}
|
||||
|
||||
if empty != "" {
|
||||
t.Errorf("empty value mutated: %q", empty)
|
||||
}
|
||||
if abs != "/already/absolute/path" {
|
||||
t.Errorf("absolute value mutated: %q", abs)
|
||||
}
|
||||
if !filepath.IsAbs(rel1) {
|
||||
t.Errorf("rel1 not absolutized: %q", rel1)
|
||||
}
|
||||
if !filepath.IsAbs(rel2) {
|
||||
t.Errorf("rel2 not absolutized: %q", rel2)
|
||||
}
|
||||
// Sanity: relative paths should land under tmp.
|
||||
if !strings.HasPrefix(rel1, tmp) {
|
||||
t.Errorf("rel1 = %q, want prefix %q", rel1, tmp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsolutizePathsNoArgs(t *testing.T) {
|
||||
if err := absolutizePaths(); err != nil {
|
||||
t.Fatalf("absolutizePaths() with no args: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintImageListTableShowsRootfsSizes(t *testing.T) {
|
||||
rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
|
||||
if err := os.WriteFile(rootfs, nil, 0o644); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue