diff --git a/core/artwork/e2e/resolution_harness_test.go b/core/artwork/e2e/resolution_harness_test.go index 1a917cb2d..1aa60d49c 100644 --- a/core/artwork/e2e/resolution_harness_test.go +++ b/core/artwork/e2e/resolution_harness_test.go @@ -31,6 +31,7 @@ import ( "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/tests" + "github.com/navidrome/navidrome/tests/harness" "github.com/navidrome/navidrome/utils/cache" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -65,8 +66,19 @@ var ( // Windows. A suite-level tempdir plus an AfterSuite close avoids the lock conflict. var suiteDBTempDir string +// Migrating the schema costs ~400ms, so it runs once per suite and specs reset by truncating. +var userTables []string + var _ = BeforeSuite(func() { suiteDBTempDir = GinkgoT().TempDir() + + DeferCleanup(configtest.SetupConfig()) + conf.Server.DbPath = filepath.Join(suiteDBTempDir, "artwork-resolution-e2e.db") + "?_journal_mode=WAL" + conf.Server.DataFolder = conf.NewDir(GinkgoT().TempDir()) + db.Db().SetMaxOpenConns(1) + db.Init(request.WithUser(context.Background(), model.User{ID: "admin-1", IsAdmin: true})) + + userTables = harness.ResettableTables() }) var _ = AfterSuite(func() { @@ -86,10 +98,8 @@ func setupResolutionHarness() { conf.Server.EnableMediaFileCoverArt = true conf.Server.ArtworkWorkerConcurrency = 1 - db.Db().SetMaxOpenConns(1) rctx = request.WithUser(GinkgoT().Context(), model.User{ID: "admin-1", UserName: "admin", IsAdmin: true}) - db.Init(rctx) - DeferCleanup(func() { Expect(tests.ClearDB()).To(Succeed()) }) + harness.TruncateDB(userTables) rds = &tests.MockDataStore{RealDS: persistence.New(db.Db())} diff --git a/tests/harness/harness.go b/tests/harness/harness.go index 9ccbdcaf3..b4da0377e 100644 --- a/tests/harness/harness.go +++ b/tests/harness/harness.go @@ -86,17 +86,13 @@ func SetupDB(ctx context.Context, users ...*model.User) *DB { return h } -// Restore reloads every table from the golden snapshot via ATTACH DATABASE — much faster than a -// rescan. FTS shadow tables are skipped; they are kept in sync by their content tables' triggers. -func (h *DB) Restore() { - sqlDB := db.Db() - _, err := sqlDB.Exec("PRAGMA foreign_keys = OFF") - Expect(err).ToNot(HaveOccurred()) - _, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", h.SnapshotPath) - Expect(err).ToNot(HaveOccurred()) - - rows, err := sqlDB.Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'") +// ResettableTables lists the tables a per-spec reset may write. FTS shadow tables are excluded; +// they are kept in sync by their content tables' triggers, and writing them directly corrupts the +// index. +func ResettableTables() []string { + rows, err := db.Db().Query("SELECT name FROM main.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'") Expect(err).ToNot(HaveOccurred()) + defer rows.Close() var tables []string for rows.Next() { var name string @@ -104,12 +100,38 @@ func (h *DB) Restore() { tables = append(tables, name) } Expect(rows.Err()).ToNot(HaveOccurred()) - rows.Close() + return tables +} + +// TruncateDB empties every resettable table, leaving the migrated schema in place. Suites whose +// specs each build their own library use this instead of a golden snapshot, since re-migrating the +// schema per spec costs ~400ms. +func TruncateDB(tables []string) { + sqlDB := db.Db() + _, err := sqlDB.Exec("PRAGMA foreign_keys = OFF") + Expect(err).ToNot(HaveOccurred()) + defer func() { _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") }() for _, table := range tables { // Table names come from sqlite_master, not user input. _, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec Expect(err).ToNot(HaveOccurred()) + } +} + +// Restore reloads every table from the golden snapshot via ATTACH DATABASE — much faster than a +// rescan. +func (h *DB) Restore() { + sqlDB := db.Db() + _, err := sqlDB.Exec("PRAGMA foreign_keys = OFF") + Expect(err).ToNot(HaveOccurred()) + _, err = sqlDB.Exec("ATTACH DATABASE ? AS snapshot", h.SnapshotPath) + Expect(err).ToNot(HaveOccurred()) + + for _, table := range ResettableTables() { + // Table names come from sqlite_master, not user input. + _, err = sqlDB.Exec(`DELETE FROM main."` + table + `"`) //nolint:gosec + Expect(err).ToNot(HaveOccurred()) _, err = sqlDB.Exec(`INSERT INTO main."` + table + `" SELECT * FROM snapshot."` + table + `"`) //nolint:gosec Expect(err).ToNot(HaveOccurred()) }