gestion_table_tournante_libre/test/rapporteur.test.js
Mathieu Benoit de46c582f4 [IMP] tests: watched series adapts to the core count, threads below 4
Below four cores the runner carried the watched series in one process,
and a relaunch after a base module took 2.5 to 3 s, over the 2 s budget
of § 14.14. The configuration now reads the cores the process may use:
from four, full processes and the whole series, as before; below, threads
on every core, and the end-to-end test moves to the long series. No test
disappears or changes behaviour. A guard refuses, in these series, what a
thread refuses and a process accepts; the reporter names the mode.

Checked: relaunch on 2 cores 1.0 to 1.3 s, was 2.5 to 3.0; on 4 cores
1.2 to 1.5 s; process.env is copied per thread, measured.

--- FR ---

[IMP] épreuves : la série surveillée suit les cœurs, threads sous 4

Sous quatre cœurs, le lanceur portait la série surveillée dans un seul
processus, et une relance après un module de base prenait 2,5 à 3 s,
au-delà du budget de 2 s du § 14.14. La configuration lit les cœurs que
le processus peut occuper : à partir de quatre, processus complets et
série entière, comme avant ; en deçà, threads sur tous les cœurs, et
l'épreuve de bout en bout passe dans la série longue. Aucune épreuve ne
disparaît ni ne change. Une garde refuse dans ces séries ce qu'un thread
refuse et qu'un processus accepte ; le rapporteur nomme le mode.

Vérifié : relance sur 2 cœurs en 1,0 à 1,3 s, contre 2,5 à 3,0 ; sur 4
cœurs en 1,2 à 1,5 s ; process.env est copié par thread, mesuré.

Assisted-by: Claude Opus 5.5
2026-10-06 04:54:01 -04:00

252 lines
12 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';
import { coeursDisponibles, ligneMachine, reglagesSeries } from './machine.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',
// Le bilan finit sur le mode que cette machine fait retenir.
ligneMachine(reglagesSeries(coeursDisponibles())),
]);
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);
});
});