Rendering thresholds must be measured in a real browser, apart from the test series that guard behaviour. A separate Vitest project runs scripts/banc/**/*.banc.js in headless Chromium at 1366 x 700, scale 1, without frame-rate limit, CPU throttling set per run; the statistics module gives medians and spreads over repeated samples. Outputs go to an ignored folder. make banc and npm run banc name the entry; its orchestrator comes with the bench measures. Checked: red first; 3410 node tests green from the index; the bench project runs its harness. --- FR --- [ADD] banc : harnais Chromium sans tête, statistiques, entrée make banc Les seuils de rendu se mesurent dans un vrai navigateur, à part des séries qui gardent le comportement. Un projet Vitest distinct joue scripts/banc/**/*.banc.js dans Chromium sans tête à 1366 × 700, facteur 1, sans limite d'images, le ralentissement du processeur réglé par lancement ; le module de statistiques rend médianes et dispersions sur des mesures répétées. Les sorties vont dans un dossier ignoré. make banc et npm run banc nomment l'entrée ; son orchestrateur vient avec les mesures du banc. Vérifié : rouge d'abord ; 3410 node verts depuis l'index ; le projet du banc joue son harnais. Assisted-by: Claude Opus 5.5
266 lines
14 KiB
JavaScript
266 lines
14 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Gardes du banc de mesure (§ 19.10, § 14.4, § 14.8). Le banc vit hors des
|
||
// trois séries de vitest.config.js : il se lance par sa propre commande, sur
|
||
// sa propre configuration, vitest.banc.config.js, qui ôte au Chromium sa
|
||
// cadence d'images. Une épreuve du banc que l'une des séries jouerait la
|
||
// mesurerait à cadence bridée, et alourdirait une série qui se lance à
|
||
// chaque modification ; une épreuve que le projet banc n'inclut pas ne se
|
||
// joue nulle part. Les gardes lisent par import vitest.config.js et le
|
||
// projet banc tel que scripts/banc/projet.js le décrit — vitest.banc.config.js
|
||
// charge Vitest, qu'une épreuve node ne charge pas (§ 14.8) —, le Makefile et
|
||
// package.json par le disque.
|
||
import assert from 'node:assert/strict';
|
||
import { readdirSync, readFileSync } from 'node:fs';
|
||
import { join, matchesGlob, posix, relative, sep } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { chargements } from '../../test/chargements.js';
|
||
import { describe, test } from '../../test/lanceur.js';
|
||
import configuration from '../../vitest.config.js';
|
||
import { optionsBanc, projetBanc } from './projet.js';
|
||
|
||
const RACINE = fileURLToPath(new URL('../../', import.meta.url));
|
||
|
||
// Ce fichier, relatif à la racine. Ses données d'épreuve écrivent en
|
||
// chaînes des chargements des outils du banc, que le relevé des chargements
|
||
// lit comme les autres : le parcours de l'arbre réel le laisse de côté.
|
||
const CE_FICHIER = relative(RACINE, fileURLToPath(import.meta.url)).split(sep).join('/');
|
||
|
||
// Les outils de page du banc : un module qui les charge mesure dans la page.
|
||
const OUTILS_DU_BANC = 'scripts/banc/navigateur.js';
|
||
|
||
// Les drapeaux qui ôtent à Chromium sa cadence d'images.
|
||
const DRAPEAUX_CADENCE = ['--disable-frame-rate-limit', '--disable-gpu-vsync'];
|
||
|
||
// Ce que le parcours de l'arbre saute : les dépendances, à toute
|
||
// profondeur, et, à la racine, le dépôt et les sorties de construction et de
|
||
// mesure.
|
||
const HORS_DU_PARCOURS_A_LA_RACINE = new Set(['.git', 'www', 'dist-electron', 'coverage', '.vitest']);
|
||
const MODULE = /\.[cm]?js$/;
|
||
|
||
// Les modules JavaScript du projet, { chemin, texte }, chemins relatifs à la
|
||
// racine en barres obliques, triés.
|
||
function modulesDuProjet(racine) {
|
||
const modules = [];
|
||
const parcourir = (dossier) => {
|
||
for (const entree of readdirSync(dossier, { withFileTypes: true })) {
|
||
const chemin = join(dossier, entree.name);
|
||
const exclu = entree.name === 'node_modules' || (dossier === racine && HORS_DU_PARCOURS_A_LA_RACINE.has(entree.name));
|
||
if (entree.isDirectory() && !exclu) parcourir(chemin);
|
||
else if (entree.isFile() && MODULE.test(entree.name)) {
|
||
modules.push({ chemin: relative(racine, chemin).split(sep).join('/'), texte: readFileSync(chemin, 'utf8') });
|
||
}
|
||
}
|
||
};
|
||
parcourir(racine);
|
||
return modules.sort((a, b) => (a.chemin < b.chemin ? -1 : a.chemin > b.chemin ? 1 : 0));
|
||
}
|
||
|
||
// Le module que vise un spécificateur, relatif à la racine, en barres
|
||
// obliques, suffixe de Vite (« ?… ») retiré : relatif au fichier, ou absolu
|
||
// depuis la racine comme Vite les résout ; null pour un paquet.
|
||
function cible(chemin, specificateur) {
|
||
const sansSuffixe = specificateur.replace(/\?.*$/, '');
|
||
if (sansSuffixe.startsWith('/')) return posix.normalize(sansSuffixe.slice(1));
|
||
if (sansSuffixe.startsWith('.')) return posix.normalize(posix.join(posix.dirname(chemin), sansSuffixe));
|
||
return null;
|
||
}
|
||
|
||
// Une épreuve que Vitest peut jouer : un fichier *.banc.js, que le projet
|
||
// banc inclut, ou *.test.js, que les séries incluent.
|
||
const EPREUVE = /\.(?:banc|test)\.[cm]?js$/;
|
||
|
||
// Les épreuves du banc parmi des modules { chemin, texte } : un fichier
|
||
// *.banc.js, ou une épreuve *.test.js qui charge les outils de page du banc,
|
||
// directement ou à travers des modules de la liste, à toute profondeur. Un
|
||
// module qui n'est pas une épreuve n'en est jamais une : une aide du banc
|
||
// qui charge les outils ne se joue que par une épreuve qui la charge.
|
||
function epreuvesDuBanc(modules) {
|
||
const textes = new Map(modules.map(({ chemin, texte }) => [chemin, texte]));
|
||
// Les modules que chaque module charge, lus une fois.
|
||
const lus = new Map();
|
||
const voisins = (chemin) => {
|
||
if (!lus.has(chemin)) {
|
||
lus.set(
|
||
chemin,
|
||
chargements(textes.get(chemin))
|
||
.filter(({ specificateur }) => specificateur !== undefined)
|
||
.map(({ specificateur }) => cible(chemin, specificateur))
|
||
.filter((vise) => vise !== null),
|
||
);
|
||
}
|
||
return lus.get(chemin);
|
||
};
|
||
const atteintLesOutils = (depart) => {
|
||
const vus = new Set([depart]);
|
||
const aParcourir = [depart];
|
||
for (let i = 0; i < aParcourir.length; i += 1) {
|
||
for (const vise of voisins(aParcourir[i])) {
|
||
if (vise === OUTILS_DU_BANC) return true;
|
||
if (vus.has(vise) || !textes.has(vise)) continue;
|
||
vus.add(vise);
|
||
aParcourir.push(vise);
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
return modules.filter(
|
||
({ chemin }) => chemin.endsWith('.banc.js') || (EPREUVE.test(chemin) && atteintLesOutils(chemin)),
|
||
);
|
||
}
|
||
|
||
const repondA = (chemin, motifs = []) => motifs.some((motif) => matchesGlob(chemin, motif));
|
||
const inclus = (chemin, { include = [], exclude = [] }) => repondA(chemin, include) && !repondA(chemin, exclude);
|
||
|
||
// Relevé des épreuves du banc mal rangées, dans l'ordre des modules reçus :
|
||
// « chemin : jouée par le projet nom » pour chaque projet des séries qui
|
||
// l'inclut, puis « chemin : hors du projet banc » quand le projet banc ne
|
||
// l'inclut pas. projets et banc : les objets test des projets de Vitest.
|
||
// Lève quand les modules ne portent aucune épreuve du banc (§ 14.2).
|
||
function releverBancs(modules, { projets, banc }) {
|
||
const bancs = epreuvesDuBanc(modules);
|
||
assert.ok(bancs.length > 0, 'aucune épreuve du banc');
|
||
return bancs.flatMap(({ chemin }) => [
|
||
...projets.filter((projet) => inclus(chemin, projet)).map(({ name }) => `${chemin} : jouée par le projet ${name}`),
|
||
...(inclus(chemin, banc) ? [] : [`${chemin} : hors du projet banc`]),
|
||
]);
|
||
}
|
||
|
||
// La cible nommée d'un Makefile : sa ligne de documentation (« ## … »), ses
|
||
// commandes (les lignes de recette qui la suivent, tabulation retirée) et sa
|
||
// déclaration .PHONY ; null quand le Makefile ne la porte pas.
|
||
function cibleDuMakefile(texte, nom) {
|
||
const lignes = texte.split('\n');
|
||
const rang = lignes.findIndex((ligne) => ligne.startsWith(`${nom}:`));
|
||
if (rang === -1) return null;
|
||
const documentation = /##\s*(.*)$/.exec(lignes[rang])?.[1] ?? null;
|
||
const commandes = [];
|
||
for (let i = rang + 1; i < lignes.length && lignes[i].startsWith('\t'); i += 1) commandes.push(lignes[i].slice(1));
|
||
const factice = lignes.some((ligne) => /^\.PHONY:/.test(ligne) && ligne.slice('.PHONY:'.length).trim().split(/\s+/).includes(nom));
|
||
return { documentation, commandes, factice };
|
||
}
|
||
|
||
const projetsDesSeries = () => configuration.test.projects.map(({ test: projet }) => projet);
|
||
|
||
describe('banc : hors des trois séries (§ 14.4, § 14.8)', () => {
|
||
test('aucune épreuve du banc ne répond aux motifs des trois projets de vitest.config.js, et le projet banc les inclut toutes', () => {
|
||
const modules = modulesDuProjet(RACINE).filter(({ chemin }) => chemin !== CE_FICHIER);
|
||
assert.ok(epreuvesDuBanc(modules).some(({ chemin }) => chemin === 'scripts/banc/harnais.banc.js'));
|
||
assert.deepEqual(projetsDesSeries().map(({ name }) => name), ['node', 'node-long', 'navigateur']);
|
||
assert.deepEqual(releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), []);
|
||
});
|
||
|
||
test('la garde relève une épreuve du banc renommée en épreuve du navigateur, une autre chargée par une épreuve de src/, directement ou par une aide, un *.banc.js hors de scripts/banc, et une série qui inclurait le banc ; une aide du banc qui n’est pas une épreuve passe', () => {
|
||
const outils = "import { mesurerImages } from './navigateur.js';\n";
|
||
const modules = [
|
||
{ chemin: 'scripts/banc/navigateur.js', texte: "import { cdp } from 'vitest/browser';\n" },
|
||
{ chemin: 'scripts/banc/statistiques.test.js', texte: "import { statistiques } from './statistiques.js';\n" },
|
||
{ chemin: 'scripts/banc/defilement.banc.js', texte: outils },
|
||
{ chemin: 'scripts/banc/defilement.navigateur.test.js', texte: outils },
|
||
{ chemin: 'scripts/banc/gestes.js', texte: `${outils}export const mesurer = (geste) => mesurerImages(geste);\n` },
|
||
{ chemin: 'scripts/banc/pivots.js', texte: "export { mesurer } from './gestes.js?brut';\n" },
|
||
{ chemin: 'scripts/banc/zoom/crans.navigateur.test.js', texte: "import { mesurerImages } from '/scripts/banc/navigateur.js';\n" },
|
||
{ chemin: 'src/interface/plan/Pivot.navigateur.test.js', texte: "import { mesurer } from '../../../scripts/banc/pivots.js';\n" },
|
||
{ chemin: 'src/interface/plan/Zoom.navigateur.test.js', texte: "const o = await import('../../../scripts/banc/navigateur.js');\n" },
|
||
{ chemin: 'test/navigateur/listes.banc.js', texte: '' },
|
||
];
|
||
assert.deepEqual(
|
||
epreuvesDuBanc(modules).map(({ chemin }) => chemin),
|
||
[
|
||
'scripts/banc/defilement.banc.js',
|
||
'scripts/banc/defilement.navigateur.test.js',
|
||
'scripts/banc/zoom/crans.navigateur.test.js',
|
||
'src/interface/plan/Pivot.navigateur.test.js',
|
||
'src/interface/plan/Zoom.navigateur.test.js',
|
||
'test/navigateur/listes.banc.js',
|
||
],
|
||
);
|
||
assert.deepEqual(releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), [
|
||
'scripts/banc/defilement.navigateur.test.js : hors du projet banc',
|
||
'scripts/banc/zoom/crans.navigateur.test.js : hors du projet banc',
|
||
'src/interface/plan/Pivot.navigateur.test.js : jouée par le projet navigateur',
|
||
'src/interface/plan/Pivot.navigateur.test.js : hors du projet banc',
|
||
'src/interface/plan/Zoom.navigateur.test.js : jouée par le projet navigateur',
|
||
'src/interface/plan/Zoom.navigateur.test.js : hors du projet banc',
|
||
'test/navigateur/listes.banc.js : hors du projet banc',
|
||
]);
|
||
const serieTropLarge = { name: 'node', include: ['scripts/**/*.js'], exclude: ['**/*.navigateur.test.js'] };
|
||
assert.deepEqual(releverBancs(modules.slice(0, 3), { projets: [serieTropLarge], banc: projetBanc() }), [
|
||
'scripts/banc/defilement.banc.js : jouée par le projet node',
|
||
]);
|
||
});
|
||
|
||
test('la garde refuse de passer sans épreuve du banc', () => {
|
||
const modules = [{ chemin: 'scripts/banc/navigateur.js', texte: "import { cdp } from 'vitest/browser';\n" }];
|
||
assert.throws(() => releverBancs(modules, { projets: projetsDesSeries(), banc: projetBanc() }), /aucune épreuve du banc/);
|
||
});
|
||
});
|
||
|
||
describe('banc : sa configuration (§ 19.10)', () => {
|
||
test('le projet banc inclut scripts/banc/**/*.banc.js, et chaque appel rend un objet neuf', () => {
|
||
const projet = projetBanc();
|
||
assert.equal(projet.name, 'banc');
|
||
assert.deepEqual(projet.include, ['scripts/banc/**/*.banc.js']);
|
||
projet.include.push('ailleurs/**/*.js');
|
||
projet.provide.cadreBanc.largeur = 1;
|
||
assert.deepEqual(projetBanc().include, ['scripts/banc/**/*.banc.js']);
|
||
assert.equal(projetBanc().provide.cadreBanc.largeur, 1366);
|
||
});
|
||
|
||
test('Chromium sans tête, cadence non bridée, cadre 1 366 × 700 au facteur 1, fichiers l’un après l’autre, délai large', () => {
|
||
const { browser, fileParallelism, testTimeout, hookTimeout, provide } = projetBanc();
|
||
assert.equal(browser.enabled, true);
|
||
assert.equal(browser.headless, true);
|
||
assert.deepEqual(browser.instances, [{ browser: 'chromium' }]);
|
||
assert.deepEqual(browser.viewport, { width: 1366, height: 700 });
|
||
const { launchOptions, contextOptions } = optionsBanc({ LANG: 'fr_CA.UTF-8' });
|
||
assert.deepEqual(launchOptions.args.slice(-2), DRAPEAUX_CADENCE);
|
||
assert.deepEqual(contextOptions, { viewport: { width: 1366, height: 700 }, deviceScaleFactor: 1 });
|
||
assert.deepEqual(provide, { cadreBanc: { largeur: 1366, hauteur: 700 } });
|
||
assert.equal(fileParallelism, false);
|
||
assert.ok(testTimeout >= 600_000, `testTimeout ${testTimeout}`);
|
||
assert.ok(hookTimeout >= 600_000, `hookTimeout ${hookTimeout}`);
|
||
});
|
||
|
||
test('optionsBanc ajoute les drapeaux de cadence aux options de test/chromium.js, sans les perdre', () => {
|
||
assert.deepEqual(optionsBanc({ LANG: 'fr_CA.UTF-8' }), {
|
||
launchOptions: { args: DRAPEAUX_CADENCE },
|
||
contextOptions: { viewport: { width: 1366, height: 700 }, deviceScaleFactor: 1 },
|
||
});
|
||
const env = { LANG: 'C', GTT_CHROMIUM: '/opt/chromium-epreuve/chrome' };
|
||
const { launchOptions } = optionsBanc(env);
|
||
assert.equal(launchOptions.executablePath, '/opt/chromium-epreuve/chrome');
|
||
assert.equal(launchOptions.env.LANG, 'C.UTF-8');
|
||
assert.deepEqual(launchOptions.args, DRAPEAUX_CADENCE);
|
||
assert.deepEqual(env, { LANG: 'C', GTT_CHROMIUM: '/opt/chromium-epreuve/chrome' });
|
||
});
|
||
});
|
||
|
||
describe('banc : sa commande', () => {
|
||
const { scripts = {} } = JSON.parse(readFileSync(join(RACINE, 'package.json'), 'utf8'));
|
||
const makefile = readFileSync(join(RACINE, 'Makefile'), 'utf8');
|
||
|
||
test('npm run banc lance scripts/banc/lancer.js, et aucun autre script de package.json ne touche au banc', () => {
|
||
assert.equal(scripts.banc, 'node scripts/banc/lancer.js');
|
||
const autres = Object.entries(scripts).filter(([nom, commande]) => nom !== 'banc' && /banc/.test(commande));
|
||
assert.deepEqual(autres, []);
|
||
});
|
||
|
||
test('make banc lance npm run banc, cible factice documentée', () => {
|
||
assert.deepEqual(cibleDuMakefile(makefile, 'banc'), {
|
||
documentation: 'Relève les constantes du banc de mesure (§ 19.10), avant une livraison',
|
||
commandes: ['$(AVEC_NODE) npm run banc'],
|
||
factice: true,
|
||
});
|
||
});
|
||
|
||
test('la lecture du Makefile relève une cible absente, une recette autre, une cible non factice', () => {
|
||
assert.equal(cibleDuMakefile('test:\n\t$(AVEC_NODE) npm test\n', 'banc'), null);
|
||
assert.deepEqual(cibleDuMakefile('.PHONY: test\nbanc: ## Mesure\n\tnode scripts/banc/lancer.js\n\nx:\n\techo\n', 'banc'), {
|
||
documentation: 'Mesure',
|
||
commandes: ['node scripts/banc/lancer.js'],
|
||
factice: false,
|
||
});
|
||
});
|
||
});
|