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
2868 lines
138 KiB
JavaScript
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);
|
|
});
|
|
});
|