diff --git a/README.md b/README.md index 0bac77a..ab412e5 100644 --- a/README.md +++ b/README.md @@ -117,26 +117,26 @@ l'exécution elle-même —, son statut et la façon de le relever. Une valeur a -| Constante | Valeur | Unité | Propriétaire | Statut | Protocole | -|---|---|---|---|---|---| -| `SEUIL_GLISSER_PX` | 4 | px | manuel | défaut de travail | vingt clics délibérés au pavé tactile de la machine de référence ; le plus grand déplacement entre pointerdown et pointerup, × 1,5, écrit dans ce registre | -| `DUREE_PRESSION_LONGUE_MS` | 500 | ms | manuel | défaut de travail | à l'écran tactile de la machine de référence, la plus courte durée qu'aucun appui bref ne déclenche, sur dix essais, écrite dans ce registre | -| `SEUIL_SIEGES_LISIBLES` | 16 | places | banc | défaut de travail | banc de mesure, cadre de 1 366 × 700 px : le plus grand nombre de places dont la liste, à la hauteur de ligne mesurée, tient dans la hauteur du cadre au zoom ajusté d'une table seule ; écrit dans ce registre | -| `K_LISTES` | 0,3 | px/cm | manuel | défaut de travail | une personne qui n'a pas écrit le logiciel lit les listes de la grande démonstration sur la machine de référence à k décroissant ; le plus petit k où elle les lit sans erreur, écrit dans ce registre | -| `LARGEUR_BLOC_NOMS_CM` | 500 | cm | manuel | défaut de travail | relecture de la grande démonstration au zoom par défaut par une personne qui n'a pas écrit le logiciel (§ 19.11) ; la largeur retenue, écrite dans ce registre | -| `HAUTEUR_LIGNE` | — | cm | execution | mesurée | mesurée à chaque montage du plan : measureText du gabarit « Ôjgq Æ W » sur la police calculée des listes ; aucune valeur n'est écrite dans ce registre | -| `ZONE_SILENCE_MM` | 8 | mm | manuel | défaut de travail | sur chaque imprimante visée : une planche de filets imprimée, son décalage mesuré à la règle, plus l'erreur de dix coupes ; puis sa marge non imprimable et le biais d'entraînement sur dix feuilles. La plus grande des deux bornes, arrondie au millimètre supérieur, écrite dans ce registre | -| `HAUTEUR_CAPITALE_NOM_MAX_MM` | 9 | mm | manuel | défaut de travail | impression réelle d'une planche ; lecture à un mètre par une personne qui n'a pas écrit le logiciel ; la hauteur retenue, écrite dans ce registre | -| `HAUTEUR_CAPITALE_NOM_PLANCHER_MM` | 4 | mm | manuel | défaut de travail | impression réelle d'une planche à hauteurs décroissantes ; lecture à un mètre par une personne qui n'a pas écrit le logiciel ; la plus petite lue sans erreur, écrite dans ce registre | -| `CORPS_TOURS_PT` | 14 | pt | manuel | défaut de travail | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | -| `CORPS_TOURS_PLANCHER_PT` | 10 | pt | manuel | défaut de travail | impression réelle d'une planche à corps décroissants ; lecture en main par une personne qui n'a pas écrit le logiciel ; le plus petit corps lu sans erreur, écrit dans ce registre | -| `CORPS_APPARTENANCE_PT` | 12 | pt | manuel | défaut de travail | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | -| `CORPS_PIED_PT` | 8 | pt | manuel | défaut de travail | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | -| `CORPS_MIN_IMPRESSION_MM` | 2,5 | mm | manuel | défaut de travail | page d'essai à corps décroissants imprimée sur l'imprimante visée, lue à bout de bras ; le plus petit corps lu sans erreur, écrit dans ce registre | -| `MARGE_PAGE_MM` | 10 | mm | manuel | défaut de travail | marge non imprimable mesurée sur chaque imprimante visée (une page de filets à 1 mm d'écart, lue à la règle) ; la plus grande, écrite dans ce registre | -| `HAUTEUR_BANDE_IMPRESSION` | — | mm | execution | mesurée | mesurée à l'ouverture de l'aperçu : tout ce qu'une feuille porte hors de [data-contenu] ; aucune valeur n'est écrite dans ce registre | -| `TOLERANCE_CHAINE_TEMOIN` | 0,01 | rapport | banc | défaut de travail | banc de mesure : le quart de l'écart relatif entre la police embarquée et une famille de repli forcée, et au moins trois fois la gigue de dix mesures ; écrit dans ce registre | -| `RALENTISSEMENT_PLEINE_CHARGE` | 2 | × | banc | défaut de travail | banc de mesure : durée d'une même génération sur un travailleur, puis sur L travailleurs ; le rapport ρ des médianes, écrit dans ce registre | +| Constante | Ce qu’elle mesure | Valeur | Statut | Propriétaire | Protocole | Valeur proposée par le banc | Configuration du relevé | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `SEUIL_GLISSER_PX` | déplacement du pointeur, entre pointerdown et pointerup, au-delà duquel un appui devient un glissement | 4 px | défaut de travail | manuel | vingt clics délibérés au pavé tactile de la machine de référence ; le plus grand déplacement entre pointerdown et pointerup, × 1,5, écrit dans ce registre | — | pavé tactile de la machine de référence | +| `DUREE_PRESSION_LONGUE_MS` | durée d'un appui immobile au-delà de laquelle il ouvre le menu d'une personne | 500 ms | défaut de travail | manuel | à l'écran tactile de la machine de référence, la plus courte durée qu'aucun appui bref ne déclenche, sur dix essais, écrite dans ce registre | — | écran tactile de la machine de référence | +| `SEUIL_SIEGES_LISIBLES` | plus grand nombre de places d'une table dont la liste de noms reste lisible | 16 places | défaut de travail | banc | banc de mesure, cadre de 1 366 × 700 px : le plus grand nombre de places dont la liste, à la hauteur de ligne mesurée, tient dans la hauteur du cadre au zoom ajusté d'une table seule ; écrit dans ce registre | 18 places | cadre de 1 366 × 700 px, facteur 1, Chromium 153.0.8010.12, Node v26.10.0, 16 processeurs logiques, processeur non ralenti, police Noto Sans | +| `K_LISTES` | échelle sous laquelle le plan retire les listes de noms | 0,3 px/cm | défaut de travail | manuel | une personne qui n'a pas écrit le logiciel lit les listes de la grande démonstration sur la machine de référence à k décroissant ; le plus petit k où elle les lit sans erreur, écrit dans ce registre | — | grande démonstration, 33 tables avec listes, machine de référence, cadre de 1 366 × 700 px | +| `LARGEUR_BLOC_NOMS_CM` | largeur, dans le dessin, du bloc de noms d'une table, où un nom trop long s'abrège | 500 cm | défaut de travail | manuel | relecture de la grande démonstration au zoom par défaut par une personne qui n'a pas écrit le logiciel (§ 19.11) ; la largeur retenue, écrite dans ce registre | — | grande démonstration au zoom par défaut | +| `HAUTEUR_LIGNE` | hauteur d'une ligne d'une liste de noms, dans le dessin | relevée à l’exécution | mesurée | exécution | mesurée à chaque montage du plan : measureText du gabarit « Ôjgq Æ W » sur la police calculée des listes ; aucune valeur n'est écrite dans ce registre | — | police calculée des listes, relevée à chaque montage du plan | +| `ZONE_SILENCE_MM` | distance en deçà de laquelle aucun texte ni aucune marque ne s'approche d'un bord de badge | 8 mm | défaut de travail | manuel | sur chaque imprimante visée : une planche de filets imprimée, son décalage mesuré à la règle, plus l'erreur de dix coupes ; puis sa marge non imprimable et le biais d'entraînement sur dix feuilles. La plus grande des deux bornes, arrondie au millimètre supérieur, écrite dans ce registre | — | imprimantes visées, chaque papier des sorties, massicot et ciseaux | +| `HAUTEUR_CAPITALE_NOM_MAX_MM` | hauteur de capitale du nom sur le badge, au plus grand corps que la mise en page lui donne | 9 mm | défaut de travail | manuel | impression réelle d'une planche ; lecture à un mètre par une personne qui n'a pas écrit le logiciel ; la hauteur retenue, écrite dans ce registre | — | badge A6 imprimé, police embarquée, lecture à un mètre | +| `HAUTEUR_CAPITALE_NOM_PLANCHER_MM` | hauteur de capitale du nom sous laquelle la mise en page ne descend pas : un nom plus long replie | 4 mm | défaut de travail | manuel | impression réelle d'une planche à hauteurs décroissantes ; lecture à un mètre par une personne qui n'a pas écrit le logiciel ; la plus petite lue sans erreur, écrite dans ce registre | — | badge A6 imprimé, police embarquée, lecture à un mètre | +| `CORPS_TOURS_PT` | corps des lignes de tour du badge | 14 pt | défaut de travail | manuel | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | — | badge A6 imprimé, police embarquée, lecture en main | +| `CORPS_TOURS_PLANCHER_PT` | corps sous lequel les lignes de tour ne descendent pas | 10 pt | défaut de travail | manuel | impression réelle d'une planche à corps décroissants ; lecture en main par une personne qui n'a pas écrit le logiciel ; le plus petit corps lu sans erreur, écrit dans ce registre | — | badge A6 imprimé, police embarquée, lecture en main | +| `CORPS_APPARTENANCE_PT` | corps de la ligne d'appartenance du badge | 12 pt | défaut de travail | manuel | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | — | badge A6 imprimé, police embarquée, lecture en main | +| `CORPS_PIED_PT` | corps du pied du badge : nom et date de l'événement, horodatage du placement | 8 pt | défaut de travail | manuel | impression réelle d'une planche ; lecture en main par une personne qui n'a pas écrit le logiciel ; le corps retenu, écrit dans ce registre | — | badge A6 imprimé, police embarquée, lecture en main | +| `CORPS_MIN_IMPRESSION_MM` | hauteur de caractère du plan imprimé en deçà de laquelle le plan se découpe en pages | 2,5 mm | défaut de travail | manuel | page d'essai à corps décroissants imprimée sur l'imprimante visée, lue à bout de bras ; le plus petit corps lu sans erreur, écrit dans ce registre | — | imprimante visée, chaque papier des sorties, lecture à bout de bras | +| `MARGE_PAGE_MM` | marge de chaque bord d'une page imprimée par l'aperçu | 10 mm | défaut de travail | manuel | marge non imprimable mesurée sur chaque imprimante visée (une page de filets à 1 mm d'écart, lue à la règle) ; la plus grande, écrite dans ce registre | — | imprimantes visées, chaque papier des sorties | +| `HAUTEUR_BANDE_IMPRESSION` | hauteur de ce que porte une feuille imprimée hors de sa zone de contenu : pied, mentions, barre, cartouche | relevée à l’exécution | mesurée | exécution | mesurée à l'ouverture de l'aperçu : tout ce qu'une feuille porte hors de [data-contenu] ; aucune valeur n'est écrite dans ce registre | — | rendu de l'aperçu, relevé à chaque ouverture | +| `TOLERANCE_CHAINE_TEMOIN` | écart relatif admis entre la longueur rendue de la chaîne témoin et celle que donnent les avances de la police | 0,01 rapport | défaut de travail | banc | banc de mesure : le quart de l'écart relatif entre la police embarquée et une famille de repli forcée, et au moins trois fois la gigue de dix mesures ; écrit dans ce registre | 0,011 rapport | cadre de 1 366 × 700 px, facteur 1, Chromium 153.0.8010.12, Node v26.10.0, 16 processeurs logiques, processeur non ralenti, police Noto Sans | +| `RALENTISSEMENT_PLEINE_CHARGE` | ralentissement d'une génération quand tous les processeurs logiques travaillent, t(L) / t(1) | 2 × | défaut de travail | banc | banc de mesure : durée d'une même génération sur un travailleur, puis sur L travailleurs ; le rapport ρ des médianes, écrit dans ce registre | 2,79 × | cadre de 1 366 × 700 px, facteur 1, Chromium 153.0.8010.12, Node v26.10.0, 16 processeurs logiques, processeur non ralenti, police Noto Sans | diff --git a/scripts/banc/lancer.js b/scripts/banc/lancer.js new file mode 100644 index 0000000..229c37b --- /dev/null +++ b/scripts/banc/lancer.js @@ -0,0 +1,348 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// L'orchestrateur du banc de mesure (§ 19.10), que lancent npm run banc et +// make banc. Son déroulé : +// +// 1. effacer les sorties du navigateur (scripts/banc/sorties/.json, +// chacune par son nom), pour qu'aucune sortie d'un passage précédent +// ne se lise comme une mesure de celui-ci ; +// 2. relever le coût du plafond a priori sous node (plafond.js) ; +// 3. jouer le projet banc de Vitest (vitest.banc.config.js), qui écrit +// les sorties ; une épreuve tombée — un contrôle du harnais, une +// mesure qui se refuse — arrête le passage sans relevé ; +// 4. lire les sorties, et assembler le relevé : la configuration de la +// machine, puis chaque mesure attendue, une fois, dans l'ordre du +// registre, avec le ralentissement sous lequel elle s'est prise ; une +// mesure manquante arrête le passage sans relevé ; +// 5. juger : une mesure de texte prise hors de la police embarquée +// arrête le passage sans relevé ; un verdict « non tenu » s'écrit au +// relevé, qui est une donnée, et rend la sortie non nulle ; +// 6. écrire scripts/banc/releve.json, puis imprimer le tableau des +// constantes, les valeurs proposées face à celles du registre et les +// écarts de leur accord. +// +// Le banc écrit un relevé, jamais le registre : src/geometrie/constantes.js +// ne se lit ici que pour l'ordre et les valeurs, et une valeur s'adopte à la +// main (releve.js). La configuration du relevé est la liste fermée du +// contrat — cadre, facteur d'échelle, versions, nombre de processeurs +// logiques, ralentissement, police — et ne nomme pas la machine : ni nom +// d'hôte, ni modèle de processeur, ni utilisateur, ni chemin, ni date +// (contrainte 27). Code de sortie : 0 quand le relevé est écrit et que +// chaque verdict est tenu, 1 sinon. +import { spawnSync } from 'node:child_process'; +import { readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { CONSTANTES } from '../../src/geometrie/constantes.js'; +import { optionsBanc, projetBanc } from './projet.js'; +import { accordRegistre, nombre, tableauConstantes, verifierReleve } from './releve.js'; + +/** La racine du projet, d'où se résolvent les chemins relatifs. */ +export const RACINE = fileURLToPath(new URL('../../', import.meta.url)); + +/** Le relevé, relatif à la racine. */ +export const CHEMIN_RELEVE = 'scripts/banc/releve.json'; + +// Le dossier des sorties du navigateur, que navigateur.js écrit. +const DOSSIER_SORTIES = 'scripts/banc/sorties'; + +/** Les sorties que les épreuves du banc écrivent, par nom de fichier sans + * extension : le harnais, puis chaque mesure. */ +export const SORTIES_NAVIGATEUR = Object.freeze(['harnais', 'temoin', 'sieges_lisibles', 'temps_image', 'parallele']); + +/** Les gardes du banc : des mesures hors du registre, qui ne proposent pas + * de constante mais disent si une hypothèse tient, par ordre de nom. */ +export const GARDES = Object.freeze(['DUREE_GENERATION_PAR_DEFAUT', 'TEMPS_IMAGE_LISTES']); + +/** Les mesures qu'un relevé porte : chaque constante du banc du registre, + * dans son ordre, puis les gardes. Une constante du banc ajoutée au + * registre est attendue d'office, et manque tant que le banc ne la relève + * pas. */ +export const ATTENDUES = Object.freeze([...CONSTANTES.filter((c) => c.proprietaire === 'banc').map((c) => c.nom), ...GARDES]); + +/** La police embarquée, sous laquelle se prend toute mesure de texte. */ +export const FAMILLE_EMBARQUEE = 'Noto Sans'; + +// Les mesures qui lisent le rendu du texte, et nomment donc leur police. +const MESURES_DE_TEXTE = Object.freeze(['SEUIL_SIEGES_LISIBLES', 'TOLERANCE_CHAINE_TEMOIN']); + +// La machine de référence du banc : le poste, son processeur ralenti ×4 par +// le navigateur. C'est le ralentissement de la configuration du relevé ; +// chaque mesure porte en plus celui sous lequel elle s'est prise, que cite +// la configuration de sa constante. +const RALENTISSEMENT_REFERENCE = 4; + +// Les constantes du banc que la machine de référence relève sans les +// proposer : ρ y reste à son défaut de travail. Leur mesure s'imprime pour +// information, jamais « à adopter ». +const RELEVEES_SANS_ADOPTION = Object.freeze(['RALENTISSEMENT_PLEINE_CHARGE']); + +// Les champs de sommet qu'une sortie peut porter en plus de ses mesures, et +// que le relevé reprend tels quels. +const CHAMPS_REPRIS = Object.freeze(['plafondAPriori', 'parallele']); + +const cheminSortie = (nom) => `${DOSSIER_SORTIES}/${nom}.json`; +const estAbsent = (erreur) => erreur?.code === 'ENOENT'; + +/** + * La configuration du relevé : le cadre du projet banc au facteur 1, les + * versions du navigateur et de Node, le nombre des processeurs logiques, le + * ralentissement de la machine de référence, la police embarquée. Du + * système, seul availableParallelism se lit : ni nom d'hôte, ni modèle de + * processeur, ni utilisateur, ni dossier. + * + * @param {{systeme: Pick, + * node: string, navigateur: string|null}} options + * @returns {import('./releve.js').ConfigurationReleve} + */ +export function configurationDuBanc({ systeme, node, navigateur }) { + return { + cadre: { ...projetBanc().provide.cadreBanc }, + facteurEchelle: 1, + navigateur, + node, + processeursLogiques: systeme.availableParallelism(), + ralentissementCpu: RALENTISSEMENT_REFERENCE, + police: FAMILLE_EMBARQUEE, + }; +} + +// Le rang d'une mesure dans le relevé : son rang au registre, puis, pour +// une garde, après toutes les constantes, par ordre de nom. +function rangeur(constantes) { + const rang = (nom) => { + const auRegistre = constantes.findIndex((c) => c.nom === nom); + return auRegistre >= 0 ? auRegistre : constantes.length; + }; + return (a, b) => rang(a.nom) - rang(b.nom) || (a.nom < b.nom ? -1 : a.nom > b.nom ? 1 : 0); +} + +/** + * Le relevé d'un passage : la configuration, les mesures de toutes les + * sorties — chaque attendue une fois, rangées dans l'ordre du registre reçu + * puis les gardes par ordre de nom —, et plafondAPriori et parallele quand + * une sortie les porte. Une mesure sans ralentissement propre reçoit celui + * de la configuration de sa sortie, quand celle-ci en porte un : en copie, + * les sorties reçues restent intactes. Les sorties se lisent dans l'ordre + * de la table ; une sortie sans mesures (le harnais) n'apporte rien. + * + * @param {{configuration: import('./releve.js').ConfigurationReleve, + * sorties: Map, + * attendues: ReadonlyArray, + * constantes?: ReadonlyArray}} options + * @returns {import('./releve.js').Releve} + * @throws {TypeError} des sorties qui ne sont pas une Map + * @throws {Error} une mesure attendue absente, une mesure en double ou + * inattendue, un champ repris porté par deux sorties, ou un relevé que + * verifierReleve refuse — le message nomme chaque faute + */ +export function assembler({ configuration, sorties, attendues, constantes = CONSTANTES }) { + if (!(sorties instanceof Map)) throw new TypeError('sorties du banc : une Map attendue'); + const mesures = []; + const repris = new Map(); + for (const [nomSortie, sortie] of sorties) { + const ralentissementSortie = sortie.configuration?.ralentissementCpu; + for (const mesure of sortie.mesures ?? []) { + if (!attendues.includes(mesure.nom)) throw new Error(`mesure inattendue : ${mesure.nom} (sortie ${nomSortie})`); + if (mesures.some((m) => m.nom === mesure.nom)) throw new Error(`mesure en double : ${mesure.nom} (sortie ${nomSortie})`); + const herite = ralentissementSortie !== undefined && !Object.hasOwn(mesure, 'ralentissementCpu'); + mesures.push(herite ? { ...mesure, ralentissementCpu: ralentissementSortie } : mesure); + } + for (const champ of CHAMPS_REPRIS) { + if (!Object.hasOwn(sortie, champ)) continue; + if (repris.has(champ)) throw new Error(`${champ} : porté par deux sorties, ${repris.get(champ).sortie} et ${nomSortie}`); + repris.set(champ, { sortie: nomSortie, valeur: sortie[champ] }); + } + } + const absentes = attendues.filter((nom) => !mesures.some((m) => m.nom === nom)); + if (absentes.length > 0) throw new Error(`mesure attendue absente : ${absentes.join(', ')}`); + const releve = { format: 1, configuration, mesures: [...mesures].sort(rangeur(constantes)) }; + for (const champ of CHAMPS_REPRIS) if (repris.has(champ)) releve[champ] = repris.get(champ).valeur; + const fautes = verifierReleve(releve, constantes); + if (fautes.length > 0) throw new Error(`relevé refusé : ${fautes.map(({ chemin, faute }) => `${chemin} : ${faute}`).join(' ; ')}`); + return releve; +} + +/** + * Le jugement d'un relevé assemblé. bloquants : chaque mesure de texte + * prise sous une autre police que la police embarquée, ou qui n'en nomme + * aucune — le relevé ne s'écrit pas. nonTenus : chaque mesure au verdict + * « nonTenu », avec son critère — le relevé s'écrit, la sortie est non + * nulle. Dans l'ordre des mesures. + * + * @param {import('./releve.js').Releve} releve + * @returns {{bloquants: string[], nonTenus: string[]}} + */ +export function jugement(releve) { + const bloquants = []; + const nonTenus = []; + for (const mesure of releve.mesures) { + if (MESURES_DE_TEXTE.includes(mesure.nom) && mesure.police !== FAMILLE_EMBARQUEE) { + bloquants.push(`${mesure.nom} mesurée sous la police « ${mesure.police ?? 'inconnue'} », et non sous ${FAMILLE_EMBARQUEE}`); + } + if (mesure.verdict === 'nonTenu') nonTenus.push(`${mesure.nom} : verdict non tenu — ${mesure.critere}`); + } + return { bloquants, nonTenus }; +} + +// Le relevé committé, lu sur le disque. +const releveCommite = (racine = RACINE) => JSON.parse(readFileSync(join(racine, CHEMIN_RELEVE), 'utf8')); + +/** + * Le tableau des constantes (§ 19.8, § 19.10) : tableauConstantes sur le + * registre et un relevé, par défaut le relevé committé. Une ligne par + * constante, sous un en-tête et son séparateur. + * + * @param {{releve?: import('./releve.js').Releve, + * constantes?: ReadonlyArray}} [options] + * @returns {string} + */ +export function regionConstantes({ releve = releveCommite(), constantes = CONSTANTES } = {}) { + return tableauConstantes(constantes, releve); +} + +// Une valeur et son unité, à la française. +const avecUnite = (valeur, unite) => (valeur === null ? 'aucune valeur' : `${nombre(valeur)} ${unite}`); + +// Ce qu'un passage imprime après l'écriture du relevé : le tableau, chaque +// constante du banc face à sa valeur proposée — égale, à adopter, ou +// relevée sans adoption —, chaque garde, puis l'accord du registre avec le +// relevé. +function imprimer(releve, sortie) { + sortie(''); + sortie(regionConstantes({ releve })); + sortie(''); + sortie('Valeurs proposées par le banc (à adopter à la main dans src/geometrie/constantes.js) :'); + for (const constante of CONSTANTES) { + if (constante.proprietaire !== 'banc') continue; + const mesure = releve.mesures.find((m) => m.nom === constante.nom); + let suite = mesure.valeurProposee === constante.valeur ? 'égale' : 'à adopter'; + if (RELEVEES_SANS_ADOPTION.includes(constante.nom)) suite = 'relevé, non adopté : défaut de travail sous la machine de référence'; + sortie( + `- ${constante.nom} : registre ${avecUnite(constante.valeur, constante.unite)}, ` + + `relevé ${avecUnite(mesure.valeurProposee, constante.unite)} — ${suite}`, + ); + } + sortie('Gardes :'); + for (const mesure of releve.mesures) { + if (!GARDES.includes(mesure.nom)) continue; + const verdict = mesure.verdict === undefined ? '' : `, verdict ${mesure.verdict === 'tenu' ? 'tenu' : 'non tenu'}`; + sortie(`- ${mesure.nom} : ${avecUnite(mesure.valeurProposee, mesure.unite)}${verdict}`); + } + const ecarts = accordRegistre(CONSTANTES, releve); + if (ecarts.length === 0) sortie('Accord du registre avec le relevé : aucun écart.'); + else { + sortie('Accord du registre avec le relevé :'); + for (const { nom, faute } of ecarts) sortie(`- ${nom} : ${faute}`); + } + sortie('La région « constantes » de README.md se met à jour par node scripts/documentation.js engendrer.'); +} + +// Le relevé du plafond a priori, par le script lui-même : son JSON imprimé. +function plafondParScript(racine) { + const resultat = spawnSync(process.execPath, [join(racine, 'scripts/banc/plafond.js')], { + cwd: racine, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + if (resultat.status !== 0) throw new Error(`plafond.js : code ${resultat.status}`); + return JSON.parse(resultat.stdout); +} + +// Le projet banc de Vitest, sorties à l'écran ; rend son code de sortie. +function vitestDuBanc(racine) { + const resultat = spawnSync(process.execPath, [join(racine, 'node_modules/vitest/vitest.mjs'), 'run', '--config', 'vitest.banc.config.js'], { + cwd: racine, + stdio: 'inherit', + }); + return resultat.status ?? 1; +} + +// La version du Chromium du banc, lancé par Playwright sous les options du +// projet banc : « Chromium ». +async function versionNavigateur() { + const { chromium } = await import('playwright'); + const { launchOptions = {} } = optionsBanc(process.env); + const navigateur = await chromium.launch({ ...launchOptions, headless: true }); + try { + return `Chromium ${navigateur.version()}`; + } finally { + await navigateur.close(); + } +} + +/** + * Un passage du banc, selon le déroulé de l'en-tête. Chaque effet se reçoit, + * pour que les épreuves le remplacent ; par défaut, ceux de la machine. + * + * @param {{racine?: string, mesurerPlafond?: () => object[], lancerVitest?: () => number, + * lireNavigateur?: () => Promise, systeme?: typeof os, node?: string, + * effacer?: (chemin: string) => void, lire?: (chemin: string) => string, + * ecrire?: (chemin: string, texte: string) => void, sortie?: (ligne: string) => void}} [effets] + * @returns {Promise<0|1>} + */ +export async function executer({ + racine = RACINE, + mesurerPlafond = () => plafondParScript(racine), + lancerVitest = () => vitestDuBanc(racine), + lireNavigateur = versionNavigateur, + systeme = os, + node = process.version, + effacer = (chemin) => rmSync(join(racine, chemin), { force: true }), + lire = (chemin) => readFileSync(join(racine, chemin), 'utf8'), + ecrire = (chemin, texte) => writeFileSync(join(racine, chemin), texte), + sortie = (ligne) => process.stdout.write(`${ligne}\n`), +} = {}) { + for (const nom of SORTIES_NAVIGATEUR) effacer(cheminSortie(nom)); + let releve; + try { + const plafondAPriori = mesurerPlafond(); + const code = lancerVitest(); + if (code !== 0) { + sortie(`Une épreuve du banc est tombée (code ${code}) : un contrôle du harnais, ou une mesure refusée. Aucun relevé écrit.`); + return 1; + } + const sorties = new Map([['plafond', { plafondAPriori }]]); + for (const nom of SORTIES_NAVIGATEUR) { + let texte; + try { + texte = lire(cheminSortie(nom)); + } catch (erreur) { + if (estAbsent(erreur)) continue; + throw erreur; + } + sorties.set(nom, JSON.parse(texte)); + } + const configuration = configurationDuBanc({ systeme, node, navigateur: await lireNavigateur() }); + releve = assembler({ configuration, sorties, attendues: ATTENDUES }); + } catch (erreur) { + sortie(`Aucun relevé écrit : ${erreur.message}`); + return 1; + } + const { bloquants, nonTenus } = jugement(releve); + if (bloquants.length > 0) { + for (const motif of bloquants) sortie(motif); + sortie('Aucun relevé écrit : une mesure de texte hors de la police embarquée ne mesure pas le logiciel.'); + return 1; + } + ecrire(CHEMIN_RELEVE, `${JSON.stringify(releve, null, 2)}\n`); + sortie(`Relevé écrit : ${CHEMIN_RELEVE}`); + imprimer(releve, sortie); + for (const motif of nonTenus) sortie(motif); + return nonTenus.length > 0 ? 1 : 0; +} + +// Vrai quand ce fichier est le script que Node a lancé, et non un module +// importé. +function estLanceDirectement() { + if (process.argv[1] === undefined) return false; + try { + return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (estLanceDirectement()) process.exitCode = await executer(); diff --git a/scripts/banc/lancer.test.js b/scripts/banc/lancer.test.js new file mode 100644 index 0000000..47cd4eb --- /dev/null +++ b/scripts/banc/lancer.test.js @@ -0,0 +1,377 @@ +// © 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); + }); +}); diff --git a/scripts/banc/releve.js b/scripts/banc/releve.js index 3a92bc1..69a6e22 100644 --- a/scripts/banc/releve.js +++ b/scripts/banc/releve.js @@ -8,8 +8,12 @@ // fait citer à la configuration de l'entrée le résumé de celle du relevé. // accordRegistre refuse ensuite toute constante du banc mesurée que le relevé // ne soutient pas. Le relevé ne nomme pas sa machine : sa configuration est -// une liste fermée de champs, et aucun texte n'y porte un chemin de compte -// ni une date. Fonctions pures, sans lecture de fichier ni horloge. +// une liste fermée de champs — cadre, facteur d'échelle, versions, nombre +// de processeurs logiques, ralentissement, police —, et aucun texte n'y +// porte un chemin de compte ni une date. Une mesure peut porter son propre +// ralentissement, celui sous lequel elle s'est prise : c'est lui que cite +// la configuration de sa constante. Fonctions pures, sans lecture de +// fichier ni horloge. import { CONSTANTES } from '../../src/geometrie/constantes.js'; @@ -19,7 +23,7 @@ import { CONSTANTES } from '../../src/geometrie/constantes.js'; * navigateur: string|null, node: string|null, processeursLogiques: number|null, * ralentissementCpu: number|null, police: string|null}} ConfigurationReleve * @typedef {{nom: string, unite: string, valeurProposee: number|null, critere: string, - * courbe?: object[], verdict?: 'tenu'|'nonTenu'}} MesureReleve + * courbe?: object[], verdict?: 'tenu'|'nonTenu', ralentissementCpu?: number|null}} MesureReleve * @typedef {{format: 1, configuration: ConfigurationReleve, mesures: MesureReleve[], * plafondAPriori?: object[], parallele?: object[]}} Releve */ @@ -38,23 +42,41 @@ const CHAMPS_CONFIGURATION = [ ]; // Les noms de champ qui désignent l'hôte, son utilisateur ou un instant, -// refusés à toute profondeur du relevé. Un nom se lit en minuscules, sans -// tiret bas ni tiret : « Hostname », « host_name » et « nom-hote » se -// valent. Il est identifiant quand il contient un mot de MOT_IDENTIFIANT -// (nomMachine, computerName), commence par un préfixe de -// PREFIXE_IDENTIFIANT (dateReleve, userName), ou vaut un nom de -// NOM_IDENTIFIANT. Ces derniers ne se lisent qu'entiers, leur début -// commençant des mots ordinaires : compteur, postérieur. +// refusés à toute profondeur du relevé. Un nom se coupe en mots — au tiret +// bas, au tiret, à l'espace et à chaque majuscule qui suit une minuscule ou +// un chiffre, ou qui ouvre un mot après un sigle (« HOSTName » : host, +// name) —, puis se lit sans accent et en minuscules : « nomHôte », +// « nom_hote » et « NOM-HOTE » se valent. Il est identifiant quand le nom +// entier, mots recollés, contient un mot de MOT_IDENTIFIANT (hostname, +// nomMachine), quand l'un de ses mots commence par un préfixe de +// PREFIXE_IDENTIFIANT (userName, currentUser, releveDate), ou quand l'un de +// ses mots vaut un nom de NOM_IDENTIFIANT (nomDuPoste, nomCompte). Ces +// derniers ne se lisent qu'entiers, leur début commençant des mots +// ordinaires : compteur, postérieur, instantané. NOM_ADMIS lève le soupçon +// sur un nom entier qui ne désigne personne : userAgent nomme le navigateur. const MOT_IDENTIFIANT = ['host', 'hote', 'machine', 'computer', 'utilisateur', 'horodatage', 'timestamp']; const PREFIXE_IDENTIFIANT = ['user', 'date', 'login']; const NOM_IDENTIFIANT = new Set(['poste', 'compte', 'instant']); +const NOM_ADMIS = new Set(['useragent']); + +// Les mots d'un nom de champ, sans accent et en minuscules. +const motsDe = (cle) => + cle + .normalize('NFD') + .replace(/\p{M}/gu, '') + .replace(/([\p{Ll}\d])(\p{Lu})/gu, '$1 $2') + .replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2') + .toLowerCase() + .split(/[\s_-]+/) + .filter((mot) => mot !== ''); function estChampIdentifiant(cle) { - const nom = cle.toLowerCase().replace(/[_-]/g, ''); + const mots = motsDe(cle); + const nom = mots.join(''); + if (NOM_ADMIS.has(nom)) return false; return ( MOT_IDENTIFIANT.some((mot) => nom.includes(mot)) || - PREFIXE_IDENTIFIANT.some((prefixe) => nom.startsWith(prefixe)) || - NOM_IDENTIFIANT.has(nom) + mots.some((mot) => PREFIXE_IDENTIFIANT.some((prefixe) => mot.startsWith(prefixe)) || NOM_IDENTIFIANT.has(mot)) ); } @@ -84,6 +106,10 @@ const estEntierPositif = (valeur) => Number.isInteger(valeur) && valeur > 0; // dépend pas de l'ordre d'écriture du fichier. const clesTriees = (objet) => Object.keys(objet).sort(); +// Un ralentissement du processeur : un facteur ≥ 1, ou null pour inconnu. +const estRalentissement = (v) => v === null || (Number.isFinite(v) && v >= 1); +const FAUTE_RALENTISSEMENT = 'nombre ≥ 1 ou null attendu'; + // Chaque champ de la configuration, avec ce qu'il admet et la faute qu'il // porte sinon. null vaut « inconnu » pour les champs qui le permettent : un // relevé qui ne connaît pas encore son navigateur reste vérifiable. @@ -96,7 +122,7 @@ const REGLES_CONFIGURATION = { navigateur: [(v) => v === null || estTexte(v), 'texte non vide ou null attendu'], node: [(v) => v === null || estTexte(v), 'texte non vide ou null attendu'], processeursLogiques: [(v) => v === null || estEntierPositif(v), 'entier positif ou null attendu'], - ralentissementCpu: [(v) => v === null || (Number.isFinite(v) && v >= 1), 'nombre ≥ 1 ou null attendu'], + ralentissementCpu: [estRalentissement, FAUTE_RALENTISSEMENT], police: [(v) => v === null || estTexte(v), 'texte non vide ou null attendu'], }; @@ -153,6 +179,9 @@ function fautesMesure(mesure, rang, vus, constantes) { if (Object.hasOwn(mesure, 'verdict') && mesure.verdict !== 'tenu' && mesure.verdict !== 'nonTenu') { faute('verdict', '« tenu » ou « nonTenu » attendu'); } + if (Object.hasOwn(mesure, 'ralentissementCpu') && !estRalentissement(mesure.ralentissementCpu)) { + faute('ralentissementCpu', FAUTE_RALENTISSEMENT); + } return fautes; } @@ -211,9 +240,13 @@ export function verifierReleve(releve, constantes = CONSTANTES) { return fautesIdentifiantes(releve, '', fautes); } -// Un nombre écrit à la française : la virgule décimale, les milliers -// séparés par une espace, comme le registre les écrit (« 1 366 »). -function nombre(valeur) { +/** + * Un nombre écrit à la française : la virgule décimale, les milliers + * séparés par une espace, comme le registre les écrit (« 1 366 »). + * @param {number} valeur + * @returns {string} + */ +export function nombre(valeur) { const [entiere, decimale] = String(valeur).split('.'); const groupee = entiere.replace(/\B(?=(\d{3})+$)/g, ' '); return decimale === undefined ? groupee : `${groupee},${decimale}`; @@ -226,11 +259,16 @@ function nombre(valeur) { * ordre quel que soit celui des champs. Un champ null se dit inconnu. * @param {ConfigurationReleve} configuration * @returns {string} - * @throws {TypeError} sur ce qui n'est pas un objet + * @throws {TypeError} sur ce qui n'est pas un objet, ou dont le cadre n'est + * pas un objet aux largeur et hauteur numériques */ export function resumeConfiguration(configuration) { if (!estObjet(configuration)) throw new TypeError('configuration de relevé : objet attendu'); - const { cadre, facteurEchelle, navigateur, node, processeursLogiques: p, ralentissementCpu: r, police } = configuration; + const { cadre, facteurEchelle, navigateur, node } = configuration; + const { processeursLogiques: p, ralentissementCpu: r, police } = configuration; + if (!estObjet(cadre) || !Number.isFinite(cadre.largeur) || !Number.isFinite(cadre.hauteur)) { + throw new TypeError('configuration de relevé : cadre absent ou sans largeur et hauteur numériques'); + } const processeurs = p === null ? 'processeurs logiques inconnus' : `${nombre(p)} ${p === 1 ? 'processeur logique' : 'processeurs logiques'}`; let ralentissement = `processeur ralenti ×${nombre(r)}`; @@ -250,20 +288,32 @@ export function resumeConfiguration(configuration) { // La mesure que le relevé porte pour une constante, undefined sinon. const mesureDe = (releve, nom) => (Array.isArray(releve.mesures) ? releve.mesures : []).find((m) => m?.nom === nom); +// Le résumé que cite une constante du banc : celui de la configuration du +// relevé, au ralentissement de sa mesure quand elle en porte un — une +// mesure prise sans ralentissement sous un relevé dont la machine de +// référence est ralentie se dit « non ralenti ». Sans mesure, ou sans +// ralentissement propre, celui de la configuration. +function resumeDeMesure(releve, mesure) { + if (mesure === undefined || !Object.hasOwn(mesure, 'ralentissementCpu')) return resumeConfiguration(releve.configuration); + return resumeConfiguration({ ...releve.configuration, ralentissementCpu: mesure.ralentissementCpu }); +} + /** * L'accord du registre avec un relevé : pour chaque constante du banc * déclarée mesurée, dans l'ordre du registre, « absente » quand le relevé ne * la porte pas ou n'en propose aucune valeur ; sinon « valeur » quand le * registre porte une autre valeur que la proposée, puis « configuration » * quand la configuration de l'entrée ne cite pas le résumé de celle du - * relevé. Une constante défaut de travail, ou d'un autre propriétaire, ne + * relevé, au ralentissement propre de la mesure quand elle en porte un. Une constante défaut de travail, ou d'un autre propriétaire, ne * produit rien : le relevé ne la soutient ni ne la contredit. * @param {ReadonlyArray} constantes * @param {Releve} releve * @returns {Array<{nom: string, faute: 'absente'|'valeur'|'configuration'}>} */ export function accordRegistre(constantes, releve) { - const resume = resumeConfiguration(releve.configuration); + // Une configuration que le résumé refuse lève d'emblée, qu'une constante + // la cite ou non. + resumeConfiguration(releve.configuration); const fautes = []; for (const { nom, valeur, configuration, proprietaire, statut } of constantes) { if (proprietaire !== 'banc' || statut !== 'mesuree') continue; @@ -273,7 +323,7 @@ export function accordRegistre(constantes, releve) { continue; } if (mesure.valeurProposee !== valeur) fautes.push({ nom, faute: 'valeur' }); - if (!configuration.includes(resume)) fautes.push({ nom, faute: 'configuration' }); + if (!configuration.includes(resumeDeMesure(releve, mesure))) fautes.push({ nom, faute: 'configuration' }); } return fautes; } @@ -315,8 +365,9 @@ function valeurProposee(constante, releve) { * constante du registre, dans son ordre — ce qu'elle mesure, sa valeur * (« relevée à l'exécution » sans valeur), son statut en toutes lettres, son * propriétaire, son protocole, la valeur que le relevé propose, et la - * configuration : le résumé de celle du relevé pour une constante du banc, - * celle du registre pour les autres. Lignes jointes par un saut de ligne, + * configuration : pour une constante du banc, le résumé de celle du relevé + * au ralentissement propre de sa mesure quand elle en porte un ; celle du + * registre pour les autres. Lignes jointes par un saut de ligne, * sans saut final. * @param {ReadonlyArray} constantes * @param {Releve} releve @@ -325,7 +376,9 @@ function valeurProposee(constante, releve) { */ export function tableauConstantes(constantes, releve) { if (constantes.length === 0) throw new RangeError('tableau des constantes : registre vide'); - const resume = resumeConfiguration(releve.configuration); + // Une configuration que le résumé refuse lève d'emblée, qu'une constante + // la cite ou non. + resumeConfiguration(releve.configuration); const lignes = constantes.map((c) => ligne([ `\`${c.nom}\``, @@ -335,7 +388,7 @@ export function tableauConstantes(constantes, releve) { PROPRIETAIRES[c.proprietaire], c.protocole, valeurProposee(c, releve), - c.proprietaire === 'banc' ? resume : c.configuration, + c.proprietaire === 'banc' ? resumeDeMesure(releve, mesureDe(releve, c.nom)) : c.configuration, ]), ); return [ligne(ENTETE), ligne(ENTETE.map(() => '---')), ...lignes].join('\n'); diff --git a/scripts/banc/releve.json b/scripts/banc/releve.json index 15f34ee..943c716 100644 --- a/scripts/banc/releve.json +++ b/scripts/banc/releve.json @@ -1,13 +1,1469 @@ { "format": 1, "configuration": { - "cadre": { "largeur": 1366, "hauteur": 700 }, + "cadre": { + "largeur": 1366, + "hauteur": 700 + }, "facteurEchelle": 1, - "navigateur": null, - "node": null, - "processeursLogiques": null, + "navigateur": "Chromium 153.0.8010.12", + "node": "v26.10.0", + "processeursLogiques": 16, "ralentissementCpu": 4, "police": "Noto Sans" }, - "mesures": [] + "mesures": [ + { + "nom": "SEUIL_SIEGES_LISIBLES", + "unite": "places", + "valeurProposee": 18, + "critere": "le plus grand nombre de places d’une table seule, ronde puis carrée, dont le bas de la liste de noms reste dans la hauteur du au zoom ajusté ; le plus petit des deux, dichotomie sur [2, 200]", + "police": "Noto Sans", + "ralentissementCpu": 1, + "formes": [ + { + "forme": "ronde", + "seuil": 58 + }, + { + "forme": "carree", + "seuil": 18 + } + ], + "sondes": [ + { + "forme": "ronde", + "places": 2, + "basListePx": 473.6078796386719, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 200, + "basListePx": 629.3965454101562, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 2, + "basListePx": 473.6078796386719, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 200, + "basListePx": 629.3965454101562, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 101, + "basListePx": 625.5232543945312, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 51, + "basListePx": 618.4696044921875, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 76, + "basListePx": 622.934326171875, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 63, + "basListePx": 621.0859375, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 57, + "basListePx": 619.9010009765625, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 60, + "basListePx": 620.316650390625, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 58, + "basListePx": 619.8966674804688, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 59, + "basListePx": 620.3200073242188, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "ronde", + "places": 57, + "basListePx": 619.9010009765625, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 58, + "basListePx": 619.8966674804688, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "ronde", + "places": 59, + "basListePx": 620.3200073242188, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 2, + "basListePx": 462.1051330566406, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 200, + "basListePx": 711.0751342773438, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 2, + "basListePx": 462.1051330566406, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 200, + "basListePx": 711.0751342773438, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 101, + "basListePx": 690.3484497070312, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 51, + "basListePx": 676.447021484375, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 26, + "basListePx": 634.857421875, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 14, + "basListePx": 591.9364624023438, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 20, + "basListePx": 639.3123779296875, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 17, + "basListePx": 595.736328125, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 18, + "basListePx": 610.2616577148438, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 19, + "basListePx": 624.7869873046875, + "basPlanPx": 620.21875, + "tient": false + }, + { + "forme": "carree", + "places": 17, + "basListePx": 595.736328125, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 18, + "basListePx": 610.2616577148438, + "basPlanPx": 620.21875, + "tient": true + }, + { + "forme": "carree", + "places": 19, + "basListePx": 624.7869873046875, + "basPlanPx": 620.21875, + "tient": false + } + ], + "refus": null + }, + { + "nom": "TOLERANCE_CHAINE_TEMOIN", + "unite": "rapport", + "valeurProposee": 0.011, + "critere": "le quart de l’écart relatif du repli (system-ui, sans-serif) à la longueur des avances de Noto Sans, et au moins trois fois la gigue de 10 mesures ; chaîne témoin à 16 px, graisse 400, crénage et ligatures coupés", + "police": "Noto Sans", + "ralentissementCpu": 1, + "corpsPx": 16, + "attenduePx": 213.264, + "embarqueePx": [ + 214, + 214, + 214, + 214, + 214, + 214, + 214, + 214, + 214, + 214 + ], + "repliPx": [ + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625, + 204.0625 + ], + "ecartRepli": 0.0431460537175, + "gigue": 0, + "biais": 0.00345112161452, + "refus": null + }, + { + "nom": "RALENTISSEMENT_PLEINE_CHARGE", + "unite": "×", + "valeurProposee": 2.79, + "critere": "t(L) / t(1), médianes de 3 répétitions ou plus : une génération de W propositions sur W travailleurs, grande démonstration, arret 200000, graine 1", + "courbe": [ + { + "coeurs": 1, + "dureesMs": [ + 407.8, + 444.9, + 420 + ], + "medianeMs": 420, + "ralentissement": 1, + "modele": 1 + }, + { + "coeurs": 2, + "dureesMs": [ + 458.7, + 472.7, + 468.6 + ], + "medianeMs": 468.6, + "ralentissement": 1.116, + "modele": 1.119 + }, + { + "coeurs": 4, + "dureesMs": [ + 578.1, + 580.4, + 559.4 + ], + "medianeMs": 578.1, + "ralentissement": 1.376, + "modele": 1.358 + }, + { + "coeurs": 8, + "dureesMs": [ + 780.2, + 785.2, + 791.9 + ], + "medianeMs": 785.2, + "ralentissement": 1.87, + "modele": 1.836 + }, + { + "coeurs": 16, + "dureesMs": [ + 1186.9, + 1169.7, + 1172 + ], + "medianeMs": 1172, + "ralentissement": 2.79, + "modele": 2.79 + } + ], + "bruite": false, + "ralentissementCpu": 1 + }, + { + "nom": "DUREE_GENERATION_PAR_DEFAUT", + "unite": "ms", + "valeurProposee": 615.2, + "critere": "médiane de 3 générations au réglage par défaut, 5 × 200000 mouvements sur 5 travailleurs, grande démonstration", + "courbe": [ + { + "coeurs": 5, + "dureesMs": [ + 619.9, + 615.2, + 611.2 + ], + "medianeMs": 615.2 + } + ], + "ralentissementCpu": 1 + }, + { + "nom": "TEMPS_IMAGE_LISTES", + "unite": "ms", + "valeurProposee": null, + "critere": "p95 du temps par image d’un glissement de 60 pas, médiane de 3 glissements, grande démonstration, 33 listes dessinées à tout k ; tenu quand il vaut au plus 33 ms à ×4, à tout k de l’ajusté à 1", + "critereMs": 33, + "ralentissementCpu": 4, + "seuil": { + "k": 0.16991981765756428, + "tenuPartout": true, + "jamaisTenu": false + }, + "pireP95Ms": 21.20000000001164, + "courbe": [ + { + "k": 0.16991981765756428, + "ralentissementCpu": 1, + "p95Ms": 12.199999999999818, + "glissements": [ + { + "images": 65, + "medianeMs": 9.099999999999909, + "p95Ms": 12.600000000000136, + "maxMs": 13.5 + }, + { + "images": 63, + "medianeMs": 7.5, + "p95Ms": 12.199999999999818, + "maxMs": 13.799999999999727 + }, + { + "images": 64, + "medianeMs": 7.4500000000000455, + "p95Ms": 11.5, + "maxMs": 12.599999999999909 + } + ] + }, + { + "k": 0.16991981765756428, + "ralentissementCpu": 4, + "p95Ms": 20.5, + "glissements": [ + { + "images": 261, + "medianeMs": 6.899999999999636, + "p95Ms": 20.399999999999636, + "maxMs": 37.70000000000027 + }, + { + "images": 253, + "medianeMs": 7, + "p95Ms": 20.5, + "maxMs": 71.40000000000146 + }, + { + "images": 251, + "medianeMs": 7, + "p95Ms": 20.700000000000728, + "maxMs": 52 + } + ] + }, + { + "k": 0.18691179942332073, + "ralentissementCpu": 1, + "p95Ms": 12.099999999998545, + "glissements": [ + { + "images": 64, + "medianeMs": 8.5, + "p95Ms": 12.699999999998909, + "maxMs": 16.100000000000364 + }, + { + "images": 74, + "medianeMs": 8.650000000000546, + "p95Ms": 11.799999999999272, + "maxMs": 14.600000000000364 + }, + { + "images": 63, + "medianeMs": 9, + "p95Ms": 12.099999999998545, + "maxMs": 14.600000000000364 + } + ] + }, + { + "k": 0.18691179942332073, + "ralentissementCpu": 4, + "p95Ms": 20.5, + "glissements": [ + { + "images": 240, + "medianeMs": 7.949999999999818, + "p95Ms": 20.5, + "maxMs": 71.69999999999891 + }, + { + "images": 258, + "medianeMs": 7, + "p95Ms": 20.299999999999272, + "maxMs": 71.40000000000146 + }, + { + "images": 237, + "medianeMs": 7.700000000000728, + "p95Ms": 20.700000000000728, + "maxMs": 54.20000000000073 + } + ] + }, + { + "k": 0.2056029793656528, + "ralentissementCpu": 1, + "p95Ms": 12.200000000000728, + "glissements": [ + { + "images": 66, + "medianeMs": 8.599999999998545, + "p95Ms": 10.599999999998545, + "maxMs": 16.5 + }, + { + "images": 68, + "medianeMs": 8.600000000002183, + "p95Ms": 12.599999999998545, + "maxMs": 15.400000000001455 + }, + { + "images": 70, + "medianeMs": 8.600000000000364, + "p95Ms": 12.200000000000728, + "maxMs": 17.399999999997817 + } + ] + }, + { + "k": 0.2056029793656528, + "ralentissementCpu": 4, + "p95Ms": 20.299999999999272, + "glissements": [ + { + "images": 228, + "medianeMs": 9.200000000000728, + "p95Ms": 20.299999999999272, + "maxMs": 52.79999999999927 + }, + { + "images": 246, + "medianeMs": 7.75, + "p95Ms": 20.200000000000728, + "maxMs": 55.20000000000073 + }, + { + "images": 248, + "medianeMs": 7.599999999998545, + "p95Ms": 20.399999999997817, + "maxMs": 55.29999999999927 + } + ] + }, + { + "k": 0.2261632773022181, + "ralentissementCpu": 1, + "p95Ms": 11.299999999999272, + "glissements": [ + { + "images": 67, + "medianeMs": 8.700000000000728, + "p95Ms": 11.299999999999272, + "maxMs": 16.799999999999272 + }, + { + "images": 144, + "medianeMs": 1.1000000000003638, + "p95Ms": 10, + "maxMs": 15.799999999999272 + }, + { + "images": 132, + "medianeMs": 1.5, + "p95Ms": 11.700000000000728, + "maxMs": 15.799999999999272 + } + ] + }, + { + "k": 0.2261632773022181, + "ralentissementCpu": 4, + "p95Ms": 20.30000000000291, + "glissements": [ + { + "images": 253, + "medianeMs": 6.900000000001455, + "p95Ms": 20.30000000000291, + "maxMs": 54.900000000001455 + }, + { + "images": 241, + "medianeMs": 7.900000000001455, + "p95Ms": 20.299999999995634, + "maxMs": 54.5 + }, + { + "images": 244, + "medianeMs": 7.700000000000728, + "p95Ms": 20.69999999999709, + "maxMs": 53.5 + } + ] + }, + { + "k": 0.24877960503243993, + "ralentissementCpu": 1, + "p95Ms": 10.200000000004366, + "glissements": [ + { + "images": 65, + "medianeMs": 9.30000000000291, + "p95Ms": 11.400000000001455, + "maxMs": 16.900000000001455 + }, + { + "images": 133, + "medianeMs": 0.8000000000029104, + "p95Ms": 10.200000000004366, + "maxMs": 16.400000000001455 + }, + { + "images": 146, + "medianeMs": 0.75, + "p95Ms": 10.19999999999709, + "maxMs": 15.400000000001455 + } + ] + }, + { + "k": 0.24877960503243993, + "ralentissementCpu": 4, + "p95Ms": 20.60000000000582, + "glissements": [ + { + "images": 257, + "medianeMs": 6.5, + "p95Ms": 21.5, + "maxMs": 52.700000000004366 + }, + { + "images": 238, + "medianeMs": 8.400000000001455, + "p95Ms": 20.599999999998545, + "maxMs": 54.60000000000582 + }, + { + "images": 233, + "medianeMs": 8.900000000001455, + "p95Ms": 20.60000000000582, + "maxMs": 54.400000000001455 + } + ] + }, + { + "k": 0.27365756553568393, + "ralentissementCpu": 1, + "p95Ms": 12.200000000004366, + "glissements": [ + { + "images": 65, + "medianeMs": 8.700000000004366, + "p95Ms": 12.900000000001455, + "maxMs": 15.099999999998545 + }, + { + "images": 66, + "medianeMs": 8.94999999999709, + "p95Ms": 12.200000000004366, + "maxMs": 17.299999999995634 + }, + { + "images": 141, + "medianeMs": 0.7000000000043656, + "p95Ms": 10.30000000000291, + "maxMs": 15.30000000000291 + } + ] + }, + { + "k": 0.27365756553568393, + "ralentissementCpu": 4, + "p95Ms": 20.5, + "glissements": [ + { + "images": 236, + "medianeMs": 8.05000000000291, + "p95Ms": 20.599999999998545, + "maxMs": 54.69999999999709 + }, + { + "images": 264, + "medianeMs": 6.44999999999709, + "p95Ms": 20.400000000001455, + "maxMs": 54.39999999999418 + }, + { + "images": 239, + "medianeMs": 8.30000000000291, + "p95Ms": 20.5, + "maxMs": 53.599999999998545 + } + ] + }, + { + "k": 0.30102332208925237, + "ralentissementCpu": 1, + "p95Ms": 11.099999999998545, + "glissements": [ + { + "images": 66, + "medianeMs": 8.400000000001455, + "p95Ms": 11.30000000000291, + "maxMs": 17.5 + }, + { + "images": 134, + "medianeMs": 0.5999999999985448, + "p95Ms": 10.299999999995634, + "maxMs": 13.900000000001455 + }, + { + "images": 136, + "medianeMs": 0.6999999999970896, + "p95Ms": 11.099999999998545, + "maxMs": 14.200000000004366 + } + ] + }, + { + "k": 0.30102332208925237, + "ralentissementCpu": 4, + "p95Ms": 20.59999999999127, + "glissements": [ + { + "images": 260, + "medianeMs": 6.349999999998545, + "p95Ms": 20.5, + "maxMs": 55.299999999995634 + }, + { + "images": 239, + "medianeMs": 7.69999999999709, + "p95Ms": 20.60000000000582, + "maxMs": 54.69999999999709 + }, + { + "images": 248, + "medianeMs": 7.049999999995634, + "p95Ms": 20.59999999999127, + "maxMs": 53.59999999999127 + } + ] + }, + { + "k": 0.33112565429817764, + "ralentissementCpu": 1, + "p95Ms": 11.69999999999709, + "glissements": [ + { + "images": 69, + "medianeMs": 8.60000000000582, + "p95Ms": 11.89999999999418, + "maxMs": 15 + }, + { + "images": 66, + "medianeMs": 8.80000000000291, + "p95Ms": 11.69999999999709, + "maxMs": 15.400000000008731 + }, + { + "images": 65, + "medianeMs": 8.30000000000291, + "p95Ms": 11.10000000000582, + "maxMs": 17 + } + ] + }, + { + "k": 0.33112565429817764, + "ralentissementCpu": 4, + "p95Ms": 20.59999999999127, + "glissements": [ + { + "images": 259, + "medianeMs": 7.19999999999709, + "p95Ms": 20.79999999998836, + "maxMs": 52.89999999999418 + }, + { + "images": 259, + "medianeMs": 6.69999999999709, + "p95Ms": 20.59999999999127, + "maxMs": 54.80000000000291 + }, + { + "images": 261, + "medianeMs": 6.599999999991269, + "p95Ms": 20.5, + "maxMs": 53.70000000001164 + } + ] + }, + { + "k": 0.3642382197279954, + "ralentissementCpu": 1, + "p95Ms": 12.30000000000291, + "glissements": [ + { + "images": 65, + "medianeMs": 8.60000000000582, + "p95Ms": 12.30000000000291, + "maxMs": 16.60000000000582 + }, + { + "images": 68, + "medianeMs": 9.400000000001455, + "p95Ms": 12.19999999999709, + "maxMs": 15.80000000000291 + }, + { + "images": 66, + "medianeMs": 8.5, + "p95Ms": 12.400000000008731, + "maxMs": 17.30000000000291 + } + ] + }, + { + "k": 0.3642382197279954, + "ralentissementCpu": 4, + "p95Ms": 20.69999999999709, + "glissements": [ + { + "images": 253, + "medianeMs": 7.30000000000291, + "p95Ms": 20.69999999999709, + "maxMs": 54.70000000001164 + }, + { + "images": 244, + "medianeMs": 7.450000000004366, + "p95Ms": 20.69999999999709, + "maxMs": 52 + }, + { + "images": 230, + "medianeMs": 9.69999999999709, + "p95Ms": 20.39999999999418, + "maxMs": 54.20000000001164 + } + ] + }, + { + "k": 0.400662041700795, + "ralentissementCpu": 1, + "p95Ms": 11.599999999991269, + "glissements": [ + { + "images": 64, + "medianeMs": 9.30000000000291, + "p95Ms": 13.099999999991269, + "maxMs": 17.5 + }, + { + "images": 135, + "medianeMs": 1, + "p95Ms": 11.599999999991269, + "maxMs": 17.60000000000582 + }, + { + "images": 133, + "medianeMs": 1, + "p95Ms": 10.60000000000582, + "maxMs": 16.30000000000291 + } + ] + }, + { + "k": 0.400662041700795, + "ralentissementCpu": 4, + "p95Ms": 21.20000000001164, + "glissements": [ + { + "images": 221, + "medianeMs": 11.799999999988358, + "p95Ms": 21.20000000001164, + "maxMs": 70.69999999999709 + }, + { + "images": 242, + "medianeMs": 8.05000000000291, + "p95Ms": 21.19999999999709, + "maxMs": 107 + }, + { + "images": 245, + "medianeMs": 8, + "p95Ms": 21.30000000000291, + "maxMs": 89.80000000000291 + } + ] + }, + { + "k": 0.44072824587087456, + "ralentissementCpu": 1, + "p95Ms": 11.400000000008731, + "glissements": [ + { + "images": 74, + "medianeMs": 9.349999999998545, + "p95Ms": 12.69999999999709, + "maxMs": 17.29999999998836 + }, + { + "images": 142, + "medianeMs": 0.9000000000087311, + "p95Ms": 11.099999999991269, + "maxMs": 17.69999999999709 + }, + { + "images": 136, + "medianeMs": 1.2999999999956344, + "p95Ms": 11.400000000008731, + "maxMs": 17.79999999998836 + } + ] + }, + { + "k": 0.44072824587087456, + "ralentissementCpu": 4, + "p95Ms": 21.19999999999709, + "glissements": [ + { + "images": 234, + "medianeMs": 9.000000000007276, + "p95Ms": 21.10000000000582, + "maxMs": 89.60000000000582 + }, + { + "images": 249, + "medianeMs": 6.099999999991269, + "p95Ms": 27.60000000000582, + "maxMs": 89 + }, + { + "images": 240, + "medianeMs": 7.400000000001455, + "p95Ms": 21.19999999999709, + "maxMs": 88.79999999998836 + } + ] + }, + { + "k": 0.48480107045796206, + "ralentissementCpu": 1, + "p95Ms": 12.80000000000291, + "glissements": [ + { + "images": 68, + "medianeMs": 9.39999999999418, + "p95Ms": 12.80000000000291, + "maxMs": 17.5 + }, + { + "images": 137, + "medianeMs": 0.9000000000087311, + "p95Ms": 12.900000000008731, + "maxMs": 17.5 + }, + { + "images": 145, + "medianeMs": 0.9000000000087311, + "p95Ms": 12.39999999999418, + "maxMs": 19.30000000000291 + } + ] + }, + { + "k": 0.48480107045796206, + "ralentissementCpu": 4, + "p95Ms": 20.90000000000873, + "glissements": [ + { + "images": 256, + "medianeMs": 7.150000000001455, + "p95Ms": 20.90000000000873, + "maxMs": 89.5 + }, + { + "images": 262, + "medianeMs": 7, + "p95Ms": 21.19999999999709, + "maxMs": 88.40000000000873 + }, + { + "images": 261, + "medianeMs": 7.599999999991269, + "p95Ms": 20.90000000000873, + "maxMs": 88.5 + } + ] + }, + { + "k": 0.5332811775037583, + "ralentissementCpu": 1, + "p95Ms": 10.69999999999709, + "glissements": [ + { + "images": 89, + "medianeMs": 8.599999999991269, + "p95Ms": 10.89999999999418, + "maxMs": 17.5 + }, + { + "images": 142, + "medianeMs": 0.9000000000087311, + "p95Ms": 10.30000000000291, + "maxMs": 17.70000000001164 + }, + { + "images": 130, + "medianeMs": 1.3999999999941792, + "p95Ms": 10.69999999999709, + "maxMs": 18.5 + } + ] + }, + { + "k": 0.5332811775037583, + "ralentissementCpu": 4, + "p95Ms": 20.89999999999418, + "glissements": [ + { + "images": 258, + "medianeMs": 7.19999999999709, + "p95Ms": 20.89999999999418, + "maxMs": 89.90000000000873 + }, + { + "images": 263, + "medianeMs": 6.400000000008731, + "p95Ms": 20.80000000000291, + "maxMs": 88.90000000000873 + }, + { + "images": 251, + "medianeMs": 7.7999999999883585, + "p95Ms": 20.89999999999418, + "maxMs": 88.40000000002328 + } + ] + }, + { + "k": 0.5866092952541341, + "ralentissementCpu": 1, + "p95Ms": 12, + "glissements": [ + { + "images": 79, + "medianeMs": 8.5, + "p95Ms": 12, + "maxMs": 17.39999999999418 + }, + { + "images": 147, + "medianeMs": 0.8999999999941792, + "p95Ms": 10.599999999976717, + "maxMs": 18 + }, + { + "images": 142, + "medianeMs": 0.9500000000116415, + "p95Ms": 12.099999999976717, + "maxMs": 17.89999999999418 + } + ] + }, + { + "k": 0.5866092952541341, + "ralentissementCpu": 4, + "p95Ms": 20.900000000023283, + "glissements": [ + { + "images": 264, + "medianeMs": 3.6000000000058208, + "p95Ms": 21, + "maxMs": 89.39999999999418 + }, + { + "images": 250, + "medianeMs": 7.30000000000291, + "p95Ms": 20.900000000023283, + "maxMs": 88.69999999998254 + }, + { + "images": 254, + "medianeMs": 7.399999999994179, + "p95Ms": 20.89999999999418, + "maxMs": 90.10000000000582 + } + ] + }, + { + "k": 0.6452702247795475, + "ralentissementCpu": 1, + "p95Ms": 11.89999999999418, + "glissements": [ + { + "images": 76, + "medianeMs": 8.5, + "p95Ms": 12.800000000017462, + "maxMs": 17.199999999982538 + }, + { + "images": 146, + "medianeMs": 1.1499999999941792, + "p95Ms": 11.89999999999418, + "maxMs": 18 + }, + { + "images": 141, + "medianeMs": 0.8999999999941792, + "p95Ms": 9.700000000011642, + "maxMs": 17.20000000001164 + } + ] + }, + { + "k": 0.6452702247795475, + "ralentissementCpu": 4, + "p95Ms": 20.89999999999418, + "glissements": [ + { + "images": 234, + "medianeMs": 10.30000000000291, + "p95Ms": 20.89999999999418, + "maxMs": 88.80000000001746 + }, + { + "images": 260, + "medianeMs": 7.05000000000291, + "p95Ms": 21, + "maxMs": 87.5 + }, + { + "images": 249, + "medianeMs": 8, + "p95Ms": 20.89999999999418, + "maxMs": 88.80000000001746 + } + ] + }, + { + "k": 0.7097972472575024, + "ralentissementCpu": 1, + "p95Ms": 11.89999999999418, + "glissements": [ + { + "images": 78, + "medianeMs": 8.700000000011642, + "p95Ms": 12.5, + "maxMs": 17.5 + }, + { + "images": 146, + "medianeMs": 1.0500000000029104, + "p95Ms": 11.89999999999418, + "maxMs": 17.29999999998836 + }, + { + "images": 137, + "medianeMs": 1.2000000000116415, + "p95Ms": 11, + "maxMs": 17.39999999999418 + } + ] + }, + { + "k": 0.7097972472575024, + "ralentissementCpu": 4, + "p95Ms": 20.800000000017462, + "glissements": [ + { + "images": 229, + "medianeMs": 9.700000000011642, + "p95Ms": 21.29999999998836, + "maxMs": 90.89999999999418 + }, + { + "images": 219, + "medianeMs": 11.10000000000582, + "p95Ms": 20.79999999998836, + "maxMs": 89.80000000001746 + }, + { + "images": 259, + "medianeMs": 7.300000000017462, + "p95Ms": 20.800000000017462, + "maxMs": 89.59999999997672 + } + ] + }, + { + "k": 0.7807769719832527, + "ralentissementCpu": 1, + "p95Ms": 9.900000000023283, + "glissements": [ + { + "images": 81, + "medianeMs": 8.60000000000582, + "p95Ms": 12.10000000000582, + "maxMs": 17.5 + }, + { + "images": 142, + "medianeMs": 1.1000000000058208, + "p95Ms": 9.900000000023283, + "maxMs": 17.5 + }, + { + "images": 141, + "medianeMs": 0.8999999999941792, + "p95Ms": 9.700000000011642, + "maxMs": 18.699999999982538 + } + ] + }, + { + "k": 0.7807769719832527, + "ralentissementCpu": 4, + "p95Ms": 20.800000000017462, + "glissements": [ + { + "images": 258, + "medianeMs": 7.599999999991269, + "p95Ms": 20.800000000017462, + "maxMs": 88.69999999998254 + }, + { + "images": 244, + "medianeMs": 7.849999999991269, + "p95Ms": 21.10000000000582, + "maxMs": 89.5 + }, + { + "images": 262, + "medianeMs": 7.349999999991269, + "p95Ms": 20.699999999982538, + "maxMs": 88.5 + } + ] + }, + { + "k": 0.858854669181578, + "ralentissementCpu": 1, + "p95Ms": 10.799999999988358, + "glissements": [ + { + "images": 83, + "medianeMs": 8.300000000017462, + "p95Ms": 12.800000000017462, + "maxMs": 17.5 + }, + { + "images": 138, + "medianeMs": 1.1999999999970896, + "p95Ms": 10.60000000000582, + "maxMs": 17.60000000000582 + }, + { + "images": 146, + "medianeMs": 1.0499999999883585, + "p95Ms": 10.799999999988358, + "maxMs": 17.60000000000582 + } + ] + }, + { + "k": 0.858854669181578, + "ralentissementCpu": 4, + "p95Ms": 21.10000000000582, + "glissements": [ + { + "images": 241, + "medianeMs": 6.7000000000116415, + "p95Ms": 21.699999999982538, + "maxMs": 87.89999999999418 + }, + { + "images": 231, + "medianeMs": 10.10000000000582, + "p95Ms": 20.800000000017462, + "maxMs": 88.80000000001746 + }, + { + "images": 239, + "medianeMs": 9.199999999982538, + "p95Ms": 21.10000000000582, + "maxMs": 88 + } + ] + }, + { + "k": 0.944740136099736, + "ralentissementCpu": 1, + "p95Ms": 10.60000000000582, + "glissements": [ + { + "images": 78, + "medianeMs": 8.599999999976717, + "p95Ms": 13.5, + "maxMs": 17.5 + }, + { + "images": 140, + "medianeMs": 0.8000000000174623, + "p95Ms": 10.60000000000582, + "maxMs": 17.5 + }, + { + "images": 146, + "medianeMs": 1, + "p95Ms": 10.099999999976717, + "maxMs": 17.70000000001164 + } + ] + }, + { + "k": 0.944740136099736, + "ralentissementCpu": 4, + "p95Ms": 20.79999999998836, + "glissements": [ + { + "images": 269, + "medianeMs": 5.2000000000116415, + "p95Ms": 20.79999999998836, + "maxMs": 88.29999999998836 + }, + { + "images": 263, + "medianeMs": 6.900000000023283, + "p95Ms": 20.60000000000582, + "maxMs": 90 + }, + { + "images": 230, + "medianeMs": 10.14999999999418, + "p95Ms": 21, + "maxMs": 88.5 + } + ] + }, + { + "k": 1.0392141497097096, + "ralentissementCpu": 1, + "p95Ms": 11.10000000000582, + "glissements": [ + { + "images": 77, + "medianeMs": 8.5, + "p95Ms": 12.299999999988358, + "maxMs": 17.60000000000582 + }, + { + "images": 141, + "medianeMs": 0.7999999999883585, + "p95Ms": 10.89999999999418, + "maxMs": 17.70000000001164 + }, + { + "images": 146, + "medianeMs": 0.8500000000058208, + "p95Ms": 11.10000000000582, + "maxMs": 17.20000000001164 + } + ] + }, + { + "k": 1.0392141497097096, + "ralentissementCpu": 4, + "p95Ms": 20.79999999998836, + "glissements": [ + { + "images": 263, + "medianeMs": 7.2000000000116415, + "p95Ms": 21.20000000001164, + "maxMs": 105.60000000000582 + }, + { + "images": 223, + "medianeMs": 10.700000000011642, + "p95Ms": 20.60000000000582, + "maxMs": 88.69999999998254 + }, + { + "images": 225, + "medianeMs": 10, + "p95Ms": 20.79999999998836, + "maxMs": 96.60000000000582 + } + ] + } + ], + "verdict": "tenu" + } + ], + "plafondAPriori": [ + { + "cle": "grande", + "N": 260, + "T": 33, + "R": 4, + "msPlafonds": 0.107, + "msDiagnostic": 0.95 + }, + { + "cle": "grande-sans-exception", + "N": 260, + "T": 33, + "R": 4, + "msPlafonds": 0.042, + "msDiagnostic": 0.402 + }, + { + "cle": "pessimiste", + "N": 260, + "T": 33, + "R": 10, + "msPlafonds": 11.212, + "msDiagnostic": 13.65 + } + ], + "parallele": [ + { + "coeurs": 1, + "dureesMs": [ + 407.8, + 444.9, + 420 + ], + "medianeMs": 420, + "ralentissement": 1, + "modele": 1 + }, + { + "coeurs": 2, + "dureesMs": [ + 458.7, + 472.7, + 468.6 + ], + "medianeMs": 468.6, + "ralentissement": 1.116, + "modele": 1.119 + }, + { + "coeurs": 4, + "dureesMs": [ + 578.1, + 580.4, + 559.4 + ], + "medianeMs": 578.1, + "ralentissement": 1.376, + "modele": 1.358 + }, + { + "coeurs": 8, + "dureesMs": [ + 780.2, + 785.2, + 791.9 + ], + "medianeMs": 785.2, + "ralentissement": 1.87, + "modele": 1.836 + }, + { + "coeurs": 16, + "dureesMs": [ + 1186.9, + 1169.7, + 1172 + ], + "medianeMs": 1172, + "ralentissement": 2.79, + "modele": 2.79 + } + ] } diff --git a/scripts/banc/releve.test.js b/scripts/banc/releve.test.js index 6ee2df2..3998ecb 100644 --- a/scripts/banc/releve.test.js +++ b/scripts/banc/releve.test.js @@ -71,6 +71,32 @@ describe('releve : verifierReleve (§ 19.10, § 14.2)', () => { assert.deepEqual(verifierReleve({ ...minimal, configuration: inconnue }), []); }); + test('la configuration est la liste fermée du contrat : ni le système ni le modèle du processeur ne s’y écrivent (contrainte 27)', () => { + for (const champ of ['systeme', 'processeur']) { + const r = releve(); + r.configuration[champ] = 'Exemple'; + const fautes = verifierReleve(r); + assert.deepEqual(chemins(fautes), [`configuration.${champ}`], champ); + assert.match( + fautes[0].faute, + /hors de la configuration admise : cadre, facteurEchelle, navigateur, node, processeursLogiques, ralentissementCpu, police$/, + ); + } + }); + + test('le ralentissement propre à une mesure : un nombre ≥ 1 ou null, comme celui de la configuration', () => { + const r = releve(); + r.mesures[0].ralentissementCpu = 1; + r.mesures[1].ralentissementCpu = null; + r.mesures[2].ralentissementCpu = 4; + assert.deepEqual(verifierReleve(r), []); + for (const fautif of [0.5, '4', Number.NaN]) { + const autre = releve(); + autre.mesures[1].ralentissementCpu = fautif; + assert.deepEqual(verifierReleve(autre), [{ chemin: 'mesures[1].ralentissementCpu', faute: 'nombre ≥ 1 ou null attendu' }], String(fautif)); + } + }); + test('un relevé sans mesure se refuse : un relevé vide ne prouve rien', () => { const fautes = verifierReleve({ ...releve(), mesures: [] }); assert.deepEqual(chemins(fautes), ['mesures']); @@ -98,22 +124,43 @@ describe('releve : verifierReleve (§ 19.10, § 14.2)', () => { for (const { faute } of fautes) assert.match(faute, /chemin de compte/); }); + // Chaque branche du chemin de compte, et la casse qu'un système de + // fichiers insensible à la casse admet. Les chemins sont inventés. + for (const chemin of [ + '/root/.cache/navigateurs/chromium/chrome', + 'lancé sous /root', + '/HOME/exemple/banc', + '/users/exemple/banc', + 'c:\\users\\exemple\\banc', + 'D:/Documents and Settings/Exemple/banc', + 'c:\\documents and settings\\exemple', + ]) { + test(`un chemin de compte écrit ${chemin} se refuse`, () => { + const r = releve(); + r.mesures[0].critere = chemin; + const fautes = verifierReleve(r); + assert.deepEqual(chemins(fautes), ['mesures[0].critere']); + assert.match(fautes[0].faute, /chemin de compte/); + }); + } + test('les chemins voisins hors des comptes passent', () => { const r = releve(); r.configuration.navigateur = '/opt/homebrew/chromium/chrome'; r.configuration.police = '/usr/share/fonts/noto/NotoSans-Regular.ttf'; r.mesures[0].critere = 'C:\\Program Files\\Chromium : liste dans la hauteur du plan'; + r.mesures[1].critere = '/rootfs/usr/lib/chromium, /root-exemple et /homebrew'; assert.deepEqual(verifierReleve(r), []); }); test('un champ hote se refuse, dans la configuration, au sommet et dans une mesure ; tout champ hors de la configuration admise aussi', () => { const r = releve(); r.configuration.hote = 'poste-exemple'; - r.configuration.systeme = 'linux'; + r.configuration.disque = 'ssd'; r.hote = 'poste-exemple'; r.mesures[1].utilisateur = 'exemple'; const fautes = verifierReleve(r); - assert.deepEqual(chemins(fautes), ['configuration.systeme', 'configuration.hote', 'hote', 'mesures[1].utilisateur']); + assert.deepEqual(chemins(fautes), ['configuration.disque', 'configuration.hote', 'hote', 'mesures[1].utilisateur']); assert.match(fautes[0].faute, /hors de la configuration admise/); assert.match(fautes[1].faute, /ne nomme pas sa machine/); assert.match(fautes[2].faute, /ne nomme pas sa machine/); @@ -154,6 +201,17 @@ describe('releve : verifierReleve (§ 19.10, § 14.2)', () => { 'horodatage', 'timestamp', 'instant', + 'nomHôte', + 'HÔTE', + 'machineName', + 'utilisateurCourant', + 'currentUser', + 'HOSTName', + 'PCUser', + 'releveDate', + 'nomCompte', + 'nomDuPoste', + 'instant_prise', ]) { test(`un champ ${cle} se refuse dans une mesure, ouverte par ailleurs`, () => { const r = releve(); @@ -166,7 +224,10 @@ describe('releve : verifierReleve (§ 19.10, § 14.2)', () => { test('les champs voisins qui ne nomment ni hôte, ni utilisateur, ni instant passent', () => { const r = releve(); - Object.assign(r.mesures[2].courbe[0], { compteur: 3, posterieur: true, update: 1, mandat: 'aucun' }); + Object.assign(r.mesures[2].courbe[0], { compteur: 3, posterieur: true, update: 1, mandat: 'aucun', instantane: 1 }); + // userAgent nomme le navigateur, pas une personne : « user » n'y est + // pas un mot seul. + r.mesures[2].courbe[1].userAgent = 'Mozilla/5.0 (X11; Linux x86_64) Chromium/140.0'; r.plafondAPriori[0].postes = 2; assert.deepEqual(verifierReleve(r), []); }); @@ -299,6 +360,18 @@ describe('releve : accordRegistre (§ 19.10, § 14.1)', () => { ]); }); + test('la configuration citée est celle de la mesure : son ralentissement propre l’emporte sur celui du relevé', () => { + const r = releve(); + r.mesures[0].ralentissementCpu = 1; + const aUn = resumeConfiguration({ ...configuration(), ralentissementCpu: 1 }); + const aQuatre = resumeConfiguration(configuration()); + const citant = (resume) => avec('SEUIL_SIEGES_LISIBLES', { statut: 'mesuree', valeur: 18, configuration: `banc de mesure, ${resume}` }); + assert.deepEqual(accordRegistre(citant(aUn), r), []); + assert.deepEqual(accordRegistre(citant(aQuatre), r), [{ nom: 'SEUIL_SIEGES_LISIBLES', faute: 'configuration' }]); + const tolerance = avec('TOLERANCE_CHAINE_TEMOIN', { statut: 'mesuree', valeur: 0.02, configuration: aQuatre }); + assert.deepEqual(accordRegistre(tolerance, r), []); + }); + test('ni une constante défaut de travail, ni une constante mesurée hors du banc ne produisent de faute ; les fautes suivent le registre', () => { const r = releve(); assert.deepEqual(accordRegistre(avec('SEUIL_SIEGES_LISIBLES', { valeur: 12 }), r), []); @@ -338,6 +411,14 @@ describe('releve : resumeConfiguration', () => { assert.equal(resumeConfiguration({ police, ...reste, cadre }), resumeConfiguration(configuration())); assert.throws(() => resumeConfiguration(null), TypeError); }); + + test('une configuration sans cadre, ou au cadre mal formé, se refuse en nommant le cadre', () => { + const { cadre, ...sansCadre } = configuration(); + for (const autre of [sansCadre, { ...configuration(), cadre: null }, { ...configuration(), cadre: { largeur: 1366 } }]) { + assert.throws(() => resumeConfiguration(autre), { name: 'TypeError', message: /cadre/ }); + assert.throws(() => tableauConstantes(registre(), { ...releve(), configuration: autre }), { name: 'TypeError', message: /cadre/ }); + } + }); }); describe('releve : tableauConstantes (§ 19.8, § 19.10)', () => { @@ -397,6 +478,18 @@ describe('releve : tableauConstantes (§ 19.8, § 19.10)', () => { }); }); + test('une mesure qui porte son ralentissement le fait citer à sa ligne : à ×1, « processeur non ralenti » sous une configuration à ×4', () => { + const r = releve(); + r.mesures[0].ralentissementCpu = 1; + const lignes = tableauConstantes(liste, r).split('\n').slice(2).map(cellules); + const configurationDe = (nom) => lignes.find((l) => l[0] === `\`${nom}\``)[7]; + assert.equal(configurationDe('SEUIL_SIEGES_LISIBLES'), resumeConfiguration({ ...configuration(), ralentissementCpu: 1 })); + assert.match(configurationDe('SEUIL_SIEGES_LISIBLES'), /, processeur non ralenti, /); + assert.doesNotMatch(configurationDe('SEUIL_SIEGES_LISIBLES'), /×4/); + assert.equal(configurationDe('TOLERANCE_CHAINE_TEMOIN'), resumeConfiguration(configuration())); + assert.equal(configurationDe('RALENTISSEMENT_PLEINE_CHARGE'), resumeConfiguration(configuration())); + }); + test('une barre verticale ou un saut de ligne dans un texte ne cassent pas le tableau ; un registre vide se refuse', () => { const registre = avec('K_LISTES', { mesure: 'échelle | zoom\nde la vue' }); const rang = registre.findIndex((c) => c.nom === 'K_LISTES'); diff --git a/scripts/documentation.js b/scripts/documentation.js index 6224faa..fc57c0c 100644 --- a/scripts/documentation.js +++ b/scripts/documentation.js @@ -13,7 +13,8 @@ // définition des réglages, la table des colonnes du CSV, le registre des // commandes et leurs libellés figés, la table des raccourcis, les // démonstrations livrées passées au diagnostic, les styles relevés au -// manifeste des captures, le registre des constantes. Aucune valeur de ces +// manifeste des captures, le registre des constantes, avec le relevé du +// banc quand il existe (scripts/banc/releve.json). Aucune valeur de ces // tableaux ne s'écrit ici : elle se lit, ou se compose par la traduction. // Seul s'écrit ici ce qui appartient au document : les en-têtes de colonnes, // le nom d'un contexte, d'une touche, d'un statut ou de l'état d'une @@ -62,6 +63,7 @@ import { diagnostiquer } from '../src/moteur/diagnostic.js'; import { analyser, configurationDepuisCharge } from '../src/stockage/document.js'; import { VERSION } from '../src/version.genere.js'; import { CHEMIN_MANIFESTE, ecartDeVersion } from './documentation/manifeste.js'; +import { tableauConstantes } from './banc/releve.js'; /** @typedef {{nom: string, debut: number, fin: number, contenu: string}} Region * debut et fin bornent le contenu dans le texte : texte.slice(debut, fin). */ @@ -570,9 +572,13 @@ export function tableauRegistre(constantes = CONSTANTES) { // --- Les producteurs -------------------------------------------------------------- +/** Le relevé du banc de mesure, relatif à la racine (§ 19.10). */ +export const CHEMIN_RELEVE = 'scripts/banc/releve.json'; + /** - * @typedef {{manifeste: () => object|null}} Contexte - * manifeste : le manifeste des captures lu, null quand il manque + * @typedef {{manifeste: () => object|null, releve: () => object|null}} Contexte + * manifeste : le manifeste des captures lu, null quand il manque ; + * releve : le relevé du banc lu, null quand il manque */ const producteur = (nom, produire) => Object.freeze({ nom, produire }); @@ -589,7 +595,10 @@ export const PRODUCTEURS = Object.freeze([ producteur('raccourcis', () => tableauRaccourcis()), producteur('demonstrations', () => tableauDemonstrations()), producteur('contrastes', (contexte) => tableauContrastes(contexte.manifeste())), - producteur('constantes', () => tableauRegistre()), + producteur('constantes', (contexte) => { + const releve = contexte.releve(); + return releve === null ? tableauRegistre() : tableauConstantes(CONSTANTES, releve); + }), ]); /** @@ -616,31 +625,35 @@ const estAbsent = (erreur) => erreur?.code === 'ENOENT'; // Le lecteur par défaut d'une racine : le texte UTF-8 d'un chemin relatif. const lecteurDe = (racine) => (chemin) => readFileSync(join(racine, chemin), 'utf8'); -// Le contexte d'un arbre : le manifeste, lu au premier besoin, une fois ; -// null quand il manque. Un manifeste qui n'est pas du JSON lève. -function contexteDe(lire) { +// Le lecteur d'un fichier JSON de l'arbre, lu au premier besoin, une fois : +// l'objet lu, ou null quand le fichier manque. Un texte qui n'est pas du +// JSON lève en nommant le chemin. +function lecteurJson(lire, chemin) { let lu; - return { - manifeste() { - if (lu !== undefined) return lu; - let texte; - try { - texte = lire(CHEMIN_MANIFESTE); - } catch (erreur) { - if (!estAbsent(erreur)) throw erreur; - lu = null; - return lu; - } - try { - lu = JSON.parse(texte); - } catch { - throw new Error(`${CHEMIN_MANIFESTE} : JSON illisible`); - } + return () => { + if (lu !== undefined) return lu; + let texte; + try { + texte = lire(chemin); + } catch (erreur) { + if (!estAbsent(erreur)) throw erreur; + lu = null; return lu; - }, + } + try { + lu = JSON.parse(texte); + } catch { + throw new Error(`${chemin} : JSON illisible`); + } + return lu; }; } +// Le contexte d'un arbre : le manifeste des captures et le relevé du banc. +function contexteDe(lire) { + return { manifeste: lecteurJson(lire, CHEMIN_MANIFESTE), releve: lecteurJson(lire, CHEMIN_RELEVE) }; +} + // Une faute de région, au chemin de son porteur. const auChemin = (chemin) => ({ region, faute }) => ({ chemin, diff --git a/test/documentation.test.js b/test/documentation.test.js index 306e3dd..9abbb0d 100644 --- a/test/documentation.test.js +++ b/test/documentation.test.js @@ -26,7 +26,17 @@ import { TABLES_INTERFACE } from '../src/interface/libelles.js'; import { diagnostiquer } from '../src/moteur/diagnostic.js'; import { analyser, configurationDepuisCharge } from '../src/stockage/document.js'; import { VERSION } from '../src/version.genere.js'; -import { PORTEURS, PRODUCTEURS, RACINE, lireRegions, verifierArbre } from '../scripts/documentation.js'; +import { CONSTANTES } from '../src/geometrie/constantes.js'; +import { tableauConstantes } from '../scripts/banc/releve.js'; +import { + PORTEURS, + PRODUCTEURS, + RACINE, + engendrerArbre, + lireRegions, + tableauRegistre, + verifierArbre, +} from '../scripts/documentation.js'; import { A_VENIR, CAPTURES, @@ -262,6 +272,65 @@ describe('documentation : les régions engendrées des documents livrés (§ 19. }); }); +// --- La région des constantes et le relevé du banc (§ 19.8, § 19.10) ---------------- + +describe('documentation : la région « constantes » lit le relevé du banc quand il existe (§ 19.10)', () => { + const RELEVE = 'scripts/banc/releve.json'; + const PORTEUR = 'README.md'; + const TEXTE = '# Essai\n\n\n\n'; + + // Un relevé inventé, d'une mesure du banc. + const releve = { + format: 1, + configuration: { + cadre: { largeur: 1366, hauteur: 700 }, + facteurEchelle: 1, + navigateur: 'Chromium 140.0', + node: 'v26.10.0', + processeursLogiques: 8, + ralentissementCpu: 4, + police: 'Noto Sans', + }, + mesures: [{ nom: 'SEUIL_SIEGES_LISIBLES', unite: 'places', valeurProposee: 17, critere: 'liste dans la hauteur du plan' }], + }; + + // Engendre le porteur d'essai sur un arbre en mémoire où le relevé vaut + // texteReleve, ou manque quand il vaut null ; rend le bilan et le texte + // écrit. + function engendrer(texteReleve) { + const ecrits = new Map(); + const lire = (chemin) => { + if (chemin === PORTEUR) return TEXTE; + if (chemin === RELEVE && texteReleve !== null) return texteReleve; + throw Object.assign(new Error(`absent : ${chemin}`), { code: 'ENOENT' }); + }; + const bilan = engendrerArbre({ porteurs: [PORTEUR], lire, ecrire: (chemin, texte) => ecrits.set(chemin, texte) }); + return { bilan, ecrit: ecrits.get(PORTEUR) }; + } + + const region = (texte) => lireRegions(texte)[0].contenu.trim(); + + test('sans relevé, la région est le tableau du registre seul', () => { + const { bilan, ecrit } = engendrer(null); + assert.equal(bilan.code, 0); + assert.equal(region(ecrit), tableauRegistre()); + }); + + test('avec un relevé, la région est le tableau des constantes, la valeur proposée à côté de celle du registre', () => { + const { bilan, ecrit } = engendrer(JSON.stringify(releve)); + assert.equal(bilan.code, 0); + assert.equal(region(ecrit), tableauConstantes(CONSTANTES, releve)); + assert.match(region(ecrit), /\| `SEUIL_SIEGES_LISIBLES` \|.*\| 17 places \| cadre de 1 366 × 700 px/); + }); + + test('un relevé illisible fait échouer la production, en nommant son chemin, et rien ne s’écrit', () => { + const { bilan, ecrit } = engendrer('{ format: 1'); + assert.equal(bilan.code, 1); + assert.equal(ecrit, undefined); + assert.match(bilan.fautes[0].faute, /scripts\/banc\/releve\.json : JSON illisible/); + }); +}); + // --- La section sur l'algorithme (§ 19.9) --------------------------------------------- // Les nombres de un à douze, en lettres : la section écrit en lettres tout