Artifact 70f52ebae9a1c96c4fe40c172a5aef0516cc7d6d006f1d53b0aedc38912e03c0:
- File
db/db_test.go
— part of check-in
[f804878bd3]
at
2026-07-22 23:58:46
on branch trunk
— test: route every fossil-binary gate through one REQUIRE_FOSSIL_BIN helper (#125)
* test: route fossil-binary tests through one REQUIRE_FOSSIL_BIN helper
REQUIRE_FOSSIL_BIN=1 turns a missing fossil binary into a test failure instead of an invisible skip, but only internal/manifest honored it. The six other fossil-dependent sites each did their own exec.LookPath + unconditional t.Skip, so on those a run with no fossil was byte-identical to a passing one.
Add testutil.RequireFossilBin, which resolves the binary the same way FossilBinary does (FOSSIL_BIN override, then PATH) and either skips or fails based on REQUIRE_FOSSIL_BIN. Route testutil.NewTestRepo, both internal/delta CLI round-trips, the four internal/merge validation tests, the internal/xfer sanity check, and the ls-branch-tip repro through it, deleting their bespoke lookup+skip logic. CI already sets REQUIRE_FOSSIL_BIN=1 on the unit-test steps, so this makes all seven sites fail loud on absence while local runs without fossil stay opt-in.
* test: route every fossil-binary gate through RequireFossilBin
The first pass left six sites still deciding for themselves whether a missing fossil binary is a skip or a failure. Three of them (internal/sync's startFossilServer and its two integration tests, manifest's two rebuild tests, the root TestPhaseB_Integration, and hash's TestSHA1_FossilValidation) skipped unconditionally, so with REQUIRE_FOSSIL_BIN=1 and no fossil on PATH those packages still reported ok -- the exact silent pass the flag exists to prevent. The other three had hand-rolled copies of the flag check, which is the duplication that let the original drift happen.
All of them now call testutil.RequireFossilBin, and HasFossil is gone: it was a second, skip-only way to ask the same question, and it is what produced the sites that ignored the flag.
Also validate the FOSSIL_BIN override with LookPath, so a stale path reports "fossil not found" instead of surfacing later as a raw fork/exec error.
* test: resolve fossil binary via RequireFossilBin at remaining call sites
* fix: correct stale module path missed by rebase auto-merge
Three imports added by earlier commits in this branch predated the rebase target and had no corresponding line on main to conflict against, so git's auto-merge carried the old module path through verbatim. (user: [email protected], size: 8340) [annotate] [blame] [check-ins using]
package db_test import ( "fmt" "os/exec" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/db" _ "github.com/danmestas/go-libfossil/internal/testdriver" "github.com/danmestas/go-libfossil/simio" "github.com/danmestas/go-libfossil/testutil" ) func TestOpenClose(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() var result int err = d.QueryRow("SELECT 1+1").Scan(&result) if err != nil { t.Fatalf("QueryRow: %v", err) } if result != 2 { t.Fatalf("got %d, want 2", result) } } func TestOpenSQLMergesURIParams(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") conn, err := db.OpenSQL(path, db.OpenConfig{}, nil) if err != nil { t.Fatalf("OpenSQL create: %v", err) } if _, err := conn.Exec("CREATE TABLE t(v INTEGER)"); err != nil { conn.Close() t.Fatalf("create table: %v", err) } if err := conn.Close(); err != nil { t.Fatalf("close create conn: %v", err) } driver, dsn := db.SQLDriverAndDSN(path, db.OpenConfig{}, map[string]string{"mode": "ro"}) if driver == "" { t.Fatal("empty driver") } if strings.Count(dsn, "?") != 1 { t.Fatalf("DSN %q contains %d query delimiters, want 1", dsn, strings.Count(dsn, "?")) } if !strings.Contains(dsn, "mode=ro") { t.Fatalf("DSN %q missing mode=ro", dsn) } ro, err := db.OpenSQL(path, db.OpenConfig{}, map[string]string{"mode": "ro"}) if err != nil { t.Fatalf("OpenSQL read-only: %v", err) } defer ro.Close() if _, err := ro.Exec("INSERT INTO t(v) VALUES(1)"); err == nil { t.Fatal("read-only OpenSQL accepted a write") } } func TestExec(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() _, err = d.Exec("CREATE TABLE test(id INTEGER PRIMARY KEY, val TEXT)") if err != nil { t.Fatalf("Exec CREATE: %v", err) } _, err = d.Exec("INSERT INTO test(val) VALUES(?)", "hello") if err != nil { t.Fatalf("Exec INSERT: %v", err) } var val string err = d.QueryRow("SELECT val FROM test WHERE id=1").Scan(&val) if err != nil { t.Fatalf("QueryRow: %v", err) } if val != "hello" { t.Fatalf("val = %q, want %q", val, "hello") } } func TestApplicationID(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() err = d.SetApplicationID(252006673) if err != nil { t.Fatalf("SetApplicationID: %v", err) } id, err := d.ApplicationID() if err != nil { t.Fatalf("ApplicationID: %v", err) } if id != 252006673 { t.Fatalf("application_id = %d, want 252006673", id) } } func TestTransaction(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() if _, err := d.Exec("CREATE TABLE test(id INTEGER PRIMARY KEY, val TEXT)"); err != nil { t.Fatalf("Exec CREATE: %v", err) } // Commit case err = d.WithTx(func(tx *db.Tx) error { _, err := tx.Exec("INSERT INTO test(val) VALUES(?)", "committed") return err }) if err != nil { t.Fatalf("WithTx commit: %v", err) } var count int if err := d.QueryRow("SELECT count(*) FROM test").Scan(&count); err != nil { t.Fatalf("count query: %v", err) } if count != 1 { t.Fatalf("count after commit = %d, want 1", count) } // Rollback case err = d.WithTx(func(tx *db.Tx) error { if _, err := tx.Exec("INSERT INTO test(val) VALUES(?)", "rolled-back"); err != nil { return err } return fmt.Errorf("deliberate error") }) if err == nil { t.Fatal("WithTx should return error") } if err := d.QueryRow("SELECT count(*) FROM test").Scan(&count); err != nil { t.Fatalf("count query after rollback: %v", err) } if count != 1 { t.Fatalf("count after rollback = %d, want 1", count) } } func TestCreateRepoSchema(t *testing.T) { path := filepath.Join(t.TempDir(), "test.fossil") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() err = db.CreateRepoSchema(d) if err != nil { t.Fatalf("CreateRepoSchema: %v", err) } // Verify repo1 (static) tables exist repo1Tables := []string{"blob", "delta", "rcvfrom", "user", "config", "shun", "private", "reportfmt", "concealed"} for _, table := range repo1Tables { var name string err := d.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name) if err != nil { t.Errorf("repo1 table %q not found: %v", table, err) } } // Verify repo2 (transient) tables exist repo2Tables := []string{"filename", "mlink", "plink", "leaf", "event", "phantom", "orphan", "unclustered", "unsent", "tag", "tagxref", "backlink", "attachment", "cherrypick", "forumpost"} for _, table := range repo2Tables { var name string err := d.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name) if err != nil { t.Errorf("repo2 table %q not found: %v", table, err) } } // Verify application_id id, err := d.ApplicationID() if err != nil { t.Fatalf("ApplicationID: %v", err) } if id != 252006673 { t.Fatalf("application_id = %d, want 252006673", id) } // Verify seed rcvfrom row var rcvid int err = d.QueryRow("SELECT rcvid FROM rcvfrom WHERE rcvid=1").Scan(&rcvid) if err != nil { t.Fatalf("seed rcvfrom row missing: %v", err) } // Verify seed tag rows (1-11) var tagCount int err = d.QueryRow("SELECT count(*) FROM tag").Scan(&tagCount) if err != nil { t.Fatalf("tag count: %v", err) } if tagCount != 11 { t.Fatalf("tag count = %d, want 11", tagCount) } // Verify specific seed tags var tagName string err = d.QueryRow("SELECT tagname FROM tag WHERE tagid=8").Scan(&tagName) if err != nil { t.Fatalf("tag 8: %v", err) } if tagName != "branch" { t.Fatalf("tag 8 name = %q, want %q", tagName, "branch") } } func TestCreateRepoSchema_FossilValidation(t *testing.T) { if testing.Short() { t.Skip("skipping fossil CLI validation in short mode") } fossilBin := testutil.RequireFossilBin(t) path := filepath.Join(t.TempDir(), "test.fossil") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } err = db.CreateRepoSchema(d) if err != nil { t.Fatalf("CreateRepoSchema: %v", err) } err = db.SeedConfig(d, simio.CryptoRand{}, "") if err != nil { t.Fatalf("SeedConfig: %v", err) } err = db.SeedUser(d, "testuser") if err != nil { t.Fatalf("SeedUser: %v", err) } d.Close() // fossil rebuild should pass on Go-created schema cmd := exec.Command(fossilBin, "rebuild", path) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil rebuild failed: %v\n%s", err, out) } } func TestRegisterDriver(t *testing.T) { // Driver should already be registered by testdriver import (added in Task 3). cfg := db.RegisteredDriver() if cfg == nil { t.Fatal("no driver registered") } if cfg.Name == "" { t.Fatal("registered driver name is empty") } t.Logf("active driver: %s", cfg.Name) dsn := cfg.BuildDSN("/tmp/test.db", db.DefaultPragmas()) t.Logf("DSN: %s", dsn) if !strings.Contains(dsn, "journal_mode") { t.Fatal("DSN missing journal_mode pragma") } if !strings.Contains(dsn, "busy_timeout") { t.Fatal("DSN missing busy_timeout pragma") } } func TestOpenWithDefaults(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.Open(path) if err != nil { t.Fatalf("Open: %v", err) } defer d.Close() if d.Driver() == "" { t.Fatal("Driver() returned empty string") } t.Logf("opened with driver: %s", d.Driver()) var mode string if err := d.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil { t.Fatalf("journal_mode query: %v", err) } if mode != "wal" { t.Fatalf("journal_mode = %q, want wal", mode) } } func TestOpenWithCustomPragmas(t *testing.T) { path := filepath.Join(t.TempDir(), "test.db") d, err := db.OpenWith(path, db.OpenConfig{ Pragmas: map[string]string{ "cache_size": "-2000", }, }) if err != nil { t.Fatalf("OpenWith: %v", err) } defer d.Close() var mode string if err := d.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil { t.Fatalf("journal_mode query: %v", err) } if mode != "wal" { t.Fatalf("journal_mode = %q, want wal (default should still apply)", mode) } }