Merge branch 'release/v1.45.0' into feat/v145-runtime

This commit is contained in:
Marc-André Bombeck committed 2026-10-22 06:41:30 +02:00
commit a859adb59c
146 files changed
+24075 -13836

No files matched your search

+3
View File
@@ -157,6 +157,9 @@ describe("container dependency installation", () => {
expect(dockerfile).toContain(
"COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./\n" +
"# The dependency patches pnpm-workspace.yaml names; the frozen install\n" +
"# refuses to run without them.\n" +
"COPY patches patches\n" +
"COPY scripts/prepare.mjs scripts/prepare.mjs\n" +
"RUN pnpm install --frozen-lockfile --prod=false",
);
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Check each module's mutation score against its floor.
*
* node scripts/mutation-floors.mjs [reports/mutation/mutation.json]
*
* Stryker's own `thresholds.break` is one number for the whole run, which
* lets a well-tested module hide a weak one. `stryker.floors.json` names a
* floor per module instead (a module is a set of path prefixes), and this
* script reads the JSON report Stryker writes and fails when any module
* scores below its floor. The score is Stryker's: killed and timed-out
* mutants over every mutant that was valid and covered or not, so a mutant no
* test reaches counts against the module.
*
* Floors sit just below the last measured score. Raise one when a change
* raises the score; a floor that is lowered to let a run pass has stopped
* being a floor.
*/
import { existsSync, readFileSync } from "node:fs";
import { relative, resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const reportPath = resolve(
root,
process.argv[2] ?? "reports/mutation/mutation.json",
);
const floorsPath = resolve(root, "stryker.floors.json");
if (!existsSync(reportPath)) {
console.error(
`no mutation report at ${reportPath}; run pnpm exec stryker run`,
);
process.exit(2);
}
const report = JSON.parse(readFileSync(reportPath, "utf8"));
const { modules } = JSON.parse(readFileSync(floorsPath, "utf8"));
const DETECTED = new Set(["Killed", "Timeout"]);
const UNDETECTED = new Set(["Survived", "NoCoverage"]);
const files = Object.entries(report.files ?? {}).map(([path, file]) => [
relative(root, resolve(root, path)).split("\\").join("/"),
file,
]);
let failed = false;
const rows = [];
for (const [name, { paths, floor }] of Object.entries(modules)) {
let detected = 0;
let undetected = 0;
let matched = 0;
for (const [path, file] of files) {
if (!paths.some((prefix) => path.startsWith(prefix))) continue;
matched += 1;
for (const mutant of file.mutants ?? []) {
if (DETECTED.has(mutant.status)) detected += 1;
else if (UNDETECTED.has(mutant.status)) undetected += 1;
}
}
const total = detected + undetected;
const score = total === 0 ? null : (100 * detected) / total;
const ok = matched > 0 && score !== null && score >= floor;
if (!ok) failed = true;
rows.push(
`${ok ? "ok " : "FAIL"} ${name.padEnd(16)} ${
score === null ? "no mutants" : `${score.toFixed(1)} %`
} (floor ${floor} %, ${detected}/${total} mutants, ${matched} files)`,
);
}
console.log(rows.join("\n"));
if (failed) {
console.error(
"\nA module scored below its floor, or the report covered none of its files.",
);
process.exit(1);
}
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Regenerate a timing file for the duration-balanced sequencer
* (`tests/durations/sequencer.ts`) from Vitest JSON reports.
*
* node scripts/test-durations.mjs <suite> <report.json> [<report.json> …]
*
* `<suite>` is `unit` or `integration` and names the file written,
* `tests/durations/<suite>.json`. Pass every shard's report of one run; a
* file measured twice keeps the longer reading. Files the reports do not
* mention keep their previous value, so a partial run refines the table
* rather than emptying it, and files that no longer exist are dropped.
*
* Produce a report with
* pnpm exec vitest run [--config …] --reporter=default \
* --reporter=json --outputFile.json=report.json
* The nightly shuffled workflow uploads the reports of a CI run, which is
* the better source: CI is where the shards have to balance.
*/
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { relative, resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const [suite, ...reports] = process.argv.slice(2);
if (!["unit", "integration"].includes(suite ?? "") || reports.length === 0) {
console.error(
"usage: node scripts/test-durations.mjs <unit|integration> <report.json>…",
);
process.exit(2);
}
const target = resolve(root, "tests/durations", `${suite}.json`);
const previous = existsSync(target)
? JSON.parse(readFileSync(target, "utf8")).files
: {};
const measured = new Map();
for (const report of reports) {
const parsed = JSON.parse(readFileSync(report, "utf8"));
for (const result of parsed.testResults ?? []) {
if (typeof result.startTime !== "number") continue;
if (typeof result.endTime !== "number") continue;
const file = relative(root, result.name).split("\\").join("/");
const seconds = Math.max(0, (result.endTime - result.startTime) / 1000);
measured.set(file, Math.max(measured.get(file) ?? 0, seconds));
}
}
if (measured.size === 0) {
console.error("the reports carry no per-file timings; nothing written");
process.exit(1);
}
const files = {};
const keys = new Set([...Object.keys(previous), ...measured.keys()]);
for (const file of [...keys].sort()) {
if (!existsSync(resolve(root, file))) continue;
const seconds = measured.get(file) ?? previous[file];
files[file] = Math.round(seconds * 10) / 10;
}
writeFileSync(
target,
JSON.stringify(
{
source: `vitest JSON report of ${measured.size} files, written ${new Date().toISOString().slice(0, 10)}`,
files,
},
null,
2,
) + "\n",
);
const total = Object.values(files).reduce((a, b) => a + b, 0);
console.log(
`${target}: ${Object.keys(files).length} files, ${Math.round(total)} s in total`,
);