mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(integration): run files in parallel, one cloned database per worker
Global setup migrates one template database and reads what a fresh install holds: its schemas, its tables and the rows the migrations seed. Each Vitest worker clones the template on its first file with CREATE DATABASE ... TEMPLATE and keeps the clone for the rest of the run, so files run in parallel without seeing each other's rows, advisory locks or queue schema. Schemas created at runtime (pg-boss, migration probes) are dropped when the next file starts. The truncate list is the catalogue now, every table in public except the migration bookkeeping, instead of a hand-kept list of 42 of 166 tables. The seeded catalogues are put back after each truncate in foreign-key order, so a test starts from what a fresh install has. The settings singleton is the one seeded table a test starts without, declared with its reason. next/headers and the boot compatibility probe are mocked once in the setup file. Two lock-wait probes now look at their own database only, since other workers' backends share pg_stat_activity.
This commit is contained in:
1 parent
fdd0f50130
commit
a6787cfc03
9 files changed
+451
-161
No files matched your search
@@ -12,6 +12,18 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { truncateAllTables } from "../../tests/integration/setup";
|
||||
import type { IntegrationDatabasePlan } from "../../tests/integration/worker-database";
|
||||
|
||||
/** What global setup reads off the migrated template, cut down to the shape. */
|
||||
const PLAN: IntegrationDatabasePlan = {
|
||||
templateSchemas: ["public"],
|
||||
tables: ["app_settings", "mood_tag_categories", "mood_tags", "users"],
|
||||
reference: [
|
||||
{ table: "app_settings", rowsJson: '[{"id":"singleton"}]' },
|
||||
{ table: "mood_tag_categories", rowsJson: '[{"id":"c1"}]' },
|
||||
{ table: "mood_tags", rowsJson: '[{"id":"t1","category_id":"c1"}]' },
|
||||
],
|
||||
};
|
||||
|
||||
function deadlock(): Error & { code: string } {
|
||||
const error = new Error("deadlock detected") as Error & { code: string };
|
||||
@@ -19,45 +31,61 @@ function deadlock(): Error & { code: string } {
|
||||
return error;
|
||||
}
|
||||
|
||||
function fakeClient(executeRaw: ReturnType<typeof vi.fn>) {
|
||||
const empty = vi.fn().mockResolvedValue([]);
|
||||
return {
|
||||
$executeRawUnsafe: executeRaw,
|
||||
moodTag: { findMany: empty, createMany: vi.fn() },
|
||||
moodTagCategory: { findMany: empty, createMany: vi.fn() },
|
||||
} as unknown as Parameters<typeof truncateAllTables>[0];
|
||||
type Execute = (query: string, ...values: unknown[]) => Promise<number>;
|
||||
|
||||
function fakeClient(executeRaw: Execute) {
|
||||
return { $executeRawUnsafe: executeRaw };
|
||||
}
|
||||
|
||||
describe("truncateAllTables", () => {
|
||||
it("retries a deadlocked truncate and succeeds once the lock clears", async () => {
|
||||
const executeRaw = vi
|
||||
.fn()
|
||||
.fn<Execute>()
|
||||
.mockRejectedValueOnce(deadlock())
|
||||
.mockRejectedValueOnce(deadlock())
|
||||
.mockResolvedValue(0);
|
||||
|
||||
await truncateAllTables(fakeClient(executeRaw));
|
||||
await truncateAllTables(fakeClient(executeRaw), PLAN);
|
||||
|
||||
expect(executeRaw).toHaveBeenCalledTimes(3);
|
||||
// Three truncate attempts, then the two seeded tables put back.
|
||||
expect(executeRaw).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("empties every catalogued table and puts the seeded rows back in key order", async () => {
|
||||
const executeRaw = vi.fn<Execute>().mockResolvedValue(0);
|
||||
|
||||
await truncateAllTables(fakeClient(executeRaw), PLAN);
|
||||
|
||||
const statements = executeRaw.mock.calls.map((call) => String(call[0]));
|
||||
expect(statements[0]).toBe(
|
||||
'TRUNCATE TABLE "app_settings", "mood_tag_categories", "mood_tags", "users" RESTART IDENTITY CASCADE;',
|
||||
);
|
||||
// The settings singleton starts empty; the catalogues come back, the
|
||||
// category before the tag that references it.
|
||||
expect(statements.slice(1)).toEqual([
|
||||
expect.stringContaining('INSERT INTO "mood_tag_categories"'),
|
||||
expect.stringContaining('INSERT INTO "mood_tags"'),
|
||||
]);
|
||||
expect(executeRaw.mock.calls[2][1]).toBe(PLAN.reference[2].rowsJson);
|
||||
});
|
||||
|
||||
it("gives up on an error that is not a deadlock, on the first attempt", async () => {
|
||||
const other = new Error("permission denied") as Error & { code: string };
|
||||
other.code = "42501";
|
||||
const executeRaw = vi.fn().mockRejectedValue(other);
|
||||
const executeRaw = vi.fn<Execute>().mockRejectedValue(other);
|
||||
|
||||
await expect(truncateAllTables(fakeClient(executeRaw))).rejects.toThrow(
|
||||
"permission denied",
|
||||
);
|
||||
await expect(
|
||||
truncateAllTables(fakeClient(executeRaw), PLAN),
|
||||
).rejects.toThrow("permission denied");
|
||||
expect(executeRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("stops retrying a deadlock that never clears", async () => {
|
||||
const executeRaw = vi.fn().mockRejectedValue(deadlock());
|
||||
const executeRaw = vi.fn<Execute>().mockRejectedValue(deadlock());
|
||||
|
||||
await expect(truncateAllTables(fakeClient(executeRaw))).rejects.toThrow(
|
||||
"deadlock detected",
|
||||
);
|
||||
await expect(
|
||||
truncateAllTables(fakeClient(executeRaw), PLAN),
|
||||
).rejects.toThrow("deadlock detected");
|
||||
expect(executeRaw).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user