Files
pad/internal/store/main_test.go
T
xarmian 6f8b6eb958 perf(store): clone Postgres test databases from a migrated template (TASK-2900) (#1260)
`storetest.NewPostgres` and its in-package twin `testStorePostgres` ran the
full 87-file migration chain for EVERY test that asked for a store. The schema
is identical for every one of them, so building it per test bought nothing.

Measured on this box, at 8910ad06, before any change:

  per store construction   CREATE DATABASE  21ms
                           migrate chain   385ms   <- 92% of the toll
                           DROP DATABASE    13ms
  Postgres store leg       wall 495.8s / test-binary CPU 93.7s

18% of that leg's wall clock was CPU inside the test binary. The other 82% was
the binary waiting on postgres, and the container's own cgroup accounting put
407.2s of CPU on the other side of the socket — 4.3x what the test process
burned. The per-test distribution is flat (slowest test 3.9%, top twenty 19%),
so there was no fat test to find: ~850 tests were each paying a fixed toll.

`CREATE DATABASE ... TEMPLATE <migrated>` is a file-level copy, and it is the
Postgres analogue of the SQLite template-and-copy this suite has had since
IDEA-1914. Both helpers now build one migrated template per test binary
(sync.Once, RWMutex-guarded against Cleanup) and clone per test.

Results, three repeats per driver on a quiet box:

  Postgres wall           495.8s -> 224.4s   -54.7%
  Postgres container CPU  407.2s -> 167.6s   -58.8%
  SQLite wall             185.6s -> 186.0s   unchanged (the control)

The change DELETES work rather than rearranging it — container CPU is the
figure that says so, and the flat SQLite row is what lets the Postgres delta be
attributed to the template rather than to the environment.

Predicted before building, and recorded on the trail before the measurement
existed: container CPU between 134s and 208s. Actual 167.6s. Rearranged, that
yields 850 store constructions per leg, against a static lower bound of 708 and
a dynamic ceiling of 970 — two instruments sharing no mechanism agreeing on a
number the bracket could only bound.

THE REGRESSION THIS INVITES, and the test that catches it:

A template that silently stopped being used would pass every other test in the
repository. A clone and a per-test migration produce byte-identical databases,
so no assertion about schema or behaviour distinguishes them, and the
build-once counter reads 1 either way — it proves the template was BUILT once,
not that anything was CLONED from it. Drop the TEMPLATE clause and the whole
cost returns silently.

So the template is stamped after migrating with a marker only a file-level copy
can carry, and both helpers assert it on the database they hand out. A table
comment carries it because it lives in the database's own pg_description; a
DATABASE comment would not, since those live in the shared pg_shdescription
keyed by an oid the clone does not inherit. schema_migrations.applied_at was
rejected as the carrier: RFC3339 is second-resolution and a fresh 385ms
migration can land inside the same second as the template's.

Mutation matrix — the class has exactly two members, because the import cycle
forces the helper to be duplicated:

  storetest twin, TEMPLATE removed    exactly ONE test fails (the marker test);
                                      the other five pass
  in-package helper, TEMPLATE removed exactly ONE test fails (its counterpart),
                                      and the run takes 21.2s against 0.9s

Both controls were run only after committing, so restoring from HEAD could not
delete uncommitted work.

No test weakened: per-test outcome sets compared before and after on both
drivers. Zero tests disappeared, zero changed outcome. The only deltas are the
three new mechanism tests, which pass on Postgres and skip on SQLite.

Cleanup() gains the Postgres half; its three existing callers need no change,
because the invariant they rely on — "tears down whatever templates this binary
built" — is what widened. A binary killed mid-run leaks its pad_tmpl_* database,
bounded by the container's lifetime, since the test stack is per-worktree
(TASK-2708) and `make test-pg` tears it down with -v.

Codex round 2 (enumeration-shaped, aimed at the two helpers' divergence) found
one real P2: the in-package helper's FAILURE path dropped its half-built clone
without WITH (FORCE), while the twin's used it. NewPostgres can fail with its
pool already open, since a migration error arrives after the connection does,
and a plain DROP is refused while a session is attached — so the un-forced form
leaked a pad_test_* database on exactly the path that most needs cleaning up.
Pre-existing and asymmetric; corrected here because this change makes the two
helpers a matched pair. Class swept: all three remaining DROP DATABASE sites in
the repository now use WITH (FORCE), which is the whole population.

Round 2 also noted the helpers differ in admin-connection plumbing — the
in-package one goes through newPostgresConn (guarded driver, Ping, pool limits)
and the twin through a raw sql.Open. Accepted rather than unified: it predates
this change, the SQL either side is fixed and parameter-free, and the only
difference is connection validation, which the twin's t.Fatalf surfaces anyway.

Deliverable 3 (t.Parallel on the SQLite leg) follows as a separate package under
the same task, per the lead's split ruling.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR
2026-09-06 12:02:20 -04:00

26 lines
754 B
Go

package store
import (
"os"
"testing"
"golang.org/x/crypto/bcrypt"
)
// TestMain lowers the bcrypt cost for the whole package. See BUG-1371
// and internal/server/main_test.go for context — the same reasoning
// applies to internal/store tests that call CreateUser directly.
//
// It also tears down the process-wide template databases after the suite
// finishes: testStoreSQLite's template FILE (IDEA-1914, store_test.go) and
// testStorePostgres's template DATABASE (TASK-2900, same file). The Postgres
// teardown is a no-op when the suite ran on SQLite, since no template was
// built.
func TestMain(m *testing.M) {
SetBcryptCostForTesting(bcrypt.MinCost)
code := m.Run()
removeSQLiteTemplate()
removePostgresTemplate()
os.Exit(code)
}