Files
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

90 lines
2.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* Small deterministic PRNG (sfc32 seeded through a string hash) for reproducible fuzzing.
* The run seed comes from $DPSAPI_E2E_SEED (default DEFAULT_SEED); each test derives its own
* stream from the run seed and its title path, so results do not depend on scheduling.
*/
import crypto from 'node:crypto';
export class Rng {
private a: number;
private b: number;
private c: number;
private d: number;
/** `seed` is any string; equal seeds produce equal streams. */
constructor(readonly seed: string) {
const h = crypto.createHash('sha256').update(seed, 'utf8').digest();
this.a = h.readUInt32LE(0);
this.b = h.readUInt32LE(4);
this.c = h.readUInt32LE(8);
this.d = h.readUInt32LE(12);
for (let i = 0; i < 12; i++) this.nextUint32();
}
/** Independent child stream, e.g. rng.fork('descriptions'). */
fork(label: string): Rng {
return new Rng(`${this.seed}/${label}`);
}
nextUint32(): number {
const t = (((this.a + this.b) >>> 0) + this.d) >>> 0;
this.d = (this.d + 1) >>> 0;
this.a = this.b ^ (this.b >>> 9);
this.b = (this.c + (this.c << 3)) >>> 0;
this.c = ((this.c << 21) | (this.c >>> 11)) >>> 0;
this.c = (this.c + t) >>> 0;
return t;
}
/** Uniform float in [0, 1). */
next(): number {
return this.nextUint32() / 4294967296;
}
/** Uniform integer in [min, max] (inclusive). */
int(min: number, max: number): number {
return min + Math.floor(this.next() * (max - min + 1));
}
bool(pTrue = 0.5): boolean {
return this.next() < pTrue;
}
pick<T>(items: readonly T[]): T {
if (items.length === 0) throw new Error('pick from empty array');
return items[this.int(0, items.length - 1)];
}
/** Fisher–Yates shuffle (returns a new array). */
shuffle<T>(items: readonly T[]): T[] {
const a = [...items];
for (let i = a.length - 1; i > 0; i--) {
const j = this.int(0, i);
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
bytes(n: number): Buffer {
const b = Buffer.alloc(n);
for (let i = 0; i < n; i++) b[i] = this.nextUint32() & 0xff;
return b;
}
/** Random string of `length` characters drawn from `alphabet` (code points). */
string(length: number, alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): string {
const chars = [...alphabet];
let s = '';
for (let i = 0; i < length; i++) s += chars[this.int(0, chars.length - 1)];
return s;
}
}
/** Seed of a test: `<run seed>::<title path joined by ' > '>`. */
export function testSeed(runSeed: string, titlePath: readonly string[]): string {
return `${runSeed}::${titlePath.join(' > ')}`;
}