Files
dpsapi/e2e/harness/global-teardown.ts
T
bryanpedini fe0cd4bf6d 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.
2026-10-06 10:52:25 +02:00

102 lines
4.1 KiB
TypeScript

// 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`);
}