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,120 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
/**
|
||||
* Per-test temporary workspaces and TCP port allocation.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { ALPINE_IMAGE, LABEL_RUN, RESOURCE_PREFIX, runId, workspacesDir } from './env';
|
||||
import { HarnessError } from './errors';
|
||||
import { runProcess, toolEnv } from './proc';
|
||||
|
||||
export class Workspace {
|
||||
/** Default SQLite path for the test (`<dir>/dpsapi.db`); nothing is created there by the harness. */
|
||||
readonly dbPath: string;
|
||||
|
||||
constructor(readonly dir: string) {
|
||||
this.dbPath = path.join(dir, 'dpsapi.db');
|
||||
}
|
||||
|
||||
/** Creates a fresh workspace directory under the run's workspaces dir. */
|
||||
static create(key: string): Workspace {
|
||||
fs.mkdirSync(workspacesDir(), { recursive: true });
|
||||
const dir = fs.mkdtempSync(path.join(workspacesDir(), `${key}-`));
|
||||
return new Workspace(dir);
|
||||
}
|
||||
|
||||
/** Absolute path inside the workspace (not created). */
|
||||
path(...parts: string[]): string {
|
||||
return path.join(this.dir, ...parts);
|
||||
}
|
||||
|
||||
/** Creates (recursively) and returns a directory inside the workspace. */
|
||||
mkdir(...parts: string[]): string {
|
||||
const p = this.path(...parts);
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
return p;
|
||||
}
|
||||
|
||||
writeFile(name: string, data: string | Buffer, mode?: number): string {
|
||||
const p = this.path(name);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, data, mode === undefined ? undefined : { mode });
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Removes the workspace, falling back to a throwaway container for root-owned files. */
|
||||
async remove(): Promise<void> {
|
||||
await forceRemoveDir(this.dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a directory tree. Files written by containers running as root cannot be removed by
|
||||
* the host user; for those, the contents are deleted from a throwaway, labelled alpine container.
|
||||
*/
|
||||
export async function forceRemoveDir(dir: string): Promise<void> {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
return;
|
||||
} catch (e) {
|
||||
const code = (e as NodeJS.ErrnoException).code;
|
||||
if (code !== 'EACCES' && code !== 'EPERM' && code !== 'ENOTEMPTY') throw e;
|
||||
}
|
||||
const name = `${RESOURCE_PREFIX}${runId()}-rm-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const r = await runProcess(
|
||||
'docker',
|
||||
['run', '--rm', '--name', name, '--label', `${LABEL_RUN}=${runId()}`, '--network', 'none', '-v', `${dir}:/victim`, ALPINE_IMAGE, 'sh', '-c', 'find /victim -mindepth 1 -delete'],
|
||||
{ env: toolEnv(), timeoutMs: 60_000, label: 'docker cleanup container' },
|
||||
);
|
||||
if (r.exitCode !== 0) throw new HarnessError('infrastructure', `cannot clean ${dir} via container: ${r.stderr}`);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
/** Lowest port used by the allocator; windows of PORT_WINDOW ports per parallel slot, below the ephemeral range. */
|
||||
const PORT_BASE = 20_000;
|
||||
const PORT_WINDOW = 500;
|
||||
const PORT_SLOTS = 24; // 20000..31999, below Linux's default ephemeral range (32768+)
|
||||
let portCursor = 0;
|
||||
|
||||
/** True when `port` can currently be bound on every local address (checked dual-stack). */
|
||||
export function isPortFree(port: number): Promise<boolean> {
|
||||
const tryListen = (host: string, ipv6Only: boolean) =>
|
||||
new Promise<'free' | 'busy' | 'unsupported'>((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.once('error', (e: NodeJS.ErrnoException) => {
|
||||
resolve(e.code === 'EADDRINUSE' || e.code === 'EACCES' ? 'busy' : 'unsupported');
|
||||
});
|
||||
srv.listen({ host, port, ipv6Only, exclusive: true }, () => srv.close(() => resolve('free')));
|
||||
});
|
||||
return tryListen('::', false).then(async (r) => {
|
||||
if (r === 'unsupported') return (await tryListen('0.0.0.0', false)) === 'free';
|
||||
return r === 'free';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a currently free TCP port. Concurrent workers draw from disjoint windows keyed
|
||||
* by their parallel index, so two running tests never get the same port from the allocator;
|
||||
* the bind check additionally skips ports used by anything else on the host.
|
||||
*/
|
||||
export async function allocatePort(parallelIndex: number): Promise<number> {
|
||||
const slot = parallelIndex % PORT_SLOTS;
|
||||
for (let attempt = 0; attempt < PORT_WINDOW; attempt++) {
|
||||
const port = PORT_BASE + slot * PORT_WINDOW + (portCursor++ % PORT_WINDOW);
|
||||
if (await isPortFree(port)) return port;
|
||||
}
|
||||
throw new HarnessError('infrastructure', `no free port in window of parallel slot ${slot}`);
|
||||
}
|
||||
|
||||
/** Holds a port busy (listening socket) until close() — for "address already in use" tests. */
|
||||
export async function occupyPort(port: number, host = '127.0.0.1'): Promise<{ close: () => Promise<void> }> {
|
||||
const srv = net.createServer((s) => s.destroy());
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
srv.once('error', reject);
|
||||
srv.listen({ host, port, exclusive: true }, () => resolve());
|
||||
});
|
||||
return { close: () => new Promise<void>((r) => srv.close(() => r())) };
|
||||
}
|
||||
Reference in new issue
Block a user