diff --git a/scripts/documentation.js b/scripts/documentation.js index 6a22e30..d9c3142 100644 --- a/scripts/documentation.js +++ b/scripts/documentation.js @@ -51,14 +51,15 @@ // porteur : un document privé de ses images ne passe pas sur un balayage // vide. // -// Ligne de commande, depuis la racine du projet ou d'ailleurs : +// Ligne de commande, depuis la racine du projet ou d'ailleurs, par un +// chemin qui traverse ou non un lien symbolique : // node scripts/documentation.js engendrer | verifier [--livraison] // [--racine ] [--porteur ]… // --racine remplace la racine du projet ; chaque --porteur, relatif à la // racine, remplace la liste PORTEURS. Code de sortie : 0 sans faute, 1 sur // une faute ou, sous --livraison, un écart de version, 2 sur des arguments // illisibles. -import { readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { COMMANDES } from '../src/application/commandes.js'; @@ -934,6 +935,23 @@ export function engendrerArbre({ return { fautes, ecrits: aEcrire.map(([chemin]) => chemin), code: 0 }; } +/** + * La ligne qui nomme un écart de version (§ 19.7) : sa portée — bloquant + * sous livraison, non bloquant au cycle court —, la version du manifeste, + * celle de la construction. La ligne de commande l'imprime, et la série node + * l'affiche au cycle court (test/documentation.test.js). + * + * @param {null|{manifeste: string|null, construction: string}} ecart celui de verifierArbre + * @param {{livraison?: boolean}} [options] + * @returns {string|null} null sans écart + */ +export function ligneEcart(ecart, { livraison = false } = {}) { + if (ecart === null) return null; + const portee = livraison ? 'bloquant' : 'non bloquant'; + const manifeste = ecart.manifeste ?? 'aucune (manifeste absent ou sans capture)'; + return `écart de version (${portee}) : manifeste ${manifeste}, construction ${ecart.construction}`; +} + const USAGE = 'usage : node scripts/documentation.js engendrer | verifier [--livraison] [--racine ] [--porteur ]…'; @@ -992,15 +1010,29 @@ export function executer(argv, { racine = RACINE, porteurs = PORTEURS, version, ...(lireOctets === undefined ? {} : { lireOctets }), }); ecrireFautes(bilan.fautes); - if (bilan.ecart !== null) { - const portee = lus.livraison ? 'bloquant' : 'non bloquant'; - const manifeste = bilan.ecart.manifeste ?? 'aucune (manifeste absent ou sans capture)'; - sortie(`écart de version (${portee}) : manifeste ${manifeste}, construction ${bilan.ecart.construction}`); - } + const ecart = ligneEcart(bilan.ecart, { livraison: lus.livraison }); + if (ecart !== null) sortie(ecart); sortie(`verifier : ${options.porteurs.length} porteur(s), ${bilan.regions} région(s), ${bilan.images} image(s), ${bilan.fautes.length} faute(s)`); return bilan.code; } -if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +/** + * Vrai quand chemin désigne ce module : Node charge le script lancé sous son + * chemin réel, et un chemin qui traverse un lien symbolique se compare donc + * par realpathSync. Un chemin absent ou illisible rend faux. + * + * @param {string|undefined} [chemin] le script que Node a lancé + * @returns {boolean} + */ +export function estLanceDirectement(chemin = process.argv[1]) { + if (chemin === undefined) return false; + try { + return realpathSync(chemin) === realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (estLanceDirectement()) { process.exitCode = executer(process.argv.slice(2)); } diff --git a/src/interface/parcours.navigateur.test.js b/src/interface/parcours.navigateur.test.js index abd0dcd..26f98db 100644 --- a/src/interface/parcours.navigateur.test.js +++ b/src/interface/parcours.navigateur.test.js @@ -9,7 +9,7 @@ // de 1 366 × 700. Un seul montage porte toutes les épreuves du fichier, qui // s'exécutent dans l'ordre : chacune part de l'événement que la précédente a // laissé, affirme sa condition avant la suivante, et finit sur un collecteur -// d'erreurs vide. +// d'erreurs vide et sans requête hors de la boucle locale (§ 19.11). // // Le parcours est le parcours photographié (§ 19.1, § 19.3) : il suit les // captures du § 19.3 que l'itération photographie — 01, 01b, 02, 02b, 03 à @@ -81,6 +81,7 @@ import { entiereDansLaFenetre, modeFourni, photographier, + requeteHorsBoucle, } from '../../test/navigateur/captures.js'; import { NOM_CAS_LIMITES, attendre, centre, octetsCasLimites } from '../../test/navigateur/ecrans.js'; import { fixerCadre, pointer, touche } from '../../test/navigateur/entrees.js'; @@ -180,8 +181,21 @@ beforeEach(({ task, onTestFailed }) => { expect(new TextEncoder().encode(titre).length, titre).toBeLessThanOrEqual(TITRE_MAX_OCTETS); }); -afterEach(() => { - expect(m.erreurs(), 'erreurs reçues par le collecteur').toEqual([]); +// Chaque épreuve, photographiée ou non, finit sans erreur au collecteur et +// sans requête hors de la boucle locale : celles d'après la dernière capture +// d'écran — les pages du PDF, les sorties, l'ordre de lecture, les fenêtres, +// la lecture et le plan bloqué — se relèvent ici. Les deux causes se +// lisent avant d'échouer et se nomment ensemble, la requête d'abord : une +// requête se nomme dans l'épreuve qui l'a émise, même quand le collecteur a +// aussi reçu une erreur. +afterEach(async () => { + const requete = await requeteHorsBoucle(); + const erreurs = m.erreurs(); + const fautes = [ + ...(requete === null ? [] : [`requête hors de la boucle locale : ${requete}`]), + ...(erreurs.length === 0 ? [] : [`erreurs reçues par le collecteur : ${JSON.stringify(erreurs)}`]), + ]; + if (fautes.length > 0) expect.fail(fautes.join(' ; ')); }); afterAll(async () => { @@ -680,7 +694,8 @@ function parPointsDeCode(a, b) { /** * Photographie une page du PDF de la planche (§ 19.6) : tramerPdf la lit - * d'abord sans rien écrire, conditions(lue) affirme ce qu'elle montre, puis, + * d'abord sans rien écrire, conditions(lue) affirme ce qu'elle montre, aucune + * requête hors de la boucle locale n'attend d'être nommée (§ 19.11), puis, * en écriture, la trame en PNG sous le dossier d'attente et dépose l'entrée ; * en vérification, compare l'entrée au manifeste. L'entrée porte la source * pdf, la taille de la page à 96 ppp pour cadre, la couche de texte de la @@ -700,6 +715,8 @@ async function photographierPdf(octets, { page, fichier }, conditions) { expect(lu.pdf).toBe(await empreinte(octets)); const [lue] = lu.pages; conditions(lue); + const requete = await requeteHorsBoucle(); + if (requete !== null) expect.fail(`capture ${fichier} : requête hors de la boucle locale : ${requete}`); const racineNeutre = etat().racine.chemin; expect(await commands.cheminsHorsRacine({ texte: lue.textes, racine: racineNeutre }), fichier).toEqual([]); const entree = { diff --git a/src/interface/transversal.navigateur.test.js b/src/interface/transversal.navigateur.test.js index 3947274..faeb53f 100644 --- a/src/interface/transversal.navigateur.test.js +++ b/src/interface/transversal.navigateur.test.js @@ -62,6 +62,7 @@ import { rgb, cloreCaptures, photographier, + requeteHorsBoucle, situer, stylesDuTheme, texteReleve, @@ -1183,6 +1184,12 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu return echecs; } + // Laisse passer autant d'images de la page qu'echecsReleves en attend au + // plus, sans lire aucun relevé : l'échec d'une requête a rejoint node. + async function laisserArriverLEchec() { + for (let image = 0; image < 60; image += 1) await new Promise((resoudre) => requestAnimationFrame(resoudre)); + } + // Le mandataire mort de test/chromium.js refuse la connexion : la requête // n'est jamais partie de la machine, et Chromium le dit par sa cause. test('réseau : une requête vers une adresse littérale inventée échoue en connexion au mandataire mort, la page de la boucle locale se charge, le relevé la nomme', async () => { @@ -1195,6 +1202,16 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu expect(await commands.requetesHorsBoucle()).toEqual([ADRESSE_INVENTEE]); }); + test('relevé : requeteHorsBoucle nomme la requête suivie de sa cause une fois l’échec reçu, vide les deux relevés, puis rend null', async () => { + await commands.requetesHorsBoucle(); + await commands.echecsHorsBoucle(); + await requeteInventee(); + await laisserArriverLEchec(); + expect(await requeteHorsBoucle()).toBe(`${ADRESSE_INVENTEE} (net::ERR_PROXY_CONNECTION_FAILED)`); + expect(await commands.echecsHorsBoucle()).toEqual([]); + expect(await requeteHorsBoucle()).toBe(null); + }); + test('un sélecteur manquant, une erreur au collecteur, une requête hors boucle : chacun échoue en nommant la capture', async () => { await avecMontage(async (m, { ecrire }) => { await expect(photographier(m, '01-premier-ecran', { ...ecrire, condition: '[data-absent]' })).rejects.toThrow( @@ -1458,13 +1475,26 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu }); }); - test('clôture : une épreuve du parcours en échec ne publie rien, jette l’attente et se nomme ; sans échec, la clôture publie', async () => { + test('clôture : une épreuve en échec ou une requête hors boucle ne publie rien, jette l’attente et se nomme ; sinon, la clôture publie', async () => { await avecMontage(async (m, { racine, racineNeutre, ecrire }) => { const declarees = ['01-premier-ecran']; const clore = (echecs) => cloreCaptures({ echecs, declarees, mode: 'ecrire', racine, racineNeutre }); await photographier(m, '01-premier-ecran', ecrire); await expect(clore(['20 « Défaire » en lecture'])).rejects.toThrow('captures non publiées : 1 épreuve(s) du parcours en échec : « 20 « Défaire » en lecture »'); expect(await commands.empreintesDossier({ racine })).toEqual([]); + // Une requête émise après la dernière capture : la clôture seule la lit. + await photographier(m, '01-premier-ecran', ecrire); + await requeteInventee(); + await echecsReleves(); + await expect(clore([])).rejects.toThrow(`captures non publiées : requête hors de la boucle locale : ${ADRESSE_INVENTEE}`); + expect(await commands.empreintesDossier({ racine })).toEqual([]); + await photographier(m, '01-premier-ecran', ecrire); + await requeteInventee(); + await echecsReleves(); + await expect(clore(['20 « Défaire » en lecture'])).rejects.toThrow( + `captures non publiées : 1 épreuve(s) du parcours en échec : « 20 « Défaire » en lecture » ; requête hors de la boucle locale : ${ADRESSE_INVENTEE}`, + ); + expect(await commands.empreintesDossier({ racine })).toEqual([]); await photographier(m, '01-premier-ecran', ecrire); expect(await clore([])).toEqual({ declarees, bilan: { ecrites: declarees, gardees: [], retirees: [], manifeste: true } }); }); diff --git a/test/documentation.test.js b/test/documentation.test.js index 5aadc72..11985b2 100644 --- a/test/documentation.test.js +++ b/test/documentation.test.js @@ -18,9 +18,13 @@ // (une image retirée, un nombre changé, un nom de fonction collé), elle en // rend une, ce qui montre qu'elle ne passe pas à vide (§ 14.2). import assert from 'node:assert/strict'; -import { readdirSync, readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { join, relative } from 'node:path'; import { describe, test } from './lanceur.js'; +import { versionVoisine } from './version_voisine.js'; +import configuration from '../vitest.config.js'; import { TABLES_APPLICATION } from '../src/application/libelles.js'; import { creerTraduction } from '../src/application/traduction.js'; import { CATALOGUE } from '../src/demo/catalogue.js'; @@ -32,6 +36,7 @@ import { VERSION } from '../src/version.genere.js'; import { CONSTANTES } from '../src/geometrie/constantes.js'; import { tableauConstantes } from '../scripts/banc/releve.js'; import { + CHEMIN_RELEVE, PORTEURS, PRODUCTEURS, RACINE, @@ -39,6 +44,7 @@ import { executer, fautesImagesCitees, imagesCitees, + ligneEcart, lireRegions, tableauRegistre, verifierArbre, @@ -401,8 +407,12 @@ describe('documentation : les régions engendrées des documents livrés (§ 19. assert.deepEqual(servies, PRODUCTEURS.map(({ nom }) => nom).sort()); }); - test('verifier, sur l’arbre réel et la liste par défaut : aucune faute, toutes les régions lues', () => { - const bilan = verifierArbre(); + test('verifier, sur l’arbre réel et la liste par défaut : aucune faute, toutes les régions lues ; l’écart de version affiché, non bloquant', () => { + // L'écrivain relève chaque ligne et la passe à la sortie d'erreur de + // l'épreuve : la série surveillée affiche l'écart de l'arbre réel. + const lignes = []; + const { bilan } = verifierEtAfficher({ ecrire: (ligne) => { lignes.push(ligne); console.warn(ligne); } }); + assert.deepEqual(lignes, bilan.ecart === null ? [] : [ligneEcart(bilan.ecart)]); assert.deepEqual(bilan.fautes, []); assert.equal(bilan.regions, PRODUCTEURS.length); assert.equal(bilan.code, 0); @@ -435,6 +445,123 @@ describe('documentation : les régions engendrées des documents livrés (§ 19. }); }); +// --- L'écart de version, la ligne de commande, la surveillance (§ 19.7, § 14.4) --------- + +// Vérifie l'arbre réel au cycle court, par lire quand il est donné, et +// affiche l'écart de version du bilan par ecrire : par défaut console.warn, +// la sortie d'erreur de l'épreuve, que le rapporteur imprime sous le nom de +// l'épreuve sans la faire échouer (§ 19.7). L'épreuve du manifeste voisin +// éprouve les deux écrivains, l'injecté et celui par défaut ; celle de +// l'arbre réel passe un écrivain qui relève puis transmet à console.warn, +// et attend la ligne de son propre écart, aucune sans écart. +// Rend { bilan, ligne } : ligne est la ligne écrite, null sans écart. +function verifierEtAfficher({ lire, ecrire = (ligne) => console.warn(ligne) } = {}) { + const bilan = verifierArbre({ lire }); + const ligne = ligneEcart(bilan.ecart); + if (ligne !== null) ecrire(ligne); + return { bilan, ligne }; +} + +// Les épreuves que la série surveillée relance quand chemin change : la +// réunion des motifs de vitest.config.js qui y répondent, comme Vitest la +// calcule. +function relanceesPar(chemin) { + const motifs = configuration.test.watchTriggerPatterns.filter(({ pattern }) => new RegExp(pattern.source, pattern.flags).test(chemin)); + return new Set(motifs.flatMap(({ testsToRun }) => [testsToRun(chemin) ?? []].flat())); +} + +describe('documentation : l’écart de version, la ligne de commande, la surveillance (§ 19.7, § 14.4)', () => { + // Le manifeste réel, chaque version réelle passée à la voisine de la + // construction : seules les versions diffèrent, images et empreintes + // restent celles de l'arbre. + const versionDuManifeste = (version) => { + const manifeste = lireManifeste(); + manifeste.captures = manifeste.captures.map((capture) => ({ ...capture, versionReelle: version })); + return lireAvec(CHEMIN_MANIFESTE, `${JSON.stringify(manifeste, null, 2)}\n`); + }; + + test('rouge : un manifeste à une version voisine — au cycle court, l’écart s’affiche, se nomme et ne bloque pas ; à la livraison, il bloque', () => { + const voisine = versionVoisine(VERSION.affichee); + const lire = versionDuManifeste(voisine); + const lignes = []; + const { bilan: court, ligne } = verifierEtAfficher({ lire, ecrire: (ecrite) => lignes.push(ecrite) }); + assert.deepEqual([court.fautes, court.code], [[], 0]); + assert.deepEqual(court.ecart, { manifeste: voisine, construction: VERSION.affichee }); + assert.deepEqual(lignes, [`écart de version (non bloquant) : manifeste ${voisine}, construction ${VERSION.affichee}`]); + assert.equal(ligne, lignes[0]); + // Sans écrivain, la ligne va à console.warn, remplacé le temps de l'appel. + const recues = []; + const avertir = console.warn; + console.warn = (...valeurs) => recues.push(valeurs.join(' ')); + try { + verifierEtAfficher({ lire }); + } finally { + console.warn = avertir; + } + assert.deepEqual(recues, lignes); + const livraison = verifierArbre({ lire, livraison: true }); + assert.equal(livraison.code, 1); + assert.equal(ligneEcart(livraison.ecart, { livraison: true }), `écart de version (bloquant) : manifeste ${voisine}, construction ${VERSION.affichee}`); + // La ligne de commande imprime la même ligne, par la même composition. + for (const [argv, code, portee] of [[['verifier'], 0, 'non bloquant'], [['verifier', '--livraison'], 1, 'bloquant']]) { + const sortie = []; + assert.equal(executer(argv, { lire, sortie: (ligne) => sortie.push(ligne) }), code); + assert.ok(sortie.includes(`écart de version (${portee}) : manifeste ${voisine}, construction ${VERSION.affichee}`), sortie.join('\n')); + } + // Sans écart, rien ne s'affiche. + const accorde = verifierEtAfficher({ lire: versionDuManifeste(VERSION.affichee), ecrire: () => assert.fail('aucune ligne sans écart') }); + assert.equal(accorde.bilan.ecart, null); + assert.equal(accorde.ligne, null); + }); + + test('un manifeste absent ne prouve aucune version : l’écart le nomme', () => { + assert.equal( + ligneEcart({ manifeste: null, construction: VERSION.affichee }), + `écart de version (non bloquant) : manifeste aucune (manifeste absent ou sans capture), construction ${VERSION.affichee}`, + ); + assert.equal(ligneEcart(null, { livraison: true }), null); + }); + + // Node charge le script lancé sous son chemin réel : un lancement par un + // chemin qui traverse un lien symbolique est un lancement direct. + test('rouge : lancé par un lien symbolique vers son dossier, le script s’exécute — un argument illisible rend l’usage et le code 2', () => { + const dossier = mkdtempSync(join(tmpdir(), 'gtt-documentation-lien-')); + try { + const lien = join(dossier, 'scripts'); + symlinkSync(join(RACINE, 'scripts'), lien, 'junction'); + const lance = spawnSync(process.execPath, [join(lien, 'documentation.js'), 'illisible'], { encoding: 'utf8' }); + assert.equal(lance.status, 2, lance.stderr); + assert.match(lance.stdout, /^usage : node scripts\/documentation\.js engendrer \| verifier/); + } finally { + rmSync(dossier, { recursive: true, force: true }); + } + }); + + // verifier lit par le disque les porteurs, le manifeste, le relevé du banc + // et les captures citées, qu'aucun graphe d'imports ne relie aux épreuves + // de la documentation : chacun a son motif de surveillance. Les lectures + // se relèvent sur l'arbre réel, avant l'appel à chaque lecteur. Les textes + // relancent les deux épreuves ; une capture relance celle de l'arbre réel, + // scripts/documentation.test.js ne lisant que ses propres images. + test('rouge : chaque fichier que verifier lit par le disque relance les épreuves de la documentation qui le lisent', () => { + const lus = new Set(); + const relever = (lecteur) => (chemin) => { + lus.add(chemin); + return lecteur(chemin); + }; + const bilan = verifierArbre({ lire: relever(lire), lireOctets: relever(lireOctets) }); + const images = [...lus].filter((chemin) => chemin.endsWith('.png')); + assert.ok(bilan.images > 0, 'aucune capture citée'); + assert.equal(images.length, bilan.images); + assert.deepEqual([...PORTEURS, CHEMIN_MANIFESTE, CHEMIN_RELEVE].filter((chemin) => !lus.has(chemin)), []); + const epreuvesDe = (chemin) => + chemin.endsWith('.png') ? ['test/documentation.test.js'] : ['test/documentation.test.js', 'scripts/documentation.test.js']; + const manquent = [...lus].flatMap((chemin) => + epreuvesDe(chemin).filter((e) => !relanceesPar(chemin).has(e)).map((e) => `${chemin} ne relance pas ${e}`)); + assert.deepEqual(manquent, []); + }); +}); + // --- 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)', () => { diff --git a/test/navigateur/captures.js b/test/navigateur/captures.js index f267c46..c671213 100644 --- a/test/navigateur/captures.js +++ b/test/navigateur/captures.js @@ -647,6 +647,24 @@ function conditionVisible(element) { return entiereDansLaFenetre(element) ? true : 'sa boîte déborde de la fenêtre ou d’un ancêtre qui la rogne : l’image la coupe'; } +/** + * La première requête hors de la boucle locale que la page a émise depuis + * le relevé précédent (§ 2, § 19.11), suivie de la cause de son échec entre + * parenthèses quand node l'a déjà reçue ; null sans requête. Vide les deux + * relevés de test/chromium.js, requêtes et échecs : chaque requête se + * nomme une fois, au premier contrôle qui la lit. Photographier, une page + * du PDF, la fin de chaque épreuve du parcours et sa clôture la lisent. + * + * @returns {Promise} + */ +export async function requeteHorsBoucle() { + const [adresse] = await commands.requetesHorsBoucle(); + const echecs = await commands.echecsHorsBoucle(); + if (adresse === undefined) return null; + const cause = echecs.find((echec) => echec.adresse === adresse)?.cause ?? null; + return cause === null ? adresse : `${adresse} (${cause})`; +} + // Ce que porte le userAgent de Chromium sans tête, le seul mode d'affichage // des captures, MODE_AFFICHAGE au manifeste (§ 19.4) : les métriques de // texte diffèrent d'un mode à l'autre. @@ -719,8 +737,8 @@ export async function photographier(m, fichier, { condition = [], graine = null, } const [erreur] = m.erreurs(); if (erreur !== undefined) throw echec(`collecteur : ${erreur.type} « ${erreur.message} »`); - const [requete] = await commands.requetesHorsBoucle(); - if (requete !== undefined) throw echec(`requête hors de la boucle locale : ${requete}`); + const requete = await requeteHorsBoucle(); + if (requete !== null) throw echec(`requête hors de la boucle locale : ${requete}`); if (innerWidth !== CADRE.largeur || innerHeight !== CADRE.hauteur) { throw echec(`cadre ${innerWidth} × ${innerHeight}, attendu ${CADRE.largeur} × ${CADRE.hauteur}`); } @@ -794,13 +812,14 @@ export async function photographier(m, fichier, { condition = [], graine = null, } /** - * Clôt un parcours photographié (§ 19.1, § 19.5, § 19.7). declarees : les - * captures que le parcours porte, CAPTURES hors A_VENIR par défaut. Une - * épreuve du fichier en échec, qu'elle ait photographié ou non, refuse la - * clôture en se nommant : en écriture, le dossier d'attente est jeté et - * rien n'est publié. Sans échec, l'écriture publie d'un bloc et rend le - * bilan de publierCaptures ; la vérification affirme que le manifeste porte - * exactement les déclarées. + * Clôt un parcours photographié (§ 19.1, § 19.5, § 19.7, § 19.11). + * declarees : les captures que le parcours porte, CAPTURES hors A_VENIR par + * défaut. Une épreuve du fichier en échec, qu'elle ait photographié ou non, + * refuse la clôture en se nommant ; une requête hors de la boucle locale + * émise depuis le dernier relevé la refuse en se nommant aussi : en + * écriture, le dossier d'attente est jeté et rien n'est publié. Sans + * échec, l'écriture publie d'un bloc et rend le bilan de publierCaptures ; + * la vérification affirme que le manifeste porte exactement les déclarées. * * @param {Object} options * @param {string[]} options.echecs les épreuves du fichier qui ont échoué @@ -817,10 +836,13 @@ export async function cloreCaptures({ echecs, declarees = null, mode = modeFourn liste = captures.map(({ fichier }) => fichier).filter((fichier) => !aVenir.includes(fichier)); } if (liste.length === 0) throw new Error('captures : aucune capture déclarée — zéro capture n’est jamais un succès'); - if (echecs.length > 0) { + const requete = await requeteHorsBoucle(); + const causes = []; + if (echecs.length > 0) causes.push(`${echecs.length} épreuve(s) du parcours en échec : ${echecs.map((nom) => `« ${nom} »`).join(', ')}`); + if (requete !== null) causes.push(`requête hors de la boucle locale : ${requete}`); + if (causes.length > 0) { if (mode === 'ecrire') await commands.viderAttente({ racine }); - const natures = echecs.map((nom) => `« ${nom} »`).join(', '); - throw new Error(`captures non ${mode === 'ecrire' ? 'publiées' : 'vérifiées'} : ${echecs.length} épreuve(s) du parcours en échec : ${natures}`); + throw new Error(`captures non ${mode === 'ecrire' ? 'publiées' : 'vérifiées'} : ${causes.join(' ; ')}`); } const memes = (a, b) => [...a].sort().join('\n') === [...b].sort().join('\n'); if (mode === 'ecrire') { diff --git a/vitest.config.js b/vitest.config.js index e8e5bc7..a11814d 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -55,7 +55,8 @@ const PROCESSUS = { pool: SERIES.pool, maxWorkers: SERIES.maxWorkers }; // composants dans son motif, et son graphe porte les commandes des captures // d'écran, qui balaient doc/captures ; la garde des lecteurs par le disque // (test/arborescence.test.js), textuelle, les compte parmi les lecteurs des -// composants. +// composants. test/documentation.test.js en est : elle lit ces motifs pour +// les fichiers que scripts/documentation.js lit par le disque. const RELANCES_D_UN_COMPOSANT = Object.freeze([ 'test/arborescence.test.js', 'test/sources_interface.test.js', @@ -69,6 +70,7 @@ const RELANCES_D_UN_COMPOSANT = Object.freeze([ 'test/chromium.test.js', 'test/machine.test.js', 'test/rapporteur.test.js', + 'test/documentation.test.js', ]); // Les épreuves node qui lisent la feuille des jetons par le système de @@ -174,6 +176,13 @@ export default defineConfig({ // captures, que scripts/documentation.js lit par le disque (§ 19.8). { pattern: /(^|\/)(README\.md|GUIDE-[^/]*\.md|doc\/captures\/manifeste\.json)$/, testsToRun: () => 'scripts/documentation.test.js' }, + // Le relevé du banc, que scripts/documentation.js lit par le disque + // pour la région « constantes » (§ 19.10) : les deux épreuves de la + // documentation, et celles qui l'importent, que le motif ne relancerait + // plus sinon. + { pattern: /(^|\/)scripts\/banc\/releve\.json$/, + testsToRun: () => ['test/documentation.test.js', 'scripts/documentation.test.js', 'src/geometrie/constantes.test.js', + 'scripts/banc/releve.test.js', 'scripts/banc/lancer.test.js'] }, ], projects: [ { test: {