Passe de performance guidée par la mesure, pas par l'intuition. Porte de qualité (`npm run check`) — la commande qui tourne des dizaines de fois par jour : - Les 5 vérifications sont indépendantes : exécutées en parallèle via scripts/check.mjs au lieu d'un enchaînement `&&` qui imposait la somme des durées ET 7 démarrages npm imbriqués. 14 s → 6 s - Cache ESLint (--cache) et compilation incrémentale tsc. 16 s → 14 s - Chemin critique restant : vitest 4,9 s, dont ~2 s de mise en place jsdom. Tests de mutation : - Config Vitest dédiée en pool `threads` sans ré-isolation. Mesuré sur le dry-run Stryker : overhead d'amorçage 12 844 ms → 1 592 ms, soit ~90 % du cycle par mutant. 5 min 02 → 3 min 40 - Le mode incrémental ramène le profil complet à 12 s après une édition. Pistes mesurées puis REJETÉES (documentées dans README-CI.md pour éviter qu'on les retente) : - Découper la mutation en 5 processus Stryker parallèles : 262 s contre 235 s. Chaque processus repaie son bac à sable et son dry-run. - Monter `concurrency` de 6 à 16 : aucun effet (~8,5 mutants/s partout), le débit est borné par l'orchestration mono-processus de Stryker. - Séparer vitest API/frontend en deux processus : aucun gain, jsdom domine. - Pool `threads` sur la suite complète : 18,8 s contre 3,5 s. Le bon réglage dépend du jeu de fichiers (jsdom ou non) — d'où deux configs distinctes. Le périmètre muté est inchangé (1511 mutants) : un score obtenu en retirant les mutants gênants vaudrait moins que pas de score. Corrections au passage : - scripts/*.mjs n'était couvert par aucune section ESLint - Les deux suites pytest ont chacune un paquet `src` : réunies dans un seul processus, le premier importé masquait l'autre. Séparées (et parallèles). Co-Authored-By: Claude <noreply@anthropic.com>
101 lines
3.5 KiB
JavaScript
101 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Porte de qualité — exécute les vérifications EN PARALLÈLE.
|
|
*
|
|
* Les cinq vérifications sont indépendantes (aucune ne lit la sortie d'une
|
|
* autre). Les enchaîner avec `&&` imposait deux coûts inutiles :
|
|
* - la somme des durées au lieu du maximum ;
|
|
* - sept invocations `npm run` imbriquées, chacune avec son propre coût de
|
|
* démarrage npm (~300 ms).
|
|
*
|
|
* On les lance donc toutes d'un coup, on tamponne les sorties, et on les
|
|
* restitue dans un ordre stable à la fin. Le détail par tâche reste affiché
|
|
* pour que le goulot d'étranglement demeure visible.
|
|
*
|
|
* `--sequential` rétablit l'exécution en série (utile pour isoler un échec
|
|
* quand deux tâches écrivent des messages entremêlés).
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
|
|
const isWin = process.platform === "win32";
|
|
const sequential = process.argv.includes("--sequential");
|
|
|
|
/** bin = binaire de node_modules/.bin (résolu par npm via PATH). */
|
|
const TASKS = [
|
|
{
|
|
name: "eslint",
|
|
cmd: "eslint",
|
|
args: [".", "--cache", "--cache-location", "node_modules/.cache/eslint/"],
|
|
},
|
|
{
|
|
name: "tsc",
|
|
cmd: "tsc",
|
|
args: ["-p", "jsconfig.json"],
|
|
},
|
|
// Séparer les tests API (node) et frontend (jsdom) en deux processus a été
|
|
// mesuré : aucun gain (4,9 s dans les deux cas — c'est la mise en place de
|
|
// jsdom qui domine, et les deux processus se disputent les cœurs). Gardé en
|
|
// un seul.
|
|
{ name: "vitest", cmd: "vitest", args: ["run"] },
|
|
{
|
|
name: "ruff",
|
|
cmd: "python",
|
|
args: ["-m", "ruff", "check", "."],
|
|
},
|
|
// Les deux suites Python tournent dans des processus distincts : chaque app a
|
|
// son propre paquet `src`, et les réunir dans un seul pytest fait que le
|
|
// premier `src` importé masque l'autre (ModuleNotFoundError: src.parser).
|
|
// Séparées, elles s'exécutent aussi en parallèle.
|
|
{ name: "pytest:geocoder", cmd: "python", args: ["-m", "pytest", "tests", "-q"], cwd: "apps/geocoder" },
|
|
{ name: "pytest:crawler", cmd: "python", args: ["-m", "pytest", "tests", "-q"], cwd: "apps/crawler" },
|
|
];
|
|
|
|
function run(task) {
|
|
return new Promise((resolve) => {
|
|
const started = Date.now();
|
|
const child = spawn(task.cmd, task.args, {
|
|
shell: isWin,
|
|
cwd: task.cwd ?? process.cwd(),
|
|
});
|
|
|
|
let out = "";
|
|
child.stdout.on("data", (b) => (out += b));
|
|
child.stderr.on("data", (b) => (out += b));
|
|
child.on("error", (err) => {
|
|
resolve({ ...task, code: 1, out: `${err.message}\n`, ms: Date.now() - started });
|
|
});
|
|
child.on("close", (code) => {
|
|
resolve({ ...task, code, out, ms: Date.now() - started });
|
|
});
|
|
});
|
|
}
|
|
|
|
const started = Date.now();
|
|
let results;
|
|
|
|
if (sequential) {
|
|
results = [];
|
|
for (const t of TASKS) results.push(await run(t));
|
|
} else {
|
|
results = await Promise.all(TASKS.map(run));
|
|
}
|
|
|
|
const failed = results.filter((r) => r.code !== 0);
|
|
|
|
for (const r of results) {
|
|
const mark = r.code === 0 ? "✓" : "✗";
|
|
console.log(`\n${mark} ${r.name} (${(r.ms / 1000).toFixed(1)}s)`);
|
|
// Sur succès on ne montre que les avertissements éventuels, pas le bruit.
|
|
if (r.code !== 0 || /warning|warn/i.test(r.out)) {
|
|
const body = r.out.trimEnd();
|
|
if (body) console.log(body.replace(/^/gm, " "));
|
|
}
|
|
}
|
|
|
|
const total = ((Date.now() - started) / 1000).toFixed(1);
|
|
|
|
if (failed.length) {
|
|
console.error(`\n✗ Porte de qualité en échec : ${failed.map((f) => f.name).join(", ")} (${total}s)`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\n✓ Tout est vert (${total}s)`);
|