gestion_table_tournante_libre/src/stockage/depot.test.js
Mathieu Benoit 0bc48fa1ae [FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.

--- FR ---

[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions

La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.

Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00

2868 lines
138 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves du dépôt (§ 8.1, § 8.2, § 8.4, § 8.6, § 8.8, § 8.9, § 14.10), toutes
// sur le système de fichiers d'épreuve, chacune comptant ses écritures : une
// ouverture n'en fait aucune. Un événement se crée, se liste et s'ouvre ; un
// geste donne une entrée puis un état ; une panne à chaque frontière d'un geste
// se reprend à l'ouverture, sans regarder la date des fichiers ; un état
// illisible nomme ses secours, qui s'ouvrent ; le journal s'apparie par
// identifiant, se répare au premier geste qui suit l'ouverture et s'élague ;
// une proposition fautive s'écarte, une périmée se garde ; le verrou ;
// l'écriture ailleurs. Les noms d'épreuve sont inventés ; les versions viennent
// du module engendré.
import assert from 'node:assert/strict';
import { PanneSimulee, creerFichiersSimules } from '../../test/fichiers_simules.js';
import { describe, test } from '../../test/lanceur.js';
import { versionVoisine } from '../../test/version_voisine.js';
import { VERSION } from '../version.genere.js';
import { serialiser, serialiserCharge } from './canonique.js';
import {
basesPrises,
creer,
deverrouiller,
enregistrer,
enregistrerAilleurs,
lister,
nommer,
ouvrir,
ouvrirSecours,
verrouiller,
} from './depot.js';
import { analyser, creerCharge } from './document.js';
import { ErreurStockage } from './erreurs.js';
import { PLAFOND_ENTREES, elaguer, fil, ligneJalon, ligneOuverture, lireJournal, reconstruire } from './journal.js';
import { SUFFIXES } from './noms.js';
const V1 = VERSION.affichee;
const V2 = versionVoisine(V1);
// La racine documents du système d'épreuve, à son chemin par défaut.
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre';
const DOC = { id: 'documents', chemin: DOCUMENTS };
const BASE = 'Soirée des Lucioles';
const ETAT = `${BASE}${SUFFIXES.etat}`;
const PRECEDENT = `${BASE}${SUFFIXES.precedent}`;
const JOURNAL = `${BASE}${SUFFIXES.journal}`;
const VERROU = `${BASE}${SUFFIXES.verrou}`;
const REPRISE = "Reprise de l'état trouvé sur le disque";
// Horodatage d'affichage de l'instant n : une minute par instant à partir de
// 8 h, le 17 mai 2031, à l'heure de l'Est.
function heure(n) {
const minutes = 8 * 60 + n;
const deux = (valeur) => String(valeur).padStart(2, '0');
return `2031-05-17T${deux(Math.floor(minutes / 60))}:${deux(minutes % 60)}:00-04:00`;
}
const CREATION = { libelle: "Création de l'événement", nomJalon: 'Création', horodatage: heure(0), produitVersion: V1 };
const geste = (n, plus = {}) => ({ libelle: `Geste ${n}`, horodatage: heure(n), produitVersion: V1, ...plus });
// --- Les charges -----------------------------------------------------------
const personne = (id, nom, prenom, appartenance) => ({
id,
nom,
prenom,
appartenance,
courriel: null,
titrePressenti: null,
notes: null,
exclu: false,
});
const table = (id, x) => ({ id, numero: id, sieges: null, forme: 'ronde', position: { x, y: 0 } });
// Placement de deux tours sur les tables 1 et 2, deux places chacune ; le
// premier tour est donné, le second fixé.
function placement(id, premierTour) {
return {
id,
graine: 48271 + id,
arret: 200000,
historique: 1000,
produitVersion: V1,
siegesAttribues: false,
tables: [1, 2],
capacites: [2, 2],
tours: 2,
participants: [1, 2, 3, 4],
placement: [
{ sieges: premierTour, reserve: [] },
{ sieges: [[1, 4], [2, 3]], reserve: [] },
],
};
}
// Retenu repris d'un placement, ses champs d'identification retirés.
function retenuDe({ id, siegesAttribues, tables, capacites, tours, participants, placement: plan }) {
return { proposition: id, siegesAttribues, tables, capacites, tours, participants, placement: plan };
}
// Charge d'épreuve : quatre personnes, deux tables, une réservation, un titre,
// une proposition cohérente.
function chargeEssai({ id = 'evt-lucioles', nom = BASE } = {}) {
const charge = creerCharge({ id, nom, siegesParDefaut: 2, tours: 2 });
charge.evenement.etat = 'propose';
charge.prochainsIds = { participant: 5, table: 3, proposition: 2 };
charge.participants = [
personne(1, 'Ombrelle', 'Iris', 'Club des Merles'),
personne(2, 'Grisaille', null, 'Club des Merles'),
personne(3, 'Pervenche', 'Théo', null),
personne(4, 'Lacasse', 'Ondine', 'Société Alpha'),
];
charge.tables = [table(1, 0), table(2, 250)];
charge.reservations = [{ participant: 1, table: 1, siege: 1, portee: 'tous', tour: null }];
charge.titres = [{ table: 1, siege: 1, libelle: 'animation' }];
charge.propositions = [placement(1, [[1, 3], [2, 4]])];
return charge;
}
// La charge d'épreuve et une seconde proposition qui se contredit : la
// personne 3 assise deux fois au premier tour, la 4 nulle part. examiner
// l'écarte (§ 8.9, point 3) : la charge ouverte n'est plus celle du fichier,
// ni celle que restitue le journal.
function avecFautive() {
const charge = chargeEssai();
charge.propositions.push(placement(2, [[1, 3], [2, 3]]));
charge.prochainsIds.proposition = 3;
return charge;
}
// La charge reçue, la note de sa première personne changée : un geste.
function modifier(charge, n) {
const suivante = structuredClone(charge);
suivante.participants[0].notes = `note ${n}`;
return suivante;
}
// La charge reçue, une personne de plus sous le prochain identifiant, le
// compteur relevé : un geste dont le correctif dépend de la charge d'où il
// part. Il pose la personne par son identifiant, ce qui lève sur une charge
// qui la porte déjà ; un correctif calculé depuis un instant d'avant
// l'inscription la pose de nouveau. La personne arrive exclue : aucune
// proposition ne la place, et aucune ne dérive.
function inscrire(charge, n) {
const suivante = structuredClone(charge);
const id = suivante.prochainsIds.participant;
suivante.participants.push({ ...personne(id, 'Brunelle', null, null), notes: `inscrite au geste ${n}`, exclu: true });
suivante.prochainsIds.participant = id + 1;
return suivante;
}
// Le geste n sur la charge reçue : une inscription quand n est impair, la
// note changée sinon. Une note se remplace d'où que parte son correctif ;
// une inscription sur deux gestes donne à deux instants voisins des listes
// de personnes différentes, si bien qu'exigerEquivalence voit un correctif
// posé depuis une autre charge que la sienne (§ 14.2).
const gesteSur = (charge, n) => (n % 2 === 1 ? inscrire(charge, n) : modifier(charge, n));
const texteDeCharge = (charge) => serialiserCharge(charge);
const etatTexte = (charge, revision = 1, produitVersion = V1) => serialiser(charge, { revision, produitVersion });
// --- Les disques -----------------------------------------------------------
// Un disque neuf dont la racine documents existe, et son premier processus.
function nouveauDisque(options = {}) {
const disque = new Map();
const fs = creerFichiersSimules({ disque, ...options });
fs.deposerDossier('documents');
return { disque, fs };
}
// Un disque neuf dont documents porte les fichiers [nom, texte, modifie?].
function disqueAvec(fichiers) {
const { disque, fs } = nouveauDisque();
for (const [nom, texte, modifie] of fichiers) fs.deposer('documents', nom, texte, modifie);
return { disque, fs };
}
// Fichiers de documents, [nom, texte], par nom, lus par un processus à part.
async function fichiersDe(disque) {
const lecteur = creerFichiersSimules({ disque });
const entrees = await lecteur.lister(DOC, '');
return entrees.filter(({ type }) => type === 'fichier').map(({ nom }) => [nom, lecteur.contenu('documents', nom)]);
}
// Les fichiers donnés, le texte de nom remplacé, ou retiré quand il est null.
function remplacer(fichiers, nom, texte) {
const autres = fichiers.filter(([autre]) => autre !== nom);
return texte === null ? autres : [...autres, [nom, texte]];
}
const texteDe = (fichiers, nom) => fichiers.find(([autre]) => autre === nom)[1];
// Un disque neuf dont documents porte les fichiers, nom posé en octets.
function disqueAvecOctets(fichiers, nom, octets) {
const { disque, fs } = disqueAvec(remplacer(fichiers, nom, null));
fs.deposerOctets('documents', nom, octets);
return { disque, fs };
}
// Les octets UTF-8 du texte, son premier « é » (C3 A9) remplacé par l'octet
// E9 qu'écrit un éditeur réglé en windows-1252 : une suite qui n'est pas de
// l'UTF-8.
function malEncode(texte) {
const octets = [...new TextEncoder().encode(texte)];
const rang = octets.findIndex((octet, i) => octet === 0xc3 && octets[i + 1] === 0xa9);
assert.ok(rang >= 0, 'aucun « é » à mal encoder');
octets.splice(rang, 2, 0xe9);
return new Uint8Array(octets);
}
// Les octets UTF-8 d'un texte de journal, sa ligne de rang donné (0 pour
// l'ouverture) ouverte par l'octet E9 suivi de « { » : une suite qui n'est
// pas de l'UTF-8, sur cette ligne seule.
function ligneMalEncodee(texte, rang) {
const lignes = texte.split('\n');
assert.ok(rang < lignes.length - 1, `aucune ligne ${rang}`);
const avant = new TextEncoder().encode(lignes.slice(0, rang).map((ligne) => `${ligne}\n`).join(''));
const apres = new TextEncoder().encode(lignes.slice(rang).join('\n'));
return new Uint8Array([...avant, 0xe9, ...apres]);
}
// Le rang de la ligne d'un texte de journal qui porte l'entrée de révision
// donnée.
const rangDeLEntree = (texte, revision) =>
texte.split('\n').findIndex((ligne) => ligne.startsWith('{"type":"entree"') && JSON.parse(ligne).revision === revision);
// Ouvre base sur le disque par un processus neuf, en affirmant qu'ouvrir
// n'écrit rien (§ 8.4).
async function rouvrir(disque, base) {
const fs = creerFichiersSimules({ disque });
const ouvert = await ouvrir(fs, DOC, base);
assert.equal(fs.compteurs.ecritures, 0, 'ouvrir écrit');
return ouvert;
}
// Inscrit, dans l'ordre, chaque appel d'une primitive d'écriture de fichier
// ou de dossier : [primitive, chemin].
function tracer(fs) {
const trace = [];
for (const nom of ['ecrireAtomique', 'ajouterLigne', 'creerDossier', 'deplacer', 'supprimer']) {
const primitive = fs[nom];
fs[nom] = (racine, chemin, ...reste) => {
trace.push([nom, chemin]);
return primitive(racine, chemin, ...reste);
};
}
return trace;
}
// Dès l'écriture du rang-ième .precedent, le premier par défaut, les
// renommages de sa racine sont refusés : l'écriture atomique de l'état qui
// suit échoue sans toucher sa cible (§ 8.8). Rend la fonction qui retire ce
// piège.
function refuserRenommageApresPrecedent(fs, rang = 1) {
const ecrire = fs.ecrireAtomique;
let precedents = 0;
fs.ecrireAtomique = async (racine, chemin, texte) => {
await ecrire(racine, chemin, texte);
if (!chemin.endsWith(SUFFIXES.precedent)) return;
precedents += 1;
if (precedents >= rang) fs.pannes.renommage(racine.id);
};
return () => {
fs.ecrireAtomique = ecrire;
fs.pannes.renommage('documents', 0);
};
}
// Rend la raison du rejet de la promesse ; échoue quand elle se résout, par
// un constat qui se termine sur message quand il est donné.
async function rejet(promesse, message) {
let raison;
await assert.rejects(
promesse,
(erreur) => {
raison = erreur;
return true;
},
message,
);
return raison;
}
// Attend le rejet d'une ErreurStockage de ce code, et la rend ; message
// nomme le cas d'une boucle dont la promesse se résout.
async function echec(promesse, code, message) {
const erreur = await rejet(promesse, message);
assert.ok(erreur instanceof ErreurStockage, String(erreur));
assert.equal(erreur.code, code, String(erreur));
return erreur;
}
// Un geste ⇔ une entrée ⇔ un état (§ 8.2) : la dernière entrée du journal
// porte la révision de l'état et restitue exactement sa charge ; le journal se
// lit en entier.
function exigerEquivalence(fs, base, { racine = 'documents', journal = base } = {}) {
const etat = analyser(fs.contenu(racine, `${base}${SUFFIXES.etat}`));
const lu = lireJournal(fs.contenu(racine, `${journal}${SUFFIXES.journal}`));
const derniere = lu.entrees.at(-1);
assert.equal(lu.ecartees, 0);
assert.equal(derniere.revision, etat.entete.revision);
assert.equal(texteDeCharge(reconstruire(lu, derniere.revision)), texteDeCharge(etat.charge));
return lu;
}
const revisionsEtLibelles = (journal) => journal.entrees.map(({ revision, libelle }) => [revision, libelle]);
// Un événement créé, puis deux gestes, une note puis une inscription
// (gesteSur) : le .precedent porte la révision 2, l'état et le journal la 3,
// et ces deux instants n'ont pas les mêmes personnes. Rend ses fichiers et le
// dernier Ouvert.
async function troisRevisions() {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
for (const n of [2, 3]) ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, n), geste(n));
return { fichiers: await fichiersDe(disque), ouvert };
}
// Le journal d'un autre événement, créé sur un disque à part : posé sous la
// base, il est JOURNAL_ETRANGER.
async function journalEtranger() {
const { fs } = nouveauDisque();
await creer(fs, DOC, chargeEssai({ id: 'evt-gala', nom: "Gala d'automne" }), CREATION);
return fs.contenu('documents', `Gala d'automne${SUFFIXES.journal}`);
}
// L'Ouvert que rend ouvrirAinsi sur un disque neuf qui porte fichiers, par un
// processus neuf, sans rien écrire, ses avertissements de codes attendus :
// pour exigerChaqueCoupure.
function depuisFichiers(fichiers, ouvrirAinsi, codes) {
return async () => {
const { disque, fs } = disqueAvec(fichiers);
const ouvert = await ouvrirAinsi(fs);
assert.equal(fs.compteurs.ecritures, 0, 'ouvrir écrit');
assert.deepEqual(
ouvert.avertissements.map(({ code }) => code),
codes,
);
return { disque, ouvert };
};
}
// Chaque coupure du premier geste qui part de l'Ouvert que rend depart, sur un
// disque neuf : après n écritures réussies, de n = 0 jusqu'au geste achevé. Un
// premier passage sans coupure désigne le journal où le geste s'inscrit, par
// la base que rend son Ouvert : celle de l'événement, ou celle que le geste
// dérive quand le journal de la base porte un autre événement. Rouvert après
// la coupure, l'événement rend le geste dès que sa ligne est à ce journal — à
// sa révision, rattrapé en mémoire, sans autre avertissement que
// JOURNAL_REPRIS (§ 8.2) ; achevé, le geste est la dernière entrée de ce
// journal. Rend le nombre de coupures tombées entre la ligne du geste et l'état
// écrit : l'épreuve en exige au moins une.
async function exigerChaqueCoupure(nom, depart) {
const options = geste(99, { libelleReprise: REPRISE });
const temoin = await depart();
const { baseJournal } = await enregistrer(temoin.ouvert, modifier(temoin.ouvert.charge, 99), options);
const journal = `${baseJournal}${SUFFIXES.journal}`;
let entreLigneEtEtat = 0;
for (let n = 0; ; n += 1) {
const { disque, ouvert } = await depart();
const apres = modifier(ouvert.charge, 99);
ouvert.fs.pannes.couperApres(n);
let acheve = true;
try {
await enregistrer(ouvert, apres, options);
} catch (erreur) {
assert.ok(erreur instanceof PanneSimulee, `${nom}, coupure après ${n} écritures : ${String(erreur)}`);
acheve = false;
}
const message = `${nom}, coupure après ${n} écritures`;
const texte = ouvert.fs.contenu('documents', journal);
const derniere = texte === null ? undefined : lireJournal(texte).entrees.at(-1);
if (acheve) assert.equal(derniere?.libelle, options.libelle, `${message} : le geste achevé finit ${journal}`);
if (derniere?.libelle === options.libelle) {
const rouvert = await rouvrir(disque, BASE);
assert.equal(texteDeCharge(rouvert.charge), texteDeCharge(apres), message);
assert.equal(rouvert.revision, derniere.revision, message);
assert.deepEqual(
rouvert.avertissements.filter(({ code }) => code !== 'JOURNAL_REPRIS'),
[],
message,
);
if (!acheve) entreLigneEtEtat += 1;
}
if (acheve) return entreLigneEtEtat;
}
}
describe('dépôt : créer, ouvrir, lister', () => {
test("créer écrit le journal, puis l'état ; ouvrir rend la même charge à la révision 1 et son journal d'une entrée, sans rien écrire", async () => {
const { disque, fs } = nouveauDisque();
const trace = tracer(fs);
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
assert.deepEqual(trace, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', ETAT],
]);
assert.equal(fs.compteurs.ecritures, 2);
assert.equal(cree.base, BASE);
assert.equal(cree.revision, 1);
const texteEtat = etatTexte(chargeEssai());
assert.equal(fs.contenu('documents', ETAT), texteEtat);
assert.equal(cree.texteEtat, texteEtat);
const journal = lireJournal(fs.contenu('documents', JOURNAL));
assert.equal(journal.evenement, 'evt-lucioles');
assert.deepEqual(revisionsEtLibelles(journal), [[1, CREATION.libelle]]);
assert.equal(journal.entrees[0].produitVersion, V1);
assert.equal(texteDeCharge(journal.entrees[0].instantane), texteDeCharge(chargeEssai()));
assert.deepEqual(
journal.jalons.map(({ revision, nom }) => [revision, nom]),
[[1, 'Création']],
);
const ouvert = await rouvrir(disque, BASE);
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(chargeEssai()));
assert.equal(ouvert.revision, 1);
assert.equal(ouvert.base, BASE);
assert.equal(ouvert.texteEtat, texteEtat);
assert.equal(ouvert.produitVersion, V1);
assert.equal(ouvert.formatPlusRecent, false);
assert.deepEqual(ouvert.journal, journal);
assert.deepEqual(ouvert.avertissements, []);
assert.deepEqual(ouvert.examen.fautives, []);
});
test("créer dérive la base du nom de l'événement sans reprendre une base du dossier, casse comprise", async () => {
const { fs } = disqueAvec([['SOIRÉE DES LUCIOLES.gtt.verrou', '{}\n']]);
const ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
assert.equal(ouvert.base, 'Soirée des Lucioles (2)');
assert.equal(fs.compteurs.ecritures, 2);
assert.equal(fs.contenu('documents', 'Soirée des Lucioles (2).gtt.json'), etatTexte(chargeEssai()));
});
test("créer refuse, sans rien écrire, une charge hors de sa forme ou dont l'état ne se relirait pas", async () => {
const { disque, fs } = nouveauDisque();
await assert.rejects(creer(fs, DOC, { ...chargeEssai(), inconnue: 1 }, CREATION), TypeError);
const orpheline = chargeEssai();
orpheline.titres = [{ table: 9, siege: 1, libelle: 'animation' }];
await assert.rejects(creer(fs, DOC, orpheline, CREATION), /ne se relirait pas/);
assert.equal(fs.compteurs.ecritures, 0);
assert.deepEqual(await fichiersDe(disque), []);
});
test('lister rend chaque événement du dossier, trié par nom, avec ou sans journal, sans rien écrire', async () => {
const { disque, fs } = nouveauDisque();
await creer(fs, DOC, chargeEssai(), CREATION);
const gala = chargeEssai({ id: 'evt-gala', nom: "Gala d'automne" });
gala.evenement.date = '2031-10-04';
fs.deposer('documents', "Gala d'automne.gtt.json", etatTexte(gala, 4), 12345);
const lecteur = creerFichiersSimules({ disque });
const entrees = await lecteur.lister(DOC, '');
const resumes = await lister(lecteur, DOC);
assert.equal(lecteur.compteurs.ecritures, 0);
assert.deepEqual(resumes, [
{
base: "Gala d'automne",
id: 'evt-gala',
nom: "Gala d'automne",
date: '2031-10-04',
etat: 'propose',
participants: 4,
modifie: 12345,
journal: null,
illisible: null,
doublon: false,
},
{
base: BASE,
id: 'evt-lucioles',
nom: BASE,
date: null,
etat: 'propose',
participants: 4,
modifie: entrees.find(({ nom }) => nom === ETAT).modifie,
journal: BASE,
illisible: null,
doublon: false,
},
]);
});
test("lister apparie par l'identifiant, jamais par le nom : journal renommé, état illisible et l'identifiant qui l'apparie, journal sans état, doublon, journal d'un autre", async () => {
const ouverture = (evenement) => `${ligneOuverture(evenement)}\n`;
const etat = (id, nom) => etatTexte(chargeEssai({ id, nom }));
const { fs } = disqueAvec([
// état lisible, son journal renommé à la main ; une copie de ce journal
['Aubepine.gtt.json', etat('evt-a', 'Aubépine'), 1000],
['Carnet.gtt-journal.jsonl', ouverture('evt-a'), 1001],
['Copie.gtt-journal.jsonl', ouverture('evt-a'), 1002],
// état illisible, son journal à côté
['Bleuet.gtt.json', '', 2000],
['Bleuet.gtt-journal.jsonl', ouverture('evt-b'), 2001],
// journal sans état
['Capucine.gtt-journal.jsonl', ouverture('evt-c'), 3000],
// deux états du même identifiant
['Dahlia.gtt.json', etat('evt-d', 'Dahlia'), 4000],
['Dahlia bis.gtt.json', etat('evt-d', 'Dahlia'), 4001],
// état lisible, à côté du journal d'un autre événement
['Esquisse.gtt.json', etat('evt-e', 'Esquisse'), 5000],
['Esquisse.gtt-journal.jsonl', ouverture('evt-x'), 5001],
// journal sans état, dont l'ouverture ne se lit pas
['Fougere.gtt-journal.jsonl', 'pas du JSON\n', 6000],
// état illisible dont le journal porte un autre événement, lisible lui
['Glycine.gtt.json', '{', 7000],
['Glycine.gtt-journal.jsonl', ouverture('evt-h'), 7001],
['Hortensia.gtt.json', etat('evt-h', 'Hortensia'), 8000],
// état illisible, ni .precedent ni journal
['Jacinthe.gtt.json', '{"entete":', 8500],
// état illisible, son .precedent lisible, sans journal
['Kalmia.gtt.json', '{', 8600],
['Kalmia.gtt.json.precedent', etat('evt-k', 'Kalmia'), 8601],
// ni état ni journal
['notes.txt', 'à lire', 9000],
]);
fs.deposerDossier('documents', 'Iris.gtt.json');
const lisible = (base, id, nom, modifie, journal, doublon = false) => ({
base,
id,
nom,
date: null,
etat: 'propose',
participants: 4,
modifie,
journal,
illisible: null,
doublon,
});
const illisible = (base, id, modifie, journal, raison) => ({
base,
id,
nom: null,
date: null,
etat: null,
participants: null,
modifie,
journal,
illisible: raison,
doublon: false,
});
assert.deepEqual(await lister(fs, DOC), [
lisible('Aubepine', 'evt-a', 'Aubépine', 1000, 'Carnet'),
lisible('Dahlia', 'evt-d', 'Dahlia', 4000, null, true),
lisible('Dahlia bis', 'evt-d', 'Dahlia', 4001, null, true),
lisible('Esquisse', 'evt-e', 'Esquisse', 5000, null),
lisible('Hortensia', 'evt-h', 'Hortensia', 8000, 'Glycine'),
illisible('Bleuet', 'evt-b', 2000, 'Bleuet', 'VIDE'),
illisible('Capucine', 'evt-c', 3000, 'Capucine', 'ABSENT'),
illisible('Fougere', null, 6000, 'Fougere', 'ABSENT'),
illisible('Glycine', null, 7000, null, 'JSON'),
illisible('Jacinthe', null, 8500, null, 'JSON'),
illisible('Kalmia', 'evt-k', 8600, null, 'JSON'),
]);
assert.equal(fs.compteurs.ecritures, 0);
});
});
describe('dépôt : un geste, une entrée, un état (§ 8.2)', () => {
test("chaque geste ajoute son entrée au journal, puis écrit le .precedent et l'état : la dernière entrée restitue la charge écrite", async () => {
const { fs } = nouveauDisque();
const trace = tracer(fs);
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
for (let n = 2; n <= 5; n += 1) {
const etatAvant = fs.contenu('documents', ETAT);
const depart = trace.length;
// Les gestes alternent note et inscription (gesteSur) ; le dernier
// revient à l'instant 3 : un retour est une entrée comme une autre, dont
// la charge est celle de sa cible (§ 8.3).
const retour = n === 5 ? { sens: 'revenir', retour: 3 } : {};
const apres = n === 5 ? reconstruire(ouvert.journal, 3) : gesteSur(ouvert.charge, n);
ouvert = await enregistrer(ouvert, apres, geste(n, retour));
assert.deepEqual(trace.slice(depart), [
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.equal(fs.contenu('documents', PRECEDENT), etatAvant);
assert.equal(ouvert.revision, n);
assert.equal(ouvert.texteEtat, fs.contenu('documents', ETAT));
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(apres));
const journal = exigerEquivalence(fs, BASE);
assert.equal(journal.entrees.length, n);
assert.deepEqual(ouvert.journal, journal);
}
assert.equal(fs.compteurs.ecritures, 2 + 4 * 3);
const derniere = ouvert.journal.entrees.at(-1);
assert.deepEqual([derniere.sens, derniere.retour], ['revenir', 3]);
assert.equal(fil(ouvert.journal).position, 3);
});
test("une entrée note la construction quand elle change, et l'état porte celle du dernier geste", async () => {
const { fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3, { produitVersion: V2 }));
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { produitVersion: V2 }));
const { entrees } = lireJournal(fs.contenu('documents', JOURNAL));
assert.deepEqual(
entrees.map(({ produitVersion }) => produitVersion),
[V1, null, V2, null],
);
assert.equal(analyser(fs.contenu('documents', ETAT)).entete.produitVersion, V2);
assert.equal(ouvert.produitVersion, V2);
assert.equal(fs.compteurs.ecritures, 2 + 3 * 3);
});
test("nommer ajoute un jalon sur l'instant courant, sans écrire l'état", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const etat = fs.contenu('documents', ETAT);
const trace = tracer(fs);
const nomme = await nommer(cree, { nom: 'Avant la génération', horodatage: heure(1) });
assert.deepEqual(trace, [['ajouterLigne', JOURNAL]]);
assert.equal(fs.contenu('documents', ETAT), etat);
assert.equal(nomme.revision, 1);
assert.deepEqual(
nomme.journal.jalons.map(({ revision, nom }) => [revision, nom]),
[
[1, 'Création'],
[1, 'Avant la génération'],
],
);
assert.deepEqual((await rouvrir(disque, BASE)).journal, nomme.journal);
});
test("un journal effacé entre deux gestes ne reçoit pas d'ajout : le geste suivant le réécrit en entier, ouverture et entrées en mémoire comprises, JOURNAL_RECREE ; un jalon de même ; puis les ajouts reprennent", async () => {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, 2), geste(2));
await fs.supprimer(DOC, JOURNAL);
const trace = tracer(fs);
ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, 3), geste(3));
assert.deepEqual(trace.splice(0), [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_RECREE', details: {} }]);
let journal = exigerEquivalence(fs, BASE);
assert.equal(journal.evenement, 'evt-lucioles');
assert.deepEqual(revisionsEtLibelles(journal), [
[1, CREATION.libelle],
[2, 'Geste 2'],
[3, 'Geste 3'],
]);
await fs.supprimer(DOC, JOURNAL);
trace.splice(0);
ouvert = await nommer(ouvert, { nom: 'Avant le tirage', horodatage: heure(4) });
assert.deepEqual(trace.splice(0), [['ecrireAtomique', JOURNAL]]);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_RECREE', details: {} }]);
journal = exigerEquivalence(fs, BASE);
assert.deepEqual(
journal.jalons.map(({ revision, nom }) => [revision, nom]),
[
[1, 'Création'],
[3, 'Avant le tirage'],
],
);
ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, 5), geste(5));
assert.deepEqual(trace.splice(0), [
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.deepEqual(ouvert.avertissements, []);
assert.equal(exigerEquivalence(fs, BASE).entrees.length, 4);
assert.deepEqual((await rouvrir(disque, BASE)).avertissements, []);
});
test("le journal présent se reconnaît comme le système compare les noms : sans égard à la casse, une base d'une autre casse ajoute en fin ; sensible à la casse, le nom exact", async () => {
const insensible = nouveauDisque();
await creer(insensible.fs, DOC, chargeEssai(), CREATION);
const autreCasse = await ouvrir(insensible.fs, DOC, BASE.toUpperCase());
const trace = tracer(insensible.fs);
const suite = await enregistrer(autreCasse, modifier(autreCasse.charge, 2), geste(2));
assert.deepEqual(trace[0], ['ajouterLigne', `${BASE.toUpperCase()}${SUFFIXES.journal}`]);
assert.deepEqual(suite.avertissements, []);
assert.equal(exigerEquivalence(insensible.fs, BASE).entrees.length, 2);
const sensible = nouveauDisque({ separateur: '/', insensibleCasse: false, documents: '/exemple/Documents' });
const racine = { id: 'documents', chemin: '/exemple/Documents' };
let ouvert = await creer(sensible.fs, racine, chargeEssai(), CREATION);
sensible.fs.deposer('documents', `${BASE.toUpperCase()}${SUFFIXES.journal}`, 'un autre fichier');
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
assert.deepEqual(ouvert.avertissements, []);
await sensible.fs.supprimer(racine, JOURNAL);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3));
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_RECREE', details: {} }]);
assert.equal(sensible.fs.contenu('documents', `${BASE.toUpperCase()}${SUFFIXES.journal}`), 'un autre fichier');
});
test("un dossier qui ne se liste plus au moment d'ajouter au journal lève ECRITURE sur le journal, sans rien écrire : ENOENT quand il a disparu, la cause d'une lecture refusée sinon", async () => {
for (const [nom, refus, cause] of [
['disparu', (chemin) => new ErreurStockage('ABSENT', { chemin }), 'ENOENT'],
['illisible', (chemin) => new ErreurStockage('LECTURE', { chemin, dossier: DOCUMENTS, cause: 'EACCES' }), 'EACCES'],
]) {
const { disque, fs } = nouveauDisque();
const ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
const avant = await fichiersDe(disque);
const ecritures = fs.compteurs.ecritures;
fs.lister = async (racine, chemin) => {
throw refus(chemin);
};
const erreur = await echec(enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2)), 'ECRITURE', nom);
assert.deepEqual(erreur.details, { chemin: JOURNAL, dossier: DOCUMENTS, cause }, nom);
assert.equal(fs.compteurs.ecritures, ecritures, nom);
assert.deepEqual(await fichiersDe(disque), avant, nom);
}
});
test("un geste refusé n'écrit rien : charge hors de sa forme, état qui ne se relirait pas, version absente, libellé ou nom vides", async () => {
const { disque, fs } = nouveauDisque();
const ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
const avant = await fichiersDe(disque);
const ecritures = fs.compteurs.ecritures;
await assert.rejects(enregistrer(ouvert, { ...ouvert.charge, inconnue: 1 }, geste(2)), TypeError);
const orpheline = modifier(ouvert.charge, 2);
orpheline.reservations = [{ participant: 9, table: 1, siege: null, portee: 'tous', tour: null }];
await assert.rejects(enregistrer(ouvert, orpheline, geste(2)), /ne se relirait pas/);
const sansVersion = { libelle: 'Geste 2', horodatage: heure(2) };
await assert.rejects(enregistrer(ouvert, modifier(ouvert.charge, 2), sansVersion), TypeError);
await assert.rejects(enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2, { libelle: '' })), TypeError);
await assert.rejects(nommer(ouvert, { nom: '', horodatage: heure(2) }), TypeError);
assert.equal(fs.compteurs.ecritures, ecritures);
assert.deepEqual(await fichiersDe(disque), avant);
});
});
describe("dépôt : une panne à chaque frontière d'un geste (§ 8.8, § 14.10)", () => {
test("coupure après la ligne de journal : la réouverture rattrape le geste en mémoire sans écrire, et le geste suivant écrit d'abord l'état rattrapé", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const etatCree = fs.contenu('documents', ETAT);
fs.pannes.couperApres(1);
const apres = modifier(cree.charge, 2);
await assert.rejects(enregistrer(cree, apres, geste(2)), PanneSimulee);
assert.equal(fs.contenu('documents', ETAT), etatCree);
assert.equal(fs.contenu('documents', PRECEDENT), null);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_REPRIS', details: {} }]);
assert.equal(ouvert.revision, 2);
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(apres));
assert.equal(ouvert.texteEtat, etatCree);
const trace = tracer(ouvert.fs);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3));
// L'état rattrapé s'écrit avant la ligne du geste : une coupure entre elle
// et l'état ne laisse le journal qu'une entrée en avance (§ 8.2).
assert.deepEqual(trace, [
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.equal(suite.revision, 3);
assert.equal(ouvert.fs.contenu('documents', PRECEDENT), etatTexte(apres, 2));
exigerEquivalence(ouvert.fs, BASE);
assert.deepEqual((await rouvrir(disque, BASE)).avertissements, []);
});
test('coupure après le .precedent : la réouverture rattrape le geste de même', async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const etatCree = fs.contenu('documents', ETAT);
fs.pannes.couperApres(2);
const apres = modifier(cree.charge, 2);
await assert.rejects(enregistrer(cree, apres, geste(2)), PanneSimulee);
assert.equal(fs.contenu('documents', PRECEDENT), etatCree);
assert.equal(fs.contenu('documents', ETAT), etatCree);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_REPRIS', details: {} }]);
assert.equal(ouvert.revision, 2);
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(apres));
});
test("renommage de l'état refusé : ETAT_NON_ECRIT nomme l'état et son dossier, l'état reste intact, le .precedent se lit, et l'Ouvert avancé continue", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const etatCree = fs.contenu('documents', ETAT);
const retirer = refuserRenommageApresPrecedent(fs);
const apres = modifier(cree.charge, 2);
const erreur = await echec(enregistrer(cree, apres, geste(2)), 'ETAT_NON_ECRIT');
const { ouvert: avance, ...details } = erreur.details;
assert.deepEqual(details, { chemin: ETAT, dossier: DOCUMENTS, cause: 'EBUSY' });
assert.ok(!erreur.message.includes('Ombrelle'), "le message de l'erreur ne porte pas l'Ouvert");
assert.equal(fs.contenu('documents', ETAT), etatCree);
assert.equal(fs.contenu('documents', `${ETAT}.ecriture`), null);
assert.equal(analyser(fs.contenu('documents', PRECEDENT)).entete.revision, 1);
assert.equal(avance.revision, 2);
assert.equal(avance.texteEtat, etatCree);
assert.equal(texteDeCharge(avance.charge), texteDeCharge(apres));
assert.equal(avance.journal.entrees.length, 2);
retirer();
const suite = await enregistrer(avance, modifier(avance.charge, 3), geste(3));
assert.equal(suite.revision, 3);
// Le geste qui suit écrit d'abord l'état de l'Ouvert avancé, que le
// renommage refusé n'a pas posé : il est le .precedent.
assert.equal(fs.contenu('documents', PRECEDENT), etatTexte(apres, 2));
exigerEquivalence(fs, BASE);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual([ouvert.revision, ouvert.avertissements], [3, []]);
});
test("ajout au journal refusé : ECRITURE nomme le journal et son dossier, et rien n'a changé ; sans base de journal (JOURNAL_ETRANGER), un dossier qui ne se liste plus lève la même ECRITURE, sur le journal de la base", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const avant = await fichiersDe(disque);
fs.pannes.ecriture('documents');
const erreur = await echec(enregistrer(cree, modifier(cree.charge, 2), geste(2)), 'ECRITURE');
assert.deepEqual(erreur.details, { chemin: JOURNAL, dossier: DOCUMENTS, cause: 'EROFS' });
assert.equal(fs.compteurs.ecritures, 3);
assert.deepEqual(await fichiersDe(disque), avant);
// Le journal de la base porte un autre événement : le geste cherche dans
// le dossier le journal du sien (§ 8.6). Par cas, ce que devient le
// dossier pour le processus, et la cause attendue : retiré avec son
// support, chaque lecture lève ABSENT, sa liste comprise, et la cause est
// ENOENT ; illisible, sa liste lève LECTURE, comme sous la coquille, dont
// la cause passe telle quelle.
const poses = [
[ETAT, etatTexte(chargeEssai())],
[JOURNAL, await journalEtranger()],
];
const pannes = [
[
'support retiré',
(lecteur) => {
const absent = async (racine, chemin) => {
throw new ErreurStockage('ABSENT', { chemin });
};
lecteur.lireTexte = absent;
lecteur.lister = absent;
},
'ENOENT',
],
[
'dossier illisible',
(lecteur) => {
lecteur.lister = async (racine, chemin) => {
throw new ErreurStockage('LECTURE', { chemin, dossier: DOCUMENTS, cause: 'EACCES' });
};
},
'EACCES',
],
// Le support se retire entre la liste où le geste cherche le journal de
// l'événement et celle d'où il dérive une base libre.
[
'support retiré entre les deux listes',
(lecteur) => {
const listerAvant = lecteur.lister;
let appels = 0;
lecteur.lister = async (racine, chemin) => {
appels += 1;
if (appels > 1) throw new ErreurStockage('ABSENT', { chemin });
return listerAvant(racine, chemin);
};
},
'ENOENT',
],
];
for (const [nom, rompre, cause] of pannes) {
const etranger = disqueAvec(poses);
const ouvert = await ouvrir(etranger.fs, DOC, BASE);
assert.equal(etranger.fs.compteurs.ecritures, 0, `${nom} : ouvrir écrit`);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ETRANGER', details: { evenement: 'evt-gala' } }], nom);
const posesAvant = await fichiersDe(etranger.disque);
rompre(etranger.fs);
const options = geste(2, { libelleReprise: REPRISE });
const refus = await echec(enregistrer(ouvert, modifier(ouvert.charge, 2), options), 'ECRITURE', nom);
assert.deepEqual(refus.details, { chemin: JOURNAL, dossier: DOCUMENTS, cause }, nom);
assert.equal(etranger.fs.compteurs.ecritures, 0, nom);
assert.deepEqual(await fichiersDe(etranger.disque), posesAvant, nom);
}
});
test('la reprise ne se fonde pas sur la date des fichiers : un dossier recopié, aux dates égales ou inversées, se rattrape pareil', async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
fs.pannes.couperApres(1);
const apres = modifier(cree.charge, 2);
await assert.rejects(enregistrer(cree, apres, geste(2)), PanneSimulee);
const fichiers = await fichiersDe(disque);
const dates = [() => 0, (nom) => (nom === JOURNAL ? 0 : 9e11)];
for (const date of dates) {
const copie = disqueAvec(fichiers.map(([nom, texte]) => [nom, texte, date(nom)]));
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_REPRIS', details: {} }]);
assert.equal(ouvert.revision, 2);
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(apres));
}
});
test('une dernière ligne de journal tronquée, sans fin de ligne, est écartée et comptée ; le premier geste réécrit le journal sans elle', async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
await enregistrer(cree, modifier(cree.charge, 2), geste(2));
const fichiers = await fichiersDe(disque);
const journal = texteDe(fichiers, JOURNAL);
const copie = disqueAvec(remplacer(fichiers, JOURNAL, `${journal}{"type":"entree","revision":3,"lib`));
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ECARTEES', details: { n: 1 } }]);
assert.equal(ouvert.revision, 2);
const trace = tracer(ouvert.fs);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3));
assert.deepEqual(trace, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
const reecrit = ouvert.fs.contenu('documents', JOURNAL);
assert.ok(reecrit.startsWith(journal), 'les lignes lues se recopient telles quelles');
assert.equal(exigerEquivalence(ouvert.fs, BASE).entrees.length, 3);
assert.deepEqual(suite.avertissements, []);
assert.deepEqual((await rouvrir(copie.disque, BASE)).avertissements, []);
});
test("le rattrapage prend la version de la construction qui a écrit le geste rattrapé : l'Ouvert la porte, l'écriture ailleurs et l'état réaligné l'inscrivent", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
fs.pannes.couperApres(1);
await assert.rejects(enregistrer(cree, modifier(cree.charge, 2), geste(2, { produitVersion: V2 })), PanneSimulee);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_REPRIS', details: {} }]);
assert.equal(ouvert.produitVersion, V2);
const ailleurs = await enregistrerAilleurs(ouvert, DOC, { existantes: [] });
const etatAilleurs = analyser(ouvert.fs.contenu('documents', `${ailleurs.base}${SUFFIXES.etat}`));
assert.equal(etatAilleurs.entete.produitVersion, V2);
// Le geste suivant écrit d'abord l'état rattrapé, à la version de son
// entrée : il est ensuite le .precedent.
await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3));
assert.equal(analyser(ouvert.fs.contenu('documents', PRECEDENT)).entete.produitVersion, V2);
exigerEquivalence(ouvert.fs, BASE);
});
test("un refus d'écriture pendant l'alignement lève ECRITURE, le geste hors du journal ; le même Ouvert le réessaie dans le même journal, sans doubler la reprise ni dériver un autre journal", async () => {
const { fichiers } = await troisRevisions();
const tete = `${texteDe(fichiers, JOURNAL).split('\n').slice(0, 3).join('\n')}\n`;
// Par cas : les fichiers posés, l'ouverture, la base du journal de
// l'événement et ses entrées après le refus. Quand le journal de la base
// porte un autre événement, la reprise crée celui de l'événement sous une
// base que son nom dérive : le nouvel essai doit l'y retrouver.
const cas = [
[
'journal incomplet (JOURNAL_INCOMPLET)',
remplacer(fichiers, JOURNAL, tete),
(fs) => ouvrir(fs, DOC, BASE),
BASE,
[
[1, CREATION.libelle],
[2, REPRISE],
],
],
[
'secours .precedent sur un état lisible, le journal de la base à un autre événement (JOURNAL_ETRANGER)',
remplacer(fichiers, JOURNAL, await journalEtranger()),
(fs) => ouvrirSecours(fs, DOC, BASE, 'precedent'),
`${BASE} (2)`,
[[2, REPRISE]],
],
];
for (const [nom, poses, ouvrirAinsi, base, apresRefus] of cas) {
const { disque, fs } = disqueAvec(poses);
const ouvert = await ouvrirAinsi(fs);
assert.equal(fs.compteurs.ecritures, 0, `${nom} : l'ouverture écrit`);
const journal = `${base}${SUFFIXES.journal}`;
const retirer = refuserRenommageApresPrecedent(fs);
const apres = modifier(ouvert.charge, 4);
const options = geste(4, { libelleReprise: REPRISE });
const erreur = await echec(enregistrer(ouvert, apres, options), 'ECRITURE');
assert.deepEqual(erreur.details, { chemin: ETAT, dossier: DOCUMENTS, cause: 'EBUSY' }, nom);
assert.deepEqual(revisionsEtLibelles(lireJournal(fs.contenu('documents', journal))), apresRefus, nom);
assert.equal(fs.contenu('documents', ETAT), texteDe(fichiers, ETAT), nom);
const nomsApresRefus = (await fichiersDe(disque)).map(([fichier]) => fichier);
retirer();
const trace = tracer(fs);
const suite = await enregistrer(ouvert, apres, options);
assert.deepEqual(
trace,
[
['ecrireAtomique', journal],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
['ajouterLigne', journal],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
],
nom,
);
assert.deepEqual(
(await fichiersDe(disque)).map(([fichier]) => fichier),
nomsApresRefus,
nom,
);
assert.deepEqual([suite.revision, suite.baseJournal], [3, base], nom);
assert.deepEqual(
revisionsEtLibelles(exigerEquivalence(fs, BASE, { journal: base })),
[...apresRefus, [3, 'Geste 4']],
nom,
);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.baseJournal, rouvert.avertissements], [3, base, []], nom);
}
});
// Les ouvertures après lesquelles l'état sur le disque ne porte pas la
// révision de la dernière entrée du journal. Chacune pose ses fichiers, puis
// rend la fonction qui rouvre sur un disque neuf l'Ouvert d'où part le
// premier geste.
const departsMalAlignes = [
[
'journal incomplet de deux entrées (JOURNAL_INCOMPLET)',
async () => {
const { fichiers } = await troisRevisions();
const tete = `${texteDe(fichiers, JOURNAL).split('\n').slice(0, 3).join('\n')}\n`;
return depuisFichiers(remplacer(fichiers, JOURNAL, tete), (fs) => ouvrir(fs, DOC, BASE), ['JOURNAL_INCOMPLET']);
},
],
[
'journal en avance de deux entrées (JOURNAL_EN_AVANCE_DE)',
async () => {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
const etat2 = fs.contenu('documents', ETAT);
for (const n of [3, 4]) ouvert = await enregistrer(ouvert, modifier(ouvert.charge, n), geste(n));
const fichiers = remplacer(await fichiersDe(disque), ETAT, etat2);
return depuisFichiers(fichiers, (lecteur) => ouvrir(lecteur, DOC, BASE), ['JOURNAL_EN_AVANCE_DE']);
},
],
[
"journal en avance d'une entrée (JOURNAL_REPRIS)",
async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
fs.pannes.couperApres(1);
await assert.rejects(enregistrer(cree, modifier(cree.charge, 2), geste(2)), PanneSimulee);
return depuisFichiers(await fichiersDe(disque), (lecteur) => ouvrir(lecteur, DOC, BASE), ['JOURNAL_REPRIS']);
},
],
[
'secours .precedent sur un état lisible',
async () => {
const { fichiers } = await troisRevisions();
return depuisFichiers(fichiers, (fs) => ouvrirSecours(fs, DOC, BASE, 'precedent'), ['JOURNAL_EN_AVANCE_DE']);
},
],
[
'secours .precedent sur un état lisible, sans journal',
async () => {
const { fichiers } = await troisRevisions();
const sansJournal = remplacer(fichiers, JOURNAL, null);
return depuisFichiers(sansJournal, (fs) => ouvrirSecours(fs, DOC, BASE, 'precedent'), []);
},
],
[
'secours .precedent sur un état lisible, le journal de la base à un autre événement (JOURNAL_ETRANGER)',
async () => {
const { fichiers } = await troisRevisions();
const etranger = remplacer(fichiers, JOURNAL, await journalEtranger());
return depuisFichiers(etranger, (fs) => ouvrirSecours(fs, DOC, BASE, 'precedent'), ['JOURNAL_ETRANGER']);
},
],
[
'secours journal sur un état lisible, le journal incomplet',
async () => {
const { fichiers } = await troisRevisions();
const tete = `${texteDe(fichiers, JOURNAL).split('\n').slice(0, 3).join('\n')}\n`;
const incomplet = remplacer(fichiers, JOURNAL, tete);
return depuisFichiers(incomplet, (fs) => ouvrirSecours(fs, DOC, BASE, 'journal'), []);
},
],
[
"Ouvert avancé d'un ETAT_NON_ECRIT",
async () => async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
const retirer = refuserRenommageApresPrecedent(fs);
const erreur = await echec(enregistrer(cree, modifier(cree.charge, 2), geste(2)), 'ETAT_NON_ECRIT');
retirer();
return { disque, ouvert: erreur.details.ouvert };
},
],
[
"Ouvert avancé d'un ETAT_NON_ECRIT tombé après l'alignement d'un journal incomplet",
async () => {
const { fichiers } = await troisRevisions();
const tete = `${texteDe(fichiers, JOURNAL).split('\n').slice(0, 3).join('\n')}\n`;
const incomplet = remplacer(fichiers, JOURNAL, tete);
return async () => {
const { disque, fs } = disqueAvec(incomplet);
const ouvert = await ouvrir(fs, DOC, BASE);
assert.equal(fs.compteurs.ecritures, 0, 'ouvrir écrit');
// Le .precedent de l'alignement passe ; celui du geste arme le refus.
const retirer = refuserRenommageApresPrecedent(fs, 2);
const options = geste(4, { libelleReprise: REPRISE });
const erreur = await echec(enregistrer(ouvert, modifier(ouvert.charge, 4), options), 'ETAT_NON_ECRIT');
retirer();
return { disque, ouvert: erreur.details.ouvert };
};
},
],
];
for (const [nom, poser] of departsMalAlignes) {
test(`coupure à chaque écriture du premier geste après une ouverture mal alignée, ${nom} : le geste inscrit au journal se rouvre toujours`, async () => {
assert.ok((await exigerChaqueCoupure(nom, await poser())) > 0, nom);
});
}
});
describe('dépôt : un état illisible et ses secours (§ 8.8)', () => {
test("un état vide, tronqué ou aux comptes faux lève ETAT_ILLISIBLE, qui nomme la base, la raison et les secours lisibles, sans rien écrire", async () => {
const { fichiers } = await troisRevisions();
const etat = texteDe(fichiers, ETAT);
const cas = [
['', 'VIDE', null],
[etat.slice(0, Math.floor(etat.length / 2)), 'JSON', null],
[
etat.replace(/"comptes":\{"participants":(\d+)/, (_, n) => `"comptes":{"participants":${Number(n) + 1}`),
'COMPTES',
'entete.comptes.participants',
],
];
for (const [texte, raison, chemin] of cas) {
const { fs } = disqueAvec(remplacer(fichiers, ETAT, texte));
const erreur = await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, { base: BASE, raison, chemin, secours: { precedent: 2, journal: 3 } });
assert.equal(fs.compteurs.ecritures, 0);
}
});
test("ouvrirSecours ouvre le .precedent, puis le dernier instant du journal, sans écrire ni rendre un événement vide", async () => {
const { fichiers, ouvert: dernier } = await troisRevisions();
const precedent = analyser(texteDe(fichiers, PRECEDENT));
const { fs } = disqueAvec(remplacer(fichiers, ETAT, ''));
const parPrecedent = await ouvrirSecours(fs, DOC, BASE, 'precedent');
assert.equal(parPrecedent.revision, 2);
assert.equal(texteDeCharge(parPrecedent.charge), texteDeCharge(precedent.charge));
assert.equal(parPrecedent.texteEtat, null);
assert.deepEqual(parPrecedent.avertissements, [{ code: 'JOURNAL_EN_AVANCE_DE', details: { n: 1 } }]);
const parJournal = await ouvrirSecours(fs, DOC, BASE, 'journal');
assert.equal(parJournal.revision, 3);
assert.equal(texteDeCharge(parJournal.charge), texteDeCharge(dernier.charge));
assert.equal(parJournal.texteEtat, null);
assert.equal(parJournal.produitVersion, V1);
assert.deepEqual(parJournal.avertissements, []);
assert.deepEqual(
[parPrecedent, parJournal].map(({ charge }) => charge.participants.length),
[4, 5],
);
assert.equal(fs.compteurs.ecritures, 0);
});
test("le premier geste après un secours réécrit l'état et ne met pas l'état illisible dans le .precedent", async () => {
const { fichiers } = await troisRevisions();
const textePrecedent = texteDe(fichiers, PRECEDENT);
// Depuis le journal : le geste suit son dernier instant.
const parJournal = disqueAvec(remplacer(fichiers, ETAT, '{"entete":'));
const depuisJournal = await ouvrirSecours(parJournal.fs, DOC, BASE, 'journal');
assert.equal(parJournal.fs.compteurs.ecritures, 0, 'ouvrirSecours écrit');
const traceJournal = tracer(parJournal.fs);
const suiteJournal = await enregistrer(depuisJournal, modifier(depuisJournal.charge, 4), geste(4));
assert.deepEqual(traceJournal, [
['ajouterLigne', JOURNAL],
['ecrireAtomique', ETAT],
]);
assert.equal(parJournal.fs.contenu('documents', PRECEDENT), textePrecedent);
assert.equal(suiteJournal.revision, 4);
exigerEquivalence(parJournal.fs, BASE);
assert.deepEqual((await rouvrir(parJournal.disque, BASE)).avertissements, []);
// Depuis le .precedent : une entrée de reprise restitue sa charge, puis
// vient le geste. Sans libellé de reprise, le geste est refusé.
const parPrecedent = disqueAvec(remplacer(fichiers, ETAT, '{"entete":'));
const depuisPrecedent = await ouvrirSecours(parPrecedent.fs, DOC, BASE, 'precedent');
assert.equal(parPrecedent.fs.compteurs.ecritures, 0, 'ouvrirSecours écrit');
await assert.rejects(enregistrer(depuisPrecedent, modifier(depuisPrecedent.charge, 4), geste(4)), TypeError);
assert.equal(parPrecedent.fs.compteurs.ecritures, 0);
const tracePrecedent = tracer(parPrecedent.fs);
const reprise = geste(4, { libelleReprise: REPRISE });
const suitePrecedent = await enregistrer(depuisPrecedent, modifier(depuisPrecedent.charge, 4), reprise);
assert.deepEqual(tracePrecedent, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', ETAT],
]);
assert.equal(parPrecedent.fs.contenu('documents', PRECEDENT), textePrecedent);
const journal = exigerEquivalence(parPrecedent.fs, BASE);
assert.deepEqual(revisionsEtLibelles(journal), [
[1, CREATION.libelle],
[2, 'Geste 2'],
[3, 'Geste 3'],
[4, REPRISE],
[5, 'Geste 4'],
]);
assert.equal(texteDeCharge(reconstruire(journal, 4)), texteDeCharge(analyser(textePrecedent).charge));
assert.equal(suitePrecedent.revision, 5);
assert.deepEqual((await rouvrir(parPrecedent.disque, BASE)).avertissements, []);
});
test("un journal sans état lève ETAT_ILLISIBLE, raison ABSENT, secours du journal ; ouvert par lui, le premier geste écrit l'état", async () => {
const { fichiers } = await troisRevisions();
const cas = [
[remplacer(fichiers, ETAT, null), { precedent: 2, journal: 3 }],
[remplacer(remplacer(fichiers, ETAT, null), PRECEDENT, null), { precedent: null, journal: 3 }],
];
for (const [restants, secours] of cas) {
const { fs } = disqueAvec(restants);
const erreur = await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, { base: BASE, raison: 'ABSENT', chemin: null, secours });
const ouvert = await ouvrirSecours(fs, DOC, BASE, 'journal');
assert.equal(fs.compteurs.ecritures, 0);
assert.equal(ouvert.revision, 3);
await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4));
exigerEquivalence(fs, BASE);
}
});
test('un .precedent seul lève ETAT_ILLISIBLE ABSENT avec son secours ; un événement sans aucun fichier lève ABSENT', async () => {
const { fichiers } = await troisRevisions();
const { fs } = disqueAvec(fichiers.filter(([nom]) => nom === PRECEDENT));
assert.deepEqual((await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE')).details, {
base: BASE,
raison: 'ABSENT',
chemin: null,
secours: { precedent: 2, journal: null },
});
assert.deepEqual((await echec(ouvrir(fs, DOC, 'Inconnu'), 'ABSENT')).details, { chemin: 'Inconnu.gtt.json' });
assert.equal(fs.compteurs.ecritures, 0);
});
test("ouvrirSecours refuse une source sans instant : .precedent absent ou illisible, journal absent ou sans entrée, source inconnue", async () => {
const { fs } = disqueAvec([[ETAT, etatTexte(chargeEssai())]]);
assert.deepEqual((await echec(ouvrirSecours(fs, DOC, BASE, 'precedent'), 'ABSENT')).details, { chemin: PRECEDENT });
assert.deepEqual((await echec(ouvrirSecours(fs, DOC, BASE, 'journal'), 'ABSENT')).details, { chemin: JOURNAL });
fs.deposer('documents', PRECEDENT, '{');
fs.deposer('documents', JOURNAL, `${ligneOuverture('evt-lucioles')}\n`);
assert.deepEqual((await echec(ouvrirSecours(fs, DOC, BASE, 'precedent'), 'ETAT_ILLISIBLE')).details, {
raison: 'JSON',
chemin: null,
});
await echec(ouvrirSecours(fs, DOC, BASE, 'journal'), 'ABSENT');
await assert.rejects(ouvrirSecours(fs, DOC, BASE, 'etat'), RangeError);
assert.equal(fs.compteurs.ecritures, 0);
});
test("ouvrirSecours sur un état lisible : le .precedent s'ouvre ; au premier geste, l'état lisible passe au .precedent et l'état s'aligne sur la reprise, avant le geste ; chaque geste qui suit restitue l'état écrit", async () => {
// Le cas d'un retenu fautif, dont le secours est le .precedent (§ 8.9).
// Le .precedent n'a pas la personne que l'état a inscrite (troisRevisions).
const { fichiers } = await troisRevisions();
const etat = texteDe(fichiers, ETAT);
const { fs } = disqueAvec(fichiers);
const ouvert = await ouvrirSecours(fs, DOC, BASE, 'precedent');
assert.equal(ouvert.texteEtat, etat);
assert.equal(ouvert.revision, 2);
const parJournal = await ouvrirSecours(fs, DOC, BASE, 'journal');
assert.equal(parJournal.revision, 3);
assert.equal(fs.compteurs.ecritures, 0);
const trace = tracer(fs);
const precedents = [];
const ecrire = fs.ecrireAtomique;
fs.ecrireAtomique = async (racine, chemin, texte) => {
if (chemin === PRECEDENT) precedents.push(texte);
return ecrire(racine, chemin, texte);
};
let suite = await enregistrer(ouvert, gesteSur(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.deepEqual(trace, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.equal(precedents[0], etat);
// Le .precedent final est l'état aligné sur la reprise, d'avant le geste ;
// l'état lisible reste au journal, à sa révision.
const precedent = analyser(fs.contenu('documents', PRECEDENT));
assert.equal(precedent.entete.revision, 4);
assert.equal(texteDeCharge(precedent.charge), texteDeCharge(ouvert.charge));
const journal = exigerEquivalence(fs, BASE);
assert.equal(texteDeCharge(reconstruire(journal, 3)), texteDeCharge(analyser(etat).charge));
// Les gestes suivants, une inscription puis une note, s'ajoutent.
for (const n of [5, 6]) {
suite = await enregistrer(suite, gesteSur(suite.charge, n), geste(n));
exigerEquivalence(fs, BASE);
}
assert.equal(suite.revision, 7);
});
test("un état illisible apparie ses secours par l'identifiant de son .precedent, jamais par le nom : le journal de l'événement sous une autre base est proposé et s'ouvre, celui d'un autre événement à la base jamais ; sans .precedent, le journal de la base que porte un état lisible n'est pas le sien", async () => {
const { fichiers } = await troisRevisions();
const tronque = (texte) => texte.slice(0, Math.floor(texte.length / 2));
const libre = `${BASE} (2)`;
const carnet = `Carnet${SUFFIXES.journal}`;
// Le journal de la base porte un autre événement : le geste crée celui de
// l'événement sous une base dérivée, puis l'état devient illisible.
const etranger = disqueAvec(remplacer(fichiers, JOURNAL, await journalEtranger()));
const avecEtranger = await ouvrir(etranger.fs, DOC, BASE);
assert.equal(etranger.fs.compteurs.ecritures, 0, 'ouvrir écrit');
await enregistrer(avecEtranger, gesteSur(avecEtranger.charge, 4), geste(4, { libelleReprise: REPRISE }));
const apresGeste = await fichiersDe(etranger.disque);
const illisibleApresGeste = remplacer(apresGeste, ETAT, tronque(texteDe(apresGeste, ETAT)));
// Un événement dont le journal a été renommé à la main sous Glycine, à
// côté d'un état Glycine illisible et sans .precedent.
const hortensia = nouveauDisque();
const charge = chargeEssai({ id: 'evt-h', nom: 'Hortensia' });
await enregistrer(await creer(hortensia.fs, DOC, charge, CREATION), gesteSur(charge, 2), geste(2));
const glycine = [
['Hortensia.gtt.json', hortensia.fs.contenu('documents', 'Hortensia.gtt.json')],
['Glycine.gtt.json', '{'],
['Glycine.gtt-journal.jsonl', hortensia.fs.contenu('documents', 'Hortensia.gtt-journal.jsonl')],
];
// Par cas : les fichiers posés, la base ouverte, les secours attendus, et
// la base du journal que le secours ouvre, que lister nomme aussi ; null
// quand aucun ne s'apparie, et ouvrirSecours('journal') lève ABSENT.
const cas = [
[
"après JOURNAL_ETRANGER, le journal de l'événement sous une base dérivée",
illisibleApresGeste,
BASE,
{ precedent: 3, journal: 4 },
libre,
],
[
'après JOURNAL_ETRANGER, sans le journal dérivé : celui de la base, étranger, ne se propose pas',
remplacer(illisibleApresGeste, `${libre}${SUFFIXES.journal}`, null),
BASE,
{ precedent: 3, journal: null },
null,
],
[
'le journal renommé à la main',
[...remplacer(remplacer(fichiers, JOURNAL, null), ETAT, '{'), [carnet, texteDe(fichiers, JOURNAL)]],
BASE,
{ precedent: 2, journal: 3 },
'Carnet',
],
[
"sans .precedent, le journal de la base porte l'événement d'un état lisible",
glycine,
'Glycine',
{ precedent: null, journal: null },
null,
],
];
for (const [nom, poses, base, secours, baseJournal] of cas) {
const { disque, fs } = disqueAvec(poses);
const erreur = await echec(ouvrir(fs, DOC, base), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, { base, raison: 'JSON', chemin: null, secours }, nom);
const resumes = (await lister(fs, DOC)).filter((autre) => autre.base === base || autre.base === baseJournal);
assert.deepEqual(
resumes.map(({ base: autre, journal, illisible }) => [autre, journal, illisible]),
[[base, baseJournal, 'JSON']],
nom,
);
if (baseJournal === null) {
const absent = await echec(ouvrirSecours(fs, DOC, base, 'journal'), 'ABSENT');
assert.deepEqual(absent.details, { chemin: `${base}${SUFFIXES.journal}` }, nom);
assert.equal(fs.compteurs.ecritures, 0, nom);
continue;
}
const parJournal = await ouvrirSecours(fs, DOC, base, 'journal');
assert.equal(fs.compteurs.ecritures, 0, nom);
assert.deepEqual(
[parJournal.revision, parJournal.baseJournal, parJournal.charge.evenement.id, parJournal.avertissements],
[secours.journal, baseJournal, 'evt-lucioles', []],
nom,
);
// Le geste suivant s'inscrit au journal de l'événement, et nulle part
// ailleurs.
const avant = new Map(await fichiersDe(disque));
await enregistrer(parJournal, gesteSur(parJournal.charge, 5), geste(5));
exigerEquivalence(fs, BASE, { journal: baseJournal });
const changes = (await fichiersDe(disque)).filter(([fichier, texte]) => avant.get(fichier) !== texte);
assert.deepEqual(
changes.map(([fichier]) => fichier).sort(),
[ETAT, `${baseJournal}${SUFFIXES.journal}`].sort(),
nom,
);
}
});
});
describe('dépôt : des octets hors UTF-8 (§ 8.8)', () => {
test("un état aux octets hors UTF-8 lève ETAT_ILLISIBLE, raison ENCODAGE, avec ses secours, sans rien écrire ; lister le montre illisible, apparié par son .precedent", async () => {
const { fichiers } = await troisRevisions();
const { fs } = disqueAvecOctets(fichiers, ETAT, malEncode(texteDe(fichiers, ETAT)));
const erreur = await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, {
base: BASE,
raison: 'ENCODAGE',
chemin: null,
secours: { precedent: 2, journal: 3 },
});
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, id, illisible, journal }) => [base, id, illisible, journal]),
[[BASE, 'evt-lucioles', 'ENCODAGE', BASE]],
);
assert.equal(fs.compteurs.ecritures, 0);
});
test("ouvert d'un secours, le premier geste met l'état aux octets hors UTF-8 dans un dossier neuf de la corbeille, daté du geste, ses octets intacts, puis écrit le journal et l'état ; un jalon n'écrit que le journal, sans toucher l'état", async () => {
const { fichiers } = await troisRevisions();
const octets = malEncode(texteDe(fichiers, ETAT));
const ecritureDuJournal = { journal: ['ajouterLigne', JOURNAL], precedent: ['ecrireAtomique', JOURNAL] };
for (const source of ['journal', 'precedent']) {
const { disque, fs } = disqueAvecOctets(fichiers, ETAT, octets);
// Le dossier de la même seconde, déjà pris pour le secours .precedent,
// fait prendre le rang suivant.
if (source === 'precedent') fs.deposerDossier('documents', 'corbeille/2031-05-17_08-04-00');
const corbeille = `corbeille/2031-05-17_08-04-00${source === 'precedent' ? '_2' : ''}`;
const ouvert = await ouvrirSecours(fs, DOC, BASE, source);
assert.deepEqual([ouvert.etatMalEncode, ouvert.texteEtat], [true, null], source);
const trace = tracer(fs);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.deepEqual(
trace,
[['creerDossier', corbeille], ['deplacer', ETAT], ecritureDuJournal[source], ['ecrireAtomique', ETAT]],
source,
);
assert.deepEqual(fs.octets('documents', `${corbeille}/${ETAT}`), octets, source);
assert.equal(fs.contenu('documents', PRECEDENT), texteDe(fichiers, PRECEDENT), source);
assert.equal(suite.etatMalEncode, false, source);
exigerEquivalence(fs, BASE);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.avertissements], [suite.revision, []], source);
}
const { fs } = disqueAvecOctets(fichiers, ETAT, octets);
const parJournal = await ouvrirSecours(fs, DOC, BASE, 'journal');
const trace = tracer(fs);
const nomme = await nommer(parJournal, { nom: 'Avant la reprise', horodatage: heure(5) });
assert.deepEqual(trace, [['ajouterLigne', JOURNAL]]);
assert.equal(nomme.etatMalEncode, true);
assert.deepEqual(fs.octets('documents', ETAT), octets);
});
test("un geste refusé après la mise de côté de l'état aux octets hors UTF-8 se réessaie du même Ouvert sans rien déplacer de nouveau : un seul dossier daté, qui garde les octets", async () => {
const { fichiers } = await troisRevisions();
const octets = malEncode(texteDe(fichiers, ETAT));
const { fs } = disqueAvecOctets(fichiers, ETAT, octets);
const ouvert = await ouvrirSecours(fs, DOC, BASE, 'journal');
const ajouter = fs.ajouterLigne;
fs.ajouterLigne = async (racine, chemin) => {
fs.ajouterLigne = ajouter;
throw new ErreurStockage('ECRITURE', { chemin, dossier: racine.chemin, cause: 'EIO' });
};
const options = geste(4, { libelleReprise: REPRISE });
await echec(enregistrer(ouvert, modifier(ouvert.charge, 4), options), 'ECRITURE');
assert.equal(fs.contenu('documents', JOURNAL), texteDe(fichiers, JOURNAL));
const trace = tracer(fs);
await enregistrer(ouvert, modifier(ouvert.charge, 4), options);
assert.deepEqual(trace, [
['ajouterLigne', JOURNAL],
['ecrireAtomique', ETAT],
]);
assert.deepEqual(
(await fs.lister(DOC, 'corbeille')).map(({ nom }) => nom),
['2031-05-17_08-04-00'],
);
assert.deepEqual(fs.octets('documents', `corbeille/2031-05-17_08-04-00/${ETAT}`), octets);
exigerEquivalence(fs, BASE);
});
test("une corbeille qui ne se liste plus, au moment de mettre de côté l'état aux octets hors UTF-8, lève ECRITURE sur la corbeille sans rien écrire : l'état reste à sa place", async () => {
const { fichiers } = await troisRevisions();
const octets = malEncode(texteDe(fichiers, ETAT));
const { fs } = disqueAvecOctets(fichiers, ETAT, octets);
const ouvert = await ouvrirSecours(fs, DOC, BASE, 'journal');
const lister = fs.lister;
fs.lister = async (racine, dossier) => {
if (dossier !== 'corbeille') return lister(racine, dossier);
throw new ErreurStockage('LECTURE', { chemin: dossier, dossier: racine.chemin, cause: 'EACCES' });
};
const ecritures = fs.compteurs.ecritures;
const erreur = await echec(enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4)), 'ECRITURE');
assert.deepEqual(erreur.details, { chemin: 'corbeille', dossier: DOCUMENTS, cause: 'EACCES' });
assert.equal(fs.compteurs.ecritures, ecritures);
assert.deepEqual(fs.octets('documents', ETAT), octets);
});
test("un .precedent aux octets hors UTF-8 ne gêne pas l'état lisible ; ouvert en secours, il lève ETAT_ILLISIBLE ENCODAGE ; un état illisible à côté de lui s'apparie par son journal", async () => {
const { fichiers } = await troisRevisions();
const { fs } = disqueAvecOctets(fichiers, PRECEDENT, malEncode(texteDe(fichiers, PRECEDENT)));
const ouvert = await ouvrir(fs, DOC, BASE);
assert.deepEqual([ouvert.revision, ouvert.etatMalEncode, ouvert.avertissements], [3, false, []]);
const erreur = await echec(ouvrirSecours(fs, DOC, BASE, 'precedent'), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, { raison: 'ENCODAGE', chemin: null });
fs.deposer('documents', ETAT, '');
const illisible = await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE');
assert.deepEqual(illisible.details, {
base: BASE,
raison: 'VIDE',
chemin: null,
secours: { precedent: null, journal: 3 },
});
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, id, illisible: raison }) => [base, id, raison]),
[[BASE, 'evt-lucioles', 'VIDE']],
);
assert.equal(fs.compteurs.ecritures, 0);
});
test("un journal dont la ligne d'ouverture n'est pas de l'UTF-8 n'empêche pas d'ouvrir l'état : il n'est ni lu, ni apparié, ni remplacé, JOURNAL_ILLISIBLE {raison: ENCODAGE} ; le premier geste écrit l'historique sous une base libre, que la réouverture apparie", async () => {
const { fichiers } = await troisRevisions();
const octets = ligneMalEncodee(texteDe(fichiers, JOURNAL), 0);
const { disque, fs } = disqueAvecOctets(fichiers, JOURNAL, octets);
const ouvert = await ouvrir(fs, DOC, BASE);
assert.equal(fs.compteurs.ecritures, 0);
assert.deepEqual([ouvert.revision, ouvert.journal, ouvert.baseJournal], [3, null, null]);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ILLISIBLE', details: { raison: 'ENCODAGE' } }]);
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, id, illisible, journal }) => [base, id, illisible, journal]),
[[BASE, 'evt-lucioles', null, null]],
);
const secours = await echec(ouvrirSecours(fs, DOC, BASE, 'journal'), 'ETAT_ILLISIBLE');
assert.deepEqual(secours.details, { raison: 'ENCODAGE', chemin: JOURNAL });
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.notEqual(suite.baseJournal, BASE);
assert.deepEqual(fs.octets('documents', JOURNAL), octets);
const journal = exigerEquivalence(fs, BASE, { journal: suite.baseJournal });
assert.deepEqual(revisionsEtLibelles(journal), [
[3, REPRISE],
[4, 'Geste 4'],
]);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.baseJournal, rouvert.avertissements], [4, suite.baseJournal, []]);
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, journal: apparie }) => [base, apparie]),
[[BASE, suite.baseJournal]],
);
});
test("un journal seul dont la ligne d'ouverture n'est pas de l'UTF-8 se liste sans identifiant, et s'ouvre en ETAT_ILLISIBLE ABSENT sans secours, non en ABSENT : il existe", async () => {
const { fichiers } = await troisRevisions();
const { fs } = disqueAvecOctets([], JOURNAL, ligneMalEncodee(texteDe(fichiers, JOURNAL), 0));
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, id, illisible, journal }) => [base, id, illisible, journal]),
[[BASE, null, 'ABSENT', BASE]],
);
const erreur = await echec(ouvrir(fs, DOC, BASE), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details, {
base: BASE,
raison: 'ABSENT',
chemin: null,
secours: { precedent: null, journal: null },
});
assert.equal(fs.compteurs.ecritures, 0);
});
test("une dernière ligne de journal coupée au milieu d'un caractère de deux octets s'écarte comme une ligne illisible, JOURNAL_ECARTEES {n: 1} : le journal reste celui de l'événement, apparié et ouvrable en secours, et le premier geste le réécrit sans elle", async () => {
const { fichiers } = await troisRevisions();
const texte = texteDe(fichiers, JOURNAL);
const coupee = new TextEncoder().encode('{"type":"entree","revision":4,"libelle":"Ajout de Th');
const octets = new Uint8Array([...new TextEncoder().encode(texte), ...coupee, 0xc3]);
const { disque, fs } = disqueAvecOctets(fichiers, JOURNAL, octets);
const ouvert = await ouvrir(fs, DOC, BASE);
assert.equal(fs.compteurs.ecritures, 0);
assert.deepEqual([ouvert.revision, ouvert.baseJournal, ouvert.journalAReecrire], [3, BASE, true]);
assert.deepEqual(revisionsEtLibelles(ouvert.journal), revisionsEtLibelles(lireJournal(texte)));
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ECARTEES', details: { n: 1 } }]);
assert.deepEqual(
(await lister(fs, DOC)).map(({ base, id, illisible, journal }) => [base, id, illisible, journal]),
[[BASE, 'evt-lucioles', null, BASE]],
);
assert.equal((await ouvrirSecours(fs, DOC, BASE, 'journal')).revision, 3);
assert.equal(fs.compteurs.ecritures, 0);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4));
assert.equal(suite.baseJournal, BASE);
const journal = exigerEquivalence(fs, BASE);
assert.deepEqual(
journal.entrees.map(({ revision }) => revision),
[1, 2, 3, 4],
);
assert.deepEqual(
(await fichiersDe(disque)).map(([nom]) => nom),
[JOURNAL, ETAT, PRECEDENT],
);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.avertissements], [4, []]);
});
test("un octet hors UTF-8 dans l'entrée 37 d'un journal de 100 entrées arrête la lecture sur elle : 36 entrées retenues, 64 écartées et comptées, le journal toujours apparié", async () => {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
for (let n = 2; n <= 100; n += 1) ouvert = await enregistrer(ouvert, modifier(ouvert.charge, n), geste(n));
const texte = fs.contenu('documents', JOURNAL);
assert.equal(lireJournal(texte).entrees.length, 100);
fs.deposerOctets('documents', JOURNAL, ligneMalEncodee(texte, rangDeLEntree(texte, 37)));
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual(
rouvert.journal.entrees.map(({ revision }) => revision),
Array.from({ length: 36 }, (_, i) => i + 1),
);
assert.equal(rouvert.journal.ecartees, 64);
assert.equal(rouvert.baseJournal, BASE);
assert.deepEqual(rouvert.avertissements, [
{ code: 'JOURNAL_INCOMPLET', details: { etat: 100, journal: 36 } },
{ code: 'JOURNAL_ECARTEES', details: { n: 64 } },
]);
});
});
describe("dépôt : l'état sans journal, et le journal apparié par identifiant (§ 8.6)", () => {
test("un état sans journal s'ouvre ; le premier geste crée le journal par une entrée de reprise, instantané de l'état trouvé", async () => {
const charge = chargeEssai();
const etat = etatTexte(charge, 7, V2);
const { disque, fs } = disqueAvec([[ETAT, etat]]);
const ouvert = await ouvrir(fs, DOC, BASE);
assert.equal(fs.compteurs.ecritures, 0);
assert.equal(ouvert.journal, null);
assert.deepEqual(ouvert.avertissements, []);
assert.equal(ouvert.revision, 7);
await assert.rejects(enregistrer(ouvert, modifier(ouvert.charge, 8), geste(8)), TypeError);
assert.equal(fs.compteurs.ecritures, 0);
const trace = tracer(fs);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 8), geste(8, { libelleReprise: REPRISE }));
assert.deepEqual(trace, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
const journal = exigerEquivalence(fs, BASE);
assert.equal(journal.evenement, 'evt-lucioles');
assert.deepEqual(
journal.entrees.map(({ revision, libelle, produitVersion, instantane }) => [
revision,
libelle,
produitVersion,
instantane !== null,
]),
[
[7, REPRISE, V2, true],
[8, 'Geste 8', V1, false],
],
);
assert.equal(texteDeCharge(reconstruire(journal, 7)), texteDeCharge(charge));
assert.equal(fs.contenu('documents', PRECEDENT), etat);
assert.equal(suite.revision, 8);
assert.deepEqual((await rouvrir(disque, BASE)).avertissements, []);
});
test("nommer un état sans journal crée le journal par l'entrée de reprise, puis le jalon, sans écrire l'état", async () => {
const etat = etatTexte(chargeEssai(), 7);
const { fs } = disqueAvec([[ETAT, etat]]);
const ouvert = await ouvrir(fs, DOC, BASE);
assert.equal(fs.compteurs.ecritures, 0, 'ouvrir écrit');
const nomme = await nommer(ouvert, { nom: 'Avant la génération', horodatage: heure(8), libelleReprise: REPRISE });
assert.equal(fs.compteurs.ecritures, 1);
assert.equal(fs.contenu('documents', ETAT), etat);
const journal = lireJournal(fs.contenu('documents', JOURNAL));
assert.deepEqual(revisionsEtLibelles(journal), [[7, REPRISE]]);
assert.deepEqual(
journal.jalons.map(({ revision, nom }) => [revision, nom]),
[[7, 'Avant la génération']],
);
assert.equal(nomme.revision, 7);
assert.deepEqual(nomme.journal, journal);
});
test("un journal renommé à la main reste apparié par l'identifiant : ouvrir le lit, le geste l'allonge, lister le nomme", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, chargeEssai(), CREATION);
await enregistrer(cree, modifier(cree.charge, 2), geste(2));
const fichiers = await fichiersDe(disque);
const carnet = `Carnet${SUFFIXES.journal}`;
const copie = disqueAvec([...remplacer(fichiers, JOURNAL, null), [carnet, texteDe(fichiers, JOURNAL)]]);
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, []);
assert.equal(ouvert.baseJournal, 'Carnet');
assert.equal(ouvert.journal.entrees.length, 2);
const trace = tracer(ouvert.fs);
await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3));
assert.deepEqual(trace, [
['ajouterLigne', carnet],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
exigerEquivalence(ouvert.fs, BASE, { journal: 'Carnet' });
const ecritures = ouvert.fs.compteurs.ecritures;
const [resume] = await lister(ouvert.fs, DOC);
assert.equal(ouvert.fs.compteurs.ecritures, ecritures, 'lister écrit');
assert.deepEqual([resume.base, resume.journal], [BASE, 'Carnet']);
});
test("le journal d'un autre événement sous le nom de la base est JOURNAL_ETRANGER, que l'état de cet événement soit dans le dossier ou non : ni lu, ni touché, ni ouvert en secours ; le premier geste crée le journal sous une base libre ; l'autre événement s'apparie à ce journal par son identifiant, même déplacé sous la base", async () => {
const { disque, fs } = nouveauDisque();
const gala = "Gala d'automne";
const journalGala = `${gala}${SUFFIXES.journal}`;
await creer(fs, DOC, chargeEssai(), CREATION);
await creer(fs, DOC, chargeEssai({ id: 'evt-gala', nom: gala }), CREATION);
const fichiers = await fichiersDe(disque);
const libre = 'Soirée des Lucioles (2)';
// Par cas : les fichiers posés — l'état de l'événement à la révision 1,
// sans .precedent, tel que le laissent creer, enregistrerAilleurs ou
// l'import d'un état seul, et le journal de Gala sous la base —, la base
// du journal auquel l'état de Gala s'apparie, null sans cet état, puis ce
// que lister rend après les gestes, [base, journal]. Sans l'état de Gala,
// aucun état lisible du dossier ne porte l'identifiant de ce journal, et
// la règle d'un état illisible (§ 8.6) le prendrait pour celui de la
// base : seul l'identifiant de l'état lisible l'écarte, au secours comme
// à l'ouverture. Sans son propre journal, l'état de Gala s'apparie à
// celui que la main a déplacé sous la base : l'état de cette base porte
// un autre événement, si bien que ce journal n'appartient pas à sa base.
const cas = [
[
"l'état de Gala dans le dossier",
remplacer(fichiers, JOURNAL, texteDe(fichiers, journalGala)),
gala,
[
[gala, gala],
[BASE, libre],
],
],
[
'le journal de Gala seul, sans son état',
[
[ETAT, texteDe(fichiers, ETAT)],
[JOURNAL, await journalEtranger()],
],
null,
[[BASE, libre]],
],
[
"l'état de Gala sans son journal, que la main a déplacé sous la base",
remplacer(remplacer(fichiers, JOURNAL, texteDe(fichiers, journalGala)), journalGala, null),
BASE,
[
[gala, BASE],
[BASE, libre],
],
],
];
for (const [nom, poses, baseGala, resumesApres] of cas) {
const copie = disqueAvec(poses);
if (baseGala !== null) {
// Gala s'ouvre sur le journal qui porte son identifiant, et son geste
// s'y ajoute.
const apparie = `${baseGala}${SUFFIXES.journal}`;
const ouvertGala = await rouvrir(copie.disque, gala);
assert.equal(ouvertGala.baseJournal, baseGala, nom);
assert.deepEqual(
[ouvertGala.journal, ouvertGala.avertissements],
[lireJournal(texteDe(poses, apparie)), []],
nom,
);
const traceGala = tracer(ouvertGala.fs);
await enregistrer(ouvertGala, modifier(ouvertGala.charge, 2), geste(2));
assert.deepEqual(
traceGala,
[
['ajouterLigne', apparie],
['ecrireAtomique', `${gala}${SUFFIXES.precedent}`],
['ecrireAtomique', `${gala}${SUFFIXES.etat}`],
],
nom,
);
exigerEquivalence(ouvertGala.fs, gala, { journal: baseGala });
}
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ETRANGER', details: { evenement: 'evt-gala' } }], nom);
assert.equal(ouvert.journal, null, nom);
assert.equal(ouvert.revision, 1, nom);
const absent = await echec(ouvrirSecours(ouvert.fs, DOC, BASE, 'journal'), 'ABSENT', nom);
assert.deepEqual(absent.details, { chemin: JOURNAL }, nom);
assert.equal(ouvert.fs.compteurs.ecritures, 0, nom);
const journalDeLaBase = ouvert.fs.contenu('documents', JOURNAL);
const trace = tracer(ouvert.fs);
await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2, { libelleReprise: REPRISE }));
assert.deepEqual(
trace,
[
['ecrireAtomique', `${libre}${SUFFIXES.journal}`],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
],
nom,
);
assert.equal(ouvert.fs.contenu('documents', JOURNAL), journalDeLaBase, nom);
exigerEquivalence(ouvert.fs, BASE, { journal: libre });
const rouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual([rouvert.avertissements, rouvert.baseJournal], [[], libre], nom);
const ecritures = rouvert.fs.compteurs.ecritures;
const resumes = await lister(rouvert.fs, DOC);
assert.equal(rouvert.fs.compteurs.ecritures, ecritures, nom);
assert.deepEqual(
resumes.map(({ base, journal }) => [base, journal]),
resumesApres,
nom,
);
}
});
test("un état copié à la main dans le dossier, sans son journal, ne s'apparie pas au journal que l'état de l'original, ou à défaut son .precedent, dit sien : la copie crée le sien au premier geste, et l'original n'en voit rien", async () => {
const { disque, fs } = nouveauDisque();
let original = await creer(fs, DOC, chargeEssai({ id: 'evt-dahlia', nom: 'Dahlia' }), CREATION);
original = await enregistrer(original, gesteSur(original.charge, 2), geste(2));
const etatOriginal = `Dahlia${SUFFIXES.etat}`;
const journalOriginal = `Dahlia${SUFFIXES.journal}`;
const copie = 'Dahlia - Copie';
// Le copier-coller d'un explorateur : l'état seul, sous un autre nom.
const poses = await fichiersDe(disque);
const fichiers = [...poses, [`${copie}${SUFFIXES.etat}`, texteDe(poses, etatOriginal)]];
// Par cas : l'état posé pour l'original, et ce que lister rend, [base,
// journal, doublon].
const cas = [
["l'original lisible", texteDe(fichiers, etatOriginal), [['Dahlia', 'Dahlia', true], [copie, null, true]]],
["l'original illisible, que son .precedent dit sien", '{', [[copie, null, false], ['Dahlia', 'Dahlia', false]]],
];
for (const [nom, etat, resumes] of cas) {
const dossier = disqueAvec(remplacer(fichiers, etatOriginal, etat));
assert.deepEqual(
(await lister(dossier.fs, DOC)).map(({ base, journal, doublon }) => [base, journal, doublon]),
resumes,
nom,
);
assert.equal(dossier.fs.compteurs.ecritures, 0, nom);
const ouvert = await rouvrir(dossier.disque, copie);
assert.deepEqual([ouvert.journal, ouvert.baseJournal, ouvert.avertissements], [null, copie, []], nom);
// La copie crée son journal par une entrée de reprise ; celui de
// l'original ne bouge pas.
await enregistrer(ouvert, gesteSur(ouvert.charge, 3), geste(3, { libelleReprise: REPRISE }));
assert.equal(ouvert.fs.contenu('documents', journalOriginal), texteDe(fichiers, journalOriginal), nom);
assert.deepEqual(
revisionsEtLibelles(exigerEquivalence(ouvert.fs, copie)),
[
[2, REPRISE],
[3, 'Geste 3'],
],
nom,
);
if (etat === '{') {
const lecteur = creerFichiersSimules({ disque: dossier.disque });
const erreur = await echec(ouvrir(lecteur, DOC, 'Dahlia'), 'ETAT_ILLISIBLE');
assert.deepEqual(erreur.details.secours, { precedent: 1, journal: 2 }, nom);
assert.equal(lecteur.compteurs.ecritures, 0, nom);
continue;
}
const rouvert = await rouvrir(dossier.disque, 'Dahlia');
assert.deepEqual([rouvert.revision, rouvert.avertissements], [2, []], nom);
assert.equal(texteDeCharge(rouvert.charge), texteDeCharge(original.charge), nom);
}
});
test("un journal en avance de plusieurs entrées laisse l'état tel quel, JOURNAL_EN_AVANCE_DE ; la première écriture, jalon ou geste, inscrit l'entrée de reprise, et chaque geste restitue l'état écrit", async () => {
// Les gestes 2 à 4 alternent note et inscription (gesteSur) : l'état remis
// à la révision 2 n'a pas la personne que le journal inscrit ensuite.
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, 2), geste(2));
const etat2 = fs.contenu('documents', ETAT);
for (const n of [3, 4]) ouvert = await enregistrer(ouvert, gesteSur(ouvert.charge, n), geste(n));
const fichiers = remplacer(await fichiersDe(disque), ETAT, etat2);
const copie = disqueAvec(fichiers);
const rouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(rouvert.avertissements, [{ code: 'JOURNAL_EN_AVANCE_DE', details: { n: 2 } }]);
assert.equal(rouvert.revision, 2);
assert.equal(rouvert.texteEtat, etat2);
// Nommer inscrit la reprise, puis le jalon sur elle ; l'état reste.
const nomme = await nommer(rouvert, { nom: 'Retour à la sauvegarde', horodatage: heure(5), libelleReprise: REPRISE });
assert.equal(rouvert.fs.compteurs.ecritures, 1);
assert.equal(nomme.revision, 5);
assert.deepEqual(nomme.avertissements, []);
assert.equal(rouvert.fs.contenu('documents', ETAT), etat2);
const trace = tracer(rouvert.fs);
let suite = await enregistrer(nomme, gesteSur(nomme.charge, 6), geste(6));
assert.equal(suite.revision, 6);
// L'état s'aligne d'abord sur la reprise que nommer a inscrite : il est
// ensuite le .precedent.
assert.deepEqual(trace, [
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
assert.equal(rouvert.fs.contenu('documents', PRECEDENT), etatTexte(analyser(etat2).charge, 5));
const journal = exigerEquivalence(rouvert.fs, BASE);
assert.deepEqual(
revisionsEtLibelles(journal).slice(3),
[
[4, 'Geste 4'],
[5, REPRISE],
[6, 'Geste 6'],
],
);
assert.equal(texteDeCharge(reconstruire(journal, 5)), texteDeCharge(analyser(etat2).charge));
assert.deepEqual(journal.jalons.at(-1).revision, 5);
// Les gestes suivants, une inscription puis une note, s'ajoutent.
for (const n of [7, 8]) {
suite = await enregistrer(suite, gesteSur(suite.charge, n), geste(n));
exigerEquivalence(rouvert.fs, BASE);
}
// Sans jalon, le premier geste inscrit lui-même la reprise, puis s'inscrit.
const directe = await rouvrir(disqueAvec(fichiers).disque, BASE);
await enregistrer(directe, gesteSur(directe.charge, 6), geste(6, { libelleReprise: REPRISE }));
assert.deepEqual(revisionsEtLibelles(exigerEquivalence(directe.fs, BASE)).slice(3), [
[4, 'Geste 4'],
[5, REPRISE],
[6, 'Geste 6'],
]);
});
test("un état en avance du journal s'ouvre tel quel, JOURNAL_INCOMPLET ; le premier geste inscrit l'entrée de reprise à la suite du journal, aligne l'état sur elle, puis s'inscrit ; chaque geste restitue l'état écrit", async () => {
const { fichiers, ouvert: dernier } = await troisRevisions();
// Le journal ne garde que son ouverture, l'entrée 1 et son jalon : il n'a
// pas la personne que l'état a inscrite (troisRevisions).
const tete = `${texteDe(fichiers, JOURNAL).split('\n').slice(0, 3).join('\n')}\n`;
const copie = disqueAvec(remplacer(fichiers, JOURNAL, tete));
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_INCOMPLET', details: { etat: 3, journal: 1 } }]);
assert.equal(ouvert.revision, 3);
assert.equal(texteDeCharge(ouvert.charge), texteDeCharge(dernier.charge));
const trace = tracer(ouvert.fs);
let suite = await enregistrer(ouvert, gesteSur(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
// La reprise seule, l'état aligné sur elle, puis le geste : une coupure ne
// laisse le journal qu'une entrée en avance au plus (§ 8.2).
assert.deepEqual(trace, [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
['ajouterLigne', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
const journal = exigerEquivalence(ouvert.fs, BASE);
// La reprise et le geste viennent de la construction de l'entrée 1 : aucun
// ne note sa version.
assert.deepEqual(
journal.entrees.map(({ revision, libelle, produitVersion }) => [revision, libelle, produitVersion]),
[
[1, CREATION.libelle, V1],
[2, REPRISE, null],
[3, 'Geste 4', null],
],
);
assert.equal(texteDeCharge(reconstruire(journal, 2)), texteDeCharge(dernier.charge));
assert.equal(suite.revision, 3);
// Les gestes suivants, une inscription puis une note, s'ajoutent.
for (const n of [5, 6]) {
suite = await enregistrer(suite, gesteSur(suite.charge, n), geste(n));
exigerEquivalence(ouvert.fs, BASE);
}
assert.deepEqual((await rouvrir(copie.disque, BASE)).avertissements, []);
});
test("une reprise qui ouvre un intervalle s'écrit en instantané : journal des révisions 1 à 50, état à la 52", async () => {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
for (let n = 2; n <= 50; n += 1) ouvert = await enregistrer(ouvert, modifier(ouvert.charge, n), geste(n));
const fichiers = await fichiersDe(disque);
const copie = disqueAvec(remplacer(fichiers, ETAT, etatTexte(modifier(ouvert.charge, 52), 52)));
const rouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(rouvert.avertissements, [{ code: 'JOURNAL_INCOMPLET', details: { etat: 52, journal: 50 } }]);
await enregistrer(rouvert, modifier(rouvert.charge, 53), geste(53, { libelleReprise: REPRISE }));
const journal = exigerEquivalence(rouvert.fs, BASE);
assert.deepEqual(
journal.entrees.slice(-2).map(({ revision, libelle, instantane }) => [revision, libelle, instantane !== null]),
[
[51, REPRISE, true],
[52, 'Geste 53', false],
],
);
});
test("un correctif qui ne s'applique pas écarte son entrée et ce qui la suit ; le premier geste réécrit le journal jusqu'à l'instant restituable", async () => {
const { fichiers } = await troisRevisions();
const lignes = texteDe(fichiers, JOURNAL).replace(/\n$/, '').split('\n');
const rang = lignes.findIndex((ligne) => JSON.parse(ligne).revision === 3);
const fausse = JSON.parse(lignes[rang]);
fausse.correctif = [{ op: 'retirer', chemin: ['participants', { id: 99 }] }];
lignes[rang] = JSON.stringify(fausse);
lignes.push(ligneJalon({ revision: 3, nom: 'Après le geste 3', horodatage: heure(3) }));
const copie = disqueAvec(remplacer(fichiers, JOURNAL, `${lignes.join('\n')}\n`));
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [
{ code: 'JOURNAL_INCOMPLET', details: { etat: 3, journal: 2 } },
{ code: 'JOURNAL_ECARTEES', details: { n: 2 } },
]);
assert.equal(ouvert.journal.entrees.length, 2);
assert.equal(ouvert.revision, 3);
await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.equal(ouvert.fs.compteurs.ecritures, 3);
const journal = exigerEquivalence(ouvert.fs, BASE);
assert.deepEqual(revisionsEtLibelles(journal).slice(2), [
[3, REPRISE],
[4, 'Geste 4'],
]);
assert.ok(!ouvert.fs.contenu('documents', JOURNAL).includes('"id":99'));
assert.deepEqual((await rouvrir(copie.disque, BASE)).avertissements, []);
});
test("un journal dont l'ouverture ne se lit pas est écarté en entier ; le premier geste le remplace par un journal neuf", async () => {
const { fichiers } = await troisRevisions();
const copie = disqueAvec(remplacer(fichiers, JOURNAL, 'pas du JSON\n{"type":"entree"}\n'));
const ouvert = await rouvrir(copie.disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ECARTEES', details: { n: 2 } }]);
assert.equal(ouvert.journal, null);
assert.equal(ouvert.baseJournal, BASE);
const trace = tracer(ouvert.fs);
await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.deepEqual(trace.map(([, chemin]) => chemin), [JOURNAL, PRECEDENT, ETAT]);
const journal = exigerEquivalence(ouvert.fs, BASE);
assert.deepEqual(revisionsEtLibelles(journal), [
[3, REPRISE],
[4, 'Geste 4'],
]);
assert.deepEqual((await rouvrir(copie.disque, BASE)).avertissements, []);
});
});
describe('dépôt : propositions fautives et périmées, retenu fautif (§ 8.9)', () => {
test("une proposition qui se contredit est écartée et nommée, PROPOSITIONS_FAUTIVES, et le fichier s'ouvre", async () => {
const { disque } = disqueAvec([[ETAT, etatTexte(avecFautive())]]);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'PROPOSITIONS_FAUTIVES', details: { ids: [2] } }]);
assert.deepEqual(
ouvert.charge.propositions.map(({ id }) => id),
[1],
);
assert.deepEqual(
ouvert.examen.fautives.map(({ id, fautes }) => [id, fautes.map(({ code }) => code)]),
[[2, ['DOUBLON', 'MANQUANT']]],
);
assert.equal(ouvert.charge.participants.length, 4);
});
test("une proposition que l'en-tête compte sans que la liste la porte s'annonce par leur nombre, PROPOSITIONS_MANQUANTES {n}, et un retenu annoncé absent, RETENU_FAUTIF : le fichier s'ouvre ; rattrapé du journal, ou après un geste, plus rien ne manque", async () => {
const charge = chargeEssai();
charge.propositions.push(placement(2, [[1, 2], [3, 4]]));
charge.prochainsIds.proposition = 3;
charge.retenu = retenuDe(charge.propositions[0]);
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, charge, CREATION);
await enregistrer(cree, modifier(cree.charge, 2), geste(2));
const fichiers = await fichiersDe(disque);
// L'état perd la proposition 1 et le retenu, ses comptes intacts.
const tronquer = (texte) => {
const fichier = JSON.parse(texte);
fichier.charge.propositions.shift();
fichier.charge.retenu = null;
return JSON.stringify(fichier);
};
const tronque = tronquer(texteDe(fichiers, ETAT));
assert.deepEqual(analyser(tronque).manquants, { propositions: 1, retenu: true });
const sansJournal = disqueAvec([[ETAT, tronque]]);
const ouvert = await rouvrir(sansJournal.disque, BASE);
assert.deepEqual(ouvert.avertissements, [
{ code: 'RETENU_FAUTIF', details: {} },
{ code: 'PROPOSITIONS_MANQUANTES', details: { n: 1 } },
]);
assert.deepEqual([ouvert.charge.propositions.map(({ id }) => id), ouvert.charge.retenu], [[2], null]);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3, { libelleReprise: REPRISE }));
assert.deepEqual(suite.avertissements, []);
const relu = analyser(sansJournal.fs.contenu('documents', ETAT));
assert.deepEqual(relu.manquants, { propositions: 0, retenu: false });
// Le journal en avance d'une entrée rend sa charge, qui ne manque de rien.
const enRetard = tronquer(texteDe(fichiers, PRECEDENT));
const avecJournal = disqueAvec(remplacer(fichiers, ETAT, enRetard));
const rattrape = await rouvrir(avecJournal.disque, BASE);
assert.deepEqual(rattrape.avertissements, [{ code: 'JOURNAL_REPRIS', details: {} }]);
assert.deepEqual([rattrape.revision, rattrape.charge.propositions.length], [2, 2]);
assert.notEqual(rattrape.charge.retenu, null);
});
test("un en-tête qui annonce 2^40 propositions s'ouvre : PROPOSITIONS_MANQUANTES ne porte que leur nombre, jamais une liste de cette taille, à côté des fautives nommées", async () => {
const fichier = JSON.parse(etatTexte(avecFautive()));
fichier.entete.comptes.propositions = 2 ** 40;
const { disque } = disqueAvec([[ETAT, JSON.stringify(fichier)]]);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [
{ code: 'PROPOSITIONS_FAUTIVES', details: { ids: [2] } },
{ code: 'PROPOSITIONS_MANQUANTES', details: { n: 2 ** 40 - 2 } },
]);
assert.ok(JSON.stringify(ouvert.avertissements).length < 200);
});
test('une proposition et un retenu périmés sont gardés et signalés, DERIVE : personne exclue, table supprimée, capacité réduite, tour retiré, personne ajoutée', async () => {
// Par péremption, ce qui a changé dans la charge depuis la production de
// sa proposition : périmées, la proposition et le retenu se gardent et se
// signalent, sans s'écarter (§ 8.9, point 3 ; § 9).
const exclure = (charge) => {
charge.participants[2].exclu = true;
};
// Chacune avec la raison de dérive qu'elle donne (examiner).
const parExclusion = 'PARTICIPANT_EXCLU';
const peremptions = [
['personne exclue', exclure, parExclusion],
[
'table supprimée',
(charge) => {
charge.tables = charge.tables.filter(({ id }) => id !== 2);
},
'TABLE_SUPPRIMEE',
],
[
// La proposition date d'une table 1 à trois sièges ; elle en a deux,
// ceux du défaut.
'capacité réduite',
(charge) => {
charge.propositions[0].capacites = [3, 2];
},
'CAPACITE_CHANGEE',
],
[
'tour retiré',
(charge) => {
charge.evenement.tours = 1;
},
'TOURS_CHANGES',
],
[
'personne ajoutée',
(charge) => {
charge.participants.push(personne(5, 'Brunelle', null, null));
charge.prochainsIds.participant = 6;
},
'PARTICIPANT_NON_PLACE',
],
];
// Chaque péremption avec une proposition et le retenu qui la reprend ;
// l'exclusion aussi avec la proposition seule, et avec le retenu seul.
const tout = { retenu: true, propositions: true };
const propositionSeule = { retenu: false, propositions: true };
const retenuSeul = { retenu: true, propositions: false };
const cas = [
...peremptions.map(([nom, perimer, raison]) => [nom, perimer, raison, tout, { ids: [1], retenu: true }]),
['personne exclue, proposition seule', exclure, parExclusion, propositionSeule, { ids: [1], retenu: false }],
['personne exclue, retenu seul', exclure, parExclusion, retenuSeul, { ids: [], retenu: true }],
];
for (const [nom, perimer, raison, { retenu, propositions }, details] of cas) {
const charge = chargeEssai();
perimer(charge);
if (retenu) charge.retenu = retenuDe(charge.propositions[0]);
if (!propositions) charge.propositions = [];
const { disque } = disqueAvec([[ETAT, etatTexte(charge)]]);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(ouvert.avertissements, [{ code: 'DERIVE', details }], nom);
assert.deepEqual(ouvert.charge.propositions, charge.propositions, nom);
assert.deepEqual(ouvert.charge.retenu, charge.retenu, nom);
const codes = (raisons) => raisons.map(({ code }) => code);
assert.deepEqual(
ouvert.examen.derives.map(({ raisons }) => codes(raisons)),
propositions ? [[raison]] : [],
nom,
);
assert.deepEqual(codes(ouvert.examen.retenu.raisons), retenu ? [raison] : [], nom);
}
});
test('un retenu qui se contredit donne RETENU_FAUTIF en premier avertissement, et reste', async () => {
const charge = avecFautive();
charge.retenu = retenuDe(placement(1, [[1, 3], [2]]));
const { disque } = disqueAvec([[ETAT, etatTexte(charge)]]);
const ouvert = await rouvrir(disque, BASE);
assert.deepEqual(
ouvert.avertissements.map(({ code }) => code),
['RETENU_FAUTIF', 'PROPOSITIONS_FAUTIVES'],
);
assert.deepEqual(ouvert.charge.retenu, charge.retenu);
assert.deepEqual(
ouvert.examen.retenu.fautes.map(({ code }) => code),
['MANQUANT'],
);
});
test("après une proposition écartée, le geste suivant s'inscrit depuis la charge du journal : sa dernière entrée restitue l'état écrit", async () => {
const { disque, fs } = nouveauDisque();
const cree = await creer(fs, DOC, avecFautive(), CREATION);
assert.deepEqual(cree.avertissements, [{ code: 'PROPOSITIONS_FAUTIVES', details: { ids: [2] } }]);
const ouvert = await rouvrir(disque, BASE);
const suite = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
exigerEquivalence(ouvert.fs, BASE);
assert.deepEqual(
analyser(ouvert.fs.contenu('documents', ETAT)).charge.propositions.map(({ id }) => id),
[1],
);
assert.deepEqual(suite.avertissements, []);
});
});
describe('dépôt : format plus récent, élagage', () => {
test("un fichier d'un format plus récent s'ouvre en lecture seule : enregistrer, nommer et enregistrerAilleurs refusent LECTURE_SEULE sans rien écrire", async () => {
const texte = etatTexte(chargeEssai()).replace('"format":1', '"format":2');
const { disque } = disqueAvec([[ETAT, texte]]);
const ouvert = await rouvrir(disque, BASE);
assert.equal(ouvert.formatPlusRecent, true);
assert.deepEqual(ouvert.avertissements, []);
const refus = { raison: 'FORMAT_PLUS_RECENT' };
const enregistrement = await echec(enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2)), 'LECTURE_SEULE');
assert.deepEqual(enregistrement.details, refus);
const nom = await echec(nommer(ouvert, { nom: 'Jalon', horodatage: heure(2) }), 'LECTURE_SEULE');
assert.deepEqual(nom.details, refus);
const ailleurs = await echec(enregistrerAilleurs(ouvert, DOC, { existantes: [] }), 'LECTURE_SEULE');
assert.deepEqual(ailleurs.details, refus);
assert.equal(ouvert.fs.compteurs.ecritures, 0);
});
test("ce qu'une construction plus récente laisse garde en lecture seule chaque ouverture, secours compris : un état ou un journal au format 2 ne se réécrit jamais ; le journal de la base se reconnaît plus récent avant toute copie du dossier, et un journal plus récent ne se dit jamais absent", async () => {
const { fichiers } = await troisRevisions();
const recent = (texte) => texte.replace('"format":1', '"format":2');
// L'état au format 2, d'une forme que ce code ne connaît pas : illisible,
// FORMAT_PLUS_RECENT. Le .precedent reste l'état au format 1 qu'il remplace.
const etatForme = recent(texteDe(fichiers, ETAT)).replace('"sieges":null', '"sieges":{"n":3}');
const journalRecent = recent(texteDe(fichiers, JOURNAL));
const plusRecent = { code: 'JOURNAL_FORMAT_PLUS_RECENT', details: { format: 2 } };
const enAvance = { code: 'JOURNAL_EN_AVANCE_DE', details: { n: 1 } };
const illisible = (journal, format = 2) => ({
code: 'ETAT_ILLISIBLE',
details: {
base: BASE,
raison: 'FORMAT_PLUS_RECENT',
chemin: 'charge.tables[0].sieges',
format,
secours: { precedent: 2, journal },
},
});
// Le secours d'un journal plus récent est refusé : il n'est pas absent.
const journalPlusRecent = {
code: 'ETAT_ILLISIBLE',
details: { raison: 'FORMAT_PLUS_RECENT', chemin: JOURNAL, format: 2 },
};
// L'état manque à côté d'un journal plus récent : son refus ne nomme
// aucun format, celui du journal s'y ajoute.
const sansEtat = (precedent) => ({
code: 'ETAT_ILLISIBLE',
details: { base: BASE, raison: 'ABSENT', chemin: null, format: 2, secours: { precedent, journal: null } },
});
const sansPrecedent = { code: 'ABSENT', details: { chemin: PRECEDENT } };
const copieALaMain = `${BASE} - Copie${SUFFIXES.journal}`;
// Par cas : l'état et le journal posés (null : celui des trois révisions),
// puis ce que rendent ouvrir, ouvrirSecours('precedent') et
// ouvrirSecours('journal') — les avertissements d'un Ouvert, ou l'erreur
// —, et ce que la main change ensuite aux fichiers posés.
const cas = [
['état et journal au format 2', etatForme, journalRecent, [illisible(null), [plusRecent], journalPlusRecent]],
['état au format 2, journal resté au format 1', etatForme, null, [illisible(3), [enAvance], []]],
// Le refus de l'état nomme son propre format, que celui du journal ne
// remplace pas.
[
'état au format 3, journal au format 2',
etatForme.replace('"format":2', '"format":3'),
journalRecent,
[illisible(null, 3), [plusRecent], journalPlusRecent],
],
['état au format 2 lisible', recent(texteDe(fichiers, ETAT)), null, [[], [enAvance], []]],
['journal seul au format 2', null, journalRecent, [[plusRecent], [plusRecent], journalPlusRecent]],
[
"journal seul au format 2, marque d'ordre d'octets en tête",
null,
`\u{FEFF}${journalRecent}`,
[[plusRecent], [plusRecent], journalPlusRecent],
],
[
'journal au format 2, sa copie à la main au format 1 sous une autre base',
null,
journalRecent,
[[plusRecent], [plusRecent], journalPlusRecent],
(poses) => [...poses, [copieALaMain, texteDe(fichiers, JOURNAL)]],
],
[
'journal au format 2 sans état',
null,
journalRecent,
[sansEtat(2), [plusRecent], journalPlusRecent],
(poses) => remplacer(poses, ETAT, null),
],
[
'journal au format 2 sans état ni .precedent',
null,
journalRecent,
[sansEtat(null), sansPrecedent, journalPlusRecent],
(poses) => remplacer(remplacer(poses, ETAT, null), PRECEDENT, null),
],
];
let enLectureSeule = 0;
for (const [nom, etat, journal, attendus, retoucher = (poses) => poses] of cas) {
const avecEtat = remplacer(fichiers, ETAT, etat ?? texteDe(fichiers, ETAT));
const restants = retoucher(remplacer(avecEtat, JOURNAL, journal ?? texteDe(fichiers, JOURNAL)));
const { disque, fs } = disqueAvec(restants);
const avant = await fichiersDe(disque);
const ouvertures = [
['ouvrir', () => ouvrir(fs, DOC, BASE)],
['secours precedent', () => ouvrirSecours(fs, DOC, BASE, 'precedent')],
['secours journal', () => ouvrirSecours(fs, DOC, BASE, 'journal')],
];
for (const [rang, [ouverture, ouvrirAinsi]] of ouvertures.entries()) {
const message = `${nom}, ${ouverture}`;
const attendu = attendus[rang];
if (!Array.isArray(attendu)) {
const erreur = await echec(ouvrirAinsi(), attendu.code);
assert.deepEqual(erreur.details, attendu.details, message);
continue;
}
const ouvert = await ouvrirAinsi();
assert.equal(ouvert.formatPlusRecent, true, message);
assert.deepEqual(ouvert.avertissements, attendu, message);
const ecritures = [
() => enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE })),
() => nommer(ouvert, { nom: 'Jalon', horodatage: heure(4), libelleReprise: REPRISE }),
() => enregistrerAilleurs(ouvert, DOC, { existantes: [] }),
];
for (const ecrire of ecritures) {
assert.deepEqual((await echec(ecrire(), 'LECTURE_SEULE')).details, { raison: 'FORMAT_PLUS_RECENT' }, message);
}
enLectureSeule += 1;
}
// La liste nomme le journal qu'ouvrir lit : celui de la base, qu'il soit
// plus récent ou non, jamais sa copie au format courant.
const ligne = (await lister(fs, DOC)).find(({ base }) => base === BASE);
assert.equal(ligne.journal, BASE, `${nom}, lister`);
assert.equal(fs.compteurs.ecritures, 0, nom);
assert.deepEqual(await fichiersDe(disque), avant, nom);
}
assert.equal(enLectureSeule, 14);
});
test("une ouverture de journal dont le format n'est pas un entier plus grand que FORMAT ne se lit pas : écartée, puis remplacée au premier geste", async () => {
const { fichiers } = await troisRevisions();
const journal = texteDe(fichiers, JOURNAL);
const ouverture = journal.slice(0, journal.indexOf('\n'));
const fausses = [
ouverture.replace('"format":1', '"format":"2"'),
ouverture.replace('"format":1', '"format":0'),
ouverture.replace(/\}$/, ',"inconnue":1}'),
];
for (const fausse of fausses) {
const copie = disqueAvec(remplacer(fichiers, JOURNAL, journal.replace(ouverture, fausse)));
const ouvert = await rouvrir(copie.disque, BASE);
assert.equal(ouvert.formatPlusRecent, false, fausse);
assert.deepEqual(ouvert.avertissements, [{ code: 'JOURNAL_ECARTEES', details: { n: 5 } }], fausse);
await enregistrer(ouvert, modifier(ouvert.charge, 4), geste(4, { libelleReprise: REPRISE }));
assert.deepEqual(
revisionsEtLibelles(exigerEquivalence(ouvert.fs, BASE)),
[
[3, REPRISE],
[4, 'Geste 4'],
],
fausse,
);
}
});
test("au-delà de PLAFOND_ENTREES, le journal s'élague sur une frontière d'instantané qui garde au moins 500 entrées, au geste qui le dépasse comme à l'écriture ailleurs ; relu, il est la version élaguée", async () => {
const { disque, fs } = nouveauDisque();
const trace = tracer(fs);
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
for (let n = 2; n <= PLAFOND_ENTREES; n += 1) ouvert = await enregistrer(ouvert, modifier(ouvert.charge, n), geste(n));
const avant = fs.contenu('documents', JOURNAL);
assert.equal(lireJournal(avant).entrees.length, PLAFOND_ENTREES);
// Jusqu'au plafond compris, chaque geste s'ajoute en fin de journal.
const ecrituresDuJournal = trace.filter(([, chemin]) => chemin === JOURNAL).map(([primitive]) => primitive);
assert.deepEqual(new Set(ecrituresDuJournal.slice(1)), new Set(['ajouterLigne']));
// Le secours .precedent écrit ailleurs : son journal de PLAFOND_ENTREES
// entrées reçoit la reprise, et s'élague comme au geste qui le dépasse.
const lecteur = creerFichiersSimules({ disque });
const secours = await ouvrirSecours(lecteur, DOC, BASE, 'precedent');
assert.equal(lecteur.compteurs.ecritures, 0, 'ouvrirSecours écrit');
const options = { existantes: [], libelleReprise: REPRISE, horodatage: heure(601) };
const copie = await enregistrerAilleurs(secours, DOC, options);
const copieRelue = exigerEquivalence(lecteur, copie.base);
assert.deepEqual(
[copie.base, copieRelue.entrees[0].revision, copieRelue.entrees.length, copieRelue.entrees.at(-1).libelle],
[`${BASE} (2)`, 101, 501, REPRISE],
);
assert.deepEqual(copie.journal, copieRelue);
const depart = trace.length;
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 601), geste(601));
assert.deepEqual(trace.slice(depart), [
['ecrireAtomique', JOURNAL],
['ecrireAtomique', PRECEDENT],
['ecrireAtomique', ETAT],
]);
const apres = fs.contenu('documents', JOURNAL);
const derniere = apres.slice(apres.lastIndexOf('\n', apres.length - 2) + 1);
assert.equal(apres, elaguer(avant + derniere));
const relu = exigerEquivalence(fs, BASE);
assert.equal(relu.entrees[0].revision, 101);
assert.equal(relu.entrees.length, 501);
assert.deepEqual(ouvert.journal, relu);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 602), geste(602));
assert.deepEqual(trace.at(-3), ['ajouterLigne', JOURNAL]);
exigerEquivalence(fs, BASE);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.avertissements, rouvert.journal.entrees.length], [602, [], 502]);
});
});
describe('dépôt : le verrou, et écrire ailleurs (§ 8.6, § 8.8)', () => {
test("le verrou de la séance se prend, se refuse à une autre séance avec ce que le système en dit, et se rend", async () => {
const { disque, fs } = nouveauDisque();
const ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
assert.deepEqual(await verrouiller(ouvert, 'seance-a'), { pris: true });
const texte = fs.contenu('documents', VERROU);
const { depuis } = JSON.parse(texte);
const voisin = await rouvrir(disque, BASE);
assert.deepEqual(await verrouiller(voisin, 'seance-b'), { pris: false, seance: 'seance-a', depuis, vivant: true });
const lointain = await ouvrir(creerFichiersSimules({ disque, hote: 'autre-poste' }), DOC, BASE);
assert.equal(lointain.fs.compteurs.ecritures, 0, 'ouvrir écrit');
assert.deepEqual(await verrouiller(lointain, 'seance-c'), {
pris: false,
seance: 'seance-a',
depuis,
vivant: null,
});
await deverrouiller(voisin, 'seance-b');
assert.equal(fs.contenu('documents', VERROU), texte);
await deverrouiller(ouvert, 'seance-a');
assert.equal(fs.contenu('documents', VERROU), null);
assert.deepEqual(await verrouiller(voisin, 'seance-b'), { pris: true });
voisin.fs.pannes.couperApres(0);
const vuDApres = await verrouiller(ouvert, 'seance-a');
assert.deepEqual([vuDApres.pris, vuDApres.seance, vuDApres.vivant], [false, 'seance-b', false]);
});
test("après un refus d'écriture, enregistrerAilleurs crée la racine visée, puis y écrit le journal entier et l'état ; une base déjà prise là-bas se dérive ; le geste reprend là", async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque, executable: 'E:\\soirees' });
fs.deposerDossier('portable');
const { portable, documents } = await fs.racines();
let ouvert = await creer(fs, portable, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
fs.pannes.ecriture('portable');
const erreur = await echec(enregistrer(ouvert, modifier(ouvert.charge, 3), geste(3)), 'ECRITURE');
assert.deepEqual(erreur.details, { chemin: JOURNAL, dossier: 'E:\\soirees\\data', cause: 'EROFS' });
// En mode portable, rien n'a sondé ni créé documents : l'opérateur l'a
// choisi, l'écriture le crée.
const ecritures = fs.compteurs.ecritures;
const trace = tracer(fs);
const ailleurs = await enregistrerAilleurs(ouvert, documents, { existantes: [] });
assert.deepEqual(trace, [
['creerDossier', ''],
['ecrireAtomique', JOURNAL],
['ecrireAtomique', ETAT],
]);
assert.equal(fs.compteurs.ecritures - ecritures, 3);
assert.deepEqual([ailleurs.racine, ailleurs.base, ailleurs.revision], [documents, BASE, 2]);
assert.equal(fs.contenu('documents', JOURNAL), fs.contenu('portable', JOURNAL));
assert.equal(fs.contenu('documents', PRECEDENT), null);
exigerEquivalence(fs, BASE);
// Écrit de nouveau là-bas, l'événement y trouve sa base prise, absente
// des bases reçues : la dérivation la voit dans le dossier.
trace.length = 0;
const libre = 'Soirée des Lucioles (2)';
const seconde = await enregistrerAilleurs(ouvert, documents, { existantes: [] });
assert.deepEqual(trace, [
['creerDossier', ''],
['ecrireAtomique', `${libre}${SUFFIXES.journal}`],
['ecrireAtomique', `${libre}${SUFFIXES.etat}`],
]);
assert.equal(seconde.base, libre);
exigerEquivalence(fs, libre);
const suite = await enregistrer(ailleurs, modifier(ailleurs.charge, 3), geste(3));
assert.equal(suite.revision, 3);
exigerEquivalence(fs, BASE);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual([rouvert.revision, rouvert.avertissements], [3, []]);
});
test("ETAT_NON_ECRIT : l'Ouvert avancé de ses détails s'écrit ailleurs, son geste compris, sous une base libre parmi celles reçues", async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque, executable: 'E:\\soirees' });
fs.deposerDossier('portable');
fs.deposerDossier('documents');
const { portable, documents } = await fs.racines();
const cree = await creer(fs, portable, chargeEssai(), CREATION);
refuserRenommageApresPrecedent(fs);
const apres = modifier(cree.charge, 2);
const erreur = await echec(enregistrer(cree, apres, geste(2)), 'ETAT_NON_ECRIT');
const ailleurs = await enregistrerAilleurs(erreur.details.ouvert, documents, { existantes: [BASE] });
assert.deepEqual([ailleurs.base, ailleurs.revision], ['Soirée des Lucioles (2)', 2]);
// creer : journal, état ; le geste : ligne, .precedent, état refusé ;
// ailleurs : la racine, qui existe déjà, le journal, l'état.
assert.equal(fs.compteurs.ecritures, 2 + 3 + 3);
exigerEquivalence(fs, ailleurs.base);
assert.equal(texteDeCharge(analyser(fs.contenu('documents', `${ailleurs.base}${SUFFIXES.etat}`)).charge), texteDeCharge(apres));
});
// Un événement du disque portable : créé, puis deux gestes, les notes 2 et
// 3 ; son état remplacé par etat quand il est donné. Le secours .precedent
// l'ouvre, sans rien écrire, à la note 2 et à la révision 2 ; puis portable
// refuse toute écriture, et le premier geste lève ECRITURE. Rend le disque,
// son processus, la racine documents et l'Ouvert du secours, à écrire
// ailleurs.
async function secoursRefuse(etat) {
const disque = new Map();
const fs = creerFichiersSimules({ disque, executable: 'E:\\soirees' });
fs.deposerDossier('portable');
const { portable, documents } = await fs.racines();
let ouvert = await creer(fs, portable, chargeEssai(), CREATION);
for (const n of [2, 3]) ouvert = await enregistrer(ouvert, modifier(ouvert.charge, n), geste(n));
if (etat !== null) fs.deposer('portable', ETAT, etat);
const ecritures = fs.compteurs.ecritures;
const secours = await ouvrirSecours(fs, portable, BASE, 'precedent');
assert.equal(fs.compteurs.ecritures, ecritures, 'ouvrirSecours écrit');
assert.deepEqual([secours.revision, secours.charge.participants[0].notes], [2, 'note 2']);
fs.pannes.ecriture('portable');
await echec(enregistrer(secours, modifier(secours.charge, 4), geste(4, { libelleReprise: REPRISE })), 'ECRITURE');
return { disque, fs, documents, secours };
}
// L'état que le secours laisse sur le disque de départ : lisible, à la
// révision 3, ou illisible.
const etatsDuSecours = [
['état lisible', null],
['état illisible', '{"entete":'],
];
test("un secours .precedent s'écrit ailleurs en paire alignée — son journal, une entrée de reprise qui restitue sa charge, puis l'état à la révision de la reprise — : rouvert là-bas, il rend la charge du secours sans rien rattraper", async () => {
for (const [nom, etat] of etatsDuSecours) {
const { disque, fs, documents, secours } = await secoursRefuse(etat);
const trace = tracer(fs);
const options = { existantes: [], libelleReprise: REPRISE, horodatage: heure(4) };
const ailleurs = await enregistrerAilleurs(secours, documents, options);
assert.deepEqual(
trace,
[
['creerDossier', ''],
['ecrireAtomique', JOURNAL],
['ecrireAtomique', ETAT],
],
nom,
);
const rouvert = await rouvrir(disque, BASE);
assert.deepEqual(
[rouvert.revision, rouvert.avertissements, rouvert.charge.participants[0].notes],
[4, [], 'note 2'],
nom,
);
assert.equal(texteDeCharge(rouvert.charge), texteDeCharge(secours.charge), nom);
const journal = exigerEquivalence(fs, BASE);
assert.deepEqual(
revisionsEtLibelles(journal),
[
[1, CREATION.libelle],
[2, 'Geste 2'],
[3, 'Geste 3'],
[4, REPRISE],
],
nom,
);
assert.deepEqual([ailleurs.revision, ailleurs.journal, ailleurs.avertissements], [4, journal, []], nom);
// Un geste depuis l'Ouvert rendu, qui remet la note 3 : son correctif
// part de la charge écrite là-bas, que porte chargeJournal. Parti de la
// note 3, il ne changerait rien, et sa dernière entrée ne restituerait
// pas l'état écrit.
await enregistrer(ailleurs, modifier(ailleurs.charge, 3), geste(5));
assert.deepEqual(
revisionsEtLibelles(exigerEquivalence(fs, BASE)).slice(-2),
[
[4, REPRISE],
[5, 'Geste 5'],
],
nom,
);
}
});
test('écrit ailleurs sans libellé de reprise, un secours .precedent lève TypeError avant toute écriture : la racine ne se crée pas', async () => {
for (const [nom, etat] of etatsDuSecours) {
const { fs, documents, secours } = await secoursRefuse(etat);
const trace = tracer(fs);
const sansLibelle = { existantes: [], horodatage: heure(4) };
await assert.rejects(enregistrerAilleurs(secours, documents, sansLibelle), TypeError, nom);
assert.deepEqual(trace, [], nom);
}
});
test("un Ouvert aligné s'écrit ailleurs avec la charge que restitue la dernière entrée de son journal, proposition fautive comprise, sous la version qui l'a produite ; l'Ouvert rendu porte la charge examinée", async () => {
const fautives = [{ code: 'PROPOSITIONS_FAUTIVES', details: { ids: [2] } }];
// Par cas : la version que la main pose dans l'en-tête de l'état créé, à
// la révision du journal, ou null. Retouché, l'en-tête nomme une autre
// construction que l'entrée qui a produit la charge : l'état écrit
// ailleurs prend celle du journal.
const cas = [
["l'Ouvert de creer", null],
['un en-tête retouché à la main', V2],
];
for (const [nom, version] of cas) {
const disque = new Map();
const fs = creerFichiersSimules({ disque, executable: 'E:\\soirees' });
fs.deposerDossier('portable');
const { portable, documents } = await fs.racines();
let ouvert = await creer(fs, portable, avecFautive(), CREATION);
if (version !== null) {
fs.deposer('portable', ETAT, etatTexte(avecFautive(), 1, version));
const ecritures = fs.compteurs.ecritures;
ouvert = await ouvrir(fs, portable, BASE);
assert.equal(fs.compteurs.ecritures, ecritures, `${nom} : ouvrir écrit`);
assert.deepEqual([ouvert.revision, ouvert.produitVersion, ouvert.avertissements], [1, version, fautives], nom);
}
assert.notEqual(texteDeCharge(ouvert.charge), texteDeCharge(ouvert.chargeJournal), nom);
const ailleurs = await enregistrerAilleurs(ouvert, documents, { existantes: [] });
const ecrit = analyser(fs.contenu('documents', ETAT));
assert.deepEqual([ecrit.entete.revision, ecrit.entete.produitVersion], [1, V1], nom);
assert.equal(texteDeCharge(ecrit.charge), texteDeCharge(avecFautive()), nom);
exigerEquivalence(fs, BASE);
assert.equal(texteDeCharge(ailleurs.chargeJournal), texteDeCharge(avecFautive()), nom);
assert.deepEqual(
[ailleurs.produitVersion, ailleurs.charge.propositions.map(({ id }) => id), ailleurs.avertissements],
[V1, [1], fautives],
nom,
);
assert.deepEqual((await rouvrir(disque, BASE)).avertissements, fautives, nom);
}
});
test("un événement sans journal s'écrit ailleurs par son seul état ; là où l'écriture est refusée, un journal lève ECRITURE, un état seul ETAT_NON_ECRIT, et une racine qui ne se crée pas ECRITURE, sans rien écrire", async () => {
const disque = new Map();
const fs = creerFichiersSimules({ disque, executable: 'E:\\soirees' });
fs.deposerDossier('documents');
fs.deposer('portable', ETAT, etatTexte(chargeEssai(), 4));
const { portable, documents } = await fs.racines();
const sansJournal = await ouvrir(fs, portable, BASE);
assert.equal(fs.compteurs.ecritures, 0, 'ouvrir écrit');
const trace = tracer(fs);
const ailleurs = await enregistrerAilleurs(sansJournal, documents, { existantes: [] });
assert.deepEqual(trace, [
['creerDossier', ''],
['ecrireAtomique', ETAT],
]);
assert.deepEqual(
[ailleurs.journal, ailleurs.revision, ailleurs.texteEtat],
[null, 4, fs.contenu('documents', ETAT)],
);
// Les renommages de documents refusés : chaque écriture atomique échoue,
// le dossier existant passe.
const avecJournal = await creer(fs, portable, chargeEssai({ id: 'evt-gala', nom: "Gala d'automne" }), CREATION);
fs.pannes.renommage('documents');
const refusJournal = await echec(enregistrerAilleurs(avecJournal, documents, { existantes: [] }), 'ECRITURE');
assert.deepEqual(refusJournal.details, {
chemin: "Gala d'automne.gtt-journal.jsonl",
dossier: DOCUMENTS,
cause: 'EBUSY',
});
assert.equal(fs.contenu('documents', "Gala d'automne.gtt.json"), null);
const refusEtat = await echec(enregistrerAilleurs(sansJournal, documents, { existantes: [] }), 'ETAT_NON_ECRIT');
const { ouvert: refuse, ...details } = refusEtat.details;
assert.deepEqual(details, { chemin: 'Soirée des Lucioles (2).gtt.json', dossier: DOCUMENTS, cause: 'EBUSY' });
assert.deepEqual([refuse.base, refuse.texteEtat, refuse.revision], ['Soirée des Lucioles (2)', null, 4]);
// Sur un disque où documents n'a jamais été créé, et qui en refuse toute
// écriture : creerDossier, seule primitive appelée, lève ECRITURE sur la
// racine elle-même ; documents n'existe toujours pas, et rien ne s'écrit.
const neuf = new Map();
const sansDocuments = creerFichiersSimules({ disque: neuf, executable: 'E:\\soirees' });
sansDocuments.deposer('portable', ETAT, etatTexte(chargeEssai(), 4));
const seul = await ouvrir(sansDocuments, portable, BASE);
assert.equal(sansDocuments.compteurs.ecritures, 0, 'ouvrir écrit');
sansDocuments.pannes.ecriture('documents');
const traceRacine = tracer(sansDocuments);
const refusRacine = await echec(enregistrerAilleurs(seul, documents, { existantes: [] }), 'ECRITURE');
assert.deepEqual(refusRacine.details, { chemin: '', dossier: DOCUMENTS, cause: 'EROFS' });
assert.deepEqual(traceRacine, [['creerDossier', '']]);
await echec(creerFichiersSimules({ disque: neuf }).lister(DOC, ''), 'ABSENT');
assert.equal(sansDocuments.contenu('portable', ETAT), etatTexte(chargeEssai(), 4));
});
test("enregistrerAilleurs avec un geste, sans créer la racine : sous la base que dérive le nom du geste, l'état d'avant le geste au .precedent — le texte sur le disque, sinon la charge ouverte —, puis le journal recopié — reprise comprise quand il le faut — suivi de l'entrée du geste, puis l'état du geste ; la paire de l'Ouvert ne s'écrit pas et ne s'apparie pas le journal neuf, et une racine absente ne se crée pas", async () => {
const { disque, fs } = nouveauDisque();
let ouvert = await creer(fs, DOC, chargeEssai(), CREATION);
ouvert = await enregistrer(ouvert, modifier(ouvert.charge, 2), geste(2));
const avant = await fichiersDe(disque);
const nouvelle = 'Gala des Saules';
const renommee = (charge) => ({ ...charge, evenement: { ...charge.evenement, nom: nouvelle } });
const options = {
existantes: [],
horodatage: heure(3),
geste: { apres: renommee(ouvert.charge), libelle: 'Renommé', produitVersion: V2 },
creerRacine: false,
};
const ecritsSous = (base) => [
['ecrireAtomique', `${base}${SUFFIXES.precedent}`],
['ecrireAtomique', `${base}${SUFFIXES.journal}`],
['ecrireAtomique', `${base}${SUFFIXES.etat}`],
];
const trace = tracer(fs);
const ailleurs = await enregistrerAilleurs(ouvert, DOC, options);
assert.deepEqual(trace, ecritsSous(nouvelle));
assert.equal(fs.contenu('documents', `${nouvelle}${SUFFIXES.precedent}`), texteDe(avant, ETAT));
assert.deepEqual(
(await fichiersDe(disque)).filter(([nom]) => !nom.startsWith(nouvelle)),
avant,
);
assert.deepEqual(
[ailleurs.racine, ailleurs.base, ailleurs.baseJournal, ailleurs.revision, ailleurs.produitVersion],
[DOC, nouvelle, nouvelle, 3, V2],
);
assert.equal(texteDeCharge(ailleurs.charge), texteDeCharge(renommee(modifier(chargeEssai(), 2))));
const journal = exigerEquivalence(fs, nouvelle);
assert.deepEqual(revisionsEtLibelles(journal), [
[1, CREATION.libelle],
[2, 'Geste 2'],
[3, 'Renommé'],
]);
assert.deepEqual(
journal.entrees.map(({ produitVersion }) => produitVersion),
[V1, null, V2],
);
assert.equal(analyser(fs.contenu('documents', `${nouvelle}${SUFFIXES.etat}`)).entete.produitVersion, V2);
const rouvert = await rouvrir(disque, nouvelle);
assert.deepEqual([rouvert.revision, rouvert.charge.evenement.nom, rouvert.avertissements], [3, nouvelle, []]);
const suite = await enregistrer(ailleurs, modifier(ailleurs.charge, 4), geste(4, { produitVersion: V2 }));
assert.deepEqual([suite.base, suite.revision], [nouvelle, 4]);
exigerEquivalence(fs, nouvelle);
// Un état sans journal, à la mise en page d'un autre outil : l'entrée de
// reprise, au libellé reçu, précède celle du geste ; sans ce libellé,
// TypeError avant toute écriture. Le .precedent neuf porte ses octets.
const enQuatre = JSON.stringify(JSON.parse(etatTexte(chargeEssai(), 4)), null, 4);
const importe = `${enQuatre.replaceAll('\n', '\r\n')}\r\n`;
const seul = disqueAvec([[ETAT, importe]]);
const sansJournal = await rouvrir(seul.disque, BASE);
const gesteSeul = { ...options, geste: { ...options.geste, apres: renommee(sansJournal.charge), produitVersion: V1 } };
await assert.rejects(enregistrerAilleurs(sansJournal, DOC, gesteSeul), TypeError);
assert.equal(sansJournal.fs.compteurs.ecritures, 0);
const repris = await enregistrerAilleurs(sansJournal, DOC, { ...gesteSeul, libelleReprise: REPRISE });
assert.deepEqual([repris.base, repris.revision], [nouvelle, 5]);
assert.deepEqual(revisionsEtLibelles(exigerEquivalence(sansJournal.fs, nouvelle)), [
[4, REPRISE],
[5, 'Renommé'],
]);
assert.equal(sansJournal.fs.contenu('documents', ETAT), importe);
assert.equal(sansJournal.fs.contenu('documents', `${nouvelle}${SUFFIXES.precedent}`), importe);
// Coupée juste après le journal neuf, l'écriture laisse la nouvelle base
// à l'événement par son .precedent : l'état de l'Ouvert, qu'aucun journal
// de sa base ne suit, ne se l'apparie pas, et s'ouvre tel qu'il était ;
// la liste ne montre que lui.
const coupe = disqueAvec([[ETAT, etatTexte(chargeEssai(), 4)]]);
const avantCoupure = await rouvrir(coupe.disque, BASE);
const ecrire = avantCoupure.fs.ecrireAtomique;
avantCoupure.fs.ecrireAtomique = async (racine, chemin, texte) => {
await ecrire(racine, chemin, texte);
if (chemin === `${nouvelle}${SUFFIXES.journal}`) avantCoupure.fs.pannes.couperApres(0);
};
await assert.rejects(
enregistrerAilleurs(avantCoupure, DOC, { ...gesteSeul, libelleReprise: REPRISE }),
PanneSimulee,
);
const apresCoupure = await rouvrir(coupe.disque, BASE);
assert.deepEqual(
[apresCoupure.revision, apresCoupure.charge.evenement.nom, apresCoupure.journal, apresCoupure.avertissements],
[4, BASE, null, []],
);
assert.deepEqual(
(await lister(creerFichiersSimules({ disque: coupe.disque }), DOC)).map(({ base, journal }) => [base, journal]),
[[BASE, null]],
);
// Un Ouvert de secours, l'état sur le disque illisible : le .precedent
// de la nouvelle base reçoit la charge ouverte, à sa révision.
const secours = disqueAvec([
[ETAT, '{'],
[PRECEDENT, etatTexte(chargeEssai(), 4)],
]);
const depuisPrecedent = await ouvrirSecours(secours.fs, DOC, BASE, 'precedent');
assert.equal(depuisPrecedent.texteEtat, null);
const traceSecours = tracer(secours.fs);
const gesteSecours = { ...gesteSeul.geste, apres: renommee(depuisPrecedent.charge) };
const renommeSecours = await enregistrerAilleurs(depuisPrecedent, DOC, {
...gesteSeul,
geste: gesteSecours,
libelleReprise: REPRISE,
});
assert.deepEqual(traceSecours, ecritsSous(nouvelle));
assert.equal(secours.fs.contenu('documents', `${nouvelle}${SUFFIXES.precedent}`), etatTexte(chargeEssai(), 4));
assert.deepEqual([renommeSecours.base, renommeSecours.revision], [nouvelle, 5]);
assert.deepEqual(
[ETAT, PRECEDENT].map((nom) => secours.fs.contenu('documents', nom)),
['{', etatTexte(chargeEssai(), 4)],
);
// Sur un disque où documents n'existe pas : creerRacine faux ne le crée
// pas, et sa liste lève ABSENT avant toute écriture.
const neuf = new Map();
const sansDocuments = creerFichiersSimules({ disque: neuf, executable: 'E:\\soirees' });
sansDocuments.deposer('portable', ETAT, etatTexte(chargeEssai(), 4));
const { portable, documents } = await sansDocuments.racines();
const portableSeul = await ouvrir(sansDocuments, portable, BASE);
const traceRacine = tracer(sansDocuments);
const absente = await echec(
enregistrerAilleurs(portableSeul, documents, { existantes: [], creerRacine: false }),
'ABSENT',
);
assert.deepEqual(absente.details, { chemin: '' });
assert.deepEqual(traceRacine, []);
await echec(creerFichiersSimules({ disque: neuf }).lister(DOC, ''), 'ABSENT');
});
test("basesPrises rend, sans rien écrire, la base de chaque fichier d'événement du dossier — état, .precedent, journal, verrou —, dans l'ordre de sa liste ; ABSENT quand la racine manque", async () => {
const { fs } = disqueAvec([
[ETAT, '{'],
[PRECEDENT, '{'],
['Archives des Lucioles.gtt-journal.jsonl', '{'],
['GALA DES SAULES.gtt.verrou', '{}\n'],
['notes.txt', 'à garder'],
['reglages_locaux.json', '{}\n'],
]);
fs.deposerDossier('documents', 'corbeille');
assert.deepEqual(await basesPrises(fs, DOC), ['Archives des Lucioles', 'GALA DES SAULES', BASE, BASE]);
assert.equal(fs.compteurs.ecritures, 0);
await echec(basesPrises(creerFichiersSimules({ disque: new Map() }), DOC), 'ABSENT');
});
test("une lecture qui échoue autrement que sur un fichier absent ou mal encodé remonte telle quelle : un chemin refusé lève CHEMIN_REFUSE, une lecture refusée LECTURE", async () => {
const { fs } = nouveauDisque();
await echec(ouvrir(fs, DOC, '../ailleurs'), 'CHEMIN_REFUSE');
await creer(fs, DOC, chargeEssai(), CREATION);
const ecritures = fs.compteurs.ecritures;
fs.lireTexte = async (racine, chemin) => {
throw new ErreurStockage('LECTURE', { chemin, dossier: DOCUMENTS, cause: 'EACCES' });
};
assert.deepEqual((await echec(ouvrir(fs, DOC, BASE), 'LECTURE')).details, {
chemin: ETAT,
dossier: DOCUMENTS,
cause: 'EACCES',
});
assert.equal(fs.compteurs.ecritures, ecritures);
});
});