Initial commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
|
||||
import { join, extname, normalize, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
export { startEphemeralPostgres, canRunEphemeralPostgres, type EphemeralPg } from "./postgres.js";
|
||||
import { startEphemeralPostgres, canRunEphemeralPostgres, type EphemeralPg } from "./postgres.js";
|
||||
|
||||
/** A Postgres available to tests: TEST_DATABASE_URL if set (e.g. a CI service),
|
||||
* else a throwaway local instance when we can run one. Sync check for skip. */
|
||||
export function hasTestPostgres(): boolean {
|
||||
return !!process.env.TEST_DATABASE_URL || canRunEphemeralPostgres();
|
||||
}
|
||||
|
||||
/** Acquire a test Postgres (env URL or ephemeral). Returns null if none available. */
|
||||
export async function acquireTestPostgres(): Promise<EphemeralPg | null> {
|
||||
if (process.env.TEST_DATABASE_URL) return { url: process.env.TEST_DATABASE_URL, stop: async () => {} };
|
||||
if (canRunEphemeralPostgres()) return startEphemeralPostgres();
|
||||
return null;
|
||||
}
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
/** repo root = packages/test-utils/src → ../../.. */
|
||||
export const REPO_ROOT = join(HERE, "..", "..", "..");
|
||||
export const FIXTURES_DIR = join(REPO_ROOT, "compiler", "fixtures");
|
||||
|
||||
const TYPES: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".css": "text/css",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".json": "application/json",
|
||||
".webmanifest": "application/manifest+json",
|
||||
".xml": "application/xml",
|
||||
".png": "image/png",
|
||||
".ico": "image/x-icon",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".svg": "image/svg+xml",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
".woff2": "font/woff2",
|
||||
".woff": "font/woff",
|
||||
};
|
||||
|
||||
/** Serve a directory of fixtures over loopback (zero external network). */
|
||||
export function serveDir(rootDir: string): Promise<{ url: string; close: () => Promise<void> }> {
|
||||
const server: Server = createServer((req, res) => {
|
||||
try {
|
||||
let p = decodeURIComponent((req.url || "/").split("?")[0]!);
|
||||
if (p === "/") p = "/index.html";
|
||||
const file = join(rootDir, normalize(p).replace(/^(\.\.[/\\])+/, ""));
|
||||
if (!existsSync(file) || statSync(file).isDirectory()) {
|
||||
res.writeHead(404);
|
||||
res.end("not found");
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
||||
res.end(readFileSync(file));
|
||||
} catch {
|
||||
res.writeHead(500);
|
||||
res.end("error");
|
||||
}
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === "object" && addr ? addr.port : 0;
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${port}`,
|
||||
close: () => new Promise<void>((r) => server.close(() => r())),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether a Playwright Chromium browser appears installed (browser tests skip if not). */
|
||||
export function hasChromium(): boolean {
|
||||
try {
|
||||
const base = process.env.PLAYWRIGHT_BROWSERS_PATH || join(homedir(), ".cache", "ms-playwright");
|
||||
if (!existsSync(base)) return false;
|
||||
return readdirSync(base).some((d) => d.startsWith("chromium"));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, existsSync, readdirSync, chmodSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createServer } from "node:net";
|
||||
|
||||
export type EphemeralPg = { url: string; stop: () => Promise<void> };
|
||||
|
||||
/** Locate the newest installed PostgreSQL bin dir (e.g. /usr/lib/postgresql/16/bin). */
|
||||
function findPgBin(): string | null {
|
||||
if (process.env.PG_BIN && existsSync(join(process.env.PG_BIN, "initdb"))) return process.env.PG_BIN;
|
||||
const root = "/usr/lib/postgresql";
|
||||
if (!existsSync(root)) return null;
|
||||
const versions = readdirSync(root)
|
||||
.filter((d) => /^\d+$/.test(d))
|
||||
.sort((a, b) => Number(b) - Number(a));
|
||||
for (const v of versions) {
|
||||
const bin = join(root, v, "bin");
|
||||
if (existsSync(join(bin, "initdb"))) return bin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether we can stand up a throwaway Postgres here: root (to su to a non-root
|
||||
* user, since postgres refuses to run as root) + the binaries present. */
|
||||
export function canRunEphemeralPostgres(): boolean {
|
||||
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
||||
return isRoot && findPgBin() !== null;
|
||||
}
|
||||
|
||||
function freePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srv = createServer();
|
||||
srv.listen(0, "127.0.0.1", () => {
|
||||
const addr = srv.address();
|
||||
const port = typeof addr === "object" && addr ? addr.port : 0;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
srv.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sh(cmd: string): { ok: boolean; out: string } {
|
||||
const r = spawnSync("bash", ["-lc", cmd], { encoding: "utf8" });
|
||||
return { ok: r.status === 0, out: (r.stdout || "") + (r.stderr || "") };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a throwaway PostgreSQL instance for tests (no Docker required). initdb's a
|
||||
* temp data dir, runs the server as a non-root user (`pgtest`) on a random loopback
|
||||
* port with trust auth, and returns a connection URL + a stop()/cleanup. Use only
|
||||
* after `canRunEphemeralPostgres()` returns true.
|
||||
*/
|
||||
export async function startEphemeralPostgres(): Promise<EphemeralPg> {
|
||||
const pgbin = findPgBin();
|
||||
if (!pgbin) throw new Error("no postgres binaries found");
|
||||
const user = "pgtest";
|
||||
|
||||
// Ensure the non-root runner exists.
|
||||
if (!sh(`id ${user}`).ok) {
|
||||
const r = sh(`useradd -m -s /bin/bash ${user}`);
|
||||
if (!sh(`id ${user}`).ok) throw new Error("could not create pgtest user: " + r.out);
|
||||
}
|
||||
|
||||
const data = mkdtempSync(join(tmpdir(), "pgdata-"));
|
||||
const sock = mkdtempSync(join(tmpdir(), "pgsock-"));
|
||||
// The runner user must own the data + socket dirs.
|
||||
chmodSync(data, 0o777);
|
||||
chmodSync(sock, 0o777);
|
||||
sh(`chown -R ${user} ${data} ${sock}`);
|
||||
|
||||
const init = sh(`su ${user} -c "${pgbin}/initdb -D ${data} -A trust -U postgres --no-sync"`);
|
||||
if (!init.ok) {
|
||||
rmSync(data, { recursive: true, force: true });
|
||||
rmSync(sock, { recursive: true, force: true });
|
||||
throw new Error("initdb failed: " + init.out.slice(-500));
|
||||
}
|
||||
|
||||
const port = await freePort();
|
||||
const start = sh(
|
||||
`su ${user} -c "${pgbin}/pg_ctl -D ${data} -o '-p ${port} -k ${sock} -c listen_addresses=127.0.0.1' -w -l ${data}/server.log start"`,
|
||||
);
|
||||
if (!start.ok) {
|
||||
const log = sh(`cat ${data}/server.log`).out;
|
||||
rmSync(data, { recursive: true, force: true });
|
||||
rmSync(sock, { recursive: true, force: true });
|
||||
throw new Error("pg_ctl start failed: " + start.out + "\n" + log.slice(-500));
|
||||
}
|
||||
|
||||
const url = `postgresql://postgres@127.0.0.1:${port}/postgres`;
|
||||
const stop = async (): Promise<void> => {
|
||||
sh(`su ${user} -c "${pgbin}/pg_ctl -D ${data} -w -m immediate stop"`);
|
||||
rmSync(data, { recursive: true, force: true });
|
||||
rmSync(sock, { recursive: true, force: true });
|
||||
};
|
||||
return { url, stop };
|
||||
}
|
||||
Reference in New Issue
Block a user