492 lines
22 KiB
JavaScript
492 lines
22 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
// Épreuves du script qui engendre les fichiers livrés (§ 10.3, § 15.5) : la
|
||
|
|
// charge de chaque démonstration — la configuration du catalogue,
|
||
|
|
// l'événement demo:<clé>, la salle en grille, les animateurs au siège 1 —,
|
||
|
|
// le fichier d'état qu'il en écrit, la variante windows-1252 du CSV des cas
|
||
|
|
// limites, et la ligne de commande, qui écrit dans le dossier donné et nulle
|
||
|
|
// part ailleurs, et réécrit les mêmes octets quand on la relance. « Nulle
|
||
|
|
// part ailleurs » s'éprouve sur une copie du projet, photographiée avant et
|
||
|
|
// après chaque lancement : tout fichier écrit hors des sept livrables s'y
|
||
|
|
// voit, le CSV des cas limites, que le script lit sans l'écrire, comme les
|
||
|
|
// autres. Le contenu des fichiers livrés eux-mêmes s'éprouve à côté d'eux,
|
||
|
|
// dans src/demo/livrees.test.js et src/demo/livrees.long.test.js.
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { spawnSync } from 'node:child_process';
|
||
|
|
import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join, relative, sep } from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
import { exporterParticipants } from '../src/csv/export.js';
|
||
|
|
import { decouper } from '../src/csv/lecture.js';
|
||
|
|
import { CATALOGUE } from '../src/demo/catalogue.js';
|
||
|
|
import { serialiser } from '../src/stockage/canonique.js';
|
||
|
|
import { GENERATION_PAR_DEFAUT, analyser, configurationDepuisCharge } from '../src/stockage/document.js';
|
||
|
|
import { VERSION } from '../src/version.genere.js';
|
||
|
|
import { describe, test } from '../test/lanceur.js';
|
||
|
|
import { versionVoisine } from '../test/version_voisine.js';
|
||
|
|
import {
|
||
|
|
chargeDeDemonstration,
|
||
|
|
enWindows1252,
|
||
|
|
livrables,
|
||
|
|
texteDeDemonstration,
|
||
|
|
varianteWindows1252,
|
||
|
|
} from './engendrer_livrables.js';
|
||
|
|
|
||
|
|
const RACINE = fileURLToPath(new URL('..', import.meta.url));
|
||
|
|
const SCRIPT = fileURLToPath(new URL('./engendrer_livrables.js', import.meta.url));
|
||
|
|
const CAS_LIMITES = join(RACINE, 'exemples', 'participants_cas_limites.csv');
|
||
|
|
|
||
|
|
// Les sept livrables que le script écrit, chemins relatifs à la racine, dans
|
||
|
|
// l'ordre où il les écrit : un fichier d'état par démonstration du
|
||
|
|
// catalogue, les deux CSV de démonstration, puis la variante windows-1252 du
|
||
|
|
// CSV des cas limites (§ 10.3).
|
||
|
|
const LIVRABLES = [
|
||
|
|
...CATALOGUE.map(({ cle }) => `src/demo/livrees/${cle}.gtt.json`),
|
||
|
|
'exemples/participants_demo_petite.csv',
|
||
|
|
'exemples/participants_demo_grande.csv',
|
||
|
|
'exemples/participants_cas_limites_cp1252.csv',
|
||
|
|
];
|
||
|
|
|
||
|
|
// Ce que le script lit, chemins relatifs à la racine du projet : package.json,
|
||
|
|
// qui déclare les modules ES, le script, le code de src/ qu'il importe et les
|
||
|
|
// CSV de exemples/, celui des cas limites compris, qu'il lit sans l'écrire.
|
||
|
|
const COPIES = ['package.json', 'scripts/engendrer_livrables.js', 'src', 'exemples'];
|
||
|
|
|
||
|
|
// Date de modification, en secondes, de chaque fichier copié : un instant
|
||
|
|
// ancien. Toute écriture ultérieure la déplace, d'octets identiques comprise,
|
||
|
|
// quelle que soit la finesse de l'horloge du système de fichiers.
|
||
|
|
const DATE_ANCIENNE = 1_000_000_000;
|
||
|
|
|
||
|
|
const MARQUE = '\u{FEFF}';
|
||
|
|
|
||
|
|
// Entrée du catalogue de clé donnée.
|
||
|
|
const entreeDe = (cle) => CATALOGUE.find((entree) => entree.cle === cle);
|
||
|
|
|
||
|
|
// Entrée d'essai : la petite démonstration, sa configuration retouchée par
|
||
|
|
// retoucher, qui reçoit une copie et la rend.
|
||
|
|
const entreeRetouchee = (retoucher) => ({
|
||
|
|
cle: 'essai',
|
||
|
|
nom: 'Essai',
|
||
|
|
construire: () => retoucher(entreeDe('petite').construire()),
|
||
|
|
});
|
||
|
|
|
||
|
|
const parTable = (a, b) => a.table - b.table;
|
||
|
|
|
||
|
|
// Dossier temporaire neuf, passé à faire, puis effacé ; rend ce que rend
|
||
|
|
// faire.
|
||
|
|
function avecDossier(faire) {
|
||
|
|
const dossier = mkdtempSync(join(tmpdir(), 'livrables-'));
|
||
|
|
try {
|
||
|
|
return faire(dossier);
|
||
|
|
} finally {
|
||
|
|
rmSync(dossier, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fichiers d'un dossier, à toute profondeur, en chemins relatifs séparés par
|
||
|
|
// « / », triés.
|
||
|
|
const fichiersSous = (dossier) =>
|
||
|
|
readdirSync(dossier, { recursive: true, withFileTypes: true })
|
||
|
|
.filter((entree) => entree.isFile())
|
||
|
|
.map((entree) => relative(dossier, join(entree.parentPath, entree.name)).split(sep).join('/'))
|
||
|
|
.sort();
|
||
|
|
|
||
|
|
// Instantané d'un dossier : la taille et la date de modification, en
|
||
|
|
// nanosecondes, de chaque fichier à toute profondeur, sous son chemin relatif
|
||
|
|
// séparé par « / ».
|
||
|
|
const instantane = (dossier) =>
|
||
|
|
new Map(
|
||
|
|
fichiersSous(dossier).map((chemin) => {
|
||
|
|
const { size, mtimeNs } = statSync(join(dossier, chemin), { bigint: true });
|
||
|
|
return [chemin, `${size} octets, modifié à ${mtimeNs} ns`];
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Chemins que deux instantanés distinguent, triés : un fichier créé, retiré,
|
||
|
|
// ou dont la taille ou la date de modification change. Un fichier réécrit avec
|
||
|
|
// les mêmes octets en fait partie, sa date de modification avance.
|
||
|
|
const differences = (avant, apres) =>
|
||
|
|
[...new Set([...avant.keys(), ...apres.keys()])].filter((chemin) => avant.get(chemin) !== apres.get(chemin)).sort();
|
||
|
|
|
||
|
|
// Lance le script de chemin donné depuis le répertoire cwd ; rend son statut
|
||
|
|
// et ses sorties.
|
||
|
|
const lancerScript = (script, cwd, ...arguments_) =>
|
||
|
|
spawnSync(process.execPath, [script, ...arguments_], { cwd, encoding: 'utf8' });
|
||
|
|
|
||
|
|
// Lance le script du projet depuis le répertoire cwd.
|
||
|
|
const lancer = (cwd, ...arguments_) => lancerScript(SCRIPT, cwd, ...arguments_);
|
||
|
|
|
||
|
|
// Dossier temporaire neuf qui porte, sous projet/, une copie de ce que le
|
||
|
|
// script lit, chaque fichier daté de DATE_ANCIENNE. Passe à faire ce dossier
|
||
|
|
// et lancerCopie, qui lance depuis lui le script copié ; efface le dossier ;
|
||
|
|
// rend ce que rend faire. Le script y est à la racine de son arbre, comme
|
||
|
|
// dans le projet : un fichier qu'il écrit hors de la destination tombe dans
|
||
|
|
// ce dossier, copie du projet ou répertoire courant, jamais dans le projet.
|
||
|
|
function avecProjetCopie(faire) {
|
||
|
|
return avecDossier((dossier) => {
|
||
|
|
for (const chemin of COPIES) {
|
||
|
|
cpSync(join(RACINE, chemin), join(dossier, 'projet', chemin), { recursive: true });
|
||
|
|
}
|
||
|
|
for (const chemin of fichiersSous(dossier)) utimesSync(join(dossier, chemin), DATE_ANCIENNE, DATE_ANCIENNE);
|
||
|
|
const copie = join(dossier, 'projet', 'scripts', 'engendrer_livrables.js');
|
||
|
|
return faire(dossier, (...arguments_) => lancerScript(copie, dossier, ...arguments_));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const echapper = (texte) => texte.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
|
|
||
|
|
describe("engendrer_livrables : la charge d'une démonstration (§ 15)", () => {
|
||
|
|
test('porte la configuration du catalogue : participants, capacités, tours, réservations, contraintes', () => {
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
const configuration = entree.construire();
|
||
|
|
const charge = chargeDeDemonstration(entree);
|
||
|
|
const lue = configurationDepuisCharge(charge);
|
||
|
|
assert.deepEqual(
|
||
|
|
{ ...lue, reservations: [...lue.reservations].sort(parTable) },
|
||
|
|
{
|
||
|
|
...configuration,
|
||
|
|
participants: configuration.participants.map(({ id, nom, appartenance }) => ({
|
||
|
|
id,
|
||
|
|
nom,
|
||
|
|
appartenance,
|
||
|
|
exclu: false,
|
||
|
|
})),
|
||
|
|
reservations: [...configuration.reservations].sort(parTable),
|
||
|
|
},
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
charge.participants.map(({ prenom }) => prenom),
|
||
|
|
configuration.participants.map(({ prenom }) => prenom),
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("l'événement demo:<clé> : nom du catalogue, sans date, en brouillon, sans filiation, les tours et le défaut de sièges de la configuration", () => {
|
||
|
|
// Sièges par défaut des quatre démonstrations (§ 15.1, § 15.3).
|
||
|
|
const defauts = new Map([['grande', 8], ['grande-sans-exception', 8], ['petite', 3], ['petite-conflit', 3]]);
|
||
|
|
assert.deepEqual(CATALOGUE.map(({ cle }) => cle).sort(), [...defauts.keys()].sort());
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
assert.deepEqual(
|
||
|
|
chargeDeDemonstration(entree).evenement,
|
||
|
|
{
|
||
|
|
id: `demo:${entree.cle}`,
|
||
|
|
nom: entree.nom,
|
||
|
|
date: null,
|
||
|
|
siegesParDefaut: defauts.get(entree.cle),
|
||
|
|
tours: entree.construire().tours,
|
||
|
|
unite: 'cm',
|
||
|
|
etat: 'brouillon',
|
||
|
|
filiation: null,
|
||
|
|
},
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('les réglages : les contraintes de la configuration, une à une, les sièges non attribués, la génération par défaut', () => {
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
assert.deepEqual(
|
||
|
|
chargeDeDemonstration(entree).reglages,
|
||
|
|
{ ...entree.construire().contraintes, attribuerSieges: false, generation: { ...GENERATION_PAR_DEFAUT } },
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const contraintes = {
|
||
|
|
separerAppartenances: false,
|
||
|
|
nouveauxVoisins: true,
|
||
|
|
nouvelleTable: false,
|
||
|
|
varierAppartenances: true,
|
||
|
|
};
|
||
|
|
const entree = entreeRetouchee((configuration) => ({ ...configuration, contraintes }));
|
||
|
|
assert.deepEqual(chargeDeDemonstration(entree).reglages, {
|
||
|
|
...contraintes,
|
||
|
|
attribuerSieges: false,
|
||
|
|
generation: { ...GENERATION_PAR_DEFAUT },
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('les tables : rondes, en grille rangée par rangée, centres à 300 cm, la dernière rangée seule incomplète', () => {
|
||
|
|
assert.deepEqual(chargeDeDemonstration(entreeDe('petite')).tables, [
|
||
|
|
{ id: 1, numero: 1, sieges: null, forme: 'ronde', position: { x: 0, y: 0 } },
|
||
|
|
{ id: 2, numero: 2, sieges: null, forme: 'ronde', position: { x: 300, y: 0 } },
|
||
|
|
{ id: 3, numero: 3, sieges: null, forme: 'ronde', position: { x: 0, y: 300 } },
|
||
|
|
{ id: 4, numero: 4, sieges: null, forme: 'ronde', position: { x: 300, y: 300 } },
|
||
|
|
]);
|
||
|
|
// 33 tables : six colonnes, cinq rangées pleines, puis trois tables.
|
||
|
|
const { tables } = chargeDeDemonstration(entreeDe('grande'));
|
||
|
|
const positions = tables.map(({ position: { x, y } }) => [x, y]);
|
||
|
|
assert.deepEqual(positions.slice(0, 7), [[0, 0], [300, 0], [600, 0], [900, 0], [1200, 0], [1500, 0], [0, 300]]);
|
||
|
|
assert.deepEqual(positions.slice(-4), [[1500, 1200], [0, 1500], [300, 1500], [600, 1500]]);
|
||
|
|
assert.equal(new Set(positions.map(String)).size, 33);
|
||
|
|
assert.ok(tables.every(({ forme }) => forme === 'ronde'));
|
||
|
|
assert.deepEqual(tables.map(({ id, numero }) => [id, numero]), positions.map((_, rang) => [rang + 1, rang + 1]));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("les sièges : le défaut est la capacité la plus fréquente, la plus grande à égalité ; toute autre capacité surcharge sa table (§ 6.1)", () => {
|
||
|
|
const surcharges = (charge) =>
|
||
|
|
charge.tables.filter(({ sieges }) => sieges !== null).map(({ numero, sieges }) => [numero, sieges]);
|
||
|
|
assert.deepEqual(surcharges(chargeDeDemonstration(entreeDe('grande'))), [[30, 7], [31, 7], [32, 7], [33, 7]]);
|
||
|
|
for (const cle of ['grande-sans-exception', 'petite', 'petite-conflit']) {
|
||
|
|
assert.deepEqual(surcharges(chargeDeDemonstration(entreeDe(cle))), [], cle);
|
||
|
|
}
|
||
|
|
const capacites = [6, 9, 6, 9];
|
||
|
|
const egalite = chargeDeDemonstration(
|
||
|
|
entreeRetouchee((configuration) => ({
|
||
|
|
...configuration,
|
||
|
|
tables: configuration.tables.map((table, rang) => ({ ...table, capacite: capacites[rang] })),
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
assert.equal(egalite.evenement.siegesParDefaut, 9);
|
||
|
|
assert.deepEqual(surcharges(egalite), [[1, 6], [3, 6]]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("grandes : chaque animateur réservé au siège 1 de sa table pour tous les tours, ce siège titré « animation », le titre pressenti porté par lui seul ; petites : ni réservation ni titre", () => {
|
||
|
|
for (const cle of ['grande', 'grande-sans-exception']) {
|
||
|
|
const entree = entreeDe(cle);
|
||
|
|
const animateurs = entree.construire().reservations;
|
||
|
|
const charge = chargeDeDemonstration(entree);
|
||
|
|
assert.equal(animateurs.length, 33, cle);
|
||
|
|
assert.deepEqual(
|
||
|
|
[...charge.reservations].sort(parTable),
|
||
|
|
animateurs
|
||
|
|
.map(({ participant, table }) => ({ participant, table, siege: 1, portee: 'tous', tour: null }))
|
||
|
|
.sort(parTable),
|
||
|
|
cle,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
[...charge.titres].sort(parTable),
|
||
|
|
charge.tables.map(({ id }) => ({ table: id, siege: 1, libelle: 'animation' })),
|
||
|
|
cle,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
charge.participants
|
||
|
|
.filter(({ titrePressenti }) => titrePressenti !== null)
|
||
|
|
.map(({ id, titrePressenti }) => [id, titrePressenti]),
|
||
|
|
animateurs.map(({ participant }) => [participant, 'animation']).sort((a, b) => a[0] - b[0]),
|
||
|
|
cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
for (const cle of ['petite', 'petite-conflit']) {
|
||
|
|
const charge = chargeDeDemonstration(entreeDe(cle));
|
||
|
|
assert.deepEqual([charge.reservations, charge.titres], [[], []], cle);
|
||
|
|
assert.ok(charge.participants.every(({ titrePressenti }) => titrePressenti === null), cle);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("ni proposition ni retenu, personne d'exclu, ni courriel ni notes ; les compteurs dépassent chaque identifiant", () => {
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
const charge = chargeDeDemonstration(entree);
|
||
|
|
assert.deepEqual([charge.propositions, charge.retenu], [[], null], entree.cle);
|
||
|
|
assert.ok(
|
||
|
|
charge.participants.every(({ exclu, courriel, notes }) => exclu === false && courriel === null && notes === null),
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
const plusGrand = (liste) => Math.max(...liste.map(({ id }) => id));
|
||
|
|
assert.deepEqual(
|
||
|
|
charge.prochainsIds,
|
||
|
|
{ participant: plusGrand(charge.participants) + 1, table: plusGrand(charge.tables) + 1, proposition: 1 },
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("refuse une configuration qu'il ne sait pas écrire : deux réservations à une table, une réservation d'un seul tour", () => {
|
||
|
|
const avecReservations = (reservations) =>
|
||
|
|
entreeRetouchee((configuration) => ({ ...configuration, reservations }));
|
||
|
|
assert.throws(
|
||
|
|
() =>
|
||
|
|
chargeDeDemonstration(
|
||
|
|
avecReservations([
|
||
|
|
{ participant: 1, table: 2, portee: 'tous' },
|
||
|
|
{ participant: 5, table: 2, portee: 'tous' },
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
/essai.*table 2/,
|
||
|
|
);
|
||
|
|
assert.throws(
|
||
|
|
() => chargeDeDemonstration(avecReservations([{ participant: 1, table: 3, portee: 'tour', tour: 2 }])),
|
||
|
|
/essai.*table 3/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("engendrer_livrables : le fichier d'état écrit (§ 8.8, § 18.6)", () => {
|
||
|
|
test('révision 1, la version de la construction qui écrit, la forme canonique ; UTF-8 sans marque, des LF seules, une fin de ligne finale', () => {
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
const texte = texteDeDemonstration(entree);
|
||
|
|
const lu = analyser(texte);
|
||
|
|
assert.deepEqual(
|
||
|
|
[lu.entete.format, lu.entete.produitVersion, lu.entete.revision],
|
||
|
|
[1, VERSION.affichee, 1],
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
assert.equal(serialiser(lu.charge, lu.entete), texte, entree.cle);
|
||
|
|
assert.ok(!texte.startsWith(MARQUE) && !texte.includes('\r'), entree.cle);
|
||
|
|
assert.ok(texte.endsWith('}\n') && !texte.endsWith('\n\n'), entree.cle);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("une autre construction n'écrit qu'un autre en-tête : la charge est la même", () => {
|
||
|
|
const voisine = versionVoisine(VERSION.affichee);
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
const ici = analyser(texteDeDemonstration(entree));
|
||
|
|
const ailleurs = analyser(texteDeDemonstration(entree, { produitVersion: voisine }));
|
||
|
|
assert.equal(ailleurs.entete.produitVersion, voisine, entree.cle);
|
||
|
|
assert.deepEqual(ailleurs.charge, ici.charge, entree.cle);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('engendrer_livrables : la variante windows-1252 (§ 10.3)', () => {
|
||
|
|
test('enWindows1252 : un octet par caractère, les signes propres à windows-1252 compris', () => {
|
||
|
|
// Octets de la table publiée de windows-1252.
|
||
|
|
assert.deepEqual(
|
||
|
|
[...enWindows1252('aé€\u{2019}œŸ\u{A0}ÿ')],
|
||
|
|
[0x61, 0xe9, 0x80, 0x92, 0x9c, 0x9f, 0xa0, 0xff],
|
||
|
|
);
|
||
|
|
const decodeur = new TextDecoder('windows-1252');
|
||
|
|
for (let octet = 0; octet < 256; octet += 1) {
|
||
|
|
assert.deepEqual([...enWindows1252(decodeur.decode(Uint8Array.of(octet)))], [octet], `octet ${octet}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('enWindows1252 refuse un caractère que windows-1252 ne porte pas, et nomme son point de code et son rang', () => {
|
||
|
|
assert.throws(
|
||
|
|
() => enWindows1252('noté ✓'),
|
||
|
|
(erreur) => erreur instanceof RangeError && /U\+2713/.test(erreur.message) && /rang 5/.test(erreur.message),
|
||
|
|
);
|
||
|
|
assert.throws(() => enWindows1252('\u{1D11E}'), /U\+1D11E/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('varianteWindows1252 : les mêmes enregistrements, séparés par des virgules, en windows-1252 sans marque', () => {
|
||
|
|
const source = [
|
||
|
|
'Nom;Prénom;Notes',
|
||
|
|
'Lanterneau;Basile;"chœur; l\u{2019}Anse, nord"',
|
||
|
|
'Coudraie;Apolline;"deux\nlignes"',
|
||
|
|
'Brunœil;;sans prénom',
|
||
|
|
];
|
||
|
|
const octets = new TextEncoder().encode(`${MARQUE}${source.join('\r\n')}\r\n`);
|
||
|
|
const variante = varianteWindows1252(octets);
|
||
|
|
assert.equal(
|
||
|
|
new TextDecoder('windows-1252').decode(variante),
|
||
|
|
[
|
||
|
|
'Nom,Prénom,Notes',
|
||
|
|
'Lanterneau,Basile,"chœur; l\u{2019}Anse, nord"',
|
||
|
|
'Coudraie,Apolline,"deux\nlignes"',
|
||
|
|
'Brunœil,,sans prénom',
|
||
|
|
'',
|
||
|
|
].join('\r\n'),
|
||
|
|
);
|
||
|
|
assert.deepEqual([...variante.subarray(0, 3)], [...enWindows1252('Nom')]);
|
||
|
|
assert.deepEqual(
|
||
|
|
decouper(new TextDecoder('windows-1252').decode(variante), ',').enregistrements,
|
||
|
|
decouper(new TextDecoder().decode(octets), ';').enregistrements,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('varianteWindows1252 refuse un fichier dont un guillemet reste ouvert', () => {
|
||
|
|
const octets = new TextEncoder().encode(`${MARQUE}Nom;Notes\r\nLanterneau;"sans fin\r\n`);
|
||
|
|
assert.throws(() => varianteWindows1252(octets), /guillemet/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('engendrer_livrables : les livrables (§ 10.3, § 15.5)', () => {
|
||
|
|
test("les sept fichiers, dans l'ordre ; les CSV de démonstration exportent les fichiers d'état écrits, la variante dérive des cas limites", () => {
|
||
|
|
const casLimites = readFileSync(CAS_LIMITES);
|
||
|
|
const fichiers = livrables({ casLimites });
|
||
|
|
assert.deepEqual(
|
||
|
|
fichiers.map(({ chemin }) => chemin),
|
||
|
|
LIVRABLES,
|
||
|
|
);
|
||
|
|
const octetsDe = new Map(fichiers.map(({ chemin, octets }) => [chemin, octets]));
|
||
|
|
for (const entree of CATALOGUE) {
|
||
|
|
assert.equal(
|
||
|
|
new TextDecoder().decode(octetsDe.get(`src/demo/livrees/${entree.cle}.gtt.json`)),
|
||
|
|
texteDeDemonstration(entree),
|
||
|
|
entree.cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
for (const cle of ['petite', 'grande']) {
|
||
|
|
const etat = new TextDecoder().decode(octetsDe.get(`src/demo/livrees/${cle}.gtt.json`));
|
||
|
|
assert.deepEqual(
|
||
|
|
octetsDe.get(`exemples/participants_demo_${cle}.csv`),
|
||
|
|
exporterParticipants(analyser(etat).charge.participants),
|
||
|
|
cle,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
assert.deepEqual(octetsDe.get('exemples/participants_cas_limites_cp1252.csv'), varianteWindows1252(casLimites));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('engendrer_livrables : la ligne de commande', () => {
|
||
|
|
test('écrit les sept livrables sous le dossier donné, relatif au répertoire courant, et rien ailleurs, au premier lancement comme au suivant', () =>
|
||
|
|
avecProjetCopie((dossier, lancerCopie) => {
|
||
|
|
// Le dossier entier, copie du projet comprise, est photographié avant et
|
||
|
|
// après chaque lancement : seuls les sept livrables, sous sortie/, y
|
||
|
|
// apparaissent. Le CSV des cas limites écrit à la main, tout autre
|
||
|
|
// fichier de la copie et le répertoire courant doivent y rester tels
|
||
|
|
// quels.
|
||
|
|
const sept = LIVRABLES.map((chemin) => `sortie/${chemin}`).sort();
|
||
|
|
const avant = instantane(dossier);
|
||
|
|
assert.ok(avant.has('projet/exemples/participants_cas_limites.csv'), 'la copie porte le CSV des cas limites');
|
||
|
|
const premier = lancerCopie('sortie');
|
||
|
|
assert.equal(premier.status, 0, premier.stderr);
|
||
|
|
const apres = instantane(dossier);
|
||
|
|
assert.deepEqual(differences(avant, apres), sept, "le premier lancement n'écrit pas exactement les sept livrables");
|
||
|
|
const attendus = livrables({ casLimites: readFileSync(CAS_LIMITES) });
|
||
|
|
for (const { chemin, octets } of attendus) {
|
||
|
|
assert.deepEqual(new Uint8Array(readFileSync(join(dossier, 'sortie', chemin))), octets, chemin);
|
||
|
|
assert.match(premier.stdout, new RegExp(`^ *créé +${echapper(chemin)} `, 'm'), chemin);
|
||
|
|
}
|
||
|
|
// Relancé, le script ne touche rien hors des sept livrables, qu'il
|
||
|
|
// réécrit sans en changer un octet : leurs dates peuvent avancer, leurs
|
||
|
|
// octets non.
|
||
|
|
const second = lancerCopie('sortie');
|
||
|
|
assert.equal(second.status, 0, second.stderr);
|
||
|
|
assert.deepEqual(
|
||
|
|
differences(apres, instantane(dossier)).filter((chemin) => !sept.includes(chemin)),
|
||
|
|
[],
|
||
|
|
'le lancement suivant écrit autre chose que les sept livrables',
|
||
|
|
);
|
||
|
|
for (const { chemin, octets } of attendus) {
|
||
|
|
assert.deepEqual(new Uint8Array(readFileSync(join(dossier, 'sortie', chemin))), octets, chemin);
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
test('relancé, il réécrit les mêmes octets, et le dit', () =>
|
||
|
|
avecDossier((dossier) => {
|
||
|
|
const premier = lancer(dossier, '.');
|
||
|
|
assert.equal(premier.status, 0, premier.stderr);
|
||
|
|
const octets = LIVRABLES.map((chemin) => readFileSync(join(dossier, chemin)));
|
||
|
|
const second = lancer(dossier, dossier);
|
||
|
|
assert.equal(second.status, 0, second.stderr);
|
||
|
|
LIVRABLES.forEach((chemin, rang) => {
|
||
|
|
assert.deepEqual(readFileSync(join(dossier, chemin)), octets[rang], chemin);
|
||
|
|
assert.match(second.stdout, new RegExp(`^ *inchangé +${echapper(chemin)} `, 'm'), chemin);
|
||
|
|
});
|
||
|
|
}));
|
||
|
|
|
||
|
|
test("une destination qui ne s'écrit pas sort en 1 en nommant la cause", () =>
|
||
|
|
avecDossier((dossier) => {
|
||
|
|
writeFileSync(join(dossier, 'occupe'), 'un fichier, pas un dossier\n');
|
||
|
|
const resultat = lancer(dossier, 'occupe');
|
||
|
|
assert.equal(resultat.status, 1, resultat.stdout);
|
||
|
|
assert.match(resultat.stderr, /^Livrables : .*occupe/m);
|
||
|
|
assert.deepEqual(fichiersSous(dossier), ['occupe']);
|
||
|
|
}));
|
||
|
|
|
||
|
|
test("un argument de trop, ou une option, sort en 2 en rappelant l'usage, sans rien écrire", () =>
|
||
|
|
avecDossier((dossier) => {
|
||
|
|
for (const arguments_ of [['a', 'b'], ['--aide']]) {
|
||
|
|
const resultat = lancer(dossier, ...arguments_);
|
||
|
|
assert.equal(resultat.status, 2, arguments_.join(' '));
|
||
|
|
assert.match(resultat.stderr, /usage : node scripts\/engendrer_livrables\.js \[destination\]/);
|
||
|
|
}
|
||
|
|
assert.deepEqual(fichiersSous(dossier), []);
|
||
|
|
}));
|
||
|
|
});
|