Repository navigation
test: share one Postgres server per package across integration tests - #1400
Merged
Merged
Conversation
…rver The migrations package tests need a database with nothing in it, so they cannot use RequireMigratedDatabase and still start a container each (92 per CI run). RequireEmptyDatabase gives them a fresh database cloned from template0 on the same per-binary server, dropped when the test ends, with the same skip/fail rules and return shape as RequireMigratedDatabase. Templates are now keyed by absolute path, so a package that names the migrations directory both relatively and via runtime.Caller migrates one template instead of two. SetupPostgresContainer and RequirePostgresContainer now say to prefer the shared helpers and when a dedicated server is still right. They are not marked Deprecated: the shared server is built on them and the dedicated cases are legitimate, so SA1019 on every call would be noise. DEVELOPMENT.md shows the shared pattern instead of a per-test container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
internal/api started 151 PostgreSQL containers per integration run, each paying about 2s to start plus the full 127-migration chain. Every call site now takes a fresh database from testhelpers.RequireMigratedDatabase (or RequireEmptyDatabase for the test that walks migrations 100 -> 101 itself) on one server per test binary. Locally the package goes from 927s to 119s and from 151 containers to 1. The package already owns an untagged TestMain for the no-network guard, so main_integration_test.go registers the shared server's teardown via an afterTests hook rather than adding a second TestMain; both the tagged and untagged builds compile. The lock-wait poll in the plan concurrency test now filters pg_stat_activity to the current database, so a backend from another test's database on the shared server cannot satisfy it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of the five store tests started its own Postgres container and ran all migrations into it, about 10s of overhead per test (issue LeanerCloud#982). setupStore now takes a fresh copy of the migrated template from testhelpers.RequireMigratedDatabase, and a TestMain stops the shared server after the last test. Assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four integration tests each started a dedicated container (issue LeanerCloud#982). None needs a schema or server-level isolation: advisory lock keys and the pg_locks holder query are per database, the pool-floor test exhausts the client pool rather than the server's max_connections, and the query-logging test creates its own table. They now take an empty database from testhelpers.RequireEmptyDatabase on one shared server, and a TestMain stops it after the last test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cleanupTestDB moved to testhelpers.RequireMigratedDatabase in e0384ad (it went in with the internal/database commit by mistake): the four tests and subtests now clone one migrated template instead of each starting and migrating a container (issue LeanerCloud#982). This adds the TestMain that stops the shared server after the last test. It goes in main_integration_test.go rather than a file of its own because .gitignore's "cleanup-lambda" entry, meant for the built binary, also matches cmd/cleanup-lambda/, so a new test file there would stay untracked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of the 19 analytics integration tests started its own Postgres container and applied the full migration chain, which was most of the package's run time. They now take a fresh database of their own from testhelpers.RequireMigratedDatabase (a TEMPLATE clone on one container per test binary), or RequireEmptyDatabase for the two Close tests, which never migrated. A TestMain stops the shared container after the last test. None of these tests touches server-wide state, so none keeps a dedicated container. Assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 13 server integration tests that needed Postgres each started a container and migrated it. They now get a fresh database of their own from testhelpers.RequireMigratedDatabase on one container per test binary, and a TestMain stops it after the last test. TestServerIntegration used internal/testutil's separate container helper only to export connection settings; it now takes an empty database from RequireEmptyDatabase, exports the same DB_* variables and pings it instead of only logging a connection string. waitersOnLadderLock counted waiting advisory locks across the whole server. It is now scoped to the test's database, since other tests' databases share the server. That makes the check stricter, not looser. No test here touches server-wide state, so none keeps a dedicated container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every scheduler integration test started its own PostgreSQL container and applied the full migration chain, 29 times per run (the completeness table test alone does it once per subtest). Use testhelpers.RequireMigratedDatabase instead: one container per test binary, migrations applied once into a template, and each test gets a fresh CREATE DATABASE ... TEMPLATE copy that is dropped when it ends. A TestMain stops the shared container after the last test (LeanerCloud#982). None of these tests touches server-wide state (roles, server settings, other sessions, connection limits), so none needs a dedicated server. Assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setupAuthTestDB started a PostgreSQL container and applied every migration for each of the 23 DB-backed auth tests. It now takes a fresh database from testhelpers.RequireMigratedDatabase on one container shared by the test binary, cloned from a template migrated once, with no bootstrap admin seeded as before. A TestMain stops the shared container after the last test (LeanerCloud#982). A migration that will not apply still fails the test rather than skipping it (#1597). No auth test touches server-wide state, so none needs a dedicated server. Assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every purchase integration test started its own postgres:16-alpine container and applied all migrations to it: 116 containers per run and most of the package's ~1000s integration time. rampStepStore, the helper behind nearly all of them, now takes a fresh database from testhelpers.RequireMigratedDatabase (issue LeanerCloud#982). The two tests that walk the migration chain themselves (Azure pricing staleness and the never-started ramp ledger migration) use RequireEmptyDatabase. The binary now starts one container and clones each test's database from a template migrated once. TestMain lives in the untagged main_nonet_test.go, so it gains an afterTests hook that main_integration_test.go fills to stop the shared server, as internal/api does. The lock-wait poll in TestCompletePlanStepBlocksOnTheRowLockAndSeesTheWinner now filters pg_stat_activity to current_database(), so a backend in another test's database cannot satisfy it. That makes the check stricter. No test needed a dedicated server: none creates roles, alters system settings, terminates other sessions or counts server-wide connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p tests Every integration test in the migrations package started its own PostgreSQL container (92 per run), about 2s each before the test did any work. Add a TestMain that stops the shared server from issue LeanerCloud#982 and move the up/down, dirty-state, cancellation, ensure_admin_user and bootstrap logging tests onto it. These tests are about the migration chain itself, so they take an empty database from RequireEmptyDatabase and keep every MigrateToVersion, RunMigrations and RollbackMigrations call they made before. The cancellation tests already created a fresh database per test on a server; migrationDB now uses an empty shared-server database for that unless CUDLY_TEST_ADMIN_DSN points at an external server. TestPGPasswordAndKeywordDSN_ConnectWithHostilePassword keeps a dedicated container: ALTER ROLE ... PASSWORD changes the cluster-wide role every shared-server database logs in as. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-migration tests (000053 through 000134) each started their own PostgreSQL container. Move them onto the package's shared server (issue LeanerCloud#982). Tests that pin a version, seed rows, and then step up or down take an empty database from RequireEmptyDatabase and walk the chain exactly as before. Tests whose first step was a full RunMigrations purely as a fixture (000064's seed check, 000065's last-admin trigger tests, 000096's seed and idempotency checks, 000102, 000103) take a clone of the migrated template from RequireMigratedDatabase instead; the template is built by the same RunMigrations call, so their starting state is unchanged. No test here touches server-wide state. 000132's ALTER DATABASE targets current_database(), and its pg_stat_activity poll filters by pid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Warning Review limit reached
This review includes 110 billable files and costs up to $27.50. View limit details
Comment |
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context), done after the merge: APPROVE Blocking: none. Non-blocking:
Attacked (held up):
The three non-blocking testhelpers items (the template built under the first caller's context, the health probe running on every call, and |
IAmJSD
added a commit
that referenced
this pull request
Oct 25, 2026
…st server (#1401) * fix(testhelpers): harden the shared test server for parallel callers A review of #1400 found three issues in shared.go that bite harder once tests on the shared server run in parallel: - The migrated template was built under the first caller's context and its result, errors included, is cached for the whole binary. A first caller that was canceled or hit its deadline mid-build failed every later RequireMigratedDatabase call. The build now runs detached from the caller, like the container start, with its own 10-minute backstop. - The Docker health probe ran on every call. Under CI load a probe that times out would skip a test whose database is reachable, a #1597-style false green. It now runs only until the shared server is up. - dropDatabase read s.admin without the mutex while TerminateSharedPostgres sets it to nil, so a cleanup after Terminate could race or panic. It now holds the mutex and treats a stopped server as nothing left to drop. Each fix has a test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(testhelpers): raise max_connections on the shared test server Tests on the shared server are about to run in parallel. Each running test can hold a pool of up to 10 connections plus the migrator's own and a spare session, about 13, and the admin pool adds up to 10. At the default -parallel (GOMAXPROCS, 16 on a dev machine) that worst case is over 200, past PostgreSQL's default max_connections of 100. Raising the server's limit to 500 is the least invasive fix: every test's pool keeps its size, so no test that holds several connections at once can start waiting on its own pool. Dedicated containers from SetupPostgresContainer are unchanged; only the shared server gets the flag, through an unexported variant that takes extra container options. Measured peak in the migrations package: 50 connections at -parallel=16, 21 at -parallel=4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(migrations): run the integration tests in parallel Since #1400 every integration test here gets its own database on the shared server, so most of them can run at once. The package was the slowest integration package on CI (190s), mostly tests walking the migration chain one after another. 62 of 69 top-level tests now call t.Parallel, and table subtests do too where each subtest creates its own database and the parent holds none. Subtests that share their parent's database stay sequential. Seven tests stay sequential, each with a one-line reason: - t.Setenv: TestMigrations_AutoHealDirty, and TestPGPasswordAndKeywordDSN_ConnectWithHostilePassword (which also keeps its dedicated server for ALTER ROLE) - swaps os.Stdout and the standard logger: TestAdminBootstrap_LogsToStderr_NotStdout - wall-clock deadline assertions: the four deadline and cancel tests in migrate_cancel_integration_test.go Advisory locks and pg_stat_activity checks in the parallel tests are scoped to the test's own database (advisory locks are per database; the pg_stat_activity queries filter by PID or current_database()), so other databases cannot affect them. Local wall time of the -race test binary: 252s sequentially on origin/main, 111s at -parallel=4, 81s at -parallel=16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 25, 2026
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 25, 2026
main's LeanerCloud#1400 added main_integration_test.go with a TestMain that stops the shared PostgreSQL container after the run. This branch added an identical TestMain to helpers_test.go, so after merging main the integration build of the package declared TestMain twice and failed to compile. Keep main's TestMain, which does the same teardown, and restore helpers_test.go to main's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 25, 2026
Bring the branch up to date with main. Conflicted files: - internal/auth/main_integration_test.go (add/add): both sides added the same integration-only TestMain that stops the shared Postgres server (main via LeanerCloud#1400). They differ only in the doc comment. Took main's file unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 25, 2026
Main (LeanerCloud#1400) moved setupAuthTestDB onto the shared Postgres server (testhelpers.RequireMigratedDatabase), which is what the stress variant did by hand to avoid starting its own container. Use the shared helper and drop the comment that contrasted the two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 25, 2026
This branch is stacked on fix/issue-686 (LeanerCloud#1276). That branch was brought up to date with origin/main in ee67abd, so merging it here brings in main and the LeanerCloud#686 conflict resolutions in one step. Conflicted files: - internal/api/main_nonet_test.go, internal/api/main_integration_test.go: both sides added a teardown hook list for the shared Postgres server (this branch: afterAllTests; main LeanerCloud#1400: afterTests). Took main's files unchanged; this branch only needed the hook, which main now provides. - internal/api/handler_history.go, getHistory: main (LeanerCloud#1200) loads the account scope once (loadHistoryScope), filters rows with scope.filter, defers the stale-approval sweep until after the summary aggregate, and returns the scope-aware SQL summary with SummaryComplete. This branch pages the stream (fetchHistoryPage) and lists the actionable class only on the first page. Combined: the page is fetched first, the scope is loaded once and applied to the actionable rows, staleExecs is declared outside the Cursor == nil branch so the sweep stays deferred (and does not run on cursor pages), and the response carries main's Summary/SummaryComplete plus this branch's paging fields. The summary is now whole-scope on every page. - internal/api/handler_history.go, fetchPurchaseHistory: main replaced the GetAllPurchaseHistory/GetPurchaseHistoryFiltered split with the shared historyFilters.purchaseHistoryFilter; this branch reads one keyset batch through ListPurchaseHistoryPage. Kept both: purchaseHistoryFilter is kept (the summary uses it) and the batch read now starts from it with Limit set to the batch size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 25, 2026
Bring the branch up to date with main. Conflicted files: - internal/server/integration_test.go (imports only): main (LeanerCloud#1400) moved TestServerIntegration onto the shared Postgres server and added strconv; this branch replaced the 100ms sleep in TestScheduledTaskIntegration with an atomic call counter and added sync/atomic. Both sides dropped "time". Kept both imports; the test bodies merged cleanly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
that referenced
this pull request
Oct 25, 2026
…gration test (#1258) * test(auth): add a 50-caller stress variant of the bootstrap-race integration test TestIntegration_CreateAdminIfNone_ConcurrentBootstrapOnce_Stress fires 50 barrier-synchronized CreateAdminIfNone callers per round for 10 rounds and requires exactly one winner, no errors and exactly one Administrators-group row after each round. Removing the pg_advisory_xact_lock call, or switching it to pg_try_advisory_xact_lock, makes it fail with 2 to 10 winners in a round. The two-caller test and the stress variant share one helper. Workers only send outcomes over a channel and every assertion runs on the test goroutine, after all workers have finished. The stress variant takes its database from the shared container (testhelpers.RequireMigratedDatabase), and a new integration-only TestMain stops that container, as in internal/config. Closes #64 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth): set up the bootstrap stress test with setupAuthTestDB Main (#1400) moved setupAuthTestDB onto the shared Postgres server (testhelpers.RequireMigratedDatabase), which is what the stress variant did by hand to avoid starting its own container. Use the shared helper and drop the comment that contrasted the two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD
added a commit
that referenced
this pull request
Oct 26, 2026
…eness test (#1443) main's Integration Tests (migrations, rest) failed on 89e1140 with a data race in TestAWSRecommendationCompletenessPersistence/SP_empty. The test points the process-wide logger at a plain bytes.Buffer and reads it with String(). Since #1400 puts every subtest's database on one shared Postgres server, a connection pool from an earlier subtest can still be creating idle connections in a background goroutine (pgxpool createIdleResources), and its "context canceled" connect error is logged into the current subtest's buffer while the test reads it. The race detector fails the run. It's timing-dependent: the previous main run passed. Wrap the buffer in a small mutex-guarded type. Assertions unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Integration Tests job spends most of its ~17 minutes starting Postgres containers. Main's last green run started 459 of them, one per test (two per test in a few tables). Each costs about 2s to start, plus a full migration of all 127 migrations. A test that does nothing else takes 4-5s, so roughly 2000s of the run's 2784s of package time was setup.
The fix was already in the repo but almost unused:
testhelpers.RequireMigratedDatabase(#982) starts one server per test binary and migrates a template database once. Each test then gets its ownCREATE DATABASE ... TEMPLATEcopy, which takes milliseconds and is still a fresh, fully migrated database. Onlyinternal/configused it. This PR moves every other package onto it.What changed
testhelpers.RequireEmptyDatabase(ctx, t)(new): a fresh, empty database (from template0) on the same shared server, dropped when the test ends. It's for tests that need an unmigrated database: migration up/down/dirty/heal tests, step-by-step migration fixtures, and connection-only tests. Integration tests cover that it's empty, isolated, dropped on cleanup, and distinct for concurrent callers.Migrated templates are keyed by absolute path, so different relative paths to the same migrations directory share one template.
SetupPostgresContainer/RequirePostgresContainer: the doc comments now point to the shared helpers and say when a dedicated server is still right. Neither helper is removed.Converted packages, each with a TestMain that stops the shared server:
internal/apiinternal/purchaseinternal/database/postgres/migrationsinternal/schedulerinternal/authinternal/analyticsinternal/serverinternal/commitmentoptsinternal/databasecmd/cleanup-lambdaPackages that already had an untagged TestMain (api, purchase) get an
afterTestshook, so the untagged and integration builds both compile.Lock checks:
pg_stat_activitylock-wait polls are now limited tocurrent_database(), so another test's database on the same server can't satisfy them.One test keeps its own container:
TestPGPasswordAndKeywordDSN_ConnectWithHostilePassword(migrations). It changes the password of the login role every shared database uses. A comment at the call site says so. Every other test runs on the shared server. Each package was read for server-wide operations (roles,ALTER SYSTEM, connection limits, killing sessions, unscoped catalog queries), and none were found besides that one.Docs: the integration-test example in
docs/DEVELOPMENT.mdnow shows the shared pattern.Results
Whole suite, run locally the way CI runs it (
go test -race -tags=integration ./..., 16-core box):The wall-time cells come from different machines (the CI runner vs a 16-core box), so they aren't directly comparable. The container count is the real driver, and it doesn't depend on the machine.
Per package, compiled
-race -tags integrationtest binary. Containers were counted withdocker eventsper testcontainers session. Times were taken while other jobs loaded the machine, so they're noisy; the container counts are exact.What remains is mostly the tests themselves:
TestApplicationRateLimiterin serverTestOpenAPI403OnPermissionGatedRoutesin apiVerification
Run on this branch's combined tree, with each package's work merged together:
go build ./...,go vet ./...andgo vet -tags integration ./...: pass.go test -race -short ./...: pass.go test -race -tags=integration -timeout=30m ./...: pass, in 346s, with 15 containers.-shuffle=on(seeds in the commit notes): pass.scripts/test-git-secrets-allowlist.sh: 137 PASS.Notes for review
TestHandler_getConfig_SourceIdentity_AdminOnly(internal/api) reaches the AWS metadata endpoint when it runs early under some shuffle seeds, and the no-network guard then fails the binary. This reproduces on main..gitignore'scleanup-lambdaentry also ignores thecmd/cleanup-lambda/directory, so a new file there is silently untracked. That's why cleanup-lambda's TestMain sits in its existingmain_integration_test.go.internal/testutil.SetupPostgresContainernow has no callers and could be deleted in a follow-up.e0384ad1(internal/database) also carries the cleanup-lambda conversion because of that.gitignorerule. A squash merge makes this moot.internal/database/postgres/testhelpers/shared.go) and the migrations-package conversions in particular.🤖 Generated with Claude Code