gestion_table_tournante_libre/scripts/documentation.test.js

619 lines
28 KiB
JavaScript
Raw Normal View History

// © 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(' '));
}
});
});