diff --git a/test/arborescence.test.js b/test/arborescence.test.js index d3459d6..de4aef5 100644 --- a/test/arborescence.test.js +++ b/test/arborescence.test.js @@ -11,10 +11,13 @@ // géométrie, ni le générateur ne lisent la version engendrée (§ 14.7) ; seuls // deux lecteurs nommés touchent le pont de la coquille (§ 13.1) ; hors de // l'horloge, l'interface et l'application n'arment aucune minuterie et ne -// lisent ni l'heure, ni l'aléa, ni la langue (§ 19.4) ; chaque paquet importé -// est déclaré dans devDependencies, et aucun paquet refusé ne l'est (§ 13.3, -// § 14.8) ; une valeur du contrat de données n'a qu'une définition, celle de -// configuration.js ; la mesure de la couverture joue la série node, publie +// lisent ni l'heure, ni l'aléa, ni la langue (§ 19.4) ; seul le point +// d'entrée lit le nombre de processeurs logiques (§ 13.4) ; seul l'aperçu lit +// le contexte 'imprimante' et appelle imprimer, et ni l'interface ni +// l'application ne touchent print (§ 11.7) ; chaque paquet importé est déclaré dans devDependencies, et aucun +// paquet refusé ne l'est (§ 13.3, § 14.8) ; une valeur du contrat de données +// n'a qu'une définition, celle de configuration.js ; la mesure de la +// couverture joue la série node sous le seul rapporteur default, publie // chaque module et tient ses seuils durs sur des modules qui existent // (§ 14.13) ; les épreuves s'écrivent selon le § 14.8, la série surveillée // n'attend ni n'écrit hors d'un temporaire, et ses motifs de surveillance ne @@ -25,7 +28,8 @@ // charge que dans les épreuves nommées (§ 14.12). Chaque relevé lit le texte // entier, commentaires et chaînes compris, hors ceux qui disent le contraire — // les accès au navigateur et au pont, et les relevés des épreuves, sautent les -// chaînes littérales : un commentaire qui nomme un appel refusé fait échouer +// chaînes littérales ; les lectures du nombre de processeurs et de +// l'imprimante sautent chaînes et commentaires : un commentaire qui nomme un appel refusé fait échouer // la garde comme l'appel lui-même. Chaque garde déclare son périmètre, refuse // de passer sur un balayage vide (§ 14.2), et d'autres épreuves la font // tourner sur des arbres temporaires qui portent, à plus d'un niveau de @@ -2028,16 +2032,21 @@ const SEUILS_GLOBAUX = ['statements', 'branches', 'functions', 'lines', '100', ' const repondA = (chemin, motifs = []) => motifs.some((motif) => matchesGlob(chemin, motif)); // La commande de la mesure : make couverture passe la main à npm run -// couverture, qui lance vitest run sur la série node, couverture active. -// Sans --coverage, la série passe et aucun seuil ne se lit. Outre ces deux -// arguments, le script n'admet que le choix du rapporteur des épreuves : une -// option --coverage.* redéfinirait, pour cette exécution, les seuils, include -// ou exclude que relève releverCouverture, et --changed, --config ou un -// filtre de fichiers changeraient ce qui se mesure. Une option et sa valeur -// se lisent en un argument : « --project node » comme « --project=node ». +// couverture, qui lance vitest run sur la série node, couverture active, sous +// le seul rapporteur default. Sans --coverage, la série passe et aucun seuil +// ne se lit. Sans --reporter default, vitest.config.js remet +// ./test/rapporteur.js, dont les plafonds de durée (§ 14.14) bornent la +// série surveillée et la série longue, et feraient échouer la série +// instrumentée, plus lente. Le script n'admet rien d'autre que ces trois +// arguments : un second rapporteur, ou un autre, ramènerait ces plafonds ou +// changerait le bilan ; une option --coverage.* redéfinirait, pour cette +// exécution, les seuils, include ou exclude que relève releverCouverture, et +// --changed, --config ou un filtre de fichiers changeraient ce qui se +// mesure. Une option et sa valeur se lisent en un argument : « --project +// node » comme « --project=node ». const RECETTE_DE_MESURE = '$(AVEC_NODE) npm run couverture'; -const ARGUMENTS_REQUIS = ['--project=node', '--coverage']; -const ARGUMENT_ADMIS = /^(?:--project=node|--coverage|--reporter=\S+)$/; +const ARGUMENTS_REQUIS = ['--project=node', '--coverage', '--reporter=default']; +const ARGUMENT_ADMIS = /^(?:--project=node|--coverage|--reporter=default)$/; const OPTION_A_VALEUR = /^--(?:project|reporter)$/; // Écarts de la commande de la mesure sous la racine : la recette de la cible @@ -2139,7 +2148,7 @@ const COUVERTURE_CONFORME = Object.freeze({ }); describe('arborescence : la couverture (§ 14.13)', () => { - test("make couverture et npm run couverture mesurent la série node ; le rapport montre chaque module ; aucun seuil global ; 100 % de branches sur chaque module nommé, présent dans l'arbre et dans la mesure", () => { + test("make couverture et npm run couverture mesurent la série node, sous le seul rapporteur default ; le rapport montre chaque module ; aucun seuil global ; 100 % de branches sur chaque module nommé, présent dans l'arbre et dans la mesure", () => { assert.deepEqual(releverCouverture(RACINE, configuration.test.coverage), []); }); @@ -2180,7 +2189,7 @@ describe('arborescence : la couverture (§ 14.13)', () => { assert.throws(() => releverCouverture(RACINE, undefined), /aucune configuration de couverture/); }); - test('la garde relève une commande qui ne mesure pas la série node, ou qui redéfinit la mesure ; make couverture passe par npm run couverture', () => { + test('la garde relève une commande qui ne mesure pas la série node, qui redéfinit la mesure, ou qui la lance sous un autre rapporteur que default ; make couverture passe par npm run couverture', () => { const relever = (fichiers) => avecArbre({ ...MODULES_MESURES, ...COMMANDE_CONFORME, ...fichiers }, (racine) => releverCouverture(racine, COUVERTURE_CONFORME), @@ -2190,21 +2199,30 @@ describe('arborescence : la couverture (§ 14.13)', () => { const scripts = [ ['vitest run --project node --reporter=default', ['npm run couverture : sans --coverage']], [ - 'vitest run --project node --coverage --coverage.include=src/moteur/indicateurs.js --coverage.exclude=src/stockage/journal.js', + 'vitest run --project node --coverage --reporter=default --coverage.include=src/moteur/indicateurs.js --coverage.exclude=src/stockage/journal.js', [ 'npm run couverture : argument non admis --coverage.include=src/moteur/indicateurs.js', 'npm run couverture : argument non admis --coverage.exclude=src/stockage/journal.js', ], ], [ - 'vitest run --project node-long --coverage', + 'vitest run --project node-long --coverage --reporter=default', ['npm run couverture : sans --project node', 'npm run couverture : argument non admis --project=node-long'], ], [ - 'vitest run --project node --coverage --changed src/moteur', + 'vitest run --project node --coverage --reporter=default --changed src/moteur', ['npm run couverture : argument non admis --changed', 'npm run couverture : argument non admis src/moteur'], ], ['vitest --project node --coverage', ['npm run couverture : ne lance pas vitest run']], + ['vitest run --project node --coverage', ['npm run couverture : sans --reporter default']], + [ + 'vitest run --project node --coverage --reporter=./test/rapporteur.js', + ['npm run couverture : sans --reporter default', 'npm run couverture : argument non admis --reporter=./test/rapporteur.js'], + ], + [ + 'vitest run --project node --coverage --reporter=default --reporter verbose', + ['npm run couverture : argument non admis --reporter=verbose'], + ], ]; for (const [script, attendu] of scripts) { assert.deepEqual(relever({ 'package.json': scriptDeMesure(script) }), attendu, script); @@ -2500,6 +2518,113 @@ describe('arborescence : une horloge (§ 19.4, § 14.7)', () => { }); }); +// Relevé « fichier:ligne forme » des formes données dans les modules et les +// composants des arbres, à toute profondeur, épreuves exclues ; par fichier, +// par ligne, puis dans l'ordre des formes. Une forme est [nom, expression à +// g, lu]. Lue dans le 'code', elle se cherche dans le texte sans chaînes ni +// commentaires ; lue dans le 'texte', parce qu'une chaîne est son nom même, +// elle se cherche dans le texte sans commentaires et ne compte que si son +// premier caractère subsiste dans le code : celle qu'une chaîne contient est +// blanchie avec elle. Un composant se lit sans ses commentaires HTML, ses +// blocs de script comme un module, son balisage tel quel. Un lecteur admis, +// {fichier, forme}, admet chaque occurrence de cette forme dans ce fichier, +// et rien d'autre ; suit une ligne « lecteur sans objet : fichier forme » par +// lecteur qui n'en porte aucune. Lève quand un arbre ne fournit aucun +// fichier. +function releverLectures(racine, { arbres, formes, lecteurs }) { + const fichiers = arbres + .flatMap((arbre) => { + const trouves = fichiersDe(join(racine, arbre)).filter( + (f) => (EXTENSIONS_MODULE.has(extname(f)) || extname(f) === '.svelte') && !EPREUVE.test(f), + ); + assert.ok(trouves.length > 0, `${arbre} ne fournit aucun fichier`); + return trouves; + }) + .sort(); + const servis = new Set(); + const releve = fichiers.flatMap((fichier) => { + const nom = enBarres(racine, fichier); + const texte = texteSansCommentaires(fichier, readFileSync(fichier, 'utf8')); + const lus = { texte, code: codeSansChaines(fichier, texte) }; + return formes + .flatMap(([forme, motif, lu], rang) => + [...lus[lu].matchAll(motif)] + .filter(({ index }) => lus.code[index] === texte[index]) + .map(({ index }) => ({ ligne: ligneDe(texte, index), rang, forme })), + ) + .filter(({ forme }) => { + const lecteur = lecteurs.find((l) => l.fichier === nom && l.forme === forme); + if (lecteur !== undefined) servis.add(lecteur); + return lecteur === undefined; + }) + .sort((a, b) => a.ligne - b.ligne || a.rang - b.rang) + .map(({ ligne, forme }) => `${nom}:${ligne} ${forme}`); + }); + const sansObjet = lecteurs.filter((l) => !servis.has(l)).map(({ fichier, forme }) => `lecteur sans objet : ${fichier} ${forme}`); + return [...releve, ...sansObjet]; +} + +// Le nombre de processeurs logiques du poste (§ 13.4) est une donnée de la +// plateforme : src/main.js le lit et le passe à l'exécuteur de la génération +// (coeursDisponibles). Ailleurs dans src/, une lecture ferait dépendre du +// poste un module que les épreuves et le banc pilotent par ce paramètre. Il +// se lit par le nom du champ — point, chaînage optionnel, déstructuration — +// ou par une chaîne entre crochets. +const LECTURES_DES_COEURS = [ + ['hardwareConcurrency', /\bhardwareConcurrency\b/g, 'code'], + ["['hardwareConcurrency']", /\[\s*(['"`])hardwareConcurrency\1\s*\]/g, 'texte'], +]; +const LECTEURS_DES_COEURS = [ + { + fichier: 'src/main.js', + forme: 'hardwareConcurrency', + raison: "construit l'exécuteur de la génération avec le nombre de processeurs logiques du poste", + }, +]; +const releverLecteursDesCoeurs = (racine) => + releverLectures(racine, { arbres: ['src'], formes: LECTURES_DES_COEURS, lecteurs: LECTEURS_DES_COEURS }); + +describe('arborescence : un seul lecteur du nombre de processeurs logiques (§ 13.4)', () => { + test('hors de src/main.js, aucun module de src/ ne lit hardwareConcurrency', () => { + assert.deepEqual(releverLecteursDesCoeurs(RACINE), []); + }); + + test('la garde relève chaque lecture par sa ligne, composants compris, à toute profondeur, hors épreuves, chaînes et commentaires ; src/main.js ne lit que par le point ; un lecteur qui ne lit plus le champ est sans objet', () => { + const fichiers = { + 'src/interface/generation/Coeurs.svelte': [ + '', + '', + '
{navigator?.hardwareConcurrency}
', + ].join('\n'), + 'src/application/sous/parallele.js': [ + "export const a = globalThis.navigator['hardwareConcurrency'];", + 'const { hardwareConcurrency } = navigator;', + 'export const b = "[\'hardwareConcurrency\']" + hardwareConcurrencyMax;', + '/* navigator.hardwareConcurrency */', + ].join('\n'), + 'src/stockage/reserve.js': 'export const c = self.navigator.hardwareConcurrency ?? 1;\n', + 'src/application/generation.test.js': 'const n = navigator.hardwareConcurrency;\n', + 'src/interface/App.navigateur.test.js': "const n = navigator['hardwareConcurrency'];\n", + 'src/main.js': "const coeurs = navigator.hardwareConcurrency;\nconst autre = navigator['hardwareConcurrency'];\n", + }; + assert.deepEqual(avecArbre(fichiers, releverLecteursDesCoeurs), [ + "src/application/sous/parallele.js:1 ['hardwareConcurrency']", + 'src/application/sous/parallele.js:2 hardwareConcurrency', + 'src/interface/generation/Coeurs.svelte:2 hardwareConcurrency', + 'src/interface/generation/Coeurs.svelte:7 hardwareConcurrency', + "src/main.js:2 ['hardwareConcurrency']", + 'src/stockage/reserve.js:1 hardwareConcurrency', + ]); + const sansLecteur = { ...fichiers, 'src/main.js': 'const coeurs = 4;\n' }; + assert.deepEqual(avecArbre(sansLecteur, releverLecteursDesCoeurs).at(-1), 'lecteur sans objet : src/main.js hardwareConcurrency'); + assert.throws(() => avecArbre({ 'src/a.test.js': '', 'test/b.js': '' }, releverLecteursDesCoeurs), /src ne fournit aucun fichier/); + }); +}); + // Paquets refusés (§ 13.3, § 14.8), chacun par le motif de son nom : un outil // qui déduit les styles en lisant les sources élague les classes d'état // composées à l'exécution ; D3 garde la transformation du zoom hors du @@ -3841,6 +3966,139 @@ describe("arborescence : le câblage de l'imprimante (§ 13.4)", () => { }); }); +// La porte d'impression (§ 11.7) : toute impression passe par l'écran +// d'aperçu. App.svelte reçoit l'imprimante et la pose sous la clé de contexte +// 'imprimante' ; dans src/interface et src/application, seul l'aperçu lit ce +// contexte et appelle la fonction imprimer de l'imprimante, et aucun module +// ne touche print — que seule l'imprimante web de src/impression appelle, sur +// la fenêtre. Une lecture du contexte se reconnaît à getContext et à la clé +// entre apostrophes, guillemets ou accents graves. print se relève au nom +// suivi d'une parenthèse, chaînage optionnel compris, quel que soit l'objet +// qui le porte ; à toute mention sur l'objet global écrit par son nom — +// window, globalThis, self —, appelée ou non, une référence ainsi passée à un +// bouton étant une impression différée ; à une chaîne entre crochets ; à une +// déstructuration qui le tire. Une référence nue au print global, ou prise +// sur un alias de window, n'est pas relevée. imprimer se relève de même : par le point, +// appelé ou non, par une chaîne entre crochets ou par une déstructuration ; +// la fonction de l'aperçu qui porte ce nom n'est pas un passage. Le balisage +// d'un composant se lit tel quel, ses attributs entre guillemets compris : +// un « .imprimer » suivi d'un guillemet y clôt une valeur, comme la clé de +// libellé « impression.commande.imprimer », et ne se relève pas. +const ARBRES_D_UNE_PORTE = ['src/interface', 'src/application']; +const PORTE_D_IMPRESSION = 'src/interface/impression/Apercu.svelte'; +const PASSAGES_D_IMPRESSION = [ + ["getContext('imprimante')", /\bgetContext\s*\(\s*(['"`])imprimante\1\s*\)/g, 'texte'], + ['print()', /\bprint\s*(?:\?\.\s*)?\(/g, 'code'], + ['.print', /\b(?:window|globalThis|self)\s*\??\.\s*print\b/g, 'code'], + ["['print']", /\[\s*(['"`])print\1\s*\]/g, 'texte'], + ['{ print }', /\{[^{}]*\bprint\b[^{}]*\}\s*=/g, 'code'], + ['.imprimer', /\.\s*imprimer\b(?!\s*['"`])/g, 'code'], + ["['imprimer']", /\[\s*(['"`])imprimer\1\s*\]/g, 'texte'], + ['{ imprimer }', /\{[^{}]*\bimprimer\b[^{}]*\}\s*=/g, 'code'], +]; +const RAISON_DE_LA_PORTE = "l'écran d'aperçu, d'où part toute impression : la page qu'il montre est celle qui s'imprime"; +const LECTEURS_DE_L_IMPRIMANTE = [ + { fichier: PORTE_D_IMPRESSION, forme: "getContext('imprimante')", raison: RAISON_DE_LA_PORTE }, + { fichier: PORTE_D_IMPRESSION, forme: '.imprimer', raison: RAISON_DE_LA_PORTE }, +]; +const releverPortesDImpression = (racine) => + releverLectures(racine, { arbres: ARBRES_D_UNE_PORTE, formes: PASSAGES_D_IMPRESSION, lecteurs: LECTEURS_DE_L_IMPRIMANTE }); + +describe("arborescence : une seule porte d'impression (§ 11.7)", () => { + test("seul src/interface/impression/Apercu.svelte lit le contexte 'imprimante' et appelle imprimer, et aucun module de src/interface ni de src/application ne touche print", () => { + assert.deepEqual(releverPortesDImpression(RACINE), []); + }); + + test("la garde relève chaque lecture du contexte, chaque appel ou référence de print et chaque passage par imprimer, par sa ligne, composants compris, à toute profondeur, hors épreuves, chaînes et commentaires ; l'aperçu n'appelle pas print() ; un lecteur qui ne lit plus le contexte ou n'imprime plus est sans objet", () => { + const fichiers = { + 'src/interface/badges/Tiroir.svelte': [ + '', + '', + '', + '', + '