scripts/mutation-sweep.mjs
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
export function selectSpecs(root, names = [], shard = '0/1') {
const all = readdirSync(join(root, 'test/mutations')).filter(n => n.endsWith('.json'))
.sort().map(n => `test/mutations/${n}`);
if (!all.length) throw new Error('zero discovered mutation specs');
const requested = names.length ? names.map(n => n.includes('/') ? n : `test/mutations/${n}`) : all;
if (new Set(requested).size !== requested.length) throw new Error('duplicate spec selection');
for (const name of requested) if (!all.includes(name)) throw new Error(`unknown spec: ${name}`);
const match = /^(\d+)\/(\d+)$/.exec(shard);
if (!match) throw new Error('shard must be zero-based i/N');
const [index, total] = match.slice(1).map(Number);
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || total < 1 || index >= total) {
throw new Error('invalid shard index/total');
}
const selected = [...requested].sort().filter((_, i) => i % total === index);
if (!selected.length) throw new Error('zero selected specs (including empty shards)');
return { discovered: all.length, selected, shard };
}
export function verifyReport(directory) {
const plan = JSON.parse(readFileSync(join(directory, 'manifest.json'), 'utf8'));
const text = readFileSync(join(directory, 'verdicts.log'), 'utf8');
if (!Array.isArray(plan.selected) || !plan.selected.length
|| new Set(plan.selected).size !== plan.selected.length) throw new Error('empty or duplicate manifest');
const lines = text.split(/\r?\n/).filter(Boolean);
const rows = lines.map(line => /^VERDICT (\S+) (OK|FAIL|UNTRUSTWORTHY) exit=(\d+)$/.exec(line));
if (rows.some(row => !row)) throw new Error('malformed VERDICT record');
const seen = rows.map(row => row[1]);
if (rows.length !== plan.selected.length || new Set(seen).size !== plan.selected.length
|| seen.some(name => !plan.selected.includes(name))) {
throw new Error(`VERDICT coverage mismatch: expected=${plan.selected.length} actual=${rows.length}`);
}
for (const row of rows) if ((row[2] === 'OK') !== (row[3] === '0')) throw new Error('inconsistent verdict/exit');
const code = rows.some(row => row[2] === 'UNTRUSTWORTHY') ? 2 : rows.some(row => row[2] === 'FAIL') ? 1 : 0;
return { expected: plan.selected.length, actual: rows.length, code };
}
export function runSweep(argv, root = process.cwd()) {
let names = [], shard = '0/1', directory;
for (let i = 0; i < argv.length; i++) {
const value = () => { if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`missing value for ${argv[i]}`); return argv[++i]; };
if (argv[i] === '--specs') {
names = value().split(/[\s,]+/).filter(Boolean);
if (!names.length) throw new Error('explicit spec selection is empty');
}
else if (argv[i] === '--shard') shard = value();
else if (argv[i] === '--report-dir') directory = resolve(value());
else if (argv[i] === '--verify' && argv.length === 2) {
const result = verifyReport(resolve(value()));
console.log(`VERDICT coverage: ${result.actual}/${result.expected}; exit=${result.code}`);
return result.code;
} else throw new Error(`unknown argument: ${argv[i]}`);
}
const plan = selectSpecs(root, names, shard);
directory ??= mkdtempSync(join(tmpdir(), 'wa-mutation-sweep-'));
const rel = relative(resolve(root), directory);
if (rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))) {
throw new Error('report directory must be outside the owner working tree');
}
mkdirSync(directory, { recursive: true });
// Exclusive creation prevents a retry from reusing or overwriting old evidence.
writeFileSync(join(directory, 'manifest.json'), JSON.stringify(plan, null, 2) + '\n', { flag: 'wx' });
writeFileSync(join(directory, 'verdicts.log'), '', { flag: 'wx' });
console.log(`Sweep: discovered=${plan.discovered} selected=${plan.selected.length} shard=${shard}`);
console.log(`Report: ${directory}`);
for (const spec of plan.selected) {
const child = spawnSync(process.execPath, ['scripts/mutate-verify.mjs', spec], {
cwd: root, encoding: 'utf8', env: process.env,
maxBuffer: 128 * 1024 * 1024,
});
const output = `${child.stdout || ''}${child.stderr || ''}`;
// Child output cannot masquerade as an authoritative sweep record.
process.stdout.write(output.split(/\r?\n/).filter(Boolean).map(line => ` ${line}\n`).join(''));
const code = child.error || child.signal || child.status === null ? 2 : child.status;
const verdict = code === 0 ? 'OK' : code === 2 ? 'UNTRUSTWORTHY' : 'FAIL';
if (child.error) console.error(` spawn failure: ${child.error.message}`);
const line = `VERDICT ${spec} ${verdict} exit=${code}\n`;
appendFileSync(join(directory, 'verdicts.log'), line);
process.stdout.write(line);
}
// Read the persisted records, not a loop counter: omissions and duplicates are failures.
const result = verifyReport(directory);
console.log(`VERDICT coverage: ${result.actual}/${result.expected}; exit=${result.code}`);
return result.code;
}
if (resolve(process.argv[1] || '') === resolve(fileURLToPath(import.meta.url))) {
try { process.exitCode = runSweep(process.argv.slice(2)); }
catch (error) { console.error(`mutation sweep: ${error.message}`); process.exitCode = 2; }
}