diff --git a/scripts/essai_demarrage.js b/scripts/essai_demarrage.js index 9fbeadb..363f414 100644 --- a/scripts/essai_demarrage.js +++ b/scripts/essai_demarrage.js @@ -57,6 +57,7 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync import { tmpdir } from 'node:os'; import path, { join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { decrireSysteme } from '../electron/fichiers.js'; import { estSous } from '../src/stockage/dossier_travail.js'; import { VERSION } from '../src/version.genere.js'; import { titreAvecVersion } from '../src/interface/libelles.js'; @@ -114,21 +115,26 @@ export function environnementDEssai(env, { exe, home, temp }, chemins = path) { /** * Les données applicatives que la coquille lancée sous env retiendrait et * qui portent l'exécutable publié : vide quand le mode portable reste - * possible. La liste est celle du § 8.6 point 2, que decrireSysteme - * (electron/fichiers.js) compose : AppData, le LOCALAPPDATA de env, le - * temporaire ; estSous compare comme le système. AppData prend chaque - * valeur que app.getPath peut rendre : sous Windows, l'APPDATA de env et - * celle de poste, dossier connu qu'aucune variable ne déplace ; sous macOS, - * Library/Application Support sous l'un et l'autre HOME ; ailleurs, - * XDG_CONFIG_HOME ou .config sous HOME. Le temporaire est TMP, puis TEMP, - * sous Windows, TMPDIR, puis /tmp, ailleurs. L'ordre est celui de cette - * liste, sans doublon ; sans exécutable publié, rien ne le porte. + * possible. La liste est celle que compose decrireSysteme + * (electron/fichiers.js, § 8.6 point 2), appelée comme la coquille l'appelle, + * et estSous compare comme le système, selon la casse et le séparateur + * qu'elle décrit. L'essai ne sait en propre que ce que rend app.getPath : + * appData prend chaque valeur qu'il peut rendre — sous Windows, l'APPDATA de + * env et celle du poste, dossier connu qu'aucune variable ne déplace ; sous + * macOS, Library/Application Support sous l'un et l'autre HOME ; ailleurs, + * XDG_CONFIG_HOME ou .config sous HOME —, decrireSysteme étant appelée une + * fois pour chacune ; temp est TMP, puis TEMP, sous Windows, TMPDIR, puis + * /tmp, ailleurs. Un autre nom demandé à app.getPath lève : l'essai + * jugerait une liste à laquelle il manque un emplacement. Les dossiers se + * rendent dans l'ordre des appels, sans doublon ; sans exécutable publié, + * rien ne le porte. * @param {Record} env l'environnement de la coquille * @param {Record} poste l'environnement du poste * @param {string} plateforme process.platform + * @param {typeof decrireSysteme} [decrire] la description du poste par la coquille * @returns {string[]} */ -export function donneesApplicativesSurExe(env, poste, plateforme) { +export function donneesApplicativesSurExe(env, poste, plateforme, decrire = decrireSysteme) { const executable = env.PORTABLE_EXECUTABLE_DIR || null; if (executable === null) return []; const chemins = plateforme === 'win32' ? path.win32 : path.posix; @@ -140,10 +146,15 @@ export function donneesApplicativesSurExe(env, poste, plateforme) { ? [env.HOME, poste.HOME].map((maison) => sousMaison(maison, 'Library', 'Application Support')) : [env.XDG_CONFIG_HOME || sousMaison(env.HOME, '.config')]; const temp = plateforme === 'win32' ? env.TMP || env.TEMP : env.TMPDIR || '/tmp'; - const systeme = { insensibleCasse: plateforme === 'win32' || plateforme === 'darwin', separateur: chemins.sep }; - const portent = [...appData, env.LOCALAPPDATA, temp].filter( - (dossier) => typeof dossier === 'string' && dossier !== '' && estSous(executable, dossier, systeme), - ); + const portent = appData.flatMap((dossierAppData) => { + const connus = { appData: dossierAppData, temp, documents: '' }; + const cheminSysteme = (nom) => { + if (!Object.hasOwn(connus, nom)) throw new Error(`app.getPath('${nom}') : l'essai ne sait pas l'émuler`); + return connus[nom]; + }; + const { emplacements } = decrire({ env, plateforme, cheminSysteme, nomProduit: '' }); + return emplacements.donneesApplicatives.filter((dossier) => estSous(executable, dossier, emplacements)); + }); return [...new Set(portent)]; } diff --git a/scripts/essai_demarrage.test.js b/scripts/essai_demarrage.test.js index 72eccef..161d018 100644 --- a/scripts/essai_demarrage.test.js +++ b/scripts/essai_demarrage.test.js @@ -12,6 +12,7 @@ import { mkdtempSync, rmSync, symlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path, { dirname, join, relative, isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { decrireSysteme } from '../electron/fichiers.js'; import { estSous } from '../src/stockage/dossier_travail.js'; import { describe, test } from '../test/lanceur.js'; import { donneesApplicativesSurExe, dossiersDEssai, environnementDEssai, estLanceDirectement } from './essai_demarrage.js'; @@ -126,6 +127,29 @@ describe('essai de démarrage : dossiers et environnement (§ 8.6)', () => { // Sans exécutable publié, rien ne le porte. assert.deepEqual(donneesApplicativesSurExe({ TMPDIR: '/' }, {}, 'linux'), []); }); + + test('la liste des données applicatives est celle que decrireSysteme compose : un emplacement qu’elle ajoute est jugé, et un app.getPath que l’essai ne sait pas émuler lève', () => { + const P = path.posix; + const R = P.join('/', 'racine-essai', 'gtt-essai-demarrage-x'); + const dossiers = dossiersDEssai(R, P); + const env = environnementDEssai({ HOME: P.join('/', 'maison-originale') }, dossiers, P); + const autre = P.join('/', 'racine-essai'); + // Une coquille qui range aussi GTT_DONNEES parmi ses données applicatives. + const avecUnDossier = (poste) => { + const decrit = decrireSysteme(poste); + const donneesApplicatives = [...decrit.emplacements.donneesApplicatives, poste.env.GTT_DONNEES]; + return { ...decrit, emplacements: { ...decrit.emplacements, donneesApplicatives } }; + }; + assert.deepEqual(donneesApplicativesSurExe({ ...env, GTT_DONNEES: autre }, {}, 'linux', avecUnDossier), [autre]); + assert.deepEqual(donneesApplicativesSurExe({ ...env, GTT_DONNEES: P.join('/', 'ailleurs') }, {}, 'linux', avecUnDossier), []); + // Une coquille qui lit un autre dossier du système : l'essai le refuse + // plutôt que de juger une liste incomplète. + const avecUserData = (poste) => { + poste.cheminSysteme('userData'); + return decrireSysteme(poste); + }; + assert.throws(() => donneesApplicativesSurExe(env, {}, 'linux', avecUserData), /userData/); + }); }); describe('essai de démarrage : lancement direct', () => { diff --git a/spec.md b/spec.md index bd0c749..6bb76aa 100644 --- a/spec.md +++ b/spec.md @@ -341,7 +341,9 @@ aperçoive**. Moyennant cet ajout, le moteur n'a **qu'un seul chemin de code** : la réservation est un prétraitement qui réduit les capacités, retire des -participants et annote les tables — jamais un second algorithme. +participants et annote les tables — jamais un second algorithme. Une garde de +l'arborescence le tient : dans le moteur, seuls la normalisation de la +configuration et le diagnostic lisent les réservations. ### 5.3 Les contraintes, activables une à une @@ -393,9 +395,10 @@ une lecture et non un jugement : | **manque** | plafond réalisé − rencontres (§ 5.5) — la grandeur qui compare deux propositions (§ 12.10) | | **écart d'itinéraire** | plafond a priori − plafond réalisé (§ 5.5, § 12.10) | -**Les agrégats portent sur tous les participants placés.** Le minimum se -calcule sur l'ensemble, ancrés compris, et **deux lignes secondaires** donnent -la même mesure restreinte aux **mobiles** et aux **ancrés**. Un participant partiellement fixé compte parmi les mobiles. +**Les agrégats portent sur tous les participants placés.** Le minimum se calcule +sur l'ensemble, ancrés compris, et **deux lignes secondaires** donnent la même +mesure restreinte aux **mobiles** et aux **ancrés**. Un participant +partiellement fixé compte parmi les mobiles. > Un ancré n'est pas structurellement désavantagé : à table de 8 avec un seul > ancré et R = 4, son plafond vaut 28, exactement celui d'un mobile visitant @@ -680,8 +683,8 @@ dit, pour que l'opérateur sache lequel des deux il fait. **Le placement se prépare, il ne suit pas la salle.** Retirer une personne du placement retenu libère sa chaise, et personne d'autre ne change de siège ni de table : la chaise reste vide jusqu'à ce qu'un geste y assoie quelqu'un, et un -geste n'assied que les personnes qu'il nomme. Quand les sièges sont attribués, le -fichier écrit cette chaise vide à sa place (§ 8.9). +geste n'assied que les personnes qu'il nomme. Quand les sièges sont attribués, +le fichier écrit cette chaise vide à sa place (§ 8.9). **Il n'existe aucune commande qui répartit les animateurs sur les tables.** L'opérateur désigne lui-même quelle personne anime quelle table (§ 2.1, @@ -738,8 +741,9 @@ proposition, une réservation, un titre, des réservations à convertir —, qu'aucune commande ne fait revenir et sur laquelle l'interface n'offre pas le geste ; d'un compteur au bout de sa règle, d'un plan qui n'est plus bloqué, d'un geste sans effet, et d'un refus du CSV relayé par l'import, qui se lève dans le -fichier. Un remède posé pour la forme enverrait l'opérateur vers un geste qui ne -change rien. +fichier ou dans l'association de ses colonnes — `NOM_NON_ASSOCIE` quand aucune +colonne n'est associée au nom. Un remède posé pour la forme enverrait +l'opérateur vers un geste qui ne change rien. ### 5.10 Le coût d'évaluation @@ -1088,9 +1092,10 @@ diamètre = max(70, places × 60 / π) en centimètres Soit 115 cm pour 6 places, 153 pour 8, 191 pour 10. **Le plancher n'est pas cosmétique** : en deçà de quatre convives — la formule atteint 70 cm à 3,67 places — ce n'est plus le périmètre qui borne la table mais le plateau, et la -formule seule dessinerait des guéridons. Les deux -valeurs sont des **réglages par défaut, non des normes vérifiées** ; un diamètre -saisi à la main l'emporte. +formule seule dessinerait des guéridons. Les deux valeurs sont des **réglages +par défaut, non des normes vérifiées** ; un diamètre saisi à la main l'emporte, +dès que le fichier d'état peut le porter : le format 1 n'a pas de champ pour +lui, et toute table s'y dessine selon la formule. **Les étiquettes ne sont pas à l'échelle, le mobilier l'est.** Une liste de dix noms couvre plusieurs mètres carrés fictifs. **Le plan à l'échelle du bâtiment @@ -1103,12 +1108,11 @@ table sans qu'aucune marque couvre la liste. Sa largeur est fixée dans le dessin, et un nom trop long s'y abrège (§ 7.4). **Aucune liste ne passe sur les chaises d'une voisine posée sur la grille** : une table créée prend la première case libre d'une grille dont les colonnes s'écartent d'une emprise, de deux -écarts et d'un bloc de noms, à un pas entier au moins, sur l'un des deux axes, de -toute table déjà posée, et les démonstrations (§ 15) sont rangées sur la même -grille. Le prix est la place : au -zoom ajusté, une grande salle descend sous le seuil qui retire les listes -(§ 17, point 6), et un grand événement peut s'ouvrir sans listes de noms, qui se -dessinent quand l'opérateur rapproche la vue. +écarts et d'un bloc de noms, à un pas entier au moins, sur l'un des deux axes, +de toute table déjà posée, et les démonstrations (§ 15) sont rangées sur la même +grille. Le prix est la place : au zoom ajusté, une grande salle descend sous le +seuil qui retire les listes (§ 17, point 6), et un grand événement peut s'ouvrir +sans listes de noms, qui se dessinent quand l'opérateur rapproche la vue. **Tant qu'aucune dimension de salle n'est saisie, le plan est exact dans ses tables et arbitraire dans son cadre.** Le logiciel porte alors, sur le plan et @@ -2918,11 +2922,12 @@ service. | `web` | la plateforme web de Capacitor, servie en local | le développement et les tests sous Linux | | `android` | la plateforme Android de Capacitor | ouverte, non requise pour la première livraison | -**Capacitor ne porte pas l'exécutable Windows, et c'est délibéré.** Il ne fournit -de plateforme de bureau que par une extension communautaire, et une plateforme -dont la mise à jour ne dépend pas du projet fige le moteur d'exécution embarqué : -l'exécutable livré porterait un Chromium que plus personne ne corrige. La coquille -du projet tient en deux fichiers — le processus principal et le script de +**Capacitor ne porte pas l'exécutable Windows, et c'est délibéré.** Il ne +fournit de plateforme de bureau que par une extension communautaire, et une +plateforme dont la mise à jour ne dépend pas du projet fige le moteur +d'exécution embarqué : l'exécutable livré porterait un Chromium que plus +personne ne corrige. La coquille du projet tient en trois fichiers — le +processus principal, ses primitives de fichiers (`fichiers.js`) et le script de préchargement — et fixe elle-même sa version d'Electron, que la construction met à jour comme n'importe quelle dépendance. @@ -2957,11 +2962,13 @@ navigateur. Tout — le moteur, la géométrie, l'interface — est écrit en **JavaScript**. Aucune partie du calcul n'est dupliquée dans un second langage. -Cette propriété doit être **préservée délibérément**. Dès que la même -arithmétique existe en deux endroits, une suite de tests peut prouver qu'ils -s'accordent **sans jamais prouver qu'ils ont raison** : les deux dérivent -ensemble et les tests restent verts. Une seule implémentation rend ce mode de -défaillance impossible. +Cette propriété doit être **préservée délibérément**, et une garde de +l'arborescence la tient : sous `src/`, `electron/` et `scripts/`, rien d'autre +que du JavaScript, des composants Svelte, des feuilles de style, des données +JSON et les scripts d'installation nommés. Dès que la même arithmétique existe +en deux endroits, une suite de tests peut prouver qu'ils s'accordent **sans +jamais prouver qu'ils ont raison** : les deux dérivent ensemble et les tests +restent verts. Une seule implémentation rend ce mode de défaillance impossible. **Une exception, nommée, et une seule** : le calcul incrémental des indicateurs du § 5.10. Elle tient à une condition écrite — **le recalcul complet est la @@ -3028,9 +3035,9 @@ quelques secondes — et c'est la condition pour qu'il soit éprouvé sérieusem du rendu et placée plus bas que le moteur : elle n'importe ni le moteur, ni l'analyseur CSV, ni le générateur de démonstrations, ni l'application, ni l'interface, ni le stockage, et ne touche pas le navigateur. Elle est -**requise** : sa garde échoue quand son arbre ne fournit aucun module. Le dessin, -la désignation d'un siège et le dépôt partagent ses conversions et ses chaises -(§ 7.2, § 7.3). +**requise** : sa garde échoue quand son arbre ne fournit aucun module. Le +dessin, la désignation d'un siège et le dépôt partagent ses conversions et ses +chaises (§ 7.2, § 7.3). Les **deux frontières de plateforme** sont le système de fichiers et l'impression : une interface unique, deux implémentations — plus, pour le système @@ -3056,10 +3063,10 @@ ou d'une plateforme ; que `src/geometrie` importe en outre du moteur, de de Svelte ; et que `src/interface` importe autre chose que Svelte, ses propres modules, `src/application` et `src/geometrie` — elle atteint le CSV et le moteur par l'application, jamais en direct. Un type que la documentation d'un module -importe compte comme un import. Le projet `node` ne chargeant pas le greffon Svelte (§ 14.8), un test de -moteur qui importerait un composant échoue au chargement — mais cette garde-là -ne couvre que les composants, et un module qui touche `document` ne tomberait -qu'à l'exécution de la branche fautive. +importe compte comme un import. Le projet `node` ne chargeant pas le greffon +Svelte (§ 14.8), un test de moteur qui importerait un composant échoue au +chargement — mais cette garde-là ne couvre que les composants, et un module qui +touche `document` ne tomberait qu'à l'exécution de la branche fautive. ### 13.5 Ce que le SVG donne et que le canvas ferait payer @@ -3145,8 +3152,9 @@ suivante ; une règle qui fait échouer la construction tient. | **sa propre commande** | l'audit par mutation (§ 14.13) | avant une livraison, sous aucun budget | **La commande surveillée ne lance que le projet `node`.** Le lanceur en porte -deux (§ 14.8) ; lancés ensemble, ils démarrent un navigateur à chaque -modification, et le niveau navigateur reste à « avant chaque livraison ». +trois (§ 14.8) — `node`, `node-long` et `navigateur` ; lancés ensemble, ils +démarrent un navigateur et la série longue à chaque modification, et le niveau +navigateur reste à « avant chaque livraison ». **Le partage entre les deux séries `node` est écrit avant la première mesure**, et non après. Trois familles tombent naturellement dans la série surveillée sans @@ -3212,10 +3220,10 @@ l'**analyseur CSV**, qui reçoivent l'horloge et l'aléa en paramètre. Un test l'arborescence le refuse, **et échoue si son balayage ne trouve aucun fichier**. Son périmètre est nommé : `src/moteur`, `src/geometrie`, `src/demo`, `src/stockage` et `src/csv`, à l'exclusion du code d'épreuve, où un tirage sert -légitimement (§ 14.12). L'interdit ne porte pas sur -le reste de l'application : l'application lit l'horloge pour horodater les -entrées de l'historique, que le stockage écrit telles qu'il les reçoit, et le -banc de mesure du § 19.10 relève des temps par image. +légitimement (§ 14.12). L'interdit ne porte pas sur le reste de l'application : +l'application lit l'horloge pour horodater les entrées de l'historique, que le +stockage écrit telles qu'il les reçoit, et le banc de mesure du § 19.10 relève +des temps par image. **Ce n'est pas une règle d'hygiène.** L'interdit de `performance.now` dans le moteur est ce qui rend vraie la phrase « à graine et entrée égales, placement @@ -3230,22 +3238,25 @@ d'horloge à l'exécution (§ 18.5) : le moteur et le générateur ne la lisent ### 14.8 L'outillage des épreuves -**Un seul lanceur, deux projets : Vitest.** Le niveau `node` et le niveau -navigateur du § 14.4 sont deux configurations du même lanceur, pas deux outils. +**Un seul lanceur, trois projets : Vitest.** Les deux séries `node` et le niveau +navigateur du § 14.4 sont trois configurations du même lanceur, pas trois +outils. | projet | environnement | ce qu'il inclut | greffon Svelte | |---|---|---|---| | `node` | `node`, aucun DOM | `src/moteur`, `src/geometrie`, `src/stockage`, `src/csv`, `src/demo`, `src/pdf`, `src/application`, et les modules purs de `src/interface` | **absent** | +| `node-long` | `node`, aucun DOM | les épreuves lourdes du niveau `node:long` (§ 14.4), suffixe `.long.test.js`, hors de la série surveillée | **absent** | | `navigateur` | un vrai moteur de rendu, piloté par le mode navigateur de Vitest | les composants de `src/interface`, les épreuves de coordonnées et de style calculé | présent | -**Pourquoi Vitest.** Il lit la **configuration Vite qui construit le livrable** : un module importé par un test se résout exactement comme un module +**Pourquoi Vitest.** Il lit la **configuration Vite qui construit le +livrable** : un module importé par un test se résout exactement comme un module importé par l'application. Deux chaînes de résolution — l'une pour construire, l'autre pour éprouver — sont le mode de défaillance du § 13.2 transposé à -l'outillage : le test passe sur un fichier que l'application ne charge jamais, un -alias diverge, et rien ne le signale. Le lanceur observe en outre le **graphe de -dépendances**, donc une modification du moteur ne rejoue que les tests du moteur, -et son mode navigateur offre **la même API d'assertion** au niveau 2, de sorte -que l'oracle de géométrie du § 7.2 s'écrit une fois. +l'outillage : le test passe sur un fichier que l'application ne charge jamais, +un alias diverge, et rien ne le signale. Le lanceur observe en outre le **graphe +de dépendances**, donc une modification du moteur ne rejoue que les tests du +moteur, et son mode navigateur offre **la même API d'assertion** au niveau 2, de +sorte que l'oracle de géométrie du § 7.2 s'écrit une fois. **Les trois durées sont des cibles, mesurées la première semaine.** Le démarrage à froid, la relance sous surveillance et la série complète se mesurent sur un @@ -3300,12 +3311,15 @@ une donnée d'épreuve qui emprunte une racine d'organisation plausible le fait échouer. Un test vérifie cette disjonction, faute de quoi la garde du § 15.6 tombe au premier exemple écrit de bonne foi. -**La co-localisation se garde.** Un test de la construction affirme que le paquet -livré ne contient **aucun fichier de test ni aucun import du lanceur**. Le § 14.15 -pose que la suite n'est pas livrée ; avec des tests rangés dans `src/`, cette -propriété tient par la seule analyse statique de Vite, c'est-à-dire par -chance, jusqu'au jour où un module de production importe une -donnée d'épreuve « pour un cas par défaut ». +**La co-localisation se garde.** Un test de la construction affirme que le +paquet livré ne contient **aucun fichier de test ni aucun import du lanceur**. +Le § 14.15 pose que la suite n'est pas livrée ; avec des tests rangés dans +`src/`, cette propriété tient par la seule analyse statique de Vite, +c'est-à-dire par chance, jusqu'au jour où un module de production importe une +donnée d'épreuve « pour un cas par défaut ». Une garde de l'arborescence exige +aussi que chaque test de `src/` importe un module de son propre répertoire, hors +d'exceptions nommées avec leur raison, et qu'aucune donnée de `test/fixtures/` +n'égale, octet pour octet, un fichier livré. ### 14.10 La carte de ce qui s'éprouve @@ -3387,14 +3401,15 @@ sait pas fragmenter un SVG ; c'est du code qu'il faut éprouver, pas une règle style. **Au niveau navigateur**, la fonction pure est confrontée à -`getScreenCTM().inverse()` sur un rendu réel, à plusieurs `k` et après défilement -de la page, à un millième de pixel d'écran près (§ 7.2). **Les trois invariants du § 7.2 ont chacun leur test** : que le `` -racine ne porte pas d'attribut `viewBox` ; que son style calculé donne une bordure -et un remplissage intérieur nuls — `getBoundingClientRect` les inclut, donc -l'origine du contenu s'en décale ; qu'aucun ancêtre, ni lui-même, ne porte de -transformation CSS. Il refuse la feuille de style qui ajoute un jour -`transform: translateZ(0)` « pour la fluidité » : le plan répondrait faux partout, -et la cause serait cherchée dans le module de géométrie. +`getScreenCTM().inverse()` sur un rendu réel, à plusieurs `k` et après +défilement de la page, à un millième de pixel d'écran près (§ 7.2). **Les trois +invariants du § 7.2 ont chacun leur test** : que le `` racine ne porte pas +d'attribut `viewBox` ; que son style calculé donne une bordure et un remplissage +intérieur nuls — `getBoundingClientRect` les inclut, donc l'origine du contenu +s'en décale ; qu'aucun ancêtre, ni lui-même, ne porte de transformation CSS. Il +refuse la feuille de style qui ajoute un jour `transform: translateZ(0)` « pour +la fluidité » : le plan répondrait faux partout, et la cause serait cherchée +dans le module de géométrie. #### Le stockage @@ -3632,16 +3647,25 @@ produire une configuration à partir d'une graine. Une boucle sur une grille de graines et de tailles, appelant les deux vérificateurs, couvre l'essentiel sans dépendance nouvelle. -**Une bibliothèque de propriétés se justifie à deux endroits, et seulement deux** : -la formule du plafond et la détection du séparateur. Toutes deux prennent des -entrées petites et combinatoires, toutes deux bénéficient du **rétrécissement** — -quand une configuration à 120 participants échoue, l'outil rend un contre-exemple à -trois tables, là où la réduction manuelle coûte une heure. Leurs oracles ne sont -pas de même nature et le texte ne les confond pas : le plafond s'arbitre par -**énumération exhaustive** ; le séparateur s'arbitre par **construction** — un -tableau de champs engendré, rendu avec un séparateur connu, et la détection doit le -retrouver. Ailleurs, non : une propriété sur la recherche rejouerait la partie -lente et épuiserait le budget du § 14.14. +**Une bibliothèque de propriétés se justifie à deux endroits du calcul, et +seulement deux** : la formule du plafond et la détection du séparateur. Toutes +deux prennent des entrées petites et combinatoires, toutes deux bénéficient du +**rétrécissement** — quand une configuration à 120 participants échoue, l'outil +rend un contre-exemple à trois tables, là où la réduction manuelle coûte une +heure. Leurs oracles ne sont pas de même nature et le texte ne les confond pas : +le plafond s'arbitre par **énumération exhaustive** ; le séparateur s'arbitre +par **construction** — un tableau de champs engendré, rendu avec un séparateur +connu, et la détection doit le retrouver. Ailleurs, non : une propriété sur la +recherche rejouerait la partie lente et épuiserait le budget du § 14.14. + +**Le stockage l'emploie pour deux propriétés de plus, nommées.** L'aller-retour +des correctifs du journal — appliquer à une charge la différence qui mène à une +autre rend l'autre, sur des paires de charges tirées — et la restitution de +chaque instant d'une séance tirée, qui vit dans la série longue parce que ses +séances lancent des générations, à compte d'arrêt court. Leurs entrées sont des +suites d'opérations, où le **rétrécissement** sert comme au plafond : le +contre-exemple rendu tient en quelques opérations. La liste est fermée : une +garde de l'arborescence refuse la bibliothèque hors de ces quatre familles. **La bibliothèque tourne sur une graine fixée.** Une bibliothèque de propriétés retire par défaut une graine neuve à chaque exécution : le même code échoue une @@ -4298,10 +4322,12 @@ Tout le reste en dérive, par un script de construction : script puis compare — la même discipline qu'au § 15.5 pour les fichiers de démonstration (§ 13.1). -**Le module porte aussi la provenance de la construction** — livraison, séance de -développement, ou construction de documentation — posée par la commande qui -l'engendre, jamais lue dans l'horloge ni dans l'environnement. Sans elle, une -capture d'écran prise pendant le développement annonce un numéro déjà livré, et le +**Le module engendré ne porte que la version.** La **provenance de la +construction** — livraison, séance de développement, ou construction de +documentation — est le mode de construction que pose la commande qui construit, +celui de Vite, que l'application reçoit avec ses dépendances ; elle n'est jamais +lue dans l'horloge ni dans l'environnement du poste. Sans elle, une capture +d'écran prise pendant le développement annonce un numéro déjà livré, et le diagnostic porte sur un binaire que personne ne détient. ### 18.4 Le `CHANGELOG.md` @@ -4474,12 +4500,13 @@ fois lors du premier geste suivant un remplacement d'exécutable, ce qui coûte correctif et non du bruit à chaque geste. Il **n'est pas** la version de format, qui reste un nombre séparé, comparé selon ses propres règles. -**Pendant une construction de documentation, le numéro est remplacé par une marque -de provenance** — dans le bandeau, sur le premier écran, au pied des pages -imprimées et sur la feuille de contrôle des badges. La raison est au § 19.4 : sans cette marque, le texte relevé -de chaque capture d'écran change à chaque incrément de version, et toutes les -images se réécrivent. Le manifeste, lui, enregistre la **version réelle**, et le -contrôle de version de la livraison la compare (§ 19.7). +**Pendant une construction de documentation, le numéro est remplacé par une +marque de provenance** — dans le bandeau, sur le premier écran, au pied des +pages imprimées et sur la feuille de contrôle des badges. La raison est au +§ 19.4 : sans cette marque, le texte relevé de chaque capture d'écran change à +chaque incrément de version, et toutes les images se réécrivent. Le manifeste, +lui, enregistre la **version réelle**, et le contrôle de version de la livraison +la compare (§ 19.7). --- diff --git a/src/interface/transversal.navigateur.test.js b/src/interface/transversal.navigateur.test.js index 6a6e7eb..b2e7df8 100644 --- a/src/interface/transversal.navigateur.test.js +++ b/src/interface/transversal.navigateur.test.js @@ -1094,14 +1094,23 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => { }); test('contrastes : chaque texte tient 4,5:1 sur son fond peint, dans les deux thèmes posés par le réglage ; les couleurs relues diffèrent d’un thème à l’autre', () => { - expect(atteints().reduce((n, r) => n + r.clair.textes.mesures + r.sombre.textes.mesures, 0)).toBeGreaterThan(0); + // Un compte par thème : un relevé vide dans l'un passerait sous la somme + // des deux, et la comparaison des couleurs n'aurait rien à comparer. + for (const theme of THEMES) { + expect(atteints().reduce((n, r) => n + r[theme].textes.mesures, 0), `textes mesurés [${theme}]`).toBeGreaterThan(0); + } const fautes = []; + let comparees = 0; for (const r of atteints()) { for (const theme of THEMES) fautes.push(...r[theme].textes.fautes.map((f) => `${r.nom} [${theme}] — ${f}`)); for (const [cle, couleurs] of r.clair.textes.couleurs) { - if (r.sombre.textes.couleurs.get(cle) === couleurs) fautes.push(`${r.nom} — ${cle} : ${couleurs} dans les deux thèmes`); + const sombres = r.sombre.textes.couleurs.get(cle); + if (sombres === undefined) continue; + comparees += 1; + if (sombres === couleurs) fautes.push(`${r.nom} — ${cle} : ${couleurs} dans les deux thèmes`); } } + expect(comparees, 'couleurs relevées dans les deux thèmes').toBeGreaterThan(0); aucuneFaute([...new Set(fautes)], 'contrastes'); }); @@ -1124,8 +1133,9 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => { test('aucune atténuation : ni opacité partielle, ni filtre, ni fusion, ni masque sur un texte, une marque ou leur ascendance, dans les deux thèmes', () => { for (const theme of THEMES) { - const mesures = atteints().reduce((n, r) => n + r[theme].textes.mesures + r[theme].marques.mesures, 0); - expect(mesures, `textes et marques mesurés [${theme}]`).toBeGreaterThan(0); + for (const sorte of ['textes', 'marques']) { + expect(atteints().reduce((n, r) => n + r[theme][sorte].mesures, 0), `${sorte} mesurés [${theme}]`).toBeGreaterThan(0); + } } const fautes = []; for (const r of atteints()) { diff --git a/test/arborescence.test.js b/test/arborescence.test.js index 98f459d..50dff80 100644 --- a/test/arborescence.test.js +++ b/test/arborescence.test.js @@ -3,27 +3,32 @@ // Gardes de l'arborescence, lues dans le texte des sources : le moteur, la // géométrie du plan, le générateur de démonstrations, le stockage et -// l'analyseur CSV n'appellent aucune source non reproductible (§ 14.7), et ne parcourent les clés d'un -// objet que par exception nommée (§ 15.5, point 4) ; le graphe d'imports de -// chaque couche, l'interface et ses composants compris, ne rejoint que ce que -// le § 13.4 lui permet ; 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 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 retirent aucune relance -// (§ 14.14). 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 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 profondeur, les -// formes qu'elle refuse et des formes voisines qu'elle admet. +// l'analyseur CSV n'appellent aucune source non reproductible (§ 14.7), et ne +// parcourent les clés d'un objet que par exception nommée (§ 15.5, point 4) ; +// le graphe d'imports de chaque couche, l'interface et ses composants compris, +// ne rejoint que ce que le § 13.4 lui permet, et ni le moteur, ni la +// 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 +// 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 +// retirent aucune relance (§ 14.14) ; dans le moteur, deux modules nommés +// lisent les réservations (§ 5.2) ; src/, electron/ et scripts/ n'ont qu'une +// langue (§ 13.2) ; une épreuve de src/ vit à côté de son module, et aucune +// donnée d'épreuve ne double un fichier livré (§ 14.9) ; fast-check ne se +// 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 +// 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 +// profondeur, les formes qu'elle refuse et des formes voisines qu'elle admet. import assert from 'node:assert/strict'; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { isBuiltin } from 'node:module'; @@ -498,7 +503,7 @@ const PAQUETS_DE_PLATEFORME = /^(?:svelte|electron|@capacitor\/.+)$/; // chacun dans son seul texte. Aucun module du parcours n'importe un fichier // admis : il ne passe qu'en paramètre, de la main de l'application. // composants : vrai quand ses composants Svelte sont des modules de la -// couche, lus par leurs blocs de script. +// couche, lus par leurs blocs de script et les import() de leur gabarit. // // Le moteur et la géométrie ne connaissent ni l'écran, ni les fichiers, ni ce // qui les commande. La géométrie est une couche à part, plus bas que le @@ -513,19 +518,32 @@ const PAQUETS_DE_PLATEFORME = /^(?:svelte|electron|@capacitor\/.+)$/; // src/application/csv.js, et le moteur par la table d'évaluation, sans que // sa garde le lui reproche, l'application le faisant par construction ; // importés en direct, le stockage, le CSV, le moteur, les démonstrations et -// test/ sont refusés. +// test/ sont refusés. Le générateur de démonstrations lit le moteur et le +// stockage, qui sérialise ses fichiers, mais ni l'application, ni +// l'interface, ni le navigateur. Le moteur, la géométrie et le générateur ne +// lisent pas la version engendrée (§ 14.7, § 18.5), à aucune profondeur : +// une démonstration ou un placement qui en dépendrait changerait à chaque +// livraison. +const VERSION_ENGENDREE = 'src/version.genere.js'; const COUCHES = [ { arbres: ['src/moteur'], requis: ['src/moteur'], - exterieures: ['src/interface', 'src/application', 'src/stockage'], + exterieures: ['src/interface', 'src/application', 'src/stockage', VERSION_ENGENDREE], paquets: PAQUETS_DE_PLATEFORME, navigateur: [], }, { arbres: ['src/geometrie'], requis: ['src/geometrie'], - exterieures: ['src/interface', 'src/application', 'src/stockage', 'src/moteur', 'src/csv', 'src/demo'], + exterieures: ['src/interface', 'src/application', 'src/stockage', 'src/moteur', 'src/csv', 'src/demo', VERSION_ENGENDREE], + paquets: PAQUETS_DE_PLATEFORME, + navigateur: [], + }, + { + arbres: ['src/demo'], + requis: ['src/demo'], + exterieures: ['src/interface', 'src/application', VERSION_ENGENDREE], paquets: PAQUETS_DE_PLATEFORME, navigateur: [], }, @@ -700,7 +718,8 @@ function cibleDe(racine, fichier, specificateur) { const composantsDe = (dossier) => fichiersDe(dossier).filter((fichier) => extname(fichier) === '.svelte').sort(); // Le texte qu'un relevé de chargements lit dans un fichier : celui de ses -// blocs de script pour un composant Svelte, le texte entier sinon. +// blocs de script et des import() de son gabarit pour un composant Svelte +// (scriptsDe), le texte entier sinon. const texteDesChargements = (fichier, texte) => (extname(fichier) === '.svelte' ? scriptsDe(texte) : texte); // Relevé des refus de la frontière d'une couche dans son graphe d'imports. @@ -1191,11 +1210,67 @@ describe('arborescence : frontière des couches (§ 13.4)', () => { ]); }); - test('src/application peut manquer ; un src/moteur, un src/geometrie, un src/stockage, un src/csv ou un src/interface sans module fait échouer la garde ; un composant seul suffit à l’interface', () => { + test('ni le moteur, ni la géométrie, ni le générateur de démonstrations ne lisent la version engendrée (§ 14.7, § 18.5) ; l’interface et l’application la lisent ; le générateur n’importe ni l’interface, ni l’application, ni une plateforme, ni le navigateur', () => { + const fichiers = { + ...MODULES_VIDES, + 'src/version.genere.js': "export const VERSION = Object.freeze({ affichee: '', technique: '' });\n", + 'src/moteur/recherche.js': "import { VERSION } from '../version.genere.js';\n", + 'src/geometrie/sous/vue.js': "import { VERSION } from '../../version.genere.js';\n", + 'src/demo/catalogue.js': [ + "import { VERSION } from '../version.genere.js';", + "import { ErreurConfiguration } from '../moteur/erreurs.js';", + "import { serialiser } from '../stockage/canonique.js';", + "import { t } from '../application/traduction.js';", + "import { x } from '../interface/x.js';", + "import { mount } from 'svelte';", + "import { readFileSync } from 'node:fs';", + 'export const langue = navigator.language;', + ].join('\n'), + 'src/demo/livrees.js': "import { CATALOGUE } from './catalogue.js';\nimport petite from './livrees/petite.gtt.json' with { type: 'json' };\n", + 'src/moteur/erreurs.js': 'export class ErreurConfiguration extends Error {}\n', + 'src/stockage/canonique.js': "import { VERSION } from '../version.genere.js';\nexport const serialiser = () => VERSION;\n", + 'src/application/a.js': "import { VERSION } from '../version.genere.js';\n", + 'src/interface/App.svelte': "\n", + }; + assert.deepEqual(avecArbre(fichiers, releverFrontiere), [ + 'src/moteur/recherche.js:1 import ../version.genere.js', + 'src/geometrie/sous/vue.js:1 import ../../version.genere.js', + 'src/demo/catalogue.js:1 import ../version.genere.js', + 'src/demo/catalogue.js:4 import ../application/traduction.js', + 'src/demo/catalogue.js:5 import ../interface/x.js', + 'src/demo/catalogue.js:6 import svelte', + 'src/demo/catalogue.js:7 import node:fs', + 'src/demo/catalogue.js:8 navigator.', + 'src/demo/catalogue.js → src/stockage/canonique.js:1 import ../version.genere.js', + ]); + }); + + test('un import() du gabarit d’un composant se relève comme celui d’un bloc de script ; le mot « import » d’un nœud de texte non', () => { + const fichiers = { + ...MODULES_VIDES, + 'src/interface/APropos.svelte': [ + '', + "

import('../stockage/texte.js') reste du texte

", + "{#await import('../stockage/depot.js') then depot}{/await}", + '{#if apropos}', + " ", + '{/if}', + ].join('\n'), + 'src/interface/apropos.js': 'export const apropos = 1;\n', + }; + assert.deepEqual(avecArbre(fichiers, releverFrontiere), [ + 'src/interface/APropos.svelte:5 import ../stockage/depot.js', + 'src/interface/APropos.svelte:7 import ../moteur/recherche.js', + ]); + }); + + test('src/application peut manquer ; un src/moteur, un src/geometrie, un src/demo, un src/stockage, un src/csv ou un src/interface sans module fait échouer la garde ; un composant seul suffit à l’interface', () => { assert.deepEqual(avecArbre(MODULES_VIDES, releverFrontiere), []); const composantSeul = { ...sansArbre(MODULES_VIDES, 'src/interface'), 'src/interface/App.svelte': '

\n' }; assert.deepEqual(avecArbre(composantSeul, releverFrontiere), []); - for (const arbre of ['src/moteur', 'src/geometrie', 'src/stockage', 'src/csv', 'src/interface']) { + for (const arbre of ['src/moteur', 'src/geometrie', 'src/demo', 'src/stockage', 'src/csv', 'src/interface']) { const sansModule = { ...sansArbre(MODULES_VIDES, arbre), [`${arbre}/a.test.js`]: '' }; assert.throws( () => avecArbre(sansModule, releverFrontiere), @@ -2206,17 +2281,44 @@ function delaisEt(texte, code, motifs) { } // Relevé des épreuves du navigateur, chaque *.navigateur.test.js du -// projet : une assertion de classe ou un délai fixe, « fichier:ligne -// refus ». Lève quand le projet n'en porte aucune. +// projet, et des modules JavaScript de test/ qu'elles importent, à toute +// profondeur, par un chemin relatif ou absolu depuis la racine : les aides +// partagées de test/navigateur/ attendent et agissent pour elles. Un module +// de src/ ne se parcourt pas : c'est le code éprouvé, que ses propres gardes +// tiennent. Chaque assertion de classe ou délai fixe se lit « accès:ligne +// refus », l'accès étant la suite des fichiers qui y mène depuis une +// épreuve, comme dans releverEcritureDesEpreuves. Lève quand le projet n'en +// porte aucune. function releverEpreuvesNavigateur(racine) { const epreuves = fichiersDuProjet(racine).filter((f) => EPREUVE_NAVIGATEUR.test(f)); assert.ok(epreuves.length > 0, 'aucune épreuve du navigateur'); - return epreuves.flatMap((fichier) => { + const sousTest = join(racine, 'test') + sep; + const aParcourir = [...epreuves]; + const acces = new Map(epreuves.map((fichier) => [fichier, relative(racine, fichier)])); + const refus = []; + for (let i = 0; i < aParcourir.length; i += 1) { + const fichier = aParcourir[i]; const texte = readFileSync(fichier, 'utf8'); - return delaisEt(texte, sansChaines(texte), [...ASSERTIONS_DE_CLASSE, ...DELAIS]).map( - ({ ligne, motif }) => `${relative(racine, fichier)}:${ligne} ${motif}`, + const code = sansChaines(texte); + for (const { indice, specificateur } of chargementsLitteraux(texte)) { + const cible = cibleDe(racine, fichier, specificateur); + if ( + estDuCode(code, indice) && + cible !== null && + cible.startsWith(sousTest) && + !acces.has(cible) && + EXTENSIONS_MODULE.has(extname(cible)) && + existsSync(cible) + ) { + acces.set(cible, `${acces.get(fichier)} → ${relative(racine, cible)}`); + aParcourir.push(cible); + } + } + refus.push( + ...delaisEt(texte, code, [...ASSERTIONS_DE_CLASSE, ...DELAIS]).map(({ ligne, motif }) => `${acces.get(fichier)}:${ligne} ${motif}`), ); - }); + } + return refus; } describe('arborescence : écriture des épreuves (§ 14.8, § 14.2)', () => { @@ -2317,6 +2419,30 @@ describe('arborescence : écriture des épreuves (§ 14.8, § 14.2)', () => { ]); assert.throws(() => avecArbre({ 'src/a.test.js': '' }, releverEpreuvesNavigateur), /aucune épreuve du navigateur/); }); + + test('la garde des épreuves du navigateur suit leurs imports relatifs vers les modules de test/, à toute profondeur, et relève le refus avec son chemin d’accès ; un module de src/ ne se parcourt pas', () => { + const fichiers = { + 'src/interface/plan/Plan.navigateur.test.js': [ + "import { cliquer } from '../../../test/navigateur/aide.js';", + "import { horloge } from '../../application/horloge.js';", + "import { donnee } from '../../../test/fixtures/donnee.json' with { type: 'json' };", + ].join('\n'), + 'test/navigateur/aide.js': [ + "import { attendre } from './profond/attente.js';", + 'export const cliquer = async () => {', + ' await new Promise((r) => setTimeout(r, 300));', + '};', + ].join('\n'), + 'test/navigateur/profond/attente.js': "export const attendre = (e) => e.classList.contains('vu');\n", + 'src/application/horloge.js': 'export const horloge = () => setTimeout(() => {}, 10);\n', + 'test/fixtures/donnee.json': '{}\n', + 'test/navigateur/aide.navigateur.test.js': "import { cliquer } from './aide.js';\n", + }; + assert.deepEqual(avecArbre(fichiers, releverEpreuvesNavigateur), [ + 'src/interface/plan/Plan.navigateur.test.js → test/navigateur/aide.js:3 setTimeout', + 'src/interface/plan/Plan.navigateur.test.js → test/navigateur/aide.js → test/navigateur/profond/attente.js:1 classList.contains', + ]); + }); }); // Le projet de la série surveillée dans une configuration de Vitest. @@ -2742,3 +2868,416 @@ describe('arborescence : les motifs de surveillance (§ 14.14)', () => { assert.throws(() => releverSurveillance(RACINE, { watchTriggerPatterns: [], projects }), /aucun motif de surveillance/); }); }); + +// --- Un seul chemin, un seul langage, des épreuves à leur place ------------- + +// Le code d'un texte sans ses chaînes ni ses commentaires : sansChaines, +// puis chaque commentaire « // … » et « /* … */ » blanchi, fins de ligne +// gardées. Les chaînes déjà blanchies n'ouvrent plus aucun commentaire. +function codeSeul(texte) { + return sansChaines(texte).replace(/\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/g, (commentaire) => commentaire.replace(/[^\n]/g, ' ')); +} + +// Les seuls lecteurs des réservations dans le moteur (§ 5.2) : la +// réservation est un prétraitement qui réduit l'instance — capacités, +// mobiles, appartenances des ancrés —, jamais un second algorithme. +const LECTEURS_DES_RESERVATIONS = [ + { + fichier: 'src/moteur/configuration.js', + raison: 'normaliser fixe les réservations et réduit l’instance que la recherche résout', + }, + { + fichier: 'src/moteur/diagnostic.js', + raison: 'le diagnostic sonde la configuration sous une partie de ses réservations pour nommer la cause', + }, +]; + +// Une lecture des réservations : le nom reservations dans le code — +// propriété, déstructuration, paramètre —, ou une propriété nommée par une +// chaîne entre crochets. +const LECTURE_DES_RESERVATIONS = /\breservations\b/g; +const RESERVATIONS_ENTRE_CROCHETS = /\[\s*(['"`])reservations\1\s*\]/g; + +// Relevé des lectures des réservations dans les modules de src/moteur, à +// toute profondeur, épreuves exclues, hors des lecteurs admis : chacune +// « fichier:ligne reservations », ou « fichier:ligne ['reservations'] » pour +// la forme à crochets, par fichier puis par ligne ; puis « exception sans +// objet : fichier » pour un lecteur admis qui n'en lit aucune. Le nom se lit +// dans le code sans chaînes ni commentaires : un typedef JSDoc qui le nomme +// ne lit rien. Lève quand src/moteur ne fournit aucun module. +function releverLecteursDesReservations(racine, admis = LECTEURS_DES_RESERVATIONS) { + const modules = modulesDe(join(racine, 'src', 'moteur')); + assert.ok(modules.length > 0, 'src/moteur ne fournit aucun module'); + const lecteurs = new Set(); + const refus = []; + for (const fichier of modules) { + const nom = enBarres(racine, fichier); + const texte = readFileSync(fichier, 'utf8'); + const code = codeSeul(texte); + // Le crochet d'une lecture subsiste dans le code ; celui d'un + // commentaire est blanchi avec lui. + const trouves = [ + ...[...code.matchAll(LECTURE_DES_RESERVATIONS)].map(({ index }) => ({ indice: index, rang: 0, motif: 'reservations' })), + ...[...texte.matchAll(RESERVATIONS_ENTRE_CROCHETS)] + .filter(({ index }) => code[index] === '[') + .map(({ index }) => ({ indice: index, rang: 1, motif: "['reservations']" })), + ]; + if (trouves.length > 0) lecteurs.add(nom); + if (admis.some((e) => e.fichier === nom)) continue; + trouves.sort((a, b) => a.indice - b.indice || a.rang - b.rang); + refus.push(...trouves.map(({ indice, motif }) => `${nom}:${ligneDe(texte, indice)} ${motif}`)); + } + const sansObjet = admis.filter(({ fichier }) => !lecteurs.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`); + return [...refus, ...sansObjet]; +} + +// Les arbres du code livré et de son outillage, où ne vit qu'une langue +// (§ 13.2) : JavaScript, composants Svelte, feuilles de style, et des +// données JSON, qui ne calculent rien. +const ARBRES_D_UNE_LANGUE = ['src', 'electron', 'scripts']; +const EXTENSIONS_D_UNE_LANGUE = new Set(['.js', '.cjs', '.mjs', '.svelte', '.css', '.json']); + +// Les scripts d'installation et de construction, chacun à sa place : ils +// posent l'outillage ou le lancent, et ne refont aucun calcul du logiciel. +const SCRIPTS_NOMMES = [ + { fichier: 'scripts/construire_windows.sh', raison: 'construit l’exécutable Windows depuis Linux (§ 16)' }, + { fichier: 'scripts/verifier_systemes.sh', raison: 'éprouve l’installation sur chaque système, dans un conteneur' }, + { fichier: 'scripts/installation/avec_node.sh', raison: 'lance une commande avec le Node du projet' }, + { fichier: 'scripts/installation/commun.sh', raison: 'fonctions communes des scripts d’installation' }, + { fichier: 'scripts/installation/shell.nix', raison: 'environnement de développement sous NixOS' }, + { + fichier: 'scripts/oracle/pcg32_reference.sh', + raison: 'compile et lance la référence de PCG32, oracle pris hors du code éprouvé (§ 14.11)', + }, + { fichier: 'scripts/windows/avec_node.ps1', raison: 'lance une commande avec le Node du projet, sous Windows' }, + { fichier: 'scripts/windows/commun.ps1', raison: 'fonctions communes des scripts Windows' }, + { fichier: 'scripts/windows/installer.ps1', raison: 'installe le projet sous Windows' }, + { fichier: 'scripts/windows/lancer.ps1', raison: 'lance le projet sous Windows' }, +]; + +// Fichiers que le disque porte sans qu'ils soient du projet : un nom caché +// (.DS_Store, l'échange .x.js.swp de Vim), une sauvegarde d'éditeur (x.js~, +// #x.js#), un reste de fusion ou de correctif (x.js.orig, x.js.rej). Le relevé +// des langues est le seul à admettre une extension par sa liste plutôt qu'à +// choisir la sienne : sans ce filtre, il tombe sur un arbre conforme dès qu'un +// éditeur est ouvert, et la série surveillée le relance à chaque sauvegarde. +// La liste de git ne les écarte pas : ces noms ne sont pas ignorés par le +// dépôt, et --others les rend comme tout fichier nouveau. +const estArtefact = (nom) => nom.startsWith('.') || nom.endsWith('~') || /^#.*#$/.test(nom) || /\.(orig|rej)$/.test(nom); + +// Relevé des fichiers d'une autre langue sous src/, electron/ et scripts/, à +// toute profondeur, artefacts d'éditeur et de fusion écartés : chacun +// « chemin : extension », ou « chemin : sans extension », dans l'ordre des +// chemins, hors des extensions admises et des scripts nommés ; puis +// « exception sans objet : fichier » pour un script nommé absent. Lève quand +// ces arbres ne portent aucun fichier. +function releverLangages(racine, nommes = SCRIPTS_NOMMES) { + const fichiers = ARBRES_D_UNE_LANGUE.flatMap((arbre) => fichiersDe(join(racine, arbre))) + .filter((fichier) => !fichier.split(sep).includes('node_modules') && !estArtefact(basename(fichier))) + .map((fichier) => enBarres(racine, fichier)) + .sort(); + assert.ok(fichiers.length > 0, 'aucun fichier sous src/, electron/ et scripts/'); + const presents = new Set(fichiers); + const refus = fichiers + .filter((chemin) => !EXTENSIONS_D_UNE_LANGUE.has(extname(chemin)) && !nommes.some((e) => e.fichier === chemin)) + .map((chemin) => `${chemin} : ${extname(chemin) || 'sans extension'}`); + const sansObjet = nommes.filter(({ fichier }) => !presents.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`); + return [...refus, ...sansObjet]; +} + +// Les épreuves de src/ qui ne vivent pas à côté d'un module de leur +// répertoire, chacune avec sa raison. +const EPREUVES_SANS_VOISIN = [ + { + fichier: 'src/interface/accueil/Accueil.navigateur.test.js', + raison: 'l’accueil n’existe qu’au sein de l’application : l’épreuve monte App par test/navigateur/montage.js', + }, + { + fichier: 'src/interface/propositions/Propositions.navigateur.test.js', + raison: 'les propositions se lisent dans l’application montée, après génération : l’épreuve monte App', + }, + { + fichier: 'src/interface/plan/ecouteurs.test.js', + raison: 'garde des sources : elle lit les composants de son répertoire par le disque, sans les importer', + }, +]; + +// Relevé des épreuves de src/ éloignées de leur module (§ 14.9) : chaque +// *.test.js de src/, à toute profondeur, dont aucun chargement de code ne +// désigne un fichier existant de son propre répertoire qui ne soit pas une +// épreuve — module ou composant ; le suffixe de Vite se retire, et +// « ./travailleur.js?worker&inline » désigne le module voisin —, hors des +// exceptions nommées : chacune « épreuve : aucun module de répertoire », +// dans l'ordre des chemins ; puis « exception sans objet : fichier » pour +// une exception qui a désormais un voisin ou n'existe plus. Lève quand src/ +// ne porte aucune épreuve. +function releverEpreuvesEloignees(racine, admis = EPREUVES_SANS_VOISIN) { + const epreuves = fichiersDe(join(racine, 'src')) + .filter((fichier) => EPREUVE.test(fichier)) + .sort(); + assert.ok(epreuves.length > 0, 'aucune épreuve sous src/'); + const sansVoisin = epreuves.filter((fichier) => { + const texte = readFileSync(fichier, 'utf8'); + const code = codeSeul(texte); + return !chargementsLitteraux(texte).some(({ indice, specificateur }) => { + const cible = cibleDe(racine, fichier, specificateur.replace(/\?.*$/, '')); + return ( + estDuCode(code, indice) && + cible !== null && + dirname(cible) === dirname(fichier) && + !EPREUVE.test(cible) && + existsSync(cible) + ); + }); + }); + const noms = sansVoisin.map((fichier) => enBarres(racine, fichier)); + const refus = noms + .filter((nom) => !admis.some((e) => e.fichier === nom)) + .map((nom) => `${nom} : aucun module de ${dirname(nom)}`); + const sansObjet = admis.filter(({ fichier }) => !noms.includes(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`); + return [...refus, ...sansObjet]; +} + +// Les fichiers livrés qu'une donnée d'épreuve ne double pas (§ 14.9) : les +// exemples du § 10.3 et les démonstrations livrées. +const ARBRES_LIVRES = ['exemples', 'src/demo/livrees']; + +// Relevé des données de test/fixtures/, à toute profondeur, égales octet +// pour octet à un fichier livré : chacune « donnée = fichier livré », dans +// l'ordre des chemins. Lève quand test/fixtures/ ou les arbres livrés ne +// portent aucun fichier. +function releverFixturesLivrees(racine) { + const fixtures = fichiersDe(join(racine, 'test', 'fixtures')).sort(); + const livres = ARBRES_LIVRES.flatMap((arbre) => fichiersDe(join(racine, arbre))).sort(); + assert.ok(fixtures.length > 0, 'test/fixtures/ ne porte aucune donnée'); + assert.ok(livres.length > 0, 'aucun fichier livré sous exemples/ ni src/demo/livrees/'); + const octets = new Map(livres.map((fichier) => [fichier, readFileSync(fichier)])); + return fixtures.flatMap((fixture) => { + const contenu = readFileSync(fixture); + return livres + .filter((livre) => octets.get(livre).equals(contenu)) + .map((livre) => `${enBarres(racine, fixture)} = ${enBarres(racine, livre)}`); + }); +} + +// Les seules épreuves qui chargent fast-check (§ 14.12) : la formule du +// plafond et la détection du séparateur, qui gagnent au rétrécissement ; et +// deux propriétés du stockage, sur des paires de charges et des séances +// tirées, dont le contre-exemple rétréci tient en quelques opérations. +const CHARGEURS_DE_FAST_CHECK = [ + { fichier: 'src/moteur/plafond.test.js', raison: 'la formule du plafond, arbitrée par énumération exhaustive' }, + { fichier: 'src/moteur/plafond.long.test.js', raison: 'la grande grille du plafond, sous la même graine écrite' }, + { fichier: 'src/csv/lecture.test.js', raison: 'la détection du séparateur, arbitrée par construction' }, + { + fichier: 'src/stockage/correctifs.test.js', + raison: 'l’aller-retour appliquer(a, difference(a, b)) sur des paires de charges tirées', + }, + { + fichier: 'src/stockage/journal.long.test.js', + raison: 'chaque instant d’une séance tirée se restitue tel qu’il s’est écrit', + }, +]; + +// Relevé des chargements de fast-check, sous-chemins compris, dans les +// fichiers JavaScript et les composants du projet, hors des épreuves +// nommées : chacun « fichier:ligne spécificateur », par fichier puis dans +// l'ordre du texte ; puis « exception sans objet : fichier » pour une +// épreuve nommée qui ne le charge pas. Un chargement écrit dans une chaîne, +// donnée d'épreuve, n'en est pas un. Lève quand le projet ne porte aucun +// module. +function releverFastCheck(racine, nommes = CHARGEURS_DE_FAST_CHECK) { + const fichiers = fichiersDuProjet(racine).filter( + (fichier) => EXTENSIONS_MODULE.has(extname(fichier)) || extname(fichier) === '.svelte', + ); + assert.ok(fichiers.length > 0, 'aucun module dans le projet'); + const chargeurs = new Set(); + const refus = []; + for (const fichier of fichiers) { + const nom = enBarres(racine, fichier); + const texte = readFileSync(fichier, 'utf8'); + const code = codeSansChaines(fichier, texte); + const trouves = chargementsLitteraux(texte).filter( + ({ indice, specificateur }) => estDuCode(code, indice) && nomDePaquet(specificateur) === 'fast-check', + ); + if (trouves.length > 0) chargeurs.add(nom); + if (nommes.some((e) => e.fichier === nom)) continue; + refus.push(...trouves.map(({ indice, specificateur }) => `${nom}:${ligneDe(texte, indice)} ${specificateur}`)); + } + const sansObjet = nommes.filter(({ fichier }) => !chargeurs.has(fichier)).map(({ fichier }) => `exception sans objet : ${fichier}`); + return [...refus, ...sansObjet]; +} + +describe('arborescence : un seul chemin de code pour les réservations (§ 5.2)', () => { + test('dans src/moteur, seuls configuration.js et diagnostic.js lisent les réservations', () => { + assert.deepEqual(releverLecteursDesReservations(RACINE), []); + }); + + test('la garde relève chaque lecture par sa ligne — propriété, déstructuration, crochet —, hors commentaires, chaînes et épreuves ; un lecteur admis qui ne lit plus est sans objet', () => { + const fichiers = { + 'src/moteur/configuration.js': 'export const normaliser = ({ reservations }) => reservations.length;\n', + 'src/moteur/diagnostic.js': 'export const sonder = (c) => c.reservations;\n', + 'src/moteur/recherche.js': [ + '// les reservations sont fixées avant la recherche', + 'export const rechercher = (configuration) => {', + ' const { tables, reservations } = configuration;', + " const motif = 'reservations';", + ' return configuration?.reservations ?? configuration["reservations"];', + '};', + ].join('\n'), + 'src/moteur/types.js': '/** @property {Reservation[]} reservations */\nexport const fixerReservations = 1;\n', + 'src/moteur/sous/profond.js': 'export const lire = (c) => c.reservations;\n', + 'src/moteur/recherche.test.js': 'const r = configuration.reservations;\n', + }; + assert.deepEqual(avecArbre(fichiers, releverLecteursDesReservations), [ + 'src/moteur/recherche.js:3 reservations', + 'src/moteur/recherche.js:5 reservations', + "src/moteur/recherche.js:5 ['reservations']", + 'src/moteur/sous/profond.js:1 reservations', + ]); + const sansLecture = { ...fichiers, 'src/moteur/diagnostic.js': 'export const sonder = (c) => c.tables;\n' }; + assert.deepEqual(avecArbre(sansLecture, releverLecteursDesReservations).at(-1), 'exception sans objet : src/moteur/diagnostic.js'); + assert.throws(() => avecArbre({ 'src/moteur/a.test.js': '' }, releverLecteursDesReservations), /src\/moteur ne fournit aucun module/); + }); +}); + +describe('arborescence : une seule langue (§ 13.2)', () => { + test('sous src/, electron/ et scripts/, rien que du JavaScript, des composants Svelte, des feuilles de style, des données JSON et les scripts d’installation nommés', () => { + assert.deepEqual(releverLangages(RACINE), []); + }); + + test('la garde relève un second langage, à toute profondeur, et un fichier sans extension ; un script nommé hors de sa place l’est aussi ; un script nommé absent est sans objet', () => { + const fichiers = { + 'src/moteur/a.js': '', + 'src/moteur/b.mjs': '', + 'electron/c.cjs': '', + 'src/interface/D.svelte': '', + 'src/interface/jetons.css': '', + 'src/demo/livrees/e.gtt.json': '{}', + 'src/moteur/calcul.py': '', + 'src/geometrie/vue.ts': '', + 'src/moteur/sous/recherche.wasm': '', + 'electron/Makefile': '', + 'scripts/outil.rb': '', + 'scripts/construire_windows.sh': '', + 'src/installation/avec_node.sh': '', + 'test/aide.py': '', + 'node_modules/p/x.py': '', + }; + const releves = avecArbre(fichiers, releverLangages); + assert.deepEqual( + releves.filter((r) => !r.startsWith('exception sans objet')), + [ + 'electron/Makefile : sans extension', + 'scripts/outil.rb : .rb', + 'src/geometrie/vue.ts : .ts', + 'src/installation/avec_node.sh : .sh', + 'src/moteur/calcul.py : .py', + 'src/moteur/sous/recherche.wasm : .wasm', + ], + ); + assert.ok(releves.includes('exception sans objet : scripts/installation/avec_node.sh')); + assert.ok(!releves.includes('exception sans objet : scripts/construire_windows.sh')); + assert.throws(() => avecArbre({ 'test/a.js': '' }, releverLangages), /aucun fichier sous src\/, electron\/ et scripts\//); + }); + + test('les fichiers cachés et les restes d\u{2019}éditeur ou de fusion ne sont pas du projet : la garde les passe, et relève encore ce qui les côtoie', () => { + const fichiers = { + 'src/moteur/recherche.js': '', + 'src/moteur/.recherche.js.swp': '', + 'src/interface/.DS_Store': '', + 'src/moteur/recherche.js~': '', + 'src/moteur/recherche.js.orig': '', + 'electron/principal.cjs.rej': '', + 'scripts/#notes.txt#': '', + 'src/moteur/calcul.py': '', + 'src/moteur/orig': '', + 'src/moteur/calcul.py.bak': '', + }; + const releves = avecArbre(fichiers, releverLangages).filter((r) => !r.startsWith('exception sans objet')); + assert.deepEqual(releves, ['src/moteur/calcul.py : .py', 'src/moteur/calcul.py.bak : .bak', 'src/moteur/orig : sans extension']); + assert.throws( + () => avecArbre({ 'src/moteur/.a.js.swp': '', 'electron/a.cjs~': '' }, releverLangages), + /aucun fichier sous src\/, electron\/ et scripts\//, + ); + }); +}); + +describe('arborescence : où vivent les épreuves (§ 14.9, § 14.12)', () => { + test('chaque épreuve de src/ importe un module de son propre répertoire, hors des exceptions nommées', () => { + assert.deepEqual(releverEpreuvesEloignees(RACINE), []); + }); + + test('la garde relève une épreuve qui n’importe que d’ailleurs — d’un sous-répertoire, d’un parent, une autre épreuve voisine —, à toute profondeur ; un composant voisin compte ; une exception sans objet se relève', () => { + const fichiers = { + 'src/moteur/plafond.js': '', + 'src/moteur/plafond.test.js': "import { plafond } from './plafond.js';\n", + 'src/moteur/loin.test.js': "import { x } from '../csv/lecture.js';\nimport { y } from './sous/y.js';\n", + 'src/moteur/sous/y.js': '', + 'src/moteur/sous/y.long.test.js': "import { x } from '../plafond.js';\n", + 'src/moteur/voisine.test.js': "import './plafond.test.js';\n// import { plafond } from './plafond.js';\n", + 'src/csv/lecture.js': '', + 'src/interface/Plan.svelte': '', + 'src/interface/Plan.navigateur.test.js': "import Plan from './Plan.svelte';\n", + 'src/interface/travailleur.js': '', + 'src/interface/Travail.navigateur.test.js': "import Travailleur from './travailleur.js?worker&inline';\n", + 'src/interface/Ecran.navigateur.test.js': "import { monter } from '../../test/montage.js';\n", + 'test/a.test.js': "import { x } from '../src/csv/lecture.js';\n", + }; + const exceptions = [ + { fichier: 'src/interface/Ecran.navigateur.test.js', raison: 'monte l’application entière' }, + { fichier: 'src/interface/Absente.navigateur.test.js', raison: 'disparue' }, + ]; + assert.deepEqual(avecArbre(fichiers, (racine) => releverEpreuvesEloignees(racine, exceptions)), [ + 'src/moteur/loin.test.js : aucun module de src/moteur', + 'src/moteur/sous/y.long.test.js : aucun module de src/moteur/sous', + 'src/moteur/voisine.test.js : aucun module de src/moteur', + 'exception sans objet : src/interface/Absente.navigateur.test.js', + ]); + assert.throws(() => avecArbre({ 'src/a.js': '' }, releverEpreuvesEloignees), /aucune épreuve sous src\//); + }); + + test('aucune donnée de test/fixtures/ n’est, octet pour octet, un fichier d’exemples/ ou de src/demo/livrees/', () => { + assert.deepEqual(releverFixturesLivrees(RACINE), []); + }); + + test('la garde relève chaque donnée d’épreuve égale à un fichier livré, à toute profondeur, et rien pour un contenu voisin', () => { + const fichiers = { + 'exemples/participants.csv': 'nom\nIris Ombrelle\n', + 'src/demo/livrees/petite.gtt.json': '{"format":1}\n', + 'test/fixtures/csv/copie.csv': 'nom\nIris Ombrelle\n', + 'test/fixtures/plan/sous/petite.gtt.json': '{"format":1}\n', + 'test/fixtures/csv/voisine.csv': 'nom\nIris Ombrelle\r\n', + }; + assert.deepEqual(avecArbre(fichiers, releverFixturesLivrees), [ + 'test/fixtures/csv/copie.csv = exemples/participants.csv', + 'test/fixtures/plan/sous/petite.gtt.json = src/demo/livrees/petite.gtt.json', + ]); + assert.throws(() => avecArbre({ 'exemples/a.csv': 'x' }, releverFixturesLivrees), /test\/fixtures\/ ne porte aucune donnée/); + assert.throws(() => avecArbre({ 'test/fixtures/a.csv': 'x' }, releverFixturesLivrees), /aucun fichier livré/); + }); + + test('fast-check ne se charge que dans les épreuves que le § 14.12 nomme', () => { + assert.deepEqual(releverFastCheck(RACINE), []); + }); + + test('la garde relève chaque chargement de fast-check hors de la liste, à toute profondeur, et non dans une chaîne ; un fichier nommé qui ne le charge plus est sans objet', () => { + const fichiers = { + 'src/moteur/plafond.test.js': "import fc from 'fast-check';\n", + 'src/moteur/recherche.test.js': "import fc from 'fast-check';\nconst arb = require('fast-check/lib/arbitrary');\n", + 'src/stockage/sous/depot.long.test.js': 'const fc = await import(`fast-check`);\n', + 'test/aide.js': "const texte = \"import fc from 'fast-check';\";\n", + 'node_modules/p/x.js': "import fc from 'fast-check';\n", + }; + const nommes = [ + { fichier: 'src/moteur/plafond.test.js', raison: 'la formule du plafond' }, + { fichier: 'src/csv/lecture.test.js', raison: 'la détection du séparateur' }, + ]; + assert.deepEqual(avecArbre(fichiers, (racine) => releverFastCheck(racine, nommes)), [ + 'src/moteur/recherche.test.js:1 fast-check', + 'src/moteur/recherche.test.js:2 fast-check/lib/arbitrary', + 'src/stockage/sous/depot.long.test.js:1 fast-check', + 'exception sans objet : src/csv/lecture.test.js', + ]); + assert.throws(() => avecArbre({ 'README.md': '' }, releverFastCheck), /aucun module dans le projet/); + }); +}); diff --git a/test/chargements.js b/test/chargements.js index fe2de15..2eed66f 100644 --- a/test/chargements.js +++ b/test/chargements.js @@ -5,8 +5,9 @@ // l'arborescence (test/arborescence.test.js) et du livrable // (test/livrable.test.js) : import et export statiques, import seul, import() // et require(). Un composant Svelte se lit par le texte de ses blocs de -// script, que rend scriptsDe. -// +// script et des import() de son gabarit, que rend scriptsDe. +import { parse } from 'svelte/compiler'; + // Le relevé lit le texte entier, commentaires et chaînes compris. Une // instruction à « from » se lit à partir de ce mot, que seuls import et // export font suivre d'une chaîne : un motif parti du mot-clé s'arrêterait à @@ -58,19 +59,51 @@ export function blocsScript(texte) { }); } -// Le texte des blocs de script d'un composant Svelte, à la longueur du -// composant : chaque caractère hors de ces blocs devient une espace, fins de -// ligne gardées, si bien que les indices et les numéros de ligne restent -// ceux du composant. Le balisage, le style et les expressions du gabarit ne -// sont pas lus : un composant ne charge un module que dans un bloc de script, -// et le texte d'un nœud — « import » dans une phrase — n'en charge aucun. +// Les import() du gabarit d'un composant Svelte, en étendues { debut, fin } +// dans l'ordre du texte : chaque nœud ImportExpression de l'arbre que rend +// l'analyseur de Svelte (svelte/compiler) hors des blocs de script et de la +// feuille de style — un bloc {#await import(…)}, un attribut, une +// expression à toute profondeur. Le texte d'un nœud n'est pas une +// expression : « import » dans une phrase n'en donne aucun. null quand +// l'analyseur refuse le composant. +function importsDuGabarit(texte) { + let arbre; + try { + arbre = parse(texte, { modern: true }); + } catch { + return null; + } + const etendues = []; + const vus = new WeakSet(); + const parcourir = (noeud) => { + if (noeud === null || typeof noeud !== 'object' || vus.has(noeud)) return; + vus.add(noeud); + if (noeud.type === 'ImportExpression') etendues.push({ debut: noeud.start, fin: noeud.end }); + for (const valeur of Object.values(noeud)) parcourir(valeur); + }; + parcourir(arbre.fragment); + return etendues.sort((a, b) => a.debut - b.debut); +} + +// Le texte qu'un relevé de chargements lit dans un composant Svelte, à la +// longueur du composant : ses blocs de script et les import() de son +// gabarit gardés, tout autre caractère devenu une espace, fins de ligne +// gardées, si bien que les indices et les numéros de ligne restent ceux du +// composant. Le balisage, le style et les autres expressions du gabarit ne +// sont pas lus : le texte d'un nœud — « import » dans une phrase — ne charge +// aucun module. Un composant que l'analyseur de Svelte refuse se lit en +// entier, gabarit compris : le relevé y trouve plus, jamais moins, et la +// construction le refuse de toute façon. export function scriptsDe(texte) { + const gabarit = importsDuGabarit(texte); + if (gabarit === null) return texte; const blanc = (morceau) => morceau.replace(/[^\n]/g, ' '); + const gardes = [...blocsScript(texte), ...gabarit].sort((a, b) => a.debut - b.debut); let sortie = ''; let fin = 0; - for (const bloc of blocsScript(texte)) { - sortie += blanc(texte.slice(fin, bloc.debut)) + texte.slice(bloc.debut, bloc.fin); - fin = bloc.fin; + for (const etendue of gardes) { + sortie += blanc(texte.slice(fin, etendue.debut)) + texte.slice(etendue.debut, etendue.fin); + fin = etendue.fin; } return sortie + blanc(texte.slice(fin)); } diff --git a/test/chargements.test.js b/test/chargements.test.js index 0bb6a32..24bb539 100644 --- a/test/chargements.test.js +++ b/test/chargements.test.js @@ -93,8 +93,8 @@ describe('chargements', () => { describe('chargements : les blocs de script d’un composant Svelte', () => { // Un composant à deux blocs, dont l'un sous attribut, du balisage qui écrit - // « import » en texte et en expression, un style à @import, et une balise - // de script en commentaire HTML. + // « import » en texte, un import() en expression du gabarit, un style à + // @import, et une balise de script en commentaire HTML. const COMPOSANT = [ '', + "{#await import('../stockage/depot.js') then depot}{/await}", + '{#if ouvert}', + " ", + ' {#each [1] as n}', + "

{n}

", + ' {/each}', + '{/if}', + "

import('./texte.js')

", + ].join('\n'); + const scripts = scriptsDe(texte); + assert.equal(scripts.length, texte.length); + assert.deepEqual(lus(scripts), ['4 ../stockage/depot.js', '6 ./panneau.js']); + }); + + test('un composant que l’analyseur de Svelte refuse se lit en entier, gabarit compris : la garde relève plus, jamais moins', () => { + const texte = "

{import('./a.js')}

\n
\n