The time budget of § 14.14 had no enforcement: a reporter now prints the total and the ten slowest tests, and fails a run whose node project exceeds 30 s or node-long 60 s. Two guards parsed imports with two patterns, one blind past an apostrophe in a comment; both now use one parser anchored on « from ». The determinism guard refuses key-order walks, and one test binds the application name across its three declarations. The end-to-end generations drop to 100 000 moves. Checked: each ceiling, the apostrophe case and a key walk are shown failing before the guard that catches them. --- FR --- [ADD] épreuves : rapporteur de budget, imports, ordre des clés Le budget de temps du § 14.14 n'avait aucune garde : un rapporteur imprime désormais la durée totale et les dix épreuves les plus lentes, et fait échouer une exécution dont le projet node dépasse 30 s ou node-long 60 s. Deux gardes lisaient les imports par deux motifs, l'un aveugle après une apostrophe en commentaire ; les deux passent par un seul analyseur ancré sur « from ». La garde de déterminisme refuse les parcours de clés, et une épreuve lie le nom de l'application à ses trois déclarations. Les générations de bout en bout passent à 100 000. Vérifié : chaque plafond, le cas de l'apostrophe et un parcours de clés sont montrés en échec avant la garde qui les attrape. Assisted-by: Claude Opus 5.5
249 lines
11 KiB
JavaScript
249 lines
11 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Le rapporteur des durées (test/rapporteur.js, § 14.14) : sa mesure au mur,
|
|
// son bilan, son refus, et sa déclaration dans vitest.config.js. Le
|
|
// rapporteur s'éprouve ici hors de Vitest : une fausse horloge date chaque
|
|
// événement, et de faux fichiers portent les épreuves et leurs durées.
|
|
import assert from 'node:assert/strict';
|
|
import configuration from '../vitest.config.js';
|
|
import Rapporteur, { PLAFONDS, bilan, dureeReunie, plusLentes } from './rapporteur.js';
|
|
import { describe, test } from './lanceur.js';
|
|
|
|
// Un faux fichier d'épreuves, tel que Vitest le passe au rapporteur : son
|
|
// identifiant, son projet, son chemin, et ses épreuves, chacune
|
|
// { nom, duree } ; une durée absente fait une épreuve sautée, sans
|
|
// diagnostic.
|
|
const fichier = (projet, chemin, epreuves = []) => ({
|
|
id: `${projet}:${chemin}`,
|
|
project: { name: projet },
|
|
relativeModuleId: chemin,
|
|
children: {
|
|
*allTests() {
|
|
for (const { nom, duree } of epreuves) {
|
|
yield { fullName: nom, diagnostic: () => (duree === undefined ? undefined : { duration: duree }) };
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
// Une épreuve telle que la lisent plusLentes et bilan.
|
|
const epreuve = (duree, fichierDEpreuve, nom, projet = 'node') => ({ duree, projet, fichier: fichierDEpreuve, nom });
|
|
|
|
// Projets mesurés tels que les lit bilan, depuis [nom, durée] : chacun suivi
|
|
// en entier.
|
|
const mesures = (paires) => new Map(paires.map(([nom, duree]) => [nom, { duree, nonSuivis: [] }]));
|
|
|
|
// Un rapporteur branché sur une horloge réglée à la main, sur des plafonds
|
|
// d'épreuve et sur un journal qui garde ses lignes. Rend le rapporteur, de
|
|
// quoi régler l'horloge, et les deux listes du journal.
|
|
function brancher() {
|
|
let maintenant = 0;
|
|
const journal = { lignes: [], erreurs: [] };
|
|
const plafonds = new Map([['node', 30_000], ['node-long', 60_000]]);
|
|
const rapporteur = new Rapporteur({ horloge: () => maintenant, plafonds });
|
|
rapporteur.onInit({
|
|
logger: { log: (texte) => journal.lignes.push(texte), error: (texte) => journal.erreurs.push(texte) },
|
|
});
|
|
return { rapporteur, journal, regler: (t) => (maintenant = t) };
|
|
}
|
|
|
|
// Exécute fin(), puis rend le code de sortie qu'elle a posé et remet celui
|
|
// d'avant : l'épreuve ne laisse pas son code au processus qui l'exécute.
|
|
function codeDeSortieDe(fin) {
|
|
const avant = process.exitCode;
|
|
process.exitCode = undefined;
|
|
try {
|
|
fin();
|
|
return process.exitCode;
|
|
} finally {
|
|
process.exitCode = avant;
|
|
}
|
|
}
|
|
|
|
describe('rapporteur : mesure', () => {
|
|
test("dureeReunie mesure la réunion des intervalles, quel que soit leur ordre : chevauchés, emboîtés, bout à bout ou disjoints", () => {
|
|
assert.equal(dureeReunie([]), 0);
|
|
assert.equal(dureeReunie([{ debut: 5, fin: 9 }]), 4);
|
|
assert.equal(dureeReunie([{ debut: 20, fin: 25 }, { debut: 0, fin: 10 }]), 15);
|
|
assert.equal(dureeReunie([{ debut: 5, fin: 20 }, { debut: 0, fin: 10 }]), 20);
|
|
assert.equal(dureeReunie([{ debut: 0, fin: 30 }, { debut: 5, fin: 10 }, { debut: 12, fin: 31 }]), 31);
|
|
assert.equal(dureeReunie([{ debut: 10, fin: 20 }, { debut: 0, fin: 10 }]), 20);
|
|
});
|
|
|
|
test('plusLentes rend les n plus longues, de la plus lente à la plus rapide, à durée égale par fichier puis par nom', () => {
|
|
const epreuves = [
|
|
epreuve(3, 'b.test.js', 'x'),
|
|
epreuve(9, 'a.test.js', 'y'),
|
|
epreuve(3, 'a.test.js', 'z'),
|
|
epreuve(3, 'a.test.js', 'w'),
|
|
epreuve(1, 'a.test.js', 'v'),
|
|
];
|
|
assert.deepEqual(plusLentes(epreuves, 4), [
|
|
epreuve(9, 'a.test.js', 'y'),
|
|
epreuve(3, 'a.test.js', 'w'),
|
|
epreuve(3, 'a.test.js', 'z'),
|
|
epreuve(3, 'b.test.js', 'x'),
|
|
]);
|
|
assert.deepEqual(plusLentes(epreuves.slice(0, 2), 10), [epreuve(9, 'a.test.js', 'y'), epreuve(3, 'b.test.js', 'x')]);
|
|
});
|
|
});
|
|
|
|
describe('rapporteur : bilan et refus', () => {
|
|
test("un projet refuse au-delà de son plafond, pas à son plafond ; un projet sans plafond ne refuse jamais", () => {
|
|
const plafonds = new Map([['node', 30_000], ['node-long', 60_000]]);
|
|
const { refus } = bilan({
|
|
duree: 70_000,
|
|
projets: mesures([['node', 30_001], ['node-long', 60_000], ['navigateur', 90_000]]),
|
|
epreuves: [],
|
|
plafonds,
|
|
});
|
|
assert.deepEqual(refus, ['Le projet node dure 30,00 s au mur, au-delà de son plafond de 30,00 s (§ 14.14).']);
|
|
assert.deepEqual(bilan({ duree: 1, projets: mesures([['node', 30_000]]), epreuves: [], plafonds }).refus, []);
|
|
});
|
|
|
|
test("une exécution sans épreuve le dit au lieu d'une liste vide", () => {
|
|
assert.deepEqual(bilan({ duree: 4, projets: new Map(), epreuves: [], plafonds: new Map() }).lignes, [
|
|
'Durée totale : 0,00 s',
|
|
'Aucune épreuve exécutée.',
|
|
]);
|
|
});
|
|
|
|
test('le bilan donne la durée totale, chaque projet trié par nom avec son plafond, et les dix épreuves les plus lentes', () => {
|
|
const epreuves = Array.from({ length: 12 }, (_, i) => epreuve(10 * (i + 1), 'a.test.js', `e${i + 1}`));
|
|
const { lignes } = bilan({
|
|
duree: 2_345,
|
|
projets: mesures([['node-long', 2_000], ['navigateur', 1_234]]),
|
|
epreuves,
|
|
plafonds: new Map([['node-long', 60_000]]),
|
|
});
|
|
assert.deepEqual(lignes, [
|
|
'Durée totale : 2,35 s',
|
|
' navigateur : 1,23 s au mur',
|
|
' node-long : 2,00 s au mur, plafond 60,00 s',
|
|
'Épreuves les plus lentes (10 sur 12) :',
|
|
' 120 ms node a.test.js > e12',
|
|
' 110 ms node a.test.js > e11',
|
|
' 100 ms node a.test.js > e10',
|
|
' 90 ms node a.test.js > e9',
|
|
' 80 ms node a.test.js > e8',
|
|
' 70 ms node a.test.js > e7',
|
|
' 60 ms node a.test.js > e6',
|
|
' 50 ms node a.test.js > e5',
|
|
' 40 ms node a.test.js > e4',
|
|
' 30 ms node a.test.js > e3',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('rapporteur : branché sur les événements de Vitest', () => {
|
|
test("chaque exécution compte à chaque projet sa durée moins le temps où seuls d'autres projets tournent, imprime son bilan, et sort en code 1 quand un projet dépasse son plafond", () => {
|
|
const { rapporteur, journal, regler } = brancher();
|
|
const a = fichier('node', 'src/a.test.js', [{ nom: 'a > un', duree: 7 }, { nom: 'a > sautée' }]);
|
|
const b = fichier('node', 'src/b.test.js', [{ nom: 'b > deux', duree: 29_000 }]);
|
|
const c = fichier('node-long', 'src/c.long.test.js', [{ nom: 'c > trois', duree: 19_000 }]);
|
|
regler(0);
|
|
rapporteur.onTestRunStart([]);
|
|
regler(100);
|
|
rapporteur.onTestModuleQueued(a);
|
|
rapporteur.onTestModuleQueued(c);
|
|
regler(400);
|
|
rapporteur.onTestModuleEnd(a);
|
|
regler(20_100);
|
|
rapporteur.onTestModuleEnd(c);
|
|
rapporteur.onTestModuleQueued(b);
|
|
regler(50_000);
|
|
rapporteur.onTestModuleEnd(b);
|
|
regler(50_500);
|
|
const code = codeDeSortieDe(() => rapporteur.onTestRunEnd([a, b, c], [], 'passed'));
|
|
assert.equal(code, 1);
|
|
// Aucun fichier ne tourne pendant 0,6 s, compté à chaque projet. node y
|
|
// ajoute ses fichiers, [100, 400] et [20 100, 50 000], soit 30,8 s : les
|
|
// 19,7 s où c tourne seul ne lui sont pas comptées. node-long y ajoute
|
|
// [100, 20 100], soit 20,6 s.
|
|
assert.deepEqual(journal.lignes.slice(0, 3), [
|
|
'Durée totale : 50,50 s',
|
|
' node : 30,80 s au mur, plafond 30,00 s',
|
|
' node-long : 20,60 s au mur, plafond 60,00 s',
|
|
]);
|
|
assert.deepEqual(journal.lignes.slice(3), [
|
|
'Épreuves les plus lentes (3 sur 3) :',
|
|
' 29000 ms node src/b.test.js > b > deux',
|
|
' 19000 ms node-long src/c.long.test.js > c > trois',
|
|
' 7 ms node src/a.test.js > a > un',
|
|
]);
|
|
assert.deepEqual(journal.erreurs, ['Le projet node dure 30,80 s au mur, au-delà de son plafond de 30,00 s (§ 14.14).']);
|
|
|
|
// Une seconde exécution, comme une relance sous surveillance, repart de
|
|
// zéro. Seul, le projet dure toute l'exécution, temps d'avant la prise de
|
|
// son fichier compris ; sous son plafond, il ne pose aucun code de
|
|
// sortie.
|
|
journal.lignes.length = 0;
|
|
journal.erreurs.length = 0;
|
|
regler(60_000);
|
|
rapporteur.onTestRunStart([]);
|
|
regler(60_200);
|
|
rapporteur.onTestModuleQueued(a);
|
|
regler(60_300);
|
|
rapporteur.onTestModuleEnd(a);
|
|
regler(60_400);
|
|
assert.equal(codeDeSortieDe(() => rapporteur.onTestRunEnd([a], [], 'passed')), undefined);
|
|
assert.deepEqual(journal.lignes.slice(0, 2), ['Durée totale : 0,40 s', ' node : 0,40 s au mur, plafond 30,00 s']);
|
|
assert.deepEqual(journal.erreurs, []);
|
|
});
|
|
|
|
test("un fichier dont le rapporteur n'a vu ni la prise ni la fin rend la durée de son projet inconnue, et refuse quand le projet a un plafond", () => {
|
|
const { rapporteur, journal, regler } = brancher();
|
|
const a = fichier('node', 'src/a.test.js', [{ nom: 'a > un', duree: 1 }]);
|
|
const b = fichier('node', 'src/b.test.js', [{ nom: 'b > deux', duree: 1 }]);
|
|
const n = fichier('navigateur', 'src/n.navigateur.test.js', [{ nom: 'n > trois', duree: 1 }]);
|
|
regler(0);
|
|
rapporteur.onTestRunStart([]);
|
|
rapporteur.onTestModuleQueued(a);
|
|
regler(10);
|
|
rapporteur.onTestModuleEnd(a);
|
|
const code = codeDeSortieDe(() => rapporteur.onTestRunEnd([a, b, n], [], 'passed'));
|
|
assert.equal(code, 1);
|
|
assert.deepEqual(journal.lignes.slice(0, 3), [
|
|
'Durée totale : 0,01 s',
|
|
' navigateur : durée inconnue, 1 fichier non suivi',
|
|
' node : durée inconnue, 1 fichier non suivi, plafond 30,00 s',
|
|
]);
|
|
assert.deepEqual(journal.erreurs, [
|
|
"Le projet node a un plafond de 30,00 s, et sa durée ne se mesure pas : le rapporteur n'a pas suivi src/b.test.js (§ 14.14).",
|
|
]);
|
|
});
|
|
|
|
test("une exécution interrompue imprime son bilan sans rien refuser", () => {
|
|
const { rapporteur, journal, regler } = brancher();
|
|
const a = fichier('node', 'src/a.test.js', [{ nom: 'a > un', duree: 1 }]);
|
|
regler(0);
|
|
rapporteur.onTestRunStart([]);
|
|
rapporteur.onTestModuleQueued(a);
|
|
regler(40_000);
|
|
assert.equal(codeDeSortieDe(() => rapporteur.onTestRunEnd([a], [], 'interrupted')), undefined);
|
|
assert.deepEqual(journal.lignes.slice(0, 2), ['Durée totale : 40,00 s', ' node : 40,00 s au mur, plafond 30,00 s']);
|
|
assert.deepEqual(journal.erreurs, []);
|
|
});
|
|
});
|
|
|
|
describe('rapporteur : déclaration', () => {
|
|
test('vitest.config.js déclare le rapporteur à côté du rapporteur par défaut', () => {
|
|
assert.deepEqual(configuration.test.reporters, ['default', './test/rapporteur.js']);
|
|
});
|
|
|
|
test("chaque plafond nomme un projet de vitest.config.js sous son nom d'exécution, et node comme node-long ont le leur (§ 14.14)", () => {
|
|
// Vitest nomme chaque instance d'un projet navigateur d'après le projet
|
|
// et le navigateur, « navigateur (chromium) » : seul ce nom s'apparie à
|
|
// un plafond.
|
|
const projets = configuration.test.projects.flatMap(({ test: { name, browser } }) =>
|
|
browser?.enabled ? browser.instances.map((instance) => `${name} (${instance.browser})`) : [name],
|
|
);
|
|
assert.ok(projets.includes('navigateur (chromium)'), `noms d'exécution : ${projets.join(', ')}`);
|
|
assert.deepEqual([...PLAFONDS.keys()].filter((projet) => !projets.includes(projet)), []);
|
|
assert.ok(PLAFONDS.has('node'), 'aucun plafond pour node');
|
|
assert.ok(PLAFONDS.has('node-long'), 'aucun plafond pour node-long');
|
|
assert.equal(PLAFONDS.get('node'), 30_000);
|
|
assert.equal(PLAFONDS.get('node-long'), 60_000);
|
|
});
|
|
});
|