Files
SnapOtter/tests/helpers/pairwise.ts
T
SnapOtterandGitHub 4ec39c556f test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)
Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate,
nightly full-suite workflows, parallel vitest forks (per-fork DBs),
Playwright parallel/serial/visual projects against production builds,
metadata-generated test suites (drift guards, hostile inputs, format
matrix, pairwise settings, property-based fuzz), Stryker mutation
testing, Schemathesis API fuzz, coverage ratchet, and fixes for three
session-poisoning bugs that caused 200+ serial-bucket failures.

Bug fix included: favicon/split/bulk-rename could hang clients forever
when ZIP streaming failed after reply.hijack().
2026-06-10 22:01:13 +08:00

78 lines
2.7 KiB
TypeScript

import type { PictAxis } from "./zod-pict.js";
/**
* Deterministic greedy pairwise (order-2) covering-array generator.
*
* Guarantees that every pair of values from any two axes appears in at least
* one generated case, which is the standard interaction-coverage target.
* In-repo and dependency-free on purpose: the natural alternative (pict-node)
* compiles Microsoft PICT from C++ at install time, which breaks on machines
* without a working native toolchain. For our axis counts (<= ~10 per tool)
* the greedy construction is within a few cases of PICT's optimum.
*
* Determinism matters: same axes in, same cases out, so CI runs are
* reproducible. No randomness is used anywhere.
*/
export function pairwise(axes: PictAxis[]): Record<string, unknown>[] {
if (axes.length === 0) return [];
if (axes.length === 1) {
return axes[0].values.map((v) => ({ [axes[0].key]: v }));
}
// Track uncovered pairs by value indexes: "i|j|a|b" with i < j.
const uncovered = new Set<string>();
for (let i = 0; i < axes.length; i++) {
for (let j = i + 1; j < axes.length; j++) {
for (let a = 0; a < axes[i].values.length; a++) {
for (let b = 0; b < axes[j].values.length; b++) {
uncovered.add(`${i}|${j}|${a}|${b}`);
}
}
}
}
const pairKey = (x: number, vx: number, y: number, vy: number): string =>
x < y ? `${x}|${y}|${vx}|${vy}` : `${y}|${x}|${vy}|${vx}`;
const cases: number[][] = [];
while (uncovered.size > 0) {
// Seed the case with the first uncovered pair (insertion order is stable).
const seed = uncovered.values().next().value as string;
const [i, j, a, b] = seed.split("|").map(Number);
const chosen: number[] = new Array(axes.length).fill(-1);
chosen[i] = a;
chosen[j] = b;
// Fill the remaining axes greedily: pick the value covering the most
// still-uncovered pairs against the axes already chosen.
for (let k = 0; k < axes.length; k++) {
if (chosen[k] !== -1) continue;
let bestValue = 0;
let bestScore = -1;
for (let v = 0; v < axes[k].values.length; v++) {
let score = 0;
for (let m = 0; m < axes.length; m++) {
if (chosen[m] === -1 || m === k) continue;
if (uncovered.has(pairKey(k, v, m, chosen[m]))) score++;
}
if (score > bestScore) {
bestScore = score;
bestValue = v;
}
}
chosen[k] = bestValue;
}
for (let x = 0; x < axes.length; x++) {
for (let y = x + 1; y < axes.length; y++) {
uncovered.delete(`${x}|${y}|${chosen[x]}|${chosen[y]}`);
}
}
cases.push(chosen);
}
return cases.map((indexes) =>
Object.fromEntries(axes.map((axis, k) => [axis.key, axis.values[indexes[k]]])),
);
}