import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; export type PostgresFixture = { projectName: string; databaseEnvFile: string; hostPort: number; connectionUrl(role: string, password: string): string; psql(sql: string): string; psqlAs(role: string, password: string, sql: string): string; stop(): void; }; const databaseEnvironment = `POSTGRES_DB=jyotisha POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres-test-password SCHEMA_OWNER_PASSWORD=schema-owner-test-password IDENTITY_RUNTIME_PASSWORD=identity-runtime-test-password APP_RUNTIME_PASSWORD=app-runtime-test-password SERVICE_RUNTIME_PASSWORD=service-runtime-test-password ADMIN_RUNTIME_PASSWORD=admin-runtime-test-password MIGRATION_RUNNER_PASSWORD=migration-runner-test-password BACKUP_READER_PASSWORD=backup-reader-test-password STAGING_BACKUP_ENCRYPTION_KEY=staging-backup-test-password SCHEMA_DATABASE_URL=postgresql://schema_owner:schema-owner-test-password@postgres:5432/jyotisha `; function isPortAvailable(port: number): boolean { return ( spawnSync( process.execPath, [ "-e", `const server = require("node:net").createServer(); server.once("error", () => process.exit(1)); server.listen({ host: "127.0.0.1", port: Number(process.argv[1]) }, () => server.close(() => process.exit(0)), );`, String(port), ], { stdio: "ignore" }, ).status === 0 ); } type PortReservation = { port: number; directory: string }; function reserveAvailablePort(): PortReservation { for (let port = 55432; port <= 55531; port += 1) { const directory = join(tmpdir(), `jyotisha-postgres-port-${port}.lock`); try { mkdirSync(directory); } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") continue; throw error; } if (isPortAvailable(port)) { return { port, directory }; } rmSync(directory, { force: true, recursive: true }); } throw new Error("no available PostgreSQL test port in 55432..55531"); } function releasePort(reservation: PortReservation): void { rmSync(reservation.directory, { force: true, recursive: true }); } const DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES = 2; const FIXTURE_SLOT_WAIT_MS = 5 * 60 * 1000; type FixtureSlot = { id: number; release(): void }; function maxConcurrentPostgresFixtures(): number { const raw = process.env.JYOTISHA_POSTGRES_MAX_FIXTURES; if (raw === undefined || raw === "") { return DEFAULT_MAX_CONCURRENT_POSTGRES_FIXTURES; } const parsed = Number(raw); if (!Number.isInteger(parsed) || parsed < 1) { throw new Error("JYOTISHA_POSTGRES_MAX_FIXTURES must be a positive integer"); } return parsed; } function fixtureSlotsRoot(): string { return join(tmpdir(), "jyotisha-postgres-slots"); } function fixtureSlotDirectory(id: number): string { return join(fixtureSlotsRoot(), `slot-${id}`); } function pidIsAlive(pid: number): boolean { if (!Number.isInteger(pid) || pid <= 0) { return false; } try { process.kill(pid, 0); return true; } catch { return false; } } function tryClaimSlot(id: number): boolean { const directory = fixtureSlotDirectory(id); mkdirSync(fixtureSlotsRoot(), { recursive: true }); try { mkdirSync(directory); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; try { const pid = Number(readFileSync(join(directory, "pid"), "utf8")); if (pidIsAlive(pid)) return false; rmSync(directory, { force: true, recursive: true }); mkdirSync(directory); } catch { return false; } } writeFileSync(join(directory, "pid"), String(process.pid)); return true; } function acquireFixtureSlot(): FixtureSlot { const limit = maxConcurrentPostgresFixtures(); const deadline = Date.now() + FIXTURE_SLOT_WAIT_MS; while (Date.now() < deadline) { for (let id = 0; id < limit; id += 1) { if (tryClaimSlot(id)) { return { id, release() { rmSync(fixtureSlotDirectory(id), { force: true, recursive: true }); }, }; } } Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50); } throw new Error( `timed out waiting for a PostgreSQL fixture slot (${limit} concurrent compose networks). Docker address pools cannot host one network per parallel test file.`, ); } export function startPostgresFixture(): PostgresFixture { const slot = acquireFixtureSlot(); const projectName = `jyotisha-postgres-${process.pid}-${Date.now()}`; let temporaryDirectory: string; try { temporaryDirectory = mkdtempSync(join(tmpdir(), "jyotisha-postgres-")); } catch (error) { slot.release(); throw error; } const databaseEnvFile = join(temporaryDirectory, "database.env"); let portReservation: PortReservation; try { portReservation = reserveAvailablePort(); } catch (error) { rmSync(temporaryDirectory, { force: true, recursive: true }); slot.release(); throw error; } const hostPort = portReservation.port; const composeArguments = [ "compose", "--project-name", projectName, "--env-file", databaseEnvFile, "-f", "../deploy/docker-compose.postgres.yml", "-f", "../deploy/docker-compose.postgres-ci.yml", ]; const environment = { ...process.env, DATABASE_ENV_FILE: databaseEnvFile, POSTGRES_HOST_PORT: String(hostPort), }; try { writeFileSync(databaseEnvFile, databaseEnvironment, { mode: 0o600 }); chmodSync(databaseEnvFile, 0o600); execFileSync( "docker", [...composeArguments, "up", "-d", "--wait", "postgres"], { env: environment, stdio: "inherit" }, ); } catch (error) { try { try { // The teardown below deletes the only account of why the server refused to start. execFileSync( "docker", [...composeArguments, "logs", "--no-color", "--tail", "80", "postgres"], { env: environment, stdio: "inherit" }, ); } catch { // Preserve the original startup failure. } try { execFileSync( "docker", [...composeArguments, "down", "-v", "--remove-orphans"], { env: environment, stdio: "inherit" }, ); } catch { // Preserve the original startup failure. } } finally { releasePort(portReservation); rmSync(temporaryDirectory, { force: true, recursive: true }); slot.release(); } throw error; } return { projectName, databaseEnvFile, hostPort, connectionUrl(role, password) { return `postgresql://${role}:${password}@127.0.0.1:${hostPort}/jyotisha`; }, psql(sql) { return execFileSync( "docker", [...composeArguments, "exec", "-T", "postgres", "psql", "-U", "postgres", "-d", "jyotisha", "-Atc", sql], { encoding: "utf8", env: environment }, ) .trim() .replace(/(^|:)false(?=:|$)/gm, "$1f"); }, psqlAs(role, password, sql) { return execFileSync( "docker", [ ...composeArguments, "exec", "-T", "-e", `PGPASSWORD=${password}`, "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", role, "-d", "jyotisha", "-Atc", sql, ], { encoding: "utf8", env: environment }, ).trim(); }, stop() { try { execFileSync( "docker", [...composeArguments, "down", "-v", "--remove-orphans"], { env: environment, stdio: "inherit" }, ); } finally { releasePort(portReservation); rmSync(temporaryDirectory, { force: true, recursive: true }); slot.release(); } }, }; }