`make banc` runs the node and browser measures, assembles releve.json in the registry's order and fails when a measure is missing, a harness check falls, a text measure is not in Noto Sans, or the time per image is not held. It prints values to adopt and never edits constantes.js; ρ is reported but stays at its working default on the reference machine. Each bench constant cites the CPU slowdown of its own measure. The reading names the machine class only: identifying keys, accented or camelCase, and account paths are refused. Checked: node 3836, browser 758, node-long 101; make banc exits 0. --- FR --- [ADD] banc : orchestrateur make banc, premier relevé, constantes `make banc` lance les mesures node et navigateur, assemble releve.json dans l'ordre du registre et échoue sur une mesure manquante, un contrôle du harnais tombé, une mesure de texte hors Noto Sans ou un temps par image non tenu. Il imprime les valeurs à adopter sans toucher constantes.js ; ρ est relevé mais reste à son défaut de travail. Chaque constante du banc cite le ralentissement de sa propre mesure. Le relevé ne nomme que la classe de machine : clés identifiantes, accentuées ou camelCase, et chemins de compte sont refusés. Vérifié : node 3836, navigateur 758, longues 101 ; make banc sort en 0. Assisted-by: Claude Opus 5.5
377 lines
17 KiB
JavaScript
377 lines
17 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Épreuves de l'orchestrateur du banc (§ 19.10, § 14.1, § 19.8), sous node
|
||
// et sans navigateur : l'assemblage du relevé, qui refuse une mesure
|
||
// manquante et range les mesures dans l'ordre du registre ; la
|
||
// configuration, liste fermée du contrat, qui ne nomme pas la machine ;
|
||
// le jugement qui rend la sortie non nulle ; la région des constantes ; le
|
||
// déroulé de executer, sur des effets factices. Les machines, les versions
|
||
// et les valeurs sont inventées ; le relevé committé n'est lu que pour sa
|
||
// conformité.
|
||
import assert from 'node:assert/strict';
|
||
import { describe, test } from '../../test/lanceur.js';
|
||
import { CONSTANTES } from '../../src/geometrie/constantes.js';
|
||
import { projetBanc } from './projet.js';
|
||
import { verifierReleve } from './releve.js';
|
||
import releveCommite from './releve.json' with { type: 'json' };
|
||
import {
|
||
ATTENDUES,
|
||
CHEMIN_RELEVE,
|
||
FAMILLE_EMBARQUEE,
|
||
GARDES,
|
||
SORTIES_NAVIGATEUR,
|
||
assembler,
|
||
configurationDuBanc,
|
||
executer,
|
||
jugement,
|
||
regionConstantes,
|
||
} from './lancer.js';
|
||
|
||
// Un système d'exploitation inventé, au nom d'hôte, au dossier personnel, à
|
||
// l'utilisateur et au modèle de processeur reconnaissables : aucun ne doit
|
||
// atteindre le relevé.
|
||
const HOTE_INVENTE = 'poste-quenouille-7';
|
||
const UTILISATEUR_INVENTE = 'quenouille';
|
||
const DOSSIER_INVENTE = '/home/quenouille';
|
||
const MODELE_INVENTE = 'Processeur Quenouille 6 cœurs';
|
||
const systemeInvente = () => ({
|
||
hostname: () => HOTE_INVENTE,
|
||
homedir: () => DOSSIER_INVENTE,
|
||
userInfo: () => ({ username: UTILISATEUR_INVENTE, homedir: DOSSIER_INVENTE }),
|
||
type: () => 'Linux',
|
||
arch: () => 'x64',
|
||
cpus: () => Array.from({ length: 6 }, () => ({ model: MODELE_INVENTE })),
|
||
availableParallelism: () => 6,
|
||
});
|
||
|
||
const configuration = () => configurationDuBanc({ systeme: systemeInvente(), node: 'v26.10.0', navigateur: 'Chromium 140.0' });
|
||
|
||
// Les sorties d'un passage du banc, de valeurs inventées : une mesure par
|
||
// constante du banc et par garde, rangées dans un ordre qui n'est pas celui
|
||
// du registre. Comme celles du navigateur, les mesures de texte et la garde
|
||
// du temps par image portent leur ralentissement ; celles du parallèle n'en
|
||
// portent pas, et leur sortie le dit dans sa configuration.
|
||
const sorties = () =>
|
||
new Map([
|
||
['plafond', { plafondAPriori: [{ cle: 'grande', N: 260, T: 33, R: 4, msPlafonds: 0.07, msDiagnostic: 0.9 }] }],
|
||
[
|
||
'parallele',
|
||
{
|
||
format: 1,
|
||
configuration: {
|
||
cadre: { largeur: 1366, hauteur: 700 },
|
||
facteurEchelle: 1,
|
||
navigateur: null,
|
||
node: null,
|
||
processeursLogiques: 6,
|
||
ralentissementCpu: 1,
|
||
police: null,
|
||
},
|
||
mesures: [
|
||
{ nom: 'RALENTISSEMENT_PLEINE_CHARGE', unite: '×', valeurProposee: 2.5, critere: 't(L) / t(1)', courbe: [] },
|
||
{ nom: 'DUREE_GENERATION_PAR_DEFAUT', unite: 'ms', valeurProposee: 600, critere: 'médiane de 3 générations' },
|
||
],
|
||
parallele: [{ coeurs: 1, ralentissement: 1 }],
|
||
},
|
||
],
|
||
[
|
||
'temps_image',
|
||
{
|
||
format: 1,
|
||
mesures: [{ nom: 'TEMPS_IMAGE_LISTES', unite: 'ms', valeurProposee: null, critere: 'p95 sous 33 ms', courbe: [], verdict: 'tenu', ralentissementCpu: 4 }],
|
||
},
|
||
],
|
||
[
|
||
'temoin',
|
||
{
|
||
format: 1,
|
||
mesures: [{ nom: 'TOLERANCE_CHAINE_TEMOIN', unite: 'rapport', valeurProposee: 0.02, critere: 'quart du repli', police: 'Noto Sans', ralentissementCpu: 1 }],
|
||
},
|
||
],
|
||
[
|
||
'sieges_lisibles',
|
||
{
|
||
format: 1,
|
||
mesures: [{ nom: 'SEUIL_SIEGES_LISIBLES', unite: 'places', valeurProposee: 17, critere: 'liste dans le plan', police: 'Noto Sans', ralentissementCpu: 1 }],
|
||
},
|
||
],
|
||
['harnais', { format: 1, releves: [{ controle: 'cadre' }] }],
|
||
]);
|
||
|
||
const noms = (releve) => releve.mesures.map(({ nom }) => nom);
|
||
const DU_BANC = CONSTANTES.filter((c) => c.proprietaire === 'banc').map((c) => c.nom);
|
||
|
||
describe('lancer : les mesures attendues', () => {
|
||
test('chaque constante du banc du registre est attendue, dans l’ordre du registre, puis les gardes, par ordre de nom', () => {
|
||
assert.ok(DU_BANC.length > 0);
|
||
assert.deepEqual([...ATTENDUES], [...DU_BANC, ...GARDES]);
|
||
assert.deepEqual([...GARDES], ['DUREE_GENERATION_PAR_DEFAUT', 'TEMPS_IMAGE_LISTES']);
|
||
assert.ok(GARDES.every((nom) => !CONSTANTES.some((c) => c.nom === nom)));
|
||
});
|
||
});
|
||
|
||
describe('lancer : assembler (§ 19.10)', () => {
|
||
test('un relevé conforme : les mesures dans l’ordre du registre, puis les gardes ; plafond et parallèle repris', () => {
|
||
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
|
||
assert.deepEqual(verifierReleve(releve), []);
|
||
assert.deepEqual(noms(releve), [
|
||
'SEUIL_SIEGES_LISIBLES',
|
||
'TOLERANCE_CHAINE_TEMOIN',
|
||
'RALENTISSEMENT_PLEINE_CHARGE',
|
||
'DUREE_GENERATION_PAR_DEFAUT',
|
||
'TEMPS_IMAGE_LISTES',
|
||
]);
|
||
assert.deepEqual(Object.keys(releve), ['format', 'configuration', 'mesures', 'plafondAPriori', 'parallele']);
|
||
assert.equal(releve.plafondAPriori[0].cle, 'grande');
|
||
assert.deepEqual(releve.parallele, [{ coeurs: 1, ralentissement: 1 }]);
|
||
assert.equal(releve.mesures[0].police, 'Noto Sans');
|
||
});
|
||
|
||
test('l’ordre des mesures ne dépend ni de l’ordre des sorties ni de celui des attendues', () => {
|
||
const inverse = new Map([...sorties()].reverse());
|
||
const attendues = [...ATTENDUES].reverse();
|
||
const releve = assembler({ configuration: configuration(), sorties: inverse, attendues });
|
||
assert.deepEqual(noms(releve), [...DU_BANC, ...GARDES]);
|
||
});
|
||
|
||
test('l’ordre des mesures est celui du registre reçu : un registre renversé les renverse, les gardes restant après', () => {
|
||
const renverse = [...CONSTANTES].reverse();
|
||
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES, constantes: renverse });
|
||
assert.deepEqual(noms(releve), [...[...DU_BANC].reverse(), ...GARDES]);
|
||
});
|
||
|
||
test('chaque mesure porte le ralentissement sous lequel elle s’est prise : le sien, sinon celui de la configuration de sa sortie', () => {
|
||
const recues = sorties();
|
||
const releve = assembler({ configuration: configuration(), sorties: recues, attendues: ATTENDUES });
|
||
assert.deepEqual(
|
||
releve.mesures.map(({ nom, ralentissementCpu }) => [nom, ralentissementCpu]),
|
||
[
|
||
['SEUIL_SIEGES_LISIBLES', 1],
|
||
['TOLERANCE_CHAINE_TEMOIN', 1],
|
||
['RALENTISSEMENT_PLEINE_CHARGE', 1],
|
||
['DUREE_GENERATION_PAR_DEFAUT', 1],
|
||
['TEMPS_IMAGE_LISTES', 4],
|
||
],
|
||
);
|
||
assert.equal(releve.configuration.ralentissementCpu, 4);
|
||
assert.ok(!Object.hasOwn(recues.get('parallele').mesures[0], 'ralentissementCpu'), 'les sorties reçues restent intactes');
|
||
const sansConfiguration = sorties();
|
||
delete sansConfiguration.get('parallele').configuration;
|
||
const autre = assembler({ configuration: configuration(), sorties: sansConfiguration, attendues: ATTENDUES });
|
||
assert.ok(!Object.hasOwn(autre.mesures[2], 'ralentissementCpu'));
|
||
});
|
||
|
||
test('une sortie manquante se refuse en nommant chaque mesure qu’elle portait', () => {
|
||
const manque = sorties();
|
||
manque.delete('parallele');
|
||
assert.throws(
|
||
() => assembler({ configuration: configuration(), sorties: manque, attendues: ATTENDUES }),
|
||
/mesure attendue absente : RALENTISSEMENT_PLEINE_CHARGE, DUREE_GENERATION_PAR_DEFAUT/,
|
||
);
|
||
const vide = new Map();
|
||
assert.throws(() => assembler({ configuration: configuration(), sorties: vide, attendues: ATTENDUES }), /absente : SEUIL_SIEGES_LISIBLES/);
|
||
});
|
||
|
||
test('une mesure en double, une mesure inattendue, un champ de sommet deux fois porté se refusent', () => {
|
||
const double = sorties();
|
||
double.set('autre', { mesures: [{ nom: 'SEUIL_SIEGES_LISIBLES', unite: 'places', valeurProposee: 3, critere: 'x' }] });
|
||
assert.throws(() => assembler({ configuration: configuration(), sorties: double, attendues: ATTENDUES }), /en double : SEUIL_SIEGES_LISIBLES/);
|
||
const inattendue = sorties();
|
||
inattendue.set('autre', { mesures: [{ nom: 'TEMPS_INVENTE', unite: 'ms', valeurProposee: 1, critere: 'x' }] });
|
||
assert.throws(() => assembler({ configuration: configuration(), sorties: inattendue, attendues: ATTENDUES }), /inattendue : TEMPS_INVENTE/);
|
||
const deuxParalleles = sorties();
|
||
deuxParalleles.set('autre', { parallele: [] });
|
||
assert.throws(() => assembler({ configuration: configuration(), sorties: deuxParalleles, attendues: ATTENDUES }), /parallele : porté par deux sorties/);
|
||
});
|
||
|
||
test('un relevé que verifierReleve refuse ne s’assemble pas : la faute est nommée', () => {
|
||
const fautive = sorties();
|
||
fautive.get('temoin').mesures[0].critere = 'relevé sous /home/quenouille/banc';
|
||
assert.throws(
|
||
() => assembler({ configuration: configuration(), sorties: fautive, attendues: ATTENDUES }),
|
||
/mesures\[1\]\.critere : chemin de compte/,
|
||
);
|
||
});
|
||
|
||
test('des sorties qui ne sont pas une table se refusent', () => {
|
||
assert.throws(() => assembler({ configuration: configuration(), sorties: {}, attendues: ATTENDUES }), TypeError);
|
||
});
|
||
});
|
||
|
||
describe('lancer : la configuration ne nomme pas la machine (contrainte 27)', () => {
|
||
test('ni le nom d’hôte, ni l’utilisateur, ni le dossier personnel, ni le modèle du processeur injectés n’y paraissent', () => {
|
||
const texte = JSON.stringify(configuration());
|
||
for (const nom of [HOTE_INVENTE, UTILISATEUR_INVENTE, DOSSIER_INVENTE, MODELE_INVENTE]) assert.ok(!texte.includes(nom), nom);
|
||
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
|
||
for (const nom of [HOTE_INVENTE, MODELE_INVENTE]) assert.ok(!JSON.stringify(releve).includes(nom), nom);
|
||
});
|
||
|
||
test('la liste fermée du contrat : cadre du projet banc, facteur, versions, processeurs logiques, ralenti ×4, police embarquée', () => {
|
||
assert.deepEqual(configuration(), {
|
||
cadre: projetBanc().provide.cadreBanc,
|
||
facteurEchelle: 1,
|
||
navigateur: 'Chromium 140.0',
|
||
node: 'v26.10.0',
|
||
processeursLogiques: 6,
|
||
ralentissementCpu: 4,
|
||
police: FAMILLE_EMBARQUEE,
|
||
});
|
||
assert.equal(FAMILLE_EMBARQUEE, 'Noto Sans');
|
||
});
|
||
});
|
||
|
||
describe('lancer : le jugement qui rend la sortie non nulle', () => {
|
||
const releve = () => assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
|
||
|
||
test('un relevé tenu sous la police embarquée : aucun motif', () => {
|
||
assert.deepEqual(jugement(releve()), { bloquants: [], nonTenus: [] });
|
||
});
|
||
|
||
test('une mesure de texte sous une autre police, ou sans police nommée : motif bloquant', () => {
|
||
const r = releve();
|
||
r.mesures[0].police = 'DejaVu Sans';
|
||
delete r.mesures[1].police;
|
||
assert.deepEqual(jugement(r).bloquants, [
|
||
'SEUIL_SIEGES_LISIBLES mesurée sous la police « DejaVu Sans », et non sous Noto Sans',
|
||
'TOLERANCE_CHAINE_TEMOIN mesurée sous la police « inconnue », et non sous Noto Sans',
|
||
]);
|
||
});
|
||
|
||
test('un verdict non tenu : motif non bloquant, qui nomme la mesure et son critère', () => {
|
||
const r = releve();
|
||
r.mesures[4].verdict = 'nonTenu';
|
||
assert.deepEqual(jugement(r), { bloquants: [], nonTenus: ['TEMPS_IMAGE_LISTES : verdict non tenu — p95 sous 33 ms'] });
|
||
});
|
||
});
|
||
|
||
describe('lancer : la région des constantes (§ 19.8)', () => {
|
||
test('autant de lignes que le registre, sous un en-tête et son séparateur', () => {
|
||
const lignes = regionConstantes().split('\n');
|
||
assert.equal(lignes.length, CONSTANTES.length + 2);
|
||
CONSTANTES.forEach((c, i) => assert.ok(lignes[i + 2].startsWith(`| \`${c.nom}\` |`), c.nom));
|
||
});
|
||
|
||
test('elle lit le relevé reçu : la valeur proposée paraît', () => {
|
||
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
|
||
const ligne = regionConstantes({ releve }).split('\n').find((l) => l.startsWith('| `SEUIL_SIEGES_LISIBLES` |'));
|
||
assert.match(ligne, /\| 17 places \|/);
|
||
});
|
||
|
||
test('chaque constante du banc cite le ralentissement de sa mesure : à ×1, « processeur non ralenti », jamais ×4', () => {
|
||
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
|
||
const lignes = regionConstantes({ releve }).split('\n');
|
||
for (const nom of DU_BANC) {
|
||
const ligne = lignes.find((l) => l.startsWith(`| \`${nom}\` |`));
|
||
assert.match(ligne, /, processeur non ralenti, police Noto Sans \|$/, nom);
|
||
assert.doesNotMatch(ligne, /ralenti ×4/, nom);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('lancer : executer, sur des effets factices', () => {
|
||
// Un passage factice : chaque effet se note dans journal, dans l'ordre.
|
||
// fichiers tient les sorties que le navigateur aurait écrites.
|
||
function passage({ codeVitest = 0, fichiers = sorties(), plafond = sorties().get('plafond').plafondAPriori } = {}) {
|
||
const journal = [];
|
||
const ecrits = new Map();
|
||
const lignes = [];
|
||
const effets = {
|
||
mesurerPlafond: () => {
|
||
journal.push('plafond');
|
||
return plafond;
|
||
},
|
||
lancerVitest: () => {
|
||
journal.push('vitest');
|
||
return codeVitest;
|
||
},
|
||
lireNavigateur: async () => 'Chromium 140.0',
|
||
systeme: systemeInvente(),
|
||
node: 'v26.10.0',
|
||
effacer: (chemin) => journal.push(`effacer ${chemin}`),
|
||
lire: (chemin) => {
|
||
const nom = /^scripts\/banc\/sorties\/([a-z_]+)\.json$/.exec(chemin)?.[1];
|
||
if (nom === undefined || !fichiers.has(nom)) throw Object.assign(new Error(`absent : ${chemin}`), { code: 'ENOENT' });
|
||
return JSON.stringify(fichiers.get(nom));
|
||
},
|
||
ecrire: (chemin, texte) => ecrits.set(chemin, texte),
|
||
sortie: (ligne) => lignes.push(ligne),
|
||
};
|
||
return { effets, journal, ecrits, lignes };
|
||
}
|
||
|
||
test('le déroulé : sorties effacées, mesures node, banc navigateur ; le relevé écrit, le tableau et les écarts imprimés ; code 0', async () => {
|
||
const p = passage();
|
||
assert.equal(await executer(p.effets), 0);
|
||
assert.deepEqual(p.journal, [...SORTIES_NAVIGATEUR.map((nom) => `effacer scripts/banc/sorties/${nom}.json`), 'plafond', 'vitest']);
|
||
assert.deepEqual([...p.ecrits.keys()], [CHEMIN_RELEVE]);
|
||
const ecrit = p.ecrits.get(CHEMIN_RELEVE);
|
||
assert.ok(ecrit.endsWith('}\n'));
|
||
const releve = JSON.parse(ecrit);
|
||
assert.deepEqual(verifierReleve(releve), []);
|
||
assert.deepEqual(noms(releve), [...ATTENDUES]);
|
||
const texte = p.lignes.join('\n');
|
||
assert.ok(texte.includes(regionConstantes({ releve })));
|
||
assert.match(texte, /SEUIL_SIEGES_LISIBLES : registre 16 places, relevé 17 places — à adopter/);
|
||
assert.match(
|
||
texte,
|
||
/RALENTISSEMENT_PLEINE_CHARGE : registre 2 ×, relevé 2,5 × — relevé, non adopté : défaut de travail sous la machine de référence/,
|
||
);
|
||
assert.doesNotMatch(texte, /RALENTISSEMENT_PLEINE_CHARGE : .*à adopter/);
|
||
assert.match(texte, /Accord du registre avec le relevé : aucun écart/);
|
||
});
|
||
|
||
test('une épreuve du banc tombée (un contrôle du harnais) : code 1, aucun relevé écrit', async () => {
|
||
const p = passage({ codeVitest: 1 });
|
||
assert.equal(await executer(p.effets), 1);
|
||
assert.equal(p.ecrits.size, 0);
|
||
assert.match(p.lignes.join('\n'), /épreuve du banc est tombée/);
|
||
});
|
||
|
||
test('une sortie manquante : code 1, aucun relevé écrit, la mesure nommée', async () => {
|
||
const fichiers = sorties();
|
||
fichiers.delete('temoin');
|
||
const p = passage({ fichiers });
|
||
assert.equal(await executer(p.effets), 1);
|
||
assert.equal(p.ecrits.size, 0);
|
||
assert.match(p.lignes.join('\n'), /absente : TOLERANCE_CHAINE_TEMOIN/);
|
||
});
|
||
|
||
test('un verdict non tenu : le relevé s’écrit, qui le porte, et le code vaut 1', async () => {
|
||
const fichiers = sorties();
|
||
fichiers.get('temps_image').mesures[0].verdict = 'nonTenu';
|
||
const p = passage({ fichiers });
|
||
assert.equal(await executer(p.effets), 1);
|
||
assert.equal(JSON.parse(p.ecrits.get(CHEMIN_RELEVE)).mesures[4].verdict, 'nonTenu');
|
||
assert.match(p.lignes.join('\n'), /TEMPS_IMAGE_LISTES : verdict non tenu/);
|
||
});
|
||
|
||
test('une mesure de texte hors de la police embarquée : code 1, aucun relevé écrit', async () => {
|
||
const fichiers = sorties();
|
||
fichiers.get('temoin').mesures[0].police = 'DejaVu Sans';
|
||
const p = passage({ fichiers });
|
||
assert.equal(await executer(p.effets), 1);
|
||
assert.equal(p.ecrits.size, 0);
|
||
assert.match(p.lignes.join('\n'), /sous la police « DejaVu Sans »/);
|
||
});
|
||
});
|
||
|
||
describe('lancer : le relevé committé', () => {
|
||
test('il est conforme, et porte chaque mesure attendue dans l’ordre du registre', () => {
|
||
assert.deepEqual(verifierReleve(releveCommite), []);
|
||
assert.deepEqual(noms(releveCommite), [...ATTENDUES]);
|
||
for (const champ of ['navigateur', 'node', 'processeursLogiques']) {
|
||
assert.notEqual(releveCommite.configuration[champ] ?? null, null, champ);
|
||
}
|
||
assert.deepEqual(Object.keys(releveCommite.configuration), [
|
||
'cadre',
|
||
'facteurEchelle',
|
||
'navigateur',
|
||
'node',
|
||
'processeursLogiques',
|
||
'ralentissementCpu',
|
||
'police',
|
||
]);
|
||
for (const mesure of releveCommite.mesures) assert.ok(Number.isFinite(mesure.ralentissementCpu), mesure.nom);
|
||
});
|
||
});
|