test(e2e): add playwright e2e harness
Process and API-only Playwright harness: binary and image setup, server/CLI runners, fake Docker Engine socket, read-only database inspection, secret redaction in reports, seeded fuzzing, leak detection and harness self-checks.
This commit is contained in:
1 parent
a02eef13a7
commit
fe0cd4bf6d
33 files changed
+6370
No files matched your search
@@ -0,0 +1,101 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
/**
|
||||
* Run-level leak detection (runner process). Throwing here makes `playwright test` exit non-zero
|
||||
* (verified with Playwright 1.63), so any leak fails the build:
|
||||
* - containers / networks / volumes labelled dpsapi.e2e=<runId>;
|
||||
* - live processes whose environment carries DPSAPI_E2E_PROC=<runId>;
|
||||
* - leftover per-test workspaces.
|
||||
* Leaked resources are cleaned up before failing.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { LABEL_RUN, PROC_MARKER_ENV } from './env';
|
||||
|
||||
function docker(args: string[]): { code: number | null; out: string; err: string } {
|
||||
const r = spawnSync('docker', args, { encoding: 'utf8', timeout: 120_000 });
|
||||
return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '' };
|
||||
}
|
||||
|
||||
function lines(s: string): string[] {
|
||||
return s.split('\n').map((l) => l.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/** PIDs of live processes whose environ contains PROC_MARKER_ENV=<runId>. */
|
||||
export function markedProcesses(runId: string): Array<{ pid: number; cmdline: string }> {
|
||||
const marker = Buffer.from(`${PROC_MARKER_ENV}=${runId}\0`);
|
||||
const out: Array<{ pid: number; cmdline: string }> = [];
|
||||
for (const d of fs.readdirSync('/proc')) {
|
||||
if (!/^\d+$/.test(d) || Number(d) === process.pid) continue;
|
||||
try {
|
||||
const env = fs.readFileSync(`/proc/${d}/environ`);
|
||||
if (env.includes(marker)) {
|
||||
const stat = fs.readFileSync(`/proc/${d}/stat`, 'utf8');
|
||||
if (/^\d+ \(.*\) Z /.test(stat)) continue; // zombie: already exited, waiting to be reaped
|
||||
out.push({ pid: Number(d), cmdline: fs.readFileSync(`/proc/${d}/cmdline`, 'utf8').split('\0').join(' ').trim() });
|
||||
}
|
||||
} catch {
|
||||
/* gone or not ours */
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default async function globalTeardown(): Promise<void> {
|
||||
const runId = process.env.DPSAPI_E2E_RUN_ID;
|
||||
const runDir = process.env.DPSAPI_E2E_RUN_DIR;
|
||||
if (!runId || !runDir) return;
|
||||
const leaks: string[] = [];
|
||||
|
||||
// Containers
|
||||
const ps = docker(['ps', '-a', '--filter', `label=${LABEL_RUN}=${runId}`, '--format', '{{.Names}}']);
|
||||
if (ps.code !== 0) leaks.push(`cannot list containers: ${ps.err.trim()}`);
|
||||
const containers = lines(ps.out);
|
||||
if (containers.length) {
|
||||
leaks.push(`containers left behind: ${containers.join(', ')}`);
|
||||
docker(['rm', '-f', '-v', ...containers]);
|
||||
}
|
||||
// Networks and volumes (phase-2 suites may create them)
|
||||
for (const kind of ['network', 'volume'] as const) {
|
||||
const r = docker([kind, 'ls', '-q', '--filter', `label=${LABEL_RUN}=${runId}`]);
|
||||
const ids = lines(r.out);
|
||||
if (ids.length) {
|
||||
leaks.push(`${kind}s left behind: ${ids.join(', ')}`);
|
||||
docker([kind, 'rm', ...ids]);
|
||||
}
|
||||
}
|
||||
|
||||
// Processes
|
||||
const procs = markedProcesses(runId);
|
||||
if (procs.length) {
|
||||
leaks.push(`processes left running: ${procs.map((p) => `${p.pid} (${p.cmdline.slice(0, 120)})`).join('; ')}`);
|
||||
for (const p of procs) {
|
||||
try {
|
||||
process.kill(p.pid, 'SIGKILL');
|
||||
} catch {
|
||||
/* gone */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspaces
|
||||
const ws = path.join(runDir, 'ws');
|
||||
const left = fs.existsSync(ws) ? fs.readdirSync(ws) : [];
|
||||
if (left.length) leaks.push(`temporary workspaces left behind: ${left.join(', ')}`);
|
||||
try {
|
||||
fs.rmSync(runDir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
// root-owned leftovers (image tests): remove through a throwaway container
|
||||
const name = `dpsapi-e2e-${runId}-final-rm`;
|
||||
docker(['run', '--rm', '--name', name, '--label', `${LABEL_RUN}=${runId}`, '--network', 'none', '-v', `${runDir}:/victim`, process.env.DPSAPI_E2E_ALPINE || 'alpine:3.22', 'sh', '-c', 'find /victim -mindepth 1 -delete']);
|
||||
fs.rmSync(runDir, { recursive: true, force: true });
|
||||
if (fs.existsSync(runDir)) leaks.push(`cannot remove run directory ${runDir}: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (leaks.length) {
|
||||
throw new Error(`[dpsapi-e2e] resource leaks detected for run ${runId} (cleaned up, failing the run):\n- ${leaks.join('\n- ')}`);
|
||||
}
|
||||
console.log(`[dpsapi-e2e] run ${runId}: no leaked containers, processes or workspaces`);
|
||||
}
|
||||
Reference in new issue
Block a user