gestion_table_tournante_libre/scripts/documentation.test.js
Mathieu Benoit 8371e8f1ec [ADD] docs: generated regions in README and guides, checked on build
Tables in the documentation must come from the code, not be copied by hand.
scripts/documentation.js fills marked regions of README.md and the guides
from the single sources: settings, shortcuts, CSV columns, constants and
measured contrasts. A check mode reports any region that differs, and a
missing manifest blocks a delivery build. Editing a carrier document reruns
the documentation tests.
Checked: red first; 3454 node, 655 browser and 76 long tests green from the
index alone.

--- FR ---

[ADD] docs : régions engendrées du README et des guides, vérifiées

Les tableaux de la documentation doivent venir du code, non d'une copie à la
main. scripts/documentation.js remplit les régions marquées de README.md et
des guides à partir des sources uniques : réglages, raccourcis, colonnes CSV,
constantes et contrastes relevés. Un mode de vérification signale toute
région qui diffère, et un manifeste absent bloque une construction de
livraison. Modifier un document porteur relance les épreuves de la
documentation.
Vérifié : rouge d'abord ; 3454 node, 655 navigateur et 76 longues vertes
depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-09 13:06:01 -04:00

618 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves des régions engendrées de la documentation (§ 19.8, § 19.7) : la
// lecture des marqueurs et ses refus, le remplacement qui ne touche rien hors
// des régions, la vérification qui nomme la région corrigée à la main, les
// sept producteurs lus sur leur source unique, l'arbre des fichiers porteurs
// et l'écart de version, non bloquant au cycle court et bloquant à la
// livraison. L'arbre de test/fixtures/documentation/ porte un document aux
// régions conformes et un manifeste inventé, sans version réelle : chaque
// épreuve qui en a besoin l'écrit dans une copie temporaire.
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from '../test/lanceur.js';
import { versionVoisine } from '../test/version_voisine.js';
import { creerFormat } from '../src/application/format.js';
import { COMMANDES } from '../src/application/commandes.js';
import { TABLES_APPLICATION, libelle } from '../src/application/libelles.js';
import { creerTraduction } from '../src/application/traduction.js';
import { ENTETES_DES_CHAMPS } from '../src/csv/colonnes.js';
import { LIVREES } from '../src/demo/livrees.js';
import { CATALOGUE } from '../src/demo/catalogue.js';
import { CONSTANTES } from '../src/geometrie/constantes.js';
import { REGLAGES_GENERATION } from '../src/interface/generation/reglages.js';
import { TABLES_INTERFACE } from '../src/interface/libelles.js';
import { RACCOURCIS } from '../src/interface/raccourcis.js';
import { diagnostiquer } from '../src/moteur/diagnostic.js';
import { analyser, configurationDepuisCharge } from '../src/stockage/document.js';
import { VERSION } from '../src/version.genere.js';
import {
ErreurRegions,
EXEMPLES_COMMANDES,
PORTEURS,
PRODUCTEURS,
engendrerArbre,
executer,
lireRegions,
producteursDe,
remplacerRegions,
tableau,
tableauColonnesCsv,
tableauCommandes,
tableauContrastes,
tableauDemonstrations,
tableauRaccourcis,
tableauReglages,
tableauRegistre,
verifierArbre,
verifierRegions,
} from './documentation.js';
const FIXTURE = fileURLToPath(new URL('../test/fixtures/documentation/', import.meta.url));
const SCRIPT = fileURLToPath(new URL('./documentation.js', import.meta.url));
const MANIFESTE = 'doc/captures/manifeste.json';
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
const { nombre } = creerFormat(t);
// Producteurs inventés, comptés : chaque appel s'inscrit dans appels.
function producteursComptes(textes) {
const appels = [];
const producteurs = new Map(
Object.entries(textes).map(([nom, texte]) => [
nom,
() => {
appels.push(nom);
return texte;
},
]),
);
return { producteurs, appels };
}
const debut = (nom) => `<!-- engendre:debut ${nom} -->`;
const fin = (nom) => `<!-- engendre:fin ${nom} -->`;
const region = (nom, contenu) => `${debut(nom)}${contenu}${fin(nom)}`;
// Les lignes d'un tableau Markdown, sans en-tête ni séparateur : chacune la
// liste de ses cellules, une barre précédée d'une barre oblique inverse
// restant dans sa cellule.
function lignesDuTableau(texte) {
return texte
.split('\n')
.slice(2)
.map((ligne) =>
ligne
.slice(1, -1)
.split(/(?<!\\)\|/)
.map((cellule) => cellule.trim()),
);
}
// Le texte privé du contenu de ses régions : ce qui reste hors des marqueurs,
// marqueurs compris.
function horsRegions(texte) {
let reste = '';
let curseur = 0;
for (const { debut: d, fin: f } of lireRegions(texte)) {
reste += texte.slice(curseur, d);
curseur = f;
}
return reste + texte.slice(curseur);
}
// Une copie temporaire de l'arbre de fixture, dont chaque capture porte la
// version donnée (aucune version n'est écrite en clair, § 18.5, point 6) ;
// rendue avec la fonction qui l'efface.
function copieDeLArbre(version = VERSION.affichee) {
const racine = mkdtempSync(join(tmpdir(), 'gtt-documentation-'));
cpSync(FIXTURE, racine, { recursive: true });
const chemin = join(racine, MANIFESTE);
const manifeste = JSON.parse(readFileSync(chemin, 'utf8'));
for (const capture of manifeste.captures) capture.versionReelle = version;
writeFileSync(chemin, `${JSON.stringify(manifeste, null, 2)}\n`);
return { racine, effacer: () => rmSync(racine, { recursive: true, force: true }) };
}
function manifesteFixture() {
return JSON.parse(readFileSync(join(FIXTURE, MANIFESTE), 'utf8'));
}
// Une sortie qui retient ses lignes.
function sortieRetenue() {
const lignes = [];
return { lignes, ecrire: (ligne) => lignes.push(ligne) };
}
describe('documentation : lire les régions (§ 19.8)', () => {
test('deux régions, dans leur ordre, chacune son nom, ses bornes et son contenu', () => {
const texte = `Avant\n${region('reglages', '\nA\n')}\nEntre\n${region('raccourcis', '')}\nAprès\n`;
const regions = lireRegions(texte);
assert.deepEqual(
regions.map(({ nom, contenu }) => ({ nom, contenu })),
[
{ nom: 'reglages', contenu: '\nA\n' },
{ nom: 'raccourcis', contenu: '' },
],
);
for (const { debut: d, fin: f, contenu } of regions) assert.equal(texte.slice(d, f), contenu);
assert.deepEqual(lireRegions('Aucune région ici.\n<!-- un commentaire ordinaire -->\n'), []);
});
test('un marqueur sans fin : échec qui nomme la région', () => {
assert.throws(
() => lireRegions(`${debut('reglages')}\ncontenu\n`),
(erreur) => {
assert.ok(erreur instanceof ErreurRegions);
assert.deepEqual(erreur.fautes.map((f) => f.region), ['reglages']);
assert.match(erreur.fautes[0].faute, /sans fin/);
return true;
},
);
});
test('une fin sans début, une région ouverte dans une autre, une fin qui en ferme une autre : échecs nommés', () => {
const cas = [
[`texte\n${fin('commandes')}\n`, 'commandes', /sans début/],
[`${debut('reglages')}\n${region('commandes', '')}\n${fin('reglages')}`, 'commandes', /ouverte dans la région « reglages »/],
[`${debut('reglages')}\n${fin('commandes')}\n`, 'commandes', /ferme la région « reglages »/],
];
for (const [texte, nom, motif] of cas) {
assert.throws(
() => lireRegions(texte),
(erreur) => {
assert.ok(erreur instanceof ErreurRegions, texte);
const faute = erreur.fautes.find((f) => f.region === nom);
assert.ok(faute !== undefined, `${texte} : ${JSON.stringify(erreur.fautes)}`);
assert.match(faute.faute, motif);
return true;
},
);
}
});
test('une région en double : échec qui la nomme', () => {
assert.throws(
() => lireRegions(`${region('raccourcis', '\n')}\n${region('raccourcis', '\n')}\n`),
(erreur) => {
assert.ok(erreur instanceof ErreurRegions);
assert.deepEqual(erreur.fautes.map((f) => [f.region, /en double/.test(f.faute)]), [['raccourcis', true]]);
return true;
},
);
});
test('un marqueur illisible — sans nom, nom en capitales, mot inconnu, commentaire jamais fermé — est un échec, jamais un texte ordinaire', () => {
for (const marqueur of [
'<!-- engendre:debut -->',
'<!-- engendre:debut Reglages -->',
'<!-- engendre:milieu reglages -->',
'<!-- engendre:debut reglages',
]) {
assert.throws(
() => lireRegions(`Avant\n${marqueur}\nAprès\n`),
(erreur) => {
assert.ok(erreur instanceof ErreurRegions, marqueur);
assert.match(erreur.fautes[0].faute, /marqueur illisible/);
return true;
},
);
}
});
});
describe('documentation : remplacer les régions (§ 19.8)', () => {
test('chaque région reçoit ce que son producteur rend, entre deux lignes vides ; un second passage ne change rien', () => {
const { producteurs } = producteursComptes({ reglages: '| a |\n|---|\n| 1 |', raccourcis: 'Texte\n' });
const texte = `# Titre\n\n${region('reglages', '\nancien\n')}\n\n${region('raccourcis', '')}\n`;
const rendu = remplacerRegions(texte, producteurs);
assert.equal(
rendu,
`# Titre\n\n${region('reglages', '\n\n| a |\n|---|\n| 1 |\n\n')}\n\n${region('raccourcis', '\n\nTexte\n\n')}\n`,
);
assert.equal(remplacerRegions(rendu, producteurs), rendu);
});
test("rien ne change hors des marqueurs : marque d'ordre, fins de ligne CRLF, espaces de fin, tabulations, espace insécable, caractère astral", () => {
const { producteurs } = producteursComptes({ commandes: 'neuf' });
const avant = `\u{FEFF}Titre\r\nligne \r\n\tretrait\u{A0}fin \u{1F600}\r\n${region('commandes', 'ancien\r\n')}\r\nqueue\t \r\n`;
const apres = remplacerRegions(avant, producteurs);
assert.notEqual(apres, avant);
assert.equal(Buffer.compare(Buffer.from(horsRegions(apres)), Buffer.from(horsRegions(avant))), 0);
const [{ debut: d }] = lireRegions(avant);
assert.equal(apres.slice(0, d), avant.slice(0, d));
assert.ok(apres.endsWith(`${fin('commandes')}\r\nqueue\t \r\n`));
});
test('un nom inconnu lève en le nommant, sans rien rendre', () => {
const { producteurs, appels } = producteursComptes({ reglages: 'x' });
assert.throws(
() => remplacerRegions(`${region('reglages', '')}\n${region('inconnue', '')}\n`, producteurs),
(erreur) => {
assert.ok(erreur instanceof ErreurRegions);
assert.deepEqual(erreur.fautes.map((f) => f.region), ['inconnue']);
assert.match(erreur.fautes[0].faute, /nom inconnu/);
return true;
},
);
assert.deepEqual(appels, []);
});
test('un texte sans région se rend tel quel', () => {
const { producteurs, appels } = producteursComptes({ reglages: 'x' });
assert.equal(remplacerRegions('Rien à engendrer.\n', producteurs), 'Rien à engendrer.\n');
assert.deepEqual(appels, []);
});
});
describe('documentation : vérifier les régions (§ 19.8, § 14.3)', () => {
const { producteurs } = producteursComptes({ reglages: 'R', raccourcis: 'K' });
const conforme = remplacerRegions(`${region('reglages', '')}\n\n${region('raccourcis', '')}\n`, producteurs);
test('des régions conformes : aucune faute', () => {
assert.deepEqual(verifierRegions(conforme, producteurs), []);
});
test('une correction à la main dans une région : une faute, qui nomme cette région seule', () => {
const corrige = conforme.replace('\n\nK\n\n', '\n\nK corrigé à la main\n\n');
assert.notEqual(corrige, conforme);
const fautes = verifierRegions(corrige, producteurs);
assert.deepEqual(fautes.map((f) => f.region), ['raccourcis']);
assert.match(fautes[0].faute, /diffère de ce que le producteur engendre/);
assert.match(fautes[0].faute, /K corrigé à la main/);
});
test('un nom inconnu, une région en double, un marqueur sans fin : des fautes, pas une exception', () => {
assert.deepEqual(
verifierRegions(`${conforme}${region('inconnue', '')}\n`, producteurs).map((f) => [f.region, /nom inconnu/.test(f.faute)]),
[['inconnue', true]],
);
assert.deepEqual(
verifierRegions(`${conforme}${region('reglages', '\n\nR\n\n')}\n`, producteurs).map((f) => f.region),
['reglages'],
);
assert.match(verifierRegions(`${conforme}${debut('reglages')}\n`, producteurs)[0].faute, /sans fin/);
});
test('zéro région : une faute, un balayage vide ne passe pas (§ 14.2)', () => {
const fautes = verifierRegions('Un document sans marqueur.\n', producteurs);
assert.equal(fautes.length, 1);
assert.equal(fautes[0].region, null);
assert.match(fautes[0].faute, /aucune région/);
});
test("un producteur qui lève : une faute qui nomme sa région et l'erreur", () => {
const enPanne = new Map([['reglages', () => { throw new Error('source illisible'); }]]);
const fautes = verifierRegions(`${region('reglages', '')}\n`, enPanne);
assert.deepEqual(fautes.map((f) => f.region), ['reglages']);
assert.match(fautes[0].faute, /source illisible/);
});
});
describe('documentation : les producteurs, chacun sur sa source unique (§ 19.8)', () => {
test('les sept producteurs, dans leur ordre ; PORTEURS est une liste figée', () => {
assert.deepEqual(
PRODUCTEURS.map(({ nom }) => nom),
['reglages', 'colonnes-csv', 'commandes', 'raccourcis', 'demonstrations', 'contrastes', 'constantes'],
);
assert.ok(Object.isFrozen(PRODUCTEURS) && Object.isFrozen(PORTEURS));
assert.deepEqual([...producteursDe({ manifeste: () => null }).keys()], PRODUCTEURS.map(({ nom }) => nom));
});
test("tableau : une ligne d'en-tête, un séparateur, une ligne par entrée ; une barre et un saut de ligne ne cassent pas la cellule", () => {
assert.equal(
tableau(['A', 'B'], [['x | y', 'deux\nlignes']]),
'| A | B |\n|---|---|\n| x \\| y | deux lignes |',
);
});
test('reglages : une ligne par entrée de la définition, son libellé, son écran et son défaut ; une entrée ajoutée paraît', () => {
const lignes = lignesDuTableau(tableauReglages());
assert.equal(lignes.length, REGLAGES_GENERATION.length);
REGLAGES_GENERATION.forEach((entree, rang) => {
const attendu = entree.genre === 'champ' ? nombre(entree.defaut) : entree.defaut ? 'cochée' : 'décochée';
assert.deepEqual(lignes[rang], [t(entree.etiquette), t(`coquille.section.${entree.section}`), attendu]);
});
const ajoutee = { ...REGLAGES_GENERATION[0], etiquette: 'generation.reglage.graine', defaut: 4_294_967_295 };
const avecAjout = lignesDuTableau(tableauReglages([...REGLAGES_GENERATION, ajoutee]));
assert.deepEqual(avecAjout.at(-1), [t('generation.reglage.graine'), t('coquille.section.generation'), '4\u{202F}294\u{202F}967\u{202F}295']);
});
test('colonnes-csv : une ligne par champ, son en-tête puis ses synonymes, rien de plus', () => {
const lignes = lignesDuTableau(tableauColonnesCsv());
assert.deepEqual(
lignes,
ENTETES_DES_CHAMPS.map(([, [entete, ...synonymes]]) => [
`\`${entete}\``,
synonymes.length === 0 ? t('format.vide') : synonymes.map((s) => `\`${s}\``).join(', '),
]),
);
assert.deepEqual(lignes.find(([c]) => c === '`titre_pressenti`'), ['`titre_pressenti`', '`titre`, `rôle`']);
});
test("commandes : une ligne par commande du registre, dans son ordre, et son libellé d'historique composé par la traduction", () => {
const lignes = lignesDuTableau(tableauCommandes());
assert.deepEqual(lignes.map(([nom]) => nom), [...COMMANDES.keys()].map((nom) => `\`${nom}\``));
for (const [cellule, exemple] of lignes) {
const nom = cellule.slice(1, -1);
const cle = `ENTREE_${nom.replace(/[A-Z]/g, (lettre) => `_${lettre}`).toUpperCase()}`;
assert.equal(exemple, libelle(cle, EXEMPLES_COMMANDES[nom]), nom);
}
assert.ok(lignes.some(([, exemple]) => exemple === 'Ajouté Iris Ombrelle'));
});
test("commandes : une commande sans exemple, ou sans libellé d'historique, lève en la nommant", () => {
const avecNouvelle = new Map([...COMMANDES, ['commandeInventee', () => null]]);
assert.throws(() => tableauCommandes(avecNouvelle), /commandeInventee/);
assert.throws(
() => tableauCommandes(avecNouvelle, { ...EXEMPLES_COMMANDES, commandeInventee: {} }),
/ENTREE_COMMANDE_INVENTEE/,
);
});
test('raccourcis : une ligne par contexte et par effet, ses touches nommées, son libellé ; chaque entrée de la table y est', () => {
const lignes = lignesDuTableau(tableauRaccourcis());
const effets = [...new Set(RACCOURCIS.map(({ contexte, effet }) => `${contexte}.${effet}`))];
assert.equal(lignes.length, effets.length);
lignes.forEach(([, , texte], rang) => assert.equal(texte, t(`raccourcis.${effets[rang]}`)));
const entrer = lignes[effets.indexOf('plan.entrer')];
assert.equal(entrer[1], 'Entrée ou Espace');
assert.equal(lignes[effets.indexOf('plan.droite')][1], '\u{2192}');
assert.equal(lignes[effets.indexOf('grille.precedente')][1], 'Maj+Tab');
assert.throws(() => tableauRaccourcis([...RACCOURCIS, { ...RACCOURCIS[0], touche: 'Pause' }]), /Pause/);
});
test('demonstrations : effectif, tables, tours et plafond a priori égaux à diagnostiquer sur chaque fichier livré', () => {
const lignes = lignesDuTableau(tableauDemonstrations());
assert.equal(lignes.length, LIVREES.length);
LIVREES.forEach(({ cle, texte }, rang) => {
const configuration = configurationDepuisCharge(analyser(texte).charge);
const { plafondAPriori } = diagnostiquer(configuration);
assert.deepEqual(lignes[rang], [
CATALOGUE.find((entree) => entree.cle === cle).nom,
nombre(configuration.participants.length),
nombre(configuration.tables.length),
nombre(configuration.tours),
nombre(plafondAPriori.min),
nombre(plafondAPriori.max),
]);
});
assert.deepEqual(lignes[0].slice(1), ['260', '33', '4', '24', '28']);
});
test("contrastes : par jeton et rôle, le plus petit ratio de chaque thème et la première capture où il l'est", () => {
assert.deepEqual(lignesDuTableau(tableauContrastes(manifesteFixture())), [
['`encre`', 'marque', '3', t('format.vide'), t('format.vide'), '5,50', '02-import-apercu'],
['`encre`', 'texte', '4,5', '9,81', '02-import-apercu', '11,20', '01-premier-ecran'],
['`encre-douce`', 'texte', '4,5', '5,20', '01-premier-ecran', '4,62', '02-import-apercu'],
['`focus`', 'marque', '3', '3,40', '01-premier-ecran', '3,90', '02-import-apercu'],
]);
});
test('contrastes : un style sans jeton, ou deux seuils pour un même rôle, lèvent ; aucun style relevé lève', () => {
const sansJeton = manifesteFixture();
delete sansJeton.captures[1].styles.sombre[0].jeton;
assert.throws(() => tableauContrastes(sansJeton), /02-import-apercu/);
const deuxSeuils = manifesteFixture();
deuxSeuils.captures[1].styles.clair[0].seuil = 3;
assert.throws(() => tableauContrastes(deuxSeuils), /seuil/);
const vide = manifesteFixture();
for (const capture of vide.captures) capture.styles = { clair: [], sombre: [] };
assert.throws(() => tableauContrastes(vide), /aucun style/);
assert.throws(() => tableauContrastes(null), /manifeste/);
});
test('constantes : une ligne par constante du registre, sa valeur, son unité, son propriétaire, son statut en toutes lettres et son protocole', () => {
const lignes = lignesDuTableau(tableauRegistre());
assert.equal(lignes.length, CONSTANTES.length);
CONSTANTES.forEach((c, rang) => {
assert.equal(lignes[rang][0], `\`${c.nom}\``);
assert.equal(lignes[rang][2], c.unite);
assert.equal(lignes[rang][3], c.proprietaire);
assert.equal(lignes[rang][4], c.statut === 'mesuree' ? 'mesurée' : 'défaut de travail');
assert.equal(lignes[rang][5].replace(/\\\|/g, '|'), c.protocole.replace(/\s*\n\s*/g, ' '));
});
const inventees = [
{ ...CONSTANTES[0], nom: 'A', valeur: 0.3, statut: 'mesuree' },
{ ...CONSTANTES[0], nom: 'B', valeur: null },
{ ...CONSTANTES[0], nom: 'C', valeur: 200_000 },
];
assert.deepEqual(
lignesDuTableau(tableauRegistre(inventees)).map((l) => [l[1], l[4]]),
[['0,3', 'mesurée'], [t('format.vide'), 'défaut de travail'], ['200\u{202F}000', 'défaut de travail']],
);
});
});
describe("documentation : l'arbre des porteurs et l'écart de version (§ 19.7, § 19.8)", () => {
test("l'arbre de fixture, à la version de la construction : conforme, code zéro, livraison comprise", () => {
const { racine, effacer } = copieDeLArbre();
try {
for (const livraison of [false, true]) {
const bilan = verifierArbre({ racine, porteurs: ['README.md'], livraison });
assert.deepEqual(bilan.fautes, []);
assert.equal(bilan.ecart, null);
assert.equal(bilan.code, 0);
}
} finally {
effacer();
}
});
test("seul l'écart de version change le code de sortie : affiché sans --livraison, bloquant avec", () => {
const voisine = versionVoisine(VERSION.affichee);
const { racine, effacer } = copieDeLArbre(voisine);
try {
const court = verifierArbre({ racine, porteurs: ['README.md'], livraison: false });
const livraison = verifierArbre({ racine, porteurs: ['README.md'], livraison: true });
assert.deepEqual(court.ecart, { manifeste: voisine, construction: VERSION.affichee });
assert.deepEqual(livraison.ecart, court.ecart);
assert.deepEqual([court.fautes, livraison.fautes], [[], []]);
assert.deepEqual([court.code, livraison.code], [0, 1]);
const sortie = sortieRetenue();
assert.equal(executer(['verifier', '--racine', racine, '--porteur', 'README.md'], { sortie: sortie.ecrire }), 0);
assert.ok(sortie.lignes.some((l) => /écart de version \(non bloquant\)/.test(l) && l.includes(voisine)), sortie.lignes.join('\n'));
const bloquee = sortieRetenue();
assert.equal(executer(['verifier', '--livraison', '--racine', racine, '--porteur', 'README.md'], { sortie: bloquee.ecrire }), 1);
assert.ok(bloquee.lignes.some((l) => /écart de version \(bloquant\)/.test(l)), bloquee.lignes.join('\n'));
} finally {
effacer();
}
});
test('la ligne de commande, en processus : --livraison sort non nul pour le seul écart, zéro sans lui', () => {
const lancer = (racine, ...options) =>
spawnSync(process.execPath, [SCRIPT, 'verifier', ...options, '--racine', racine, '--porteur', 'README.md'], { encoding: 'utf8' });
const avecEcart = copieDeLArbre(versionVoisine(VERSION.affichee));
const sansEcart = copieDeLArbre();
try {
assert.equal(lancer(avecEcart.racine, '--livraison').status, 1);
assert.equal(lancer(avecEcart.racine).status, 0);
const conforme = lancer(sansEcart.racine, '--livraison');
assert.equal(conforme.status, 0, conforme.stdout + conforme.stderr);
assert.equal(spawnSync(process.execPath, [SCRIPT, 'publier'], { encoding: 'utf8' }).status, 2);
} finally {
avecEcart.effacer();
sansEcart.effacer();
}
});
test("un manifeste absent est un écart : l'arbre ne prouve aucune version", () => {
const { racine, effacer } = copieDeLArbre();
try {
rmSync(join(racine, MANIFESTE));
writeFileSync(join(racine, 'SANS-CONTRASTES.md'), `${region('colonnes-csv', `\n\n${tableauColonnesCsv()}\n\n`)}\n`);
const bilan = verifierArbre({ racine, porteurs: ['SANS-CONTRASTES.md'], livraison: true });
assert.deepEqual(bilan.fautes, []);
assert.deepEqual(bilan.ecart, { manifeste: null, construction: VERSION.affichee });
assert.equal(bilan.code, 1);
} finally {
effacer();
}
});
test("un fichier hors de PORTEURS n'est pas lu : seuls le porteur et le manifeste le sont", () => {
const { racine, effacer } = copieDeLArbre();
try {
const lus = [];
const lire = (chemin) => {
lus.push(chemin);
return readFileSync(join(racine, chemin), 'utf8');
};
assert.equal(verifierArbre({ racine, porteurs: ['README.md'], lire }).code, 0);
assert.deepEqual(lus, ['README.md', MANIFESTE]);
const avecHors = verifierArbre({ racine, porteurs: ['README.md', 'HORS-PORTEURS.md'] });
assert.deepEqual(avecHors.fautes.map((f) => f.chemin), ['HORS-PORTEURS.md']);
assert.match(avecHors.fautes[0].faute, /sans fin/);
} finally {
effacer();
}
});
test('zéro région dans un porteur, un porteur absent, aucun porteur : des fautes, code non nul', () => {
const { racine, effacer } = copieDeLArbre();
try {
writeFileSync(join(racine, 'VIDE.md'), '# Rien\n');
const bilan = verifierArbre({ racine, porteurs: ['README.md', 'VIDE.md', 'ABSENT.md'] });
assert.deepEqual(bilan.fautes.map((f) => f.chemin), ['VIDE.md', 'ABSENT.md']);
assert.match(bilan.fautes[0].faute, /aucune région/);
assert.match(bilan.fautes[1].faute, /absent/);
assert.equal(bilan.code, 1);
const sansPorteur = verifierArbre({ racine, porteurs: [] });
assert.equal(sansPorteur.code, 1);
assert.match(sansPorteur.fautes[0].faute, /aucun fichier porteur/);
} finally {
effacer();
}
});
test("sans liste donnée, verifier, engendrer et la ligne de commande s'en tiennent à PORTEURS : mêmes lectures, mêmes écritures, même bilan qu'avec PORTEURS en clair", () => {
const { racine, effacer } = copieDeLArbre();
// Un lancement espionné : ce qu'il lit, ce qu'il écrirait, ce qu'il
// affiche et ce qu'il rend, la liste de porteurs donnée ou non.
const espionne = (lancer, options) => {
const lus = [];
const ecrits = [];
const sortie = sortieRetenue();
const lire = (chemin) => {
lus.push(chemin);
return readFileSync(join(racine, chemin), 'utf8');
};
const rendu = lancer({ ...options, lire, ecrire: (chemin) => ecrits.push(chemin), sortie: sortie.ecrire });
return { rendu, lus, ecrits, lignes: sortie.lignes };
};
const lanceurs = [
['verifierArbre', (options) => verifierArbre({ racine, ...options })],
['engendrerArbre', (options) => engendrerArbre({ racine, ...options })],
['executer verifier', (options) => executer(['verifier', '--racine', racine], options)],
['executer engendrer', (options) => executer(['engendrer', '--racine', racine], options)],
];
try {
for (const [nom, lancer] of lanceurs) {
const implicite = espionne(lancer, {});
assert.deepEqual(implicite, espionne(lancer, { porteurs: PORTEURS }), nom);
assert.deepEqual(implicite.lus.filter((chemin) => chemin !== MANIFESTE), [...PORTEURS], nom);
}
} finally {
effacer();
}
});
test('engendrer réécrit les seules régions, octets du reste identiques ; vérifier passe ensuite, et un second passage n’écrit rien', () => {
const { racine, effacer } = copieDeLArbre();
try {
const noms = PRODUCTEURS.map(({ nom }) => nom);
const avant = `\u{FEFF}# Annexe\r\n\r\n${noms.map((nom) => `## ${nom} \r\n${region(nom, '\nà refaire\n')}\r\n`).join('\t\r\n')}fin\u{A0}\r\n`;
writeFileSync(join(racine, 'ANNEXE.md'), avant);
const porteurs = ['README.md', 'ANNEXE.md'];
assert.notEqual(verifierArbre({ racine, porteurs }).code, 0);
const premier = engendrerArbre({ racine, porteurs });
assert.deepEqual([premier.fautes, premier.ecrits, premier.code], [[], ['ANNEXE.md'], 0]);
const apres = readFileSync(join(racine, 'ANNEXE.md'), 'utf8');
assert.equal(Buffer.compare(Buffer.from(horsRegions(apres)), Buffer.from(horsRegions(avant))), 0);
assert.deepEqual(lireRegions(apres).map((r) => r.nom), noms);
assert.deepEqual(verifierArbre({ racine, porteurs, livraison: true }).code, 0);
assert.deepEqual(engendrerArbre({ racine, porteurs }).ecrits, []);
assert.equal(readFileSync(join(racine, 'README.md'), 'utf8'), readFileSync(join(FIXTURE, 'README.md'), 'utf8'));
} finally {
effacer();
}
});
test("engendrer n'écrit rien quand un porteur est en faute", () => {
const { racine, effacer } = copieDeLArbre();
try {
writeFileSync(join(racine, 'A.md'), `${region('reglages', '')}\n`);
writeFileSync(join(racine, 'B.md'), `${debut('commandes')}\n`);
const ecrits = [];
const bilan = engendrerArbre({ racine, porteurs: ['A.md', 'B.md'], ecrire: (chemin) => ecrits.push(chemin) });
assert.equal(bilan.code, 1);
assert.deepEqual(bilan.fautes.map((f) => f.chemin), ['B.md']);
assert.deepEqual([bilan.ecrits, ecrits], [[], []]);
assert.equal(readFileSync(join(racine, 'A.md'), 'utf8'), `${region('reglages', '')}\n`);
} finally {
effacer();
}
});
test("le README de l'arbre de fixture est tel qu'engendré : sa région de contrastes est celle du manifeste de fixture", () => {
const [{ nom, contenu }] = lireRegions(readFileSync(join(FIXTURE, 'README.md'), 'utf8'));
assert.equal(nom, 'contrastes');
assert.equal(contenu, `\n\n${tableauContrastes(manifesteFixture())}\n\n`);
});
test("la ligne de commande refuse un mode inconnu, --livraison hors de verifier et une option sans valeur, code 2, sans rien lire", () => {
for (const argv of [[], ['publier'], ['engendrer', '--livraison'], ['verifier', '--racine'], ['verifier', '--inconnue']]) {
const sortie = sortieRetenue();
const lus = [];
assert.equal(executer(argv, { sortie: sortie.ecrire, lire: (chemin) => lus.push(chemin) }), 2, argv.join(' '));
assert.deepEqual(lus, []);
assert.ok(sortie.lignes.some((l) => /engendrer \| verifier/.test(l)), argv.join(' '));
}
});
});