1283 lines
66 KiB
JavaScript
1283 lines
66 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Épreuves des placements du fichier d'état (§ 8.9, § 9, § 4.4, § 5.7) : la
|
|||
|
|
// conversion entre la proposition du moteur et la forme positionnelle, la
|
|||
|
|
// numérotation des générations qui s'accumulent, le contrôle de cohérence
|
|||
|
|
// sur un fichier abîmé, aux capacités égales ou non, ce que ce contrôle
|
|||
|
|
// n'attrape pas, la dérive d'une proposition sur une configuration changée,
|
|||
|
|
// le partage d'une charge lue entre propositions gardées et écartées, sous
|
|||
|
|
// un compteur relevé au-delà de chaque identifiant lu, la forme nommée, et
|
|||
|
|
// des résultats qui ne dépendent que des arguments. Les charges d'épreuve
|
|||
|
|
// reprennent la petite démonstration (§ 15.3) et son plan parfait ; leurs
|
|||
|
|
// noms sont inventés.
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { describe, test } from '../../test/lanceur.js';
|
|||
|
|
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
|||
|
|
import { indexerPlan, normaliser } from '../moteur/configuration.js';
|
|||
|
|
import { ErreurConfiguration } from '../moteur/erreurs.js';
|
|||
|
|
import { HISTORIQUE_PAR_DEFAUT, rechercher, regenerer } from '../moteur/recherche.js';
|
|||
|
|
import { VERSION } from '../version.genere.js';
|
|||
|
|
import { serialiser } from './canonique.js';
|
|||
|
|
import { analyser, configurationDepuisCharge, creerCharge } from './document.js';
|
|||
|
|
import { derive, examiner, fautes, formeNommee, planDepuisFichier, versFichier } from './placements.js';
|
|||
|
|
|
|||
|
|
const ENTETE = Object.freeze({ revision: 1, produitVersion: VERSION.affichee });
|
|||
|
|
|
|||
|
|
// Fige une valeur à toute profondeur et la rend : une fonction qui
|
|||
|
|
// écrirait dans ce qu'elle reçoit lève alors TypeError.
|
|||
|
|
function geler(valeur) {
|
|||
|
|
if (typeof valeur === 'object' && valeur !== null) {
|
|||
|
|
for (const enfant of Object.values(valeur)) geler(enfant);
|
|||
|
|
Object.freeze(valeur);
|
|||
|
|
}
|
|||
|
|
return valeur;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La configuration de la petite démonstration (§ 15.3), neuve à chaque
|
|||
|
|
// appel : 12 personnes, 4 tables de 3 sièges, 4 tours, aucune réservation.
|
|||
|
|
const petite = () => CATALOGUE.find(({ cle }) => cle === 'petite').construire();
|
|||
|
|
|
|||
|
|
// Trois propositions du moteur sur la petite démonstration, figées.
|
|||
|
|
const GENERATION = geler(rechercher(petite(), { graine: 5, arret: 2_000, nombre: 3 }));
|
|||
|
|
|
|||
|
|
// Le plan parfait de la petite démonstration, en proposition du moteur.
|
|||
|
|
const PARFAITE = Object.freeze({ id: 1, graine: 48_271, arret: 20_000, historique: 1_000, plan: PLAN_PARFAIT_PETITE });
|
|||
|
|
|
|||
|
|
// La forme positionnelle attendue de PARFAITE, sièges non attribués, écrite
|
|||
|
|
// à la main : une copie neuve à chaque appel.
|
|||
|
|
function fichierParfait() {
|
|||
|
|
return {
|
|||
|
|
id: 1,
|
|||
|
|
graine: 48_271,
|
|||
|
|
arret: 20_000,
|
|||
|
|
historique: 1_000,
|
|||
|
|
produitVersion: VERSION.affichee,
|
|||
|
|
siegesAttribues: false,
|
|||
|
|
tables: [1, 2, 3, 4],
|
|||
|
|
capacites: [3, 3, 3, 3],
|
|||
|
|
tours: 4,
|
|||
|
|
participants: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|||
|
|
placement: [
|
|||
|
|
{ sieges: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], reserve: [] },
|
|||
|
|
{ sieges: [[6, 9, 12], [3, 8, 11], [2, 5, 10], [1, 4, 7]], reserve: [] },
|
|||
|
|
{ sieges: [[4, 8, 10], [2, 7, 12], [1, 6, 11], [3, 5, 9]], reserve: [] },
|
|||
|
|
{ sieges: [[5, 7, 11], [1, 9, 10], [3, 4, 12], [2, 6, 8]], reserve: [] },
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fichierParfait, cohérent lui aussi, ses tables déclarées dans l'ordre 3,
|
|||
|
|
// 1, 2, 4, chaque tour portant ses listes dans cet ordre, et ses
|
|||
|
|
// participants déclarés à rebours : un ordre de sortie qui suivrait celui
|
|||
|
|
// de la déclaration s'y voit.
|
|||
|
|
function fichierPermute() {
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
const ordre = [2, 0, 1, 3];
|
|||
|
|
p.tables = ordre.map((rang) => p.tables[rang]);
|
|||
|
|
p.capacites = ordre.map((rang) => p.capacites[rang]);
|
|||
|
|
for (const tour of p.placement) tour.sieges = ordre.map((rang) => tour.sieges[rang]);
|
|||
|
|
p.participants.reverse();
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fichierParfait, cohérent lui aussi, dont le tour 1 laisse les personnes 6
|
|||
|
|
// et 3 à la réserve, écrite dans cet ordre, hors de l'ordre croissant.
|
|||
|
|
function fichierAReserve() {
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
p.placement[0] = { sieges: [[1, 2], [4, 5], [7, 8, 9], [10, 11, 12]], reserve: [6, 3] };
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fichierParfait, cohérent lui aussi, la table 4 portée à quatre sièges et
|
|||
|
|
// déclarée la première : tables 4, 1, 2, 3, capacités 4, 3, 3, 3. Au tour 1,
|
|||
|
|
// la table 3 cède la personne 9 à la table 4, pleine à sa propre capacité,
|
|||
|
|
// au-delà de celle de ses voisines. Une capacité lue au rang de
|
|||
|
|
// l'identifiant moins un, et non au rang de la déclaration, donnerait 3 à la
|
|||
|
|
// table 4 et 4 à la table 1.
|
|||
|
|
function fichierInegal() {
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
const ordre = [3, 0, 1, 2];
|
|||
|
|
p.tables = ordre.map((rang) => p.tables[rang]);
|
|||
|
|
p.capacites = [4, 3, 3, 3];
|
|||
|
|
for (const tour of p.placement) tour.sieges = ordre.map((rang) => tour.sieges[rang]);
|
|||
|
|
p.placement[0].sieges = [[9, 10, 11, 12], [1, 2, 3], [4, 5, 6], [7, 8]];
|
|||
|
|
return p;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Noms et prénoms inventés des personnes 1 à 12 de la charge d'épreuve, puis
|
|||
|
|
// ceux de la personne 13, que des épreuves ajoutent.
|
|||
|
|
const PERSONNES = Object.freeze([
|
|||
|
|
['Aubépine', 'Agathe'],
|
|||
|
|
['Bruyère', 'Basile'],
|
|||
|
|
['Chardon', 'Céleste'],
|
|||
|
|
['Doucette', 'Désiré'],
|
|||
|
|
['Églantine', 'Edmée'],
|
|||
|
|
['Fougère', 'Firmin'],
|
|||
|
|
['Giroflée', 'Gisèle'],
|
|||
|
|
['Houblon', 'Honoré'],
|
|||
|
|
['Isatis', 'Ida'],
|
|||
|
|
['Jonquille', 'Joachim'],
|
|||
|
|
['Lavande', 'Lazare'],
|
|||
|
|
['Mélisse', 'Mathurin'],
|
|||
|
|
]);
|
|||
|
|
const TREIZIEME = Object.freeze(['Nigelle', 'Noémie']);
|
|||
|
|
// Appartenances inventées, données aux groupes de la démonstration dans
|
|||
|
|
// l'ordre où ils apparaissent, par identifiant croissant.
|
|||
|
|
const CERCLES = Object.freeze(['Cercle Aulne', 'Cercle Bouleau', 'Cercle Charme']);
|
|||
|
|
|
|||
|
|
const personne = (id, nom, prenom, appartenance = null) => ({
|
|||
|
|
id,
|
|||
|
|
nom,
|
|||
|
|
prenom,
|
|||
|
|
appartenance,
|
|||
|
|
courriel: null,
|
|||
|
|
titrePressenti: null,
|
|||
|
|
notes: null,
|
|||
|
|
exclu: false,
|
|||
|
|
});
|
|||
|
|
const table = (id, numero, sieges = null) => ({ id, numero, sieges, forme: 'ronde', position: { x: 300 * id, y: 0 } });
|
|||
|
|
|
|||
|
|
// La charge de la petite démonstration, sans proposition : ses personnes
|
|||
|
|
// sous des noms inventés et leurs groupes, ses tables aux sièges du défaut,
|
|||
|
|
// 3, ses 4 tours.
|
|||
|
|
function chargePetite() {
|
|||
|
|
const configuration = petite();
|
|||
|
|
const charge = creerCharge({ id: 'evt-pinsons', nom: 'Soirée des Pinsons', siegesParDefaut: 3, tours: 4 });
|
|||
|
|
const cercleDe = new Map();
|
|||
|
|
charge.participants = configuration.participants.map(({ id, appartenance }) => {
|
|||
|
|
if (!cercleDe.has(appartenance)) cercleDe.set(appartenance, CERCLES[cercleDe.size]);
|
|||
|
|
const [nom, prenom] = PERSONNES[id - 1];
|
|||
|
|
return personne(id, nom, prenom, cercleDe.get(appartenance));
|
|||
|
|
});
|
|||
|
|
charge.tables = configuration.tables.map(({ id, numero }) => table(id, numero));
|
|||
|
|
charge.prochainsIds = { participant: 13, table: 5, proposition: 1 };
|
|||
|
|
return charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// chargePetite avec les propositions de GENERATION, converties sans
|
|||
|
|
// attribution de sièges sur sa propre configuration et numérotées à partir
|
|||
|
|
// de prochainsIds.proposition, qui avance au-delà de la dernière.
|
|||
|
|
function chargeAvecGeneration() {
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
const configuration = configurationDepuisCharge(charge);
|
|||
|
|
const decalage = charge.prochainsIds.proposition - 1;
|
|||
|
|
charge.propositions = GENERATION.map((proposition) =>
|
|||
|
|
versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: false, decalage }),
|
|||
|
|
);
|
|||
|
|
charge.prochainsIds.proposition = decalage + GENERATION.length + 1;
|
|||
|
|
charge.evenement.etat = 'propose';
|
|||
|
|
return charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Le retenu tiré d'une proposition de fichier : son identifiant à la clé
|
|||
|
|
// proposition, une copie de son plan.
|
|||
|
|
const retenuDe = ({ id, siegesAttribues, tables, capacites, tours, participants, placement }) =>
|
|||
|
|
structuredClone({ proposition: id, siegesAttribues, tables, capacites, tours, participants, placement });
|
|||
|
|
|
|||
|
|
// La charge écrite dans son fichier, puis relue : ce que trouve l'ouverture.
|
|||
|
|
const relire = (charge) => analyser(serialiser(charge, ENTETE)).charge;
|
|||
|
|
|
|||
|
|
// Le fichier de la charge, abîmé : abimer reçoit le JSON du texte écrit et
|
|||
|
|
// le modifie, puis le texte qui en résulte est relu par l'analyse, qui ne
|
|||
|
|
// contrôle des propositions et du retenu que leur conteneur.
|
|||
|
|
function abimerFichier(charge, abimer) {
|
|||
|
|
const ecrit = JSON.parse(serialiser(charge, ENTETE));
|
|||
|
|
abimer(ecrit);
|
|||
|
|
return analyser(JSON.stringify(ecrit)).charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Exige que appel lève TypeError, et que son message réponde à motif.
|
|||
|
|
function leveTypeError(appel, motif, message) {
|
|||
|
|
assert.throws(appel, (erreur) => erreur instanceof TypeError && motif.test(erreur.message), message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Une ligne attendue de la forme nommée.
|
|||
|
|
const ligne = (tour, idTable, numero, siege, participant, nom, prenom) => ({
|
|||
|
|
tour,
|
|||
|
|
table: idTable,
|
|||
|
|
numero,
|
|||
|
|
siege,
|
|||
|
|
participant,
|
|||
|
|
nom,
|
|||
|
|
prenom,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('versFichier et planDepuisFichier : la proposition du moteur et sa forme positionnelle (§ 8.9)', () => {
|
|||
|
|
test('le plan parfait de la petite démonstration donne la forme positionnelle attendue, et planDepuisFichier rend le plan', () => {
|
|||
|
|
const fichier = versFichier(PARFAITE, petite(), { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 });
|
|||
|
|
assert.deepStrictEqual(fichier, fichierParfait());
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(fichier), PLAN_PARFAIT_PETITE);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("chaque proposition de deux générations, sous l'historique par défaut du moteur puis sous un autre : graine, arrêt, historique et produitVersion portés, les trois premiers régénèrent le plan, listes de table croissantes, le plan rendu tel quel", () => {
|
|||
|
|
// GENERATION suit l'historique que le moteur prend par défaut ; la
|
|||
|
|
// seconde génération en demande un autre : sur elle seule, un fichier
|
|||
|
|
// qui porte le défaut au lieu de l'historique reçu se distingue, et sa
|
|||
|
|
// régénération rend un autre plan (§ 8.9).
|
|||
|
|
const seconde = rechercher(petite(), { graine: 5, arret: 500, nombre: 1, historique: 37 });
|
|||
|
|
assert.notEqual(HISTORIQUE_PAR_DEFAUT, 37);
|
|||
|
|
assert.deepStrictEqual(seconde.map(({ historique }) => historique), [37]);
|
|||
|
|
assert.equal(GENERATION.length, 3);
|
|||
|
|
for (const proposition of [...GENERATION, ...seconde]) {
|
|||
|
|
for (const attribuerSieges of [false, true]) {
|
|||
|
|
const fichier = versFichier(proposition, petite(), { produitVersion: VERSION.affichee, attribuerSieges, decalage: 0 });
|
|||
|
|
const { id, graine, arret, historique } = proposition;
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
[fichier.id, fichier.graine, fichier.arret, fichier.historique, fichier.produitVersion],
|
|||
|
|
[id, graine, arret, historique, VERSION.affichee],
|
|||
|
|
);
|
|||
|
|
assert.equal(fichier.siegesAttribues, attribuerSieges);
|
|||
|
|
assert.deepStrictEqual(fichier.tables, [1, 2, 3, 4]);
|
|||
|
|
assert.deepStrictEqual(fichier.capacites, [3, 3, 3, 3]);
|
|||
|
|
assert.equal(fichier.tours, 4);
|
|||
|
|
assert.deepStrictEqual(fichier.participants, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
|||
|
|
assert.equal(fichier.placement.length, 4);
|
|||
|
|
for (const { sieges, reserve } of fichier.placement) {
|
|||
|
|
for (const liste of sieges) assert.deepStrictEqual(liste, [...liste].sort((a, b) => a - b));
|
|||
|
|
assert.deepStrictEqual(reserve, []);
|
|||
|
|
}
|
|||
|
|
assert.deepStrictEqual(fautes(fichier), []);
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(fichier), proposition.plan);
|
|||
|
|
assert.deepStrictEqual(regenerer(petite(), fichier), proposition.plan);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("sièges non attribués : chaque liste de table se trie par identifiant croissant ; attribués : l'ordre des sièges est gardé ; la réserve se trie dans les deux cas", () => {
|
|||
|
|
const configuration = petite();
|
|||
|
|
configuration.participants.push({ id: 13, nom: TREIZIEME[0], prenom: TREIZIEME[1], appartenance: null });
|
|||
|
|
const inverses = PLAN_PARFAIT_PETITE.tours.map((listes) => listes.map((liste) => [...liste].reverse()));
|
|||
|
|
inverses[0][3] = [11, 10];
|
|||
|
|
const proposition = {
|
|||
|
|
...PARFAITE,
|
|||
|
|
plan: { tables: [1, 2, 3, 4], tours: inverses, reserves: [[13, 12], [13], [13], [13]] },
|
|||
|
|
};
|
|||
|
|
const reserves = [[12, 13], [13], [13], [13]];
|
|||
|
|
|
|||
|
|
const nonAttribues = versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 });
|
|||
|
|
assert.equal(nonAttribues.siegesAttribues, false);
|
|||
|
|
assert.deepStrictEqual(nonAttribues.participants, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
|||
|
|
const tries = PLAN_PARFAIT_PETITE.tours.map((listes) => listes.map((liste) => [...liste]));
|
|||
|
|
tries[0][3] = [10, 11];
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
nonAttribues.placement,
|
|||
|
|
tries.map((sieges, r) => ({ sieges, reserve: reserves[r] })),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const attribues = versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: true, decalage: 0 });
|
|||
|
|
assert.equal(attribues.siegesAttribues, true);
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
attribues.placement,
|
|||
|
|
inverses.map((sieges, r) => ({ sieges, reserve: reserves[r] })),
|
|||
|
|
);
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(attribues), planDepuisFichier(nonAttribues));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("les tables dans l'ordre du plan, chacune avec sa capacité dans la configuration, quel que soit l'ordre où la configuration les range", () => {
|
|||
|
|
const options = { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 };
|
|||
|
|
const configuration = petite();
|
|||
|
|
configuration.tables[3].capacite = 4;
|
|||
|
|
configuration.tables.reverse();
|
|||
|
|
const [proposition] = rechercher(configuration, { graine: 5, arret: 2_000, nombre: 1 });
|
|||
|
|
assert.deepStrictEqual(proposition.plan.tables, [4, 3, 2, 1]);
|
|||
|
|
const engendre = versFichier(proposition, configuration, options);
|
|||
|
|
assert.deepStrictEqual([engendre.tables, engendre.capacites], [[4, 3, 2, 1], [4, 3, 3, 3]]);
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(engendre), proposition.plan);
|
|||
|
|
const parfait = versFichier(PARFAITE, configuration, options);
|
|||
|
|
assert.deepStrictEqual([parfait.tables, parfait.capacites], [[1, 2, 3, 4], [3, 3, 3, 4]]);
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(parfait), PLAN_PARFAIT_PETITE);
|
|||
|
|
// Les participants se déclarent par identifiant croissant, même quand
|
|||
|
|
// la configuration les range à rebours.
|
|||
|
|
const renversee = petite();
|
|||
|
|
renversee.participants.reverse();
|
|||
|
|
const [tiree] = rechercher(renversee, { graine: 5, arret: 2_000, nombre: 1 });
|
|||
|
|
assert.deepStrictEqual(versFichier(tiree, renversee, options).participants, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("planDepuisFichier rend chaque réserve par identifiant croissant, comme le moteur les rend, quel que soit l'ordre écrit", () => {
|
|||
|
|
const p = fichierAReserve();
|
|||
|
|
assert.deepStrictEqual(fautes(p), []);
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(p).reserves[0], [3, 6]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("versFichier ne modifie ni la proposition ni la configuration, et ne partage aucune liste avec elles ; planDepuisFichier non plus avec le fichier", () => {
|
|||
|
|
const configuration = geler(petite());
|
|||
|
|
for (const proposition of GENERATION) {
|
|||
|
|
const fichier = versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: true, decalage: 0 });
|
|||
|
|
assert.notEqual(fichier.tables, proposition.plan.tables);
|
|||
|
|
fichier.placement.forEach(({ sieges, reserve }, r) => {
|
|||
|
|
assert.notEqual(reserve, proposition.plan.reserves[r]);
|
|||
|
|
sieges.forEach((liste, i) => assert.notEqual(liste, proposition.plan.tours[r][i]));
|
|||
|
|
});
|
|||
|
|
const gele = geler(fichier);
|
|||
|
|
const plan = planDepuisFichier(gele);
|
|||
|
|
assert.notEqual(plan.tables, gele.tables);
|
|||
|
|
plan.tours.forEach((listes, r) => {
|
|||
|
|
assert.notEqual(plan.reserves[r], gele.placement[r].reserve);
|
|||
|
|
listes.forEach((liste, i) => assert.notEqual(liste, gele.placement[r].sieges[i]));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('versFichier refuse un drapeau, une version ou un décalage qui manquent ou sortent de leur domaine, et un plan que la configuration contredit : TypeError', () => {
|
|||
|
|
// Une troisième proposition : le décalage −2 lui donnerait l'identifiant
|
|||
|
|
// 1, que le contrôle de forme admet.
|
|||
|
|
const troisieme = { ...PARFAITE, id: 3 };
|
|||
|
|
for (const options of [
|
|||
|
|
{ produitVersion: VERSION.affichee, decalage: 0 },
|
|||
|
|
{ produitVersion: VERSION.affichee, attribuerSieges: 'non', decalage: 0 },
|
|||
|
|
{ attribuerSieges: false, decalage: 0 },
|
|||
|
|
{ produitVersion: '', attribuerSieges: false, decalage: 0 },
|
|||
|
|
]) {
|
|||
|
|
assert.throws(() => versFichier(troisieme, petite(), options), TypeError, JSON.stringify(options));
|
|||
|
|
}
|
|||
|
|
// Un décalage hors de son domaine se refuse en son nom, avant toute
|
|||
|
|
// forme.
|
|||
|
|
for (const decalage of [undefined, -2, 0.5, '0']) {
|
|||
|
|
leveTypeError(
|
|||
|
|
() => versFichier(troisieme, petite(), { produitVersion: VERSION.affichee, attribuerSieges: false, decalage }),
|
|||
|
|
/décalage/,
|
|||
|
|
`décalage ${JSON.stringify(decalage) ?? 'absent'}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
const options = { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 };
|
|||
|
|
const avecTreize = petite();
|
|||
|
|
avecTreize.participants.push({ id: 13, nom: TREIZIEME[0], appartenance: null });
|
|||
|
|
leveTypeError(() => versFichier(PARFAITE, avecTreize, options), /"MANQUANT"/, 'personne 13 non placée');
|
|||
|
|
const tableReduite = petite();
|
|||
|
|
tableReduite.tables[3].capacite = 2;
|
|||
|
|
leveTypeError(() => versFichier(PARFAITE, tableReduite, options), /"LONGUEUR"/, 'table 4 réduite');
|
|||
|
|
const cinqTours = petite();
|
|||
|
|
cinqTours.tours = 5;
|
|||
|
|
leveTypeError(() => versFichier(PARFAITE, cinqTours, options), /"LONGUEUR"/, 'cinq tours');
|
|||
|
|
const sansTable = petite();
|
|||
|
|
sansTable.tables.pop();
|
|||
|
|
leveTypeError(() => versFichier(PARFAITE, sansTable, options), /"FORME"/, 'table 4 absente');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("le fichier écrit se relit : examiner garde chaque proposition telle qu'écrite, sans faute ni dérive", () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const examen = examiner(relire(charge));
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.derives, []);
|
|||
|
|
assert.deepStrictEqual(examen.retenu, { fautes: [], raisons: [] });
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, charge.propositions);
|
|||
|
|
examen.charge.propositions.forEach((proposition, rang) => {
|
|||
|
|
assert.deepStrictEqual(planDepuisFichier(proposition), GENERATION[rang].plan);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("des générations qui s'accumulent (§ 5.7) : chacune décalée de prochainsIds.proposition − 1, qui avance ensuite ; après un effacement qui épargne le retenu, aucun identifiant ne revient, et examiner garde chaque proposition relue", () => {
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
const configuration = configurationDepuisCharge(charge);
|
|||
|
|
// L'appelant décale une génération de prochainsIds.proposition − 1,
|
|||
|
|
// puis porte le compteur au-delà du dernier identifiant écrit.
|
|||
|
|
const generer = (graine) => {
|
|||
|
|
const decalage = charge.prochainsIds.proposition - 1;
|
|||
|
|
const options = { produitVersion: VERSION.affichee, attribuerSieges: false, decalage };
|
|||
|
|
const propositions = rechercher(configuration, { graine, arret: 2_000, nombre: 3 });
|
|||
|
|
for (const proposition of propositions) charge.propositions.push(versFichier(proposition, configuration, options));
|
|||
|
|
charge.prochainsIds.proposition = decalage + propositions.length + 1;
|
|||
|
|
};
|
|||
|
|
generer(5);
|
|||
|
|
generer(9);
|
|||
|
|
charge.evenement.etat = 'propose';
|
|||
|
|
assert.deepStrictEqual(charge.propositions.map(({ id }) => id), [1, 2, 3, 4, 5, 6]);
|
|||
|
|
const examen = examiner(relire(charge));
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, charge.propositions);
|
|||
|
|
// Effacer la liste en épargnant le retenu (§ 5.7) garde la proposition
|
|||
|
|
// 2 seule ; la génération suivante prend 7, 8 et 9, aucun des
|
|||
|
|
// identifiants effacés.
|
|||
|
|
charge.retenu = retenuDe(charge.propositions[1]);
|
|||
|
|
charge.propositions = [charge.propositions[1]];
|
|||
|
|
charge.evenement.etat = 'retenu';
|
|||
|
|
generer(13);
|
|||
|
|
assert.deepStrictEqual(charge.propositions.map(({ id }) => id), [2, 7, 8, 9]);
|
|||
|
|
const apres = examiner(relire(charge));
|
|||
|
|
assert.deepStrictEqual(apres.fautives, []);
|
|||
|
|
assert.deepStrictEqual(apres.charge.propositions, charge.propositions);
|
|||
|
|
assert.deepStrictEqual(apres.retenu, { fautes: [], raisons: [] });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('fautes : la corruption délibérée (§ 8.9, points 1 à 3)', () => {
|
|||
|
|
// Les quatre façons d'abîmer une proposition de la petite démonstration,
|
|||
|
|
// chacune dans son tour : chaque fonction abîme la proposition reçue et
|
|||
|
|
// rend la faute que le contrôle doit nommer.
|
|||
|
|
const QUATRE_FACONS = [
|
|||
|
|
['une longueur fausse', (p) => {
|
|||
|
|
const { sieges } = p.placement[0];
|
|||
|
|
sieges[1].push(sieges[0].shift());
|
|||
|
|
return { code: 'LONGUEUR', tour: 1, table: 2, declare: 3, ecrit: 4 };
|
|||
|
|
}],
|
|||
|
|
['un identifiant dupliqué', (p) => {
|
|||
|
|
const { sieges, reserve } = p.placement[1];
|
|||
|
|
reserve.push(sieges[0][0]);
|
|||
|
|
return { code: 'DOUBLON', tour: 2, participant: sieges[0][0] };
|
|||
|
|
}],
|
|||
|
|
['un identifiant inconnu', (p) => {
|
|||
|
|
p.placement[2].reserve.push(13);
|
|||
|
|
return { code: 'INCONNU', tour: 3, participant: 13 };
|
|||
|
|
}],
|
|||
|
|
['un identifiant manquant sans entrée de réserve', (p) => {
|
|||
|
|
const perdu = p.placement[3].sieges[2].pop();
|
|||
|
|
return { code: 'MANQUANT', tour: 4, participant: perdu };
|
|||
|
|
}],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
test('trois propositions, la deuxième abîmée de quatre façons tour à tour : fautes nomme chacune avec ses coordonnées, examiner écarte la seule fautive et garde les deux autres', () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
assert.deepStrictEqual(charge.propositions[1].tables, [1, 2, 3, 4]);
|
|||
|
|
for (const [libelle, abimer] of QUATRE_FACONS) {
|
|||
|
|
let attendue;
|
|||
|
|
const lue = abimerFichier(charge, (ecrit) => {
|
|||
|
|
attendue = abimer(ecrit.charge.propositions[1]);
|
|||
|
|
});
|
|||
|
|
const abimee = lue.propositions[1];
|
|||
|
|
assert.equal(abimee.id, 2, libelle);
|
|||
|
|
assert.notDeepStrictEqual(abimee, charge.propositions[1], libelle);
|
|||
|
|
assert.deepStrictEqual(fautes(abimee), [attendue], libelle);
|
|||
|
|
const examen = examiner(lue);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, [{ rang: 1, id: 2, fautes: [attendue] }], libelle);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, [charge.propositions[0], charge.propositions[2]], libelle);
|
|||
|
|
assert.deepStrictEqual(examen.derives, [], libelle);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("les quatre défauts dans la même proposition sont nommés dans l'ordre des tours", () => {
|
|||
|
|
const abimee = structuredClone(chargeAvecGeneration().propositions[1]);
|
|||
|
|
const attendues = QUATRE_FACONS.map(([, abimer]) => abimer(abimee));
|
|||
|
|
assert.deepStrictEqual(fautes(abimee), [attendues[0], attendues[1], attendues[2], attendues[3]]);
|
|||
|
|
assert.deepStrictEqual(attendues.map(({ tour }) => tour), [1, 2, 3, 4]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("dans un tour : le nombre de listes, chaque table dans l'ordre déclaré, puis doublons, inconnus et manquants par identifiant croissant ; le nombre de tours avant tout", () => {
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
p.tours = 5;
|
|||
|
|
p.placement[1] = { sieges: [[6, 9, 12, 3], [8], [2, 10, 14, 9], [1, 4, 7], [20]], reserve: [15, 1, 13, 15] };
|
|||
|
|
assert.deepStrictEqual(fautes(p), [
|
|||
|
|
{ code: 'LONGUEUR', tour: null, table: null, declare: 5, ecrit: 4 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 2, table: null, declare: 4, ecrit: 5 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 2, table: 1, declare: 3, ecrit: 4 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 2, table: 3, declare: 3, ecrit: 4 },
|
|||
|
|
{ code: 'DOUBLON', tour: 2, participant: 1 },
|
|||
|
|
{ code: 'DOUBLON', tour: 2, participant: 9 },
|
|||
|
|
{ code: 'INCONNU', tour: 2, participant: 13 },
|
|||
|
|
{ code: 'INCONNU', tour: 2, participant: 14 },
|
|||
|
|
{ code: 'INCONNU', tour: 2, participant: 15 },
|
|||
|
|
{ code: 'INCONNU', tour: 2, participant: 20 },
|
|||
|
|
{ code: 'MANQUANT', tour: 2, participant: 5 },
|
|||
|
|
{ code: 'MANQUANT', tour: 2, participant: 11 },
|
|||
|
|
]);
|
|||
|
|
// Plus de tours écrits que déclarés : la même faute, dans l'autre sens.
|
|||
|
|
const enTrop = { ...fichierParfait(), tours: 3 };
|
|||
|
|
assert.deepStrictEqual(fautes(enTrop), [{ code: 'LONGUEUR', tour: null, table: null, declare: 3, ecrit: 4 }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("un identifiant se nomme une fois par tour : inconnu écrit deux fois, déclaré écrit trois fois ; une liste de moins que les tables nomme aussi ses absents", () => {
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
p.placement[0].reserve = [13, 2, 13, 2];
|
|||
|
|
p.placement[2].sieges.pop();
|
|||
|
|
assert.deepStrictEqual(fautes(p), [
|
|||
|
|
{ code: 'DOUBLON', tour: 1, participant: 2 },
|
|||
|
|
{ code: 'INCONNU', tour: 1, participant: 13 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 3, table: null, declare: 4, ecrit: 3 },
|
|||
|
|
{ code: 'MANQUANT', tour: 3, participant: 3 },
|
|||
|
|
{ code: 'MANQUANT', tour: 3, participant: 5 },
|
|||
|
|
{ code: 'MANQUANT', tour: 3, participant: 9 },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("les tables suivent l'ordre déclaré, les identifiants l'ordre croissant, quel que soit l'ordre de la déclaration des participants", () => {
|
|||
|
|
const permute = fichierPermute();
|
|||
|
|
assert.deepStrictEqual(permute.tables, [3, 1, 2, 4]);
|
|||
|
|
assert.deepStrictEqual(fautes(permute), []);
|
|||
|
|
const longues = fichierPermute();
|
|||
|
|
longues.placement[0].sieges = [[7, 8, 9, 4], [1, 2, 3, 5], [6], [10, 11, 12]];
|
|||
|
|
assert.deepStrictEqual(fautes(longues), [
|
|||
|
|
{ code: 'LONGUEUR', tour: 1, table: 3, declare: 3, ecrit: 4 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 1, table: 1, declare: 3, ecrit: 4 },
|
|||
|
|
]);
|
|||
|
|
const melees = fichierPermute();
|
|||
|
|
melees.placement[0].sieges[0] = [7, 8];
|
|||
|
|
melees.placement[0].sieges[1] = [1, 3];
|
|||
|
|
melees.placement[0].reserve = [5, 3];
|
|||
|
|
assert.deepStrictEqual(fautes(melees), [
|
|||
|
|
{ code: 'DOUBLON', tour: 1, participant: 3 },
|
|||
|
|
{ code: 'DOUBLON', tour: 1, participant: 5 },
|
|||
|
|
{ code: 'MANQUANT', tour: 1, participant: 2 },
|
|||
|
|
{ code: 'MANQUANT', tour: 1, participant: 9 },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("capacités inégales, tables déclarées hors de l'ordre des identifiants : chaque liste se mesure à la capacité de son rang dans la déclaration, et la proposition se relit sans être écartée", () => {
|
|||
|
|
const inegal = fichierInegal();
|
|||
|
|
assert.deepStrictEqual(fautes(inegal), []);
|
|||
|
|
// La table 4 au-delà de ses quatre sièges, la table 1 au-delà de ses
|
|||
|
|
// trois mais en deçà de quatre ; la table 3 vide n'est pas fautive.
|
|||
|
|
const debordees = fichierInegal();
|
|||
|
|
debordees.placement[0].sieges = [[8, 9, 10, 11, 12], [1, 2, 3, 7], [4, 5, 6], []];
|
|||
|
|
assert.deepStrictEqual(fautes(debordees), [
|
|||
|
|
{ code: 'LONGUEUR', tour: 1, table: 4, declare: 4, ecrit: 5 },
|
|||
|
|
{ code: 'LONGUEUR', tour: 1, table: 1, declare: 3, ecrit: 4 },
|
|||
|
|
]);
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.tables[3].sieges = 4;
|
|||
|
|
charge.propositions = [inegal];
|
|||
|
|
charge.prochainsIds.proposition = 2;
|
|||
|
|
charge.evenement.etat = 'propose';
|
|||
|
|
const examen = examiner(relire(charge));
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.derives, []);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, [inegal]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("une table moins remplie que sa capacité n'est pas fautive : la longueur déclarée borne la liste, une salle peut avoir des places vides", () => {
|
|||
|
|
const configuration = petite();
|
|||
|
|
configuration.participants[4].exclu = true;
|
|||
|
|
const [proposition] = rechercher(configuration, { graine: 9, arret: 1_000, nombre: 1 });
|
|||
|
|
const fichier = versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 });
|
|||
|
|
assert.deepStrictEqual(fichier.participants, [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12]);
|
|||
|
|
for (const { sieges } of fichier.placement) {
|
|||
|
|
assert.deepStrictEqual(sieges.map((liste) => liste.length).sort((a, b) => a - b), [2, 3, 3, 3]);
|
|||
|
|
}
|
|||
|
|
assert.deepStrictEqual(fautes(fichier), []);
|
|||
|
|
const versLaReserve = fichierParfait();
|
|||
|
|
versLaReserve.placement[0] = { sieges: [[1, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], reserve: [2] };
|
|||
|
|
assert.deepStrictEqual(fautes(versLaReserve), []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la forme se contrôle d'abord : la première valeur hors de sa règle est nommée FORME, par son chemin dans la proposition", () => {
|
|||
|
|
const cas = [
|
|||
|
|
['un nombre', () => 42, ''],
|
|||
|
|
['null', () => null, ''],
|
|||
|
|
['une liste', () => [], ''],
|
|||
|
|
['identifiant 0', (p) => { p.id = 0; return p; }, 'id'],
|
|||
|
|
['identifiant en chaîne', (p) => { p.id = '1'; return p; }, 'id'],
|
|||
|
|
['graine négative', (p) => { p.graine = -1; return p; }, 'graine'],
|
|||
|
|
['graine de 2^32', (p) => { p.graine = 2 ** 32; return p; }, 'graine'],
|
|||
|
|
['arrêt nul', (p) => { p.arret = 0; return p; }, 'arret'],
|
|||
|
|
['historique fractionnaire', (p) => { p.historique = 1.5; return p; }, 'historique'],
|
|||
|
|
['version vide', (p) => { p.produitVersion = ''; return p; }, 'produitVersion'],
|
|||
|
|
['drapeau en chaîne', (p) => { p.siegesAttribues = 'non'; return p; }, 'siegesAttribues'],
|
|||
|
|
['tables en objet', (p) => { p.tables = {}; return p; }, 'tables'],
|
|||
|
|
['identifiant de table 0', (p) => { p.tables[3] = 0; return p; }, 'tables[3]'],
|
|||
|
|
["capacité d'un siège", (p) => { p.capacites[2] = 1; return p; }, 'capacites[2]'],
|
|||
|
|
['zéro tour', (p) => { p.tours = 0; return p; }, 'tours'],
|
|||
|
|
['participant en chaîne', (p) => { p.participants[1] = '2'; return p; }, 'participants[1]'],
|
|||
|
|
['placement en objet', (p) => { p.placement = {}; return p; }, 'placement'],
|
|||
|
|
['tour en liste', (p) => { p.placement[0] = []; return p; }, 'placement[0]'],
|
|||
|
|
['sièges en chaîne', (p) => { p.placement[1].sieges = 'x'; return p; }, 'placement[1].sieges'],
|
|||
|
|
['liste de table en nombre', (p) => { p.placement[2].sieges[1] = 7; return p; }, 'placement[2].sieges[1]'],
|
|||
|
|
['occupant fractionnaire', (p) => { p.placement[3].sieges[0][2] = 2.5; return p; }, 'placement[3].sieges[0][2]'],
|
|||
|
|
['occupant au-delà des entiers exacts', (p) => { p.placement[3].sieges[1][0] = 2 ** 53; return p; }, 'placement[3].sieges[1][0]'],
|
|||
|
|
['réserve absente', (p) => { delete p.placement[0].reserve; return p; }, 'placement[0].reserve'],
|
|||
|
|
['graine absente', (p) => { delete p.graine; return p; }, 'graine'],
|
|||
|
|
['deux clés inconnues : la plus petite', (p) => { p.zeta = 1; p.alpha = 2; return p; }, 'alpha'],
|
|||
|
|
["une clé inconnue qui ne s'écrit pas comme un identifiant", (p) => { p['clé inconnue'] = 1; return p; }, '["clé inconnue"]'],
|
|||
|
|
['une clé inconnue dans un tour', (p) => { p.placement[1].note = 'x'; return p; }, 'placement[1].note'],
|
|||
|
|
['un champ fautif avant une clé inconnue', (p) => { p.aaa = 1; p.tours = 0; return p; }, 'tours'],
|
|||
|
|
['une table déclarée deux fois', (p) => { p.tables[2] = 2; return p; }, 'tables[2]'],
|
|||
|
|
['des capacités moins nombreuses que les tables', (p) => { p.capacites.pop(); return p; }, 'capacites'],
|
|||
|
|
['des capacités plus nombreuses que les tables', (p) => { p.capacites.push(3); return p; }, 'capacites'],
|
|||
|
|
['un participant déclaré deux fois', (p) => { p.participants[3] = 3; return p; }, 'participants[3]'],
|
|||
|
|
['une forme fautive dans un placement incohérent : la forme seule', (p) => {
|
|||
|
|
p.placement[0].sieges[0].pop();
|
|||
|
|
p.historique = 0;
|
|||
|
|
return p;
|
|||
|
|
}, 'historique'],
|
|||
|
|
];
|
|||
|
|
const ecarts = cas.flatMap(([libelle, abimer, chemin]) => {
|
|||
|
|
const obtenues = fautes(abimer(fichierParfait()));
|
|||
|
|
const attendues = [{ code: 'FORME', chemin }];
|
|||
|
|
return JSON.stringify(obtenues) === JSON.stringify(attendues)
|
|||
|
|
? []
|
|||
|
|
: [`${libelle} : ${JSON.stringify(obtenues)} au lieu de ${JSON.stringify(attendues)}`];
|
|||
|
|
});
|
|||
|
|
assert.deepStrictEqual(ecarts, []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("le retenu se contrôle sous sa propre forme : la clé proposition au lieu de l'identifiant, ni graine, ni arrêt, ni historique, ni version", () => {
|
|||
|
|
const retenu = retenuDe(fichierParfait());
|
|||
|
|
assert.deepStrictEqual(fautes(retenu), []);
|
|||
|
|
assert.deepStrictEqual(fautes({ ...retenuDe(fichierParfait()), graine: 1 }), [{ code: 'FORME', chemin: 'graine' }]);
|
|||
|
|
const sansDrapeau = retenuDe(fichierParfait());
|
|||
|
|
delete sansDrapeau.siegesAttribues;
|
|||
|
|
assert.deepStrictEqual(fautes(sansDrapeau), [{ code: 'FORME', chemin: 'siegesAttribues' }]);
|
|||
|
|
assert.deepStrictEqual(fautes({ ...retenuDe(fichierParfait()), proposition: 0 }), [{ code: 'FORME', chemin: 'proposition' }]);
|
|||
|
|
retenu.placement[1].sieges[3].pop();
|
|||
|
|
assert.deepStrictEqual(fautes(retenu), [{ code: 'MANQUANT', tour: 2, participant: 7 }]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("ce que le contrôle n'attrape pas, et ne prétend pas attraper (§ 8.9, point 2)", () => {
|
|||
|
|
test("une permutation dans une table, et l'échange du contenu de deux tables de même capacité, gardent le multiensemble et les longueurs : fautes rend [], examiner garde la proposition", () => {
|
|||
|
|
for (const siegesAttribues of [true, false]) {
|
|||
|
|
const reference = { ...fichierParfait(), siegesAttribues };
|
|||
|
|
const permutee = structuredClone(reference);
|
|||
|
|
permutee.placement[0].sieges[0] = [3, 1, 2];
|
|||
|
|
const echangee = structuredClone(reference);
|
|||
|
|
const { sieges } = echangee.placement[1];
|
|||
|
|
[sieges[0], sieges[1]] = [sieges[1], sieges[0]];
|
|||
|
|
for (const abimee of [permutee, echangee]) {
|
|||
|
|
assert.notDeepStrictEqual(abimee, reference);
|
|||
|
|
assert.deepStrictEqual(fautes(abimee), []);
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.propositions = [abimee];
|
|||
|
|
charge.prochainsIds.proposition = 2;
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.derives, []);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, [abimee]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('derive : la péremption délibérée (§ 8.9, point 3 ; § 9 ; § 4.4)', () => {
|
|||
|
|
test('exclure 5, supprimer la table 2, réduire la table 3, passer à 3 tours, ajouter 13 : chaque raison nommée, triées par code puis identifiant ; examiner conserve chaque proposition et le retenu', () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
charge.retenu = retenuDe(charge.propositions[1]);
|
|||
|
|
charge.evenement.etat = 'retenu';
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
charge.tables = charge.tables.filter(({ id }) => id !== 2);
|
|||
|
|
charge.tables.find(({ id }) => id === 3).sieges = 2;
|
|||
|
|
charge.evenement.tours = 3;
|
|||
|
|
charge.participants.push(personne(13, ...TREIZIEME));
|
|||
|
|
charge.prochainsIds.participant = 14;
|
|||
|
|
const raisons = [
|
|||
|
|
{ code: 'CAPACITE_CHANGEE', table: 3, avant: 3, maintenant: 2 },
|
|||
|
|
{ code: 'PARTICIPANT_EXCLU', participant: 5 },
|
|||
|
|
{ code: 'PARTICIPANT_NON_PLACE', participant: 13 },
|
|||
|
|
{ code: 'TABLE_SUPPRIMEE', table: 2 },
|
|||
|
|
{ code: 'TOURS_CHANGES', avant: 4, maintenant: 3 },
|
|||
|
|
];
|
|||
|
|
const lue = relire(charge);
|
|||
|
|
for (const proposition of lue.propositions) assert.deepStrictEqual(derive(proposition, lue), raisons);
|
|||
|
|
const examen = examiner(lue);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.derives, [1, 2, 3].map((id) => ({ id, raisons })));
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, charge.propositions);
|
|||
|
|
assert.deepStrictEqual(examen.retenu, { fautes: [], raisons });
|
|||
|
|
assert.equal(examen.charge.retenu, lue.retenu);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("chaque changement seul donne sa seule raison ; ce qui ne touche ni les personnes placées, ni le mobilier, ni les tours n'est pas une dérive", () => {
|
|||
|
|
const cas = [
|
|||
|
|
['exclure la personne 5', (c) => { c.participants[4].exclu = true; },
|
|||
|
|
[{ code: 'PARTICIPANT_EXCLU', participant: 5 }]],
|
|||
|
|
['supprimer la personne 7', (c) => { c.participants.splice(6, 1); },
|
|||
|
|
[{ code: 'PARTICIPANT_SUPPRIME', participant: 7 }]],
|
|||
|
|
['ajouter la personne 13', (c) => { c.participants.push(personne(13, ...TREIZIEME)); },
|
|||
|
|
[{ code: 'PARTICIPANT_NON_PLACE', participant: 13 }]],
|
|||
|
|
['ajouter la personne 13, exclue', (c) => { c.participants.push({ ...personne(13, ...TREIZIEME), exclu: true }); },
|
|||
|
|
[]],
|
|||
|
|
['supprimer la table 2', (c) => { c.tables.splice(1, 1); },
|
|||
|
|
[{ code: 'TABLE_SUPPRIMEE', table: 2 }]],
|
|||
|
|
['ajouter la table 5', (c) => { c.tables.push(table(5, 5)); },
|
|||
|
|
[{ code: 'TABLE_AJOUTEE', table: 5 }]],
|
|||
|
|
['réduire la table 3 à deux sièges', (c) => { c.tables[2].sieges = 2; },
|
|||
|
|
[{ code: 'CAPACITE_CHANGEE', table: 3, avant: 3, maintenant: 2 }]],
|
|||
|
|
['agrandir la table 1 à quatre sièges', (c) => { c.tables[0].sieges = 4; },
|
|||
|
|
[{ code: 'CAPACITE_CHANGEE', table: 1, avant: 3, maintenant: 4 }]],
|
|||
|
|
['passer le défaut à quatre sièges', (c) => { c.evenement.siegesParDefaut = 4; },
|
|||
|
|
[1, 2, 3, 4].map((id) => ({ code: 'CAPACITE_CHANGEE', table: id, avant: 3, maintenant: 4 }))],
|
|||
|
|
['surcharger la table 4 à la valeur du défaut', (c) => { c.tables[3].sieges = 3; },
|
|||
|
|
[]],
|
|||
|
|
['passer à cinq tours', (c) => { c.evenement.tours = 5; },
|
|||
|
|
[{ code: 'TOURS_CHANGES', avant: 4, maintenant: 5 }]],
|
|||
|
|
['passer à trois tours', (c) => { c.evenement.tours = 3; },
|
|||
|
|
[{ code: 'TOURS_CHANGES', avant: 4, maintenant: 3 }]],
|
|||
|
|
["renommer, changer d'appartenance, renuméroter, déplacer et changer de forme, attribuer les sièges", (c) => {
|
|||
|
|
c.participants[0].nom = 'Aubier';
|
|||
|
|
c.participants[1].appartenance = null;
|
|||
|
|
c.tables[1].numero = 7;
|
|||
|
|
c.tables[2].position = { x: -40, y: 125.5 };
|
|||
|
|
c.tables[3].forme = 'carree';
|
|||
|
|
c.reglages.attribuerSieges = true;
|
|||
|
|
}, []],
|
|||
|
|
];
|
|||
|
|
for (const [libelle, changer, raisons] of cas) {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
changer(charge);
|
|||
|
|
for (const proposition of charge.propositions) {
|
|||
|
|
assert.deepStrictEqual(derive(proposition, charge), raisons, libelle);
|
|||
|
|
}
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, [], libelle);
|
|||
|
|
assert.equal(examen.charge.propositions.length, 3, libelle);
|
|||
|
|
const attendues = raisons.length === 0 ? [] : [1, 2, 3].map((id) => ({ id, raisons }));
|
|||
|
|
assert.deepStrictEqual(examen.derives, attendues, libelle);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("les raisons d'un même code se rangent par identifiant, quel que soit l'ordre de la déclaration ou de la charge", () => {
|
|||
|
|
const p = fichierPermute();
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.evenement.siegesParDefaut = 4;
|
|||
|
|
charge.tables = charge.tables.filter(({ id }) => id !== 3 && id !== 1);
|
|||
|
|
charge.tables.push(table(6, 6), table(5, 5));
|
|||
|
|
charge.participants[8].exclu = true;
|
|||
|
|
charge.participants[1].exclu = true;
|
|||
|
|
charge.participants = charge.participants.filter(({ id }) => id !== 11 && id !== 4);
|
|||
|
|
charge.participants.push(personne(14, 'Ortie', 'Ovide'), personne(13, ...TREIZIEME));
|
|||
|
|
assert.deepStrictEqual(derive(p, charge), [
|
|||
|
|
{ code: 'CAPACITE_CHANGEE', table: 2, avant: 3, maintenant: 4 },
|
|||
|
|
{ code: 'CAPACITE_CHANGEE', table: 4, avant: 3, maintenant: 4 },
|
|||
|
|
{ code: 'PARTICIPANT_EXCLU', participant: 2 },
|
|||
|
|
{ code: 'PARTICIPANT_EXCLU', participant: 9 },
|
|||
|
|
{ code: 'PARTICIPANT_NON_PLACE', participant: 13 },
|
|||
|
|
{ code: 'PARTICIPANT_NON_PLACE', participant: 14 },
|
|||
|
|
{ code: 'PARTICIPANT_SUPPRIME', participant: 4 },
|
|||
|
|
{ code: 'PARTICIPANT_SUPPRIME', participant: 11 },
|
|||
|
|
{ code: 'TABLE_AJOUTEE', table: 5 },
|
|||
|
|
{ code: 'TABLE_AJOUTEE', table: 6 },
|
|||
|
|
{ code: 'TABLE_SUPPRIMEE', table: 1 },
|
|||
|
|
{ code: 'TABLE_SUPPRIMEE', table: 3 },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("capacités inégales, tables déclarées hors de l'ordre des identifiants : chaque capacité déclarée se compare à celle de sa table, et seule celle qui change est nommée", () => {
|
|||
|
|
const p = fichierInegal();
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.tables[3].sieges = 4;
|
|||
|
|
assert.deepStrictEqual(derive(p, charge), []);
|
|||
|
|
charge.tables[3].sieges = 5;
|
|||
|
|
assert.deepStrictEqual(derive(p, charge), [{ code: 'CAPACITE_CHANGEE', table: 4, avant: 4, maintenant: 5 }]);
|
|||
|
|
charge.tables[3].sieges = 4;
|
|||
|
|
charge.evenement.siegesParDefaut = 4;
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
derive(p, charge),
|
|||
|
|
[1, 2, 3].map((id) => ({ code: 'CAPACITE_CHANGEE', table: id, avant: 3, maintenant: 4 })),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("une personne exclue à la génération puis réintégrée n'est pas placée : PARTICIPANT_NON_PLACE", () => {
|
|||
|
|
const configuration = petite();
|
|||
|
|
configuration.participants[4].exclu = true;
|
|||
|
|
const [proposition] = rechercher(configuration, { graine: 9, arret: 1_000, nombre: 1 });
|
|||
|
|
const fichier = versFichier(proposition, configuration, { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 });
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
assert.deepStrictEqual(derive(fichier, charge), []);
|
|||
|
|
charge.participants[4].exclu = false;
|
|||
|
|
assert.deepStrictEqual(derive(fichier, charge), [{ code: 'PARTICIPANT_NON_PLACE', participant: 5 }]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('sans dérive, le plan passe indexerPlan sur la configuration courante ; une table ajoutée est une dérive, que le moteur refuserait', () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const instance = normaliser(configurationDepuisCharge(charge));
|
|||
|
|
for (const proposition of charge.propositions) {
|
|||
|
|
assert.deepStrictEqual(derive(proposition, charge), []);
|
|||
|
|
indexerPlan(instance, planDepuisFichier(proposition));
|
|||
|
|
}
|
|||
|
|
charge.tables.push(table(5, 5));
|
|||
|
|
const agrandie = normaliser(configurationDepuisCharge(charge));
|
|||
|
|
for (const proposition of charge.propositions) {
|
|||
|
|
assert.deepStrictEqual(derive(proposition, charge), [{ code: 'TABLE_AJOUTEE', table: 5 }]);
|
|||
|
|
assert.throws(
|
|||
|
|
() => indexerPlan(agrandie, planDepuisFichier(proposition)),
|
|||
|
|
(erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'PLAN_TABLE_ABSENTE' && erreur.details.table === 5,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("examiner : le partage d'une charge lue (§ 8.9, point 3)", () => {
|
|||
|
|
test("un retenu fautif est signalé et reste dans la charge, sans raison de dérive : la reprise par le .precedent n'est pas de son ressort", () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const retenu = retenuDe(charge.propositions[0]);
|
|||
|
|
const perdu = retenu.placement[1].sieges[3].pop();
|
|||
|
|
charge.retenu = retenu;
|
|||
|
|
charge.evenement.etat = 'retenu';
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
const lue = relire(charge);
|
|||
|
|
const examen = examiner(lue);
|
|||
|
|
assert.deepStrictEqual(examen.retenu, { fautes: [{ code: 'MANQUANT', tour: 2, participant: perdu }], raisons: [] });
|
|||
|
|
assert.equal(examen.charge.retenu, lue.retenu);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, []);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, charge.propositions);
|
|||
|
|
const raisons = [{ code: 'PARTICIPANT_EXCLU', participant: 5 }];
|
|||
|
|
assert.deepStrictEqual(examen.derives, [1, 2, 3].map((id) => ({ id, raisons })));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("un retenu de forme quelconque, que l'analyse admet, est fautif sous la forme du retenu", () => {
|
|||
|
|
const sansCle = abimerFichier(chargeAvecGeneration(), (ecrit) => {
|
|||
|
|
ecrit.charge.retenu = { inconnue: [] };
|
|||
|
|
ecrit.entete.comptes.retenu = 1;
|
|||
|
|
});
|
|||
|
|
assert.deepStrictEqual(examiner(sansCle).retenu, { fautes: [{ code: 'FORME', chemin: 'proposition' }], raisons: [] });
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const sansProposition = retenuDe(charge.propositions[0]);
|
|||
|
|
delete sansProposition.proposition;
|
|||
|
|
charge.retenu = sansProposition;
|
|||
|
|
assert.deepStrictEqual(examiner(charge).retenu, { fautes: [{ code: 'FORME', chemin: 'proposition' }], raisons: [] });
|
|||
|
|
assert.equal(examiner(charge).charge.retenu, sansProposition);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un retenu cohérent et périmé porte ses raisons, sans faute ; sans retenu, ni faute ni raison', () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
assert.deepStrictEqual(examiner(charge).retenu, { fautes: [], raisons: [] });
|
|||
|
|
charge.retenu = retenuDe(charge.propositions[2]);
|
|||
|
|
assert.deepStrictEqual(examiner(charge).retenu, { fautes: [], raisons: [] });
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
assert.deepStrictEqual(examiner(charge).retenu, {
|
|||
|
|
fautes: [],
|
|||
|
|
raisons: [{ code: 'PARTICIPANT_EXCLU', participant: 5 }],
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("deux propositions cohérentes de même identifiant : la seconde dans l'ordre de la liste est fautive, IDENTIFIANT_REPETE, et le fichier s'ouvre sans elle ; une fautive n'écarte pas la cohérente qui porte son identifiant", () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const [p1, p2, p3] = charge.propositions;
|
|||
|
|
const homonyme = { ...structuredClone(p3), id: 2 };
|
|||
|
|
charge.propositions = [p1, p2, homonyme, p3];
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, [{ rang: 2, id: 2, fautes: [{ code: 'IDENTIFIANT_REPETE' }] }]);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, [p1, p2, p3]);
|
|||
|
|
assert.equal(examen.charge.propositions[1], p2);
|
|||
|
|
// Au travers du fichier, que l'analyse admet avec ses deux propositions 2.
|
|||
|
|
const lue = examiner(relire(charge));
|
|||
|
|
assert.deepStrictEqual(lue.fautives.map(({ id, fautes }) => [id, fautes]), [[2, [{ code: 'IDENTIFIANT_REPETE' }]]]);
|
|||
|
|
assert.deepStrictEqual(lue.charge.propositions.map(({ id }) => id), [1, 2, 3]);
|
|||
|
|
|
|||
|
|
const abimee = { ...structuredClone(p1), tours: 5 };
|
|||
|
|
charge.propositions = [abimee, p1, p2, p3];
|
|||
|
|
const seconde = examiner(charge);
|
|||
|
|
assert.deepStrictEqual(seconde.fautives, [
|
|||
|
|
{ rang: 0, id: 1, fautes: [{ code: 'LONGUEUR', tour: null, table: null, declare: 5, ecrit: 4 }] },
|
|||
|
|
]);
|
|||
|
|
assert.deepStrictEqual(seconde.charge.propositions, [p1, p2, p3]);
|
|||
|
|
// Après la cohérente, une fautive de même identifiant se nomme par sa
|
|||
|
|
// propre faute, et non comme une répétition.
|
|||
|
|
charge.propositions = [p1, abimee];
|
|||
|
|
assert.deepStrictEqual(examiner(charge).fautives, [
|
|||
|
|
{ rang: 1, id: 1, fautes: [{ code: 'LONGUEUR', tour: null, table: null, declare: 5, ecrit: 4 }] },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("une proposition cohérente dont l'identifiant atteint prochainsIds.proposition est fautive, IDENTIFIANT_HORS_COMPTEUR, et le fichier s'ouvre sans elle ; un retenu dont la proposition d'origine l'atteint porte cette faute, reste, et garde ses raisons de dérive", () => {
|
|||
|
|
const HORS = { code: 'IDENTIFIANT_HORS_COMPTEUR' };
|
|||
|
|
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
|
|||
|
|
// première. Un octet change dans le fichier écrit : chaque fois,
|
|||
|
|
// l'analyse le lit, et examiner n'écarte que la proposition touchée.
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const [p1, p2, p3] = charge.propositions;
|
|||
|
|
charge.retenu = retenuDe(p1);
|
|||
|
|
charge.evenement.etat = 'retenu';
|
|||
|
|
const texte = serialiser(charge, ENTETE);
|
|||
|
|
const unOctet = ([avant, apres]) => {
|
|||
|
|
assert.equal(texte.split(avant).length, 2, `« ${avant} » doit figurer une fois`);
|
|||
|
|
return examiner(analyser(texte.replace(avant, apres)).charge);
|
|||
|
|
};
|
|||
|
|
// L'identifiant de la troisième proposition, puis le compteur, réécrits.
|
|||
|
|
const troisieme = (id) => ['{"id":3,"graine"', `{"id":${id},"graine"`];
|
|||
|
|
const compteur = (prochain) => ['"proposition":4}', `"proposition":${prochain}}`];
|
|||
|
|
const cas = [
|
|||
|
|
['la troisième au compteur', troisieme(4), [{ rang: 2, id: 4, fautes: [HORS] }], [p1, p2]],
|
|||
|
|
['la troisième au-delà', troisieme(9), [{ rang: 2, id: 9, fautes: [HORS] }], [p1, p2]],
|
|||
|
|
['la troisième répète la deuxième', troisieme(2),
|
|||
|
|
[{ rang: 2, id: 2, fautes: [{ code: 'IDENTIFIANT_REPETE' }] }], [p1, p2]],
|
|||
|
|
['la troisième à 0', troisieme(0), [{ rang: 2, id: null, fautes: [{ code: 'FORME', chemin: 'id' }] }], [p1, p2]],
|
|||
|
|
['le compteur abaissé à 2', compteur(2),
|
|||
|
|
[{ rang: 1, id: 2, fautes: [HORS] }, { rang: 2, id: 3, fautes: [HORS] }], [p1]],
|
|||
|
|
];
|
|||
|
|
for (const [libelle, octet, fautives, gardees] of cas) {
|
|||
|
|
const examen = unOctet(octet);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, fautives, libelle);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, gardees, libelle);
|
|||
|
|
assert.deepStrictEqual(examen.retenu, { fautes: [], raisons: [] }, libelle);
|
|||
|
|
}
|
|||
|
|
// Abaissé à 1, le compteur écarte chaque proposition, et le retenu, dont
|
|||
|
|
// l'origine l'atteint, porte la faute sans être retiré. Son placement se
|
|||
|
|
// tient : sa dérive se nomme encore.
|
|||
|
|
const auPlusBas = unOctet(compteur(1));
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
auPlusBas.fautives.map(({ id, fautes }) => [id, fautes]),
|
|||
|
|
[[1, [HORS]], [2, [HORS]], [3, [HORS]]],
|
|||
|
|
);
|
|||
|
|
assert.deepStrictEqual(auPlusBas.charge.propositions, []);
|
|||
|
|
assert.deepStrictEqual(auPlusBas.retenu, { fautes: [HORS], raisons: [] });
|
|||
|
|
assert.deepStrictEqual(auPlusBas.charge.retenu, charge.retenu);
|
|||
|
|
const exclue = structuredClone(charge);
|
|||
|
|
exclue.participants[4].exclu = true;
|
|||
|
|
exclue.prochainsIds.proposition = 1;
|
|||
|
|
assert.deepStrictEqual(examiner(exclue).retenu, {
|
|||
|
|
fautes: [HORS],
|
|||
|
|
raisons: [{ code: 'PARTICIPANT_EXCLU', participant: 5 }],
|
|||
|
|
});
|
|||
|
|
// Une proposition qui se contredit se nomme par ses propres fautes,
|
|||
|
|
// même quand son identifiant atteint aussi le compteur ; un retenu qui se
|
|||
|
|
// contredit aussi, sans raison de dérive.
|
|||
|
|
const abimee = { ...structuredClone(p3), id: 4, tours: 5 };
|
|||
|
|
const contradictoire = { ...charge, propositions: [p1, abimee], retenu: { ...retenuDe(abimee), proposition: 4 } };
|
|||
|
|
const longueur = { code: 'LONGUEUR', tour: null, table: null, declare: 5, ecrit: 4 };
|
|||
|
|
const examen = examiner(contradictoire);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, [{ rang: 1, id: 4, fautes: [longueur] }]);
|
|||
|
|
assert.deepStrictEqual(examen.retenu, { fautes: [longueur], raisons: [] });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la charge rendue relève prochainsIds.proposition au-delà de chaque identifiant lu — d'une proposition écartée, quelle que soit sa faute, et de l'origine du retenu, cohérent ou non — ; le compteur ne recule jamais, ce qui n'est pas un identifiant n'y entre pas, et il reste un entier exact que l'analyse relit", () => {
|
|||
|
|
const HORS = 'IDENTIFIANT_HORS_COMPTEUR';
|
|||
|
|
const MAX = Number.MAX_SAFE_INTEGER;
|
|||
|
|
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de la
|
|||
|
|
// troisième. Chaque cas abîme la charge du fichier écrit, que l'analyse
|
|||
|
|
// relit et qui se fige avant l'examen. Il nomme les écartées, [id,
|
|||
|
|
// code], et les fautes du retenu, ce que le cas éprouve, puis le
|
|||
|
|
// compteur attendu.
|
|||
|
|
const base = chargeAvecGeneration();
|
|||
|
|
base.retenu = retenuDe(base.propositions[2]);
|
|||
|
|
base.evenement.etat = 'retenu';
|
|||
|
|
const cas = [
|
|||
|
|
["le fichier tel qu'écrit", () => {}, [], [], 4],
|
|||
|
|
['le compteur porté à 9', (c) => { c.prochainsIds.proposition = 9; }, [], [], 9],
|
|||
|
|
['le compteur abaissé à 2', (c) => { c.prochainsIds.proposition = 2; }, [[2, HORS], [3, HORS]], [HORS], 4],
|
|||
|
|
['le compteur abaissé à 1', (c) => { c.prochainsIds.proposition = 1; },
|
|||
|
|
[[1, HORS], [2, HORS], [3, HORS]], [HORS], 4],
|
|||
|
|
['la troisième à 9', (c) => { c.propositions[2].id = 9; }, [[9, HORS]], [], 10],
|
|||
|
|
['la troisième à 9, qui se contredit', (c) => { Object.assign(c.propositions[2], { id: 9, tours: 5 }); },
|
|||
|
|
[[9, 'LONGUEUR']], [], 10],
|
|||
|
|
['la troisième répète la deuxième', (c) => { c.propositions[2].id = 2; }, [[2, 'IDENTIFIANT_REPETE']], [], 4],
|
|||
|
|
['la troisième à 9.5', (c) => { c.propositions[2].id = 9.5; }, [[null, 'FORME']], [], 4],
|
|||
|
|
['la troisième à 2^53', (c) => { c.propositions[2].id = 2 ** 53; }, [[null, 'FORME']], [], 4],
|
|||
|
|
["l'origine du retenu à 9", (c) => { c.retenu.proposition = 9; }, [], [HORS], 10],
|
|||
|
|
["l'origine du retenu à 9, qui se contredit", (c) => { Object.assign(c.retenu, { proposition: 9, tours: 5 }); },
|
|||
|
|
[], ['LONGUEUR'], 10],
|
|||
|
|
["l'origine du retenu à 9, hors de sa règle", (c) => {
|
|||
|
|
Object.assign(c.retenu, { proposition: 9, toura: c.retenu.tours });
|
|||
|
|
delete c.retenu.tours;
|
|||
|
|
}, [], ['FORME'], 10],
|
|||
|
|
["l'origine du retenu à 9.5", (c) => { c.retenu.proposition = 9.5; }, [], ['FORME'], 4],
|
|||
|
|
["l'origine du retenu au plus grand entier exact", (c) => { c.retenu.proposition = MAX; }, [], [HORS], MAX],
|
|||
|
|
];
|
|||
|
|
// Le compteur que l'analyse relit dans le fichier de la charge rendue,
|
|||
|
|
// ou le refus qu'elle lève.
|
|||
|
|
const compteurRelu = (charge) => {
|
|||
|
|
try {
|
|||
|
|
return relire(charge).prochainsIds.proposition;
|
|||
|
|
} catch (erreur) {
|
|||
|
|
return `${erreur.name} ${erreur.message}`;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
const observes = cas.map(([libelle, abimer]) => {
|
|||
|
|
const lue = geler(abimerFichier(base, (ecrit) => abimer(ecrit.charge)));
|
|||
|
|
const examen = examiner(lue);
|
|||
|
|
return [
|
|||
|
|
libelle,
|
|||
|
|
examen.fautives.map(({ id, fautes: [premiere] }) => [id, premiere.code]),
|
|||
|
|
examen.retenu.fautes.map(({ code }) => code),
|
|||
|
|
examen.charge.prochainsIds,
|
|||
|
|
compteurRelu(examen.charge),
|
|||
|
|
];
|
|||
|
|
});
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
observes,
|
|||
|
|
cas.map(([libelle, , ecartees, fautesDuRetenu, compteur]) => [
|
|||
|
|
libelle,
|
|||
|
|
ecartees,
|
|||
|
|
fautesDuRetenu,
|
|||
|
|
{ participant: 13, table: 5, proposition: compteur },
|
|||
|
|
compteur,
|
|||
|
|
]),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("un compteur abaissé à la main, puis une génération numérotée depuis la charge rendue, comme le dit versFichier : aucun identifiant écarté ne revient, et l'origine du retenu n'est jamais réattribuée", () => {
|
|||
|
|
// Les propositions 1, 2 et 3 sous le compteur 4 ; le retenu vient de
|
|||
|
|
// l'une d'elles. Le compteur, abaissé à 1, 2 ou 3 dans le fichier écrit,
|
|||
|
|
// écarte celles qu'il ne couvre plus. La génération suivante, décalée de
|
|||
|
|
// prochainsIds.proposition − 1 de la charge rendue, prend 4, 5 et 6 ; à
|
|||
|
|
// la réouverture, l'identifiant de l'origine du retenu ne désigne que
|
|||
|
|
// l'origine elle-même, quand elle est gardée, et rien sinon.
|
|||
|
|
const suivante = rechercher(petite(), { graine: 13, arret: 2_000, nombre: 3 });
|
|||
|
|
const observes = [];
|
|||
|
|
const attendus = [];
|
|||
|
|
for (const abaisse of [1, 2, 3]) {
|
|||
|
|
for (const rang of [0, 1, 2]) {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const origine = charge.propositions[rang];
|
|||
|
|
charge.retenu = retenuDe(origine);
|
|||
|
|
charge.evenement.etat = 'retenu';
|
|||
|
|
const texte = serialiser(charge, ENTETE);
|
|||
|
|
assert.equal(texte.split('"proposition":4}').length, 2, 'le compteur doit figurer une fois');
|
|||
|
|
const abime = texte.replace('"proposition":4}', `"proposition":${abaisse}}`);
|
|||
|
|
const ouverte = examiner(analyser(abime).charge).charge;
|
|||
|
|
// L'appelant, comme le dit versFichier : la génération décalée de
|
|||
|
|
// prochainsIds.proposition − 1, puis le compteur porté au-delà.
|
|||
|
|
const configuration = configurationDepuisCharge(ouverte);
|
|||
|
|
const decalage = ouverte.prochainsIds.proposition - 1;
|
|||
|
|
const options = { produitVersion: VERSION.affichee, attribuerSieges: false, decalage };
|
|||
|
|
const generee = {
|
|||
|
|
...ouverte,
|
|||
|
|
prochainsIds: { ...ouverte.prochainsIds, proposition: decalage + suivante.length + 1 },
|
|||
|
|
propositions: [
|
|||
|
|
...ouverte.propositions,
|
|||
|
|
...suivante.map((proposition) => versFichier(proposition, configuration, options)),
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
const rouverte = examiner(relire(generee));
|
|||
|
|
const libelle = `compteur abaissé à ${abaisse}, retenu de la proposition ${origine.id}`;
|
|||
|
|
observes.push([
|
|||
|
|
libelle,
|
|||
|
|
rouverte.charge.propositions.map(({ id }) => id),
|
|||
|
|
rouverte.fautives,
|
|||
|
|
rouverte.retenu,
|
|||
|
|
rouverte.charge.propositions.filter(({ id }) => id === origine.id).map(({ graine }) => graine),
|
|||
|
|
]);
|
|||
|
|
attendus.push([
|
|||
|
|
libelle,
|
|||
|
|
[...[1, 2, 3].filter((id) => id < abaisse), 4, 5, 6],
|
|||
|
|
[],
|
|||
|
|
{ fautes: [], raisons: [] },
|
|||
|
|
origine.id < abaisse ? [origine.graine] : [],
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
assert.deepStrictEqual(observes, attendus);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("une proposition qui ne se lit pas est écartée seule : rang dans la liste, id null quand son identifiant n'en est pas un ; null, et un retenu égaré parmi les propositions, aussi", () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const lue = abimerFichier(charge, (ecrit) => {
|
|||
|
|
const [, deuxieme] = ecrit.charge.propositions;
|
|||
|
|
ecrit.charge.propositions.unshift(42, { id: 1, inconnue: true }, { ...deuxieme, id: 'deux' }, null, retenuDe(deuxieme));
|
|||
|
|
ecrit.entete.comptes.propositions = 8;
|
|||
|
|
});
|
|||
|
|
const examen = examiner(lue);
|
|||
|
|
// Le retenu égaré passe la forme du retenu : seule la règle d'une
|
|||
|
|
// proposition, que l'examen applique à chaque élément de la liste,
|
|||
|
|
// l'écarte.
|
|||
|
|
assert.deepStrictEqual(fautes(lue.propositions[4]), []);
|
|||
|
|
assert.deepStrictEqual(examen.fautives, [
|
|||
|
|
{ rang: 0, id: null, fautes: [{ code: 'FORME', chemin: '' }] },
|
|||
|
|
{ rang: 1, id: 1, fautes: [{ code: 'FORME', chemin: 'graine' }] },
|
|||
|
|
{ rang: 2, id: null, fautes: [{ code: 'FORME', chemin: 'id' }] },
|
|||
|
|
{ rang: 3, id: null, fautes: [{ code: 'FORME', chemin: '' }] },
|
|||
|
|
{ rang: 4, id: null, fautes: [{ code: 'FORME', chemin: 'id' }] },
|
|||
|
|
]);
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, charge.propositions);
|
|||
|
|
assert.deepStrictEqual(examen.derives, []);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la charge rendue garde l'ordre de la liste ; les dérives se rangent par identifiant, les fautives par rang", () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
const [p1, p2, p3] = charge.propositions;
|
|||
|
|
const abimee = { ...structuredClone(p2), id: 4, tours: 5 };
|
|||
|
|
charge.propositions = [p3, abimee, p1, p2];
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
const raisons = [{ code: 'PARTICIPANT_EXCLU', participant: 5 }];
|
|||
|
|
assert.deepStrictEqual(examen.charge.propositions, [p3, p1, p2]);
|
|||
|
|
assert.deepStrictEqual(examen.derives, [1, 2, 3].map((id) => ({ id, raisons })));
|
|||
|
|
assert.deepStrictEqual(examen.fautives.map(({ rang, id }) => [rang, id]), [[1, 4]]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('examiner ne modifie pas la charge reçue et rend une charge neuve, dont le reste est celui de la charge reçue', () => {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
charge.propositions[0].placement[0].reserve.push(13);
|
|||
|
|
charge.retenu = retenuDe(charge.propositions[1]);
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
const avant = structuredClone(charge);
|
|||
|
|
geler(charge);
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
assert.deepStrictEqual(charge, avant);
|
|||
|
|
assert.notEqual(examen.charge, charge);
|
|||
|
|
assert.deepStrictEqual(examen.charge, { ...avant, propositions: avant.propositions.slice(1) });
|
|||
|
|
assert.equal(examen.charge.participants, charge.participants);
|
|||
|
|
assert.equal(examen.charge.retenu, charge.retenu);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('formeNommee : la lecture et le signalement (§ 8.9, point 4)', () => {
|
|||
|
|
test('le tour 1 du plan parfait : douze lignes, table 1 → 1, 2, 3, avec les noms et prénoms de la charge, siège null sans attribution', () => {
|
|||
|
|
const lignes = formeNommee(fichierParfait(), chargePetite());
|
|||
|
|
assert.equal(lignes.length, 48);
|
|||
|
|
assert.deepStrictEqual(lignes.filter(({ tour }) => tour === 1), [
|
|||
|
|
ligne(1, 1, 1, null, 1, 'Aubépine', 'Agathe'),
|
|||
|
|
ligne(1, 1, 1, null, 2, 'Bruyère', 'Basile'),
|
|||
|
|
ligne(1, 1, 1, null, 3, 'Chardon', 'Céleste'),
|
|||
|
|
ligne(1, 2, 2, null, 4, 'Doucette', 'Désiré'),
|
|||
|
|
ligne(1, 2, 2, null, 5, 'Églantine', 'Edmée'),
|
|||
|
|
ligne(1, 2, 2, null, 6, 'Fougère', 'Firmin'),
|
|||
|
|
ligne(1, 3, 3, null, 7, 'Giroflée', 'Gisèle'),
|
|||
|
|
ligne(1, 3, 3, null, 8, 'Houblon', 'Honoré'),
|
|||
|
|
ligne(1, 3, 3, null, 9, 'Isatis', 'Ida'),
|
|||
|
|
ligne(1, 4, 4, null, 10, 'Jonquille', 'Joachim'),
|
|||
|
|
ligne(1, 4, 4, null, 11, 'Lavande', 'Lazare'),
|
|||
|
|
ligne(1, 4, 4, null, 12, 'Mélisse', 'Mathurin'),
|
|||
|
|
]);
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
lignes.map(({ tour, numero, participant }) => [tour, numero, participant]),
|
|||
|
|
PLAN_PARFAIT_PETITE.tours.flatMap((listes, r) => listes.flatMap((ids, i) => ids.map((id) => [r + 1, i + 1, id]))),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("sièges attribués : le siège est le rang dans la liste, à partir de 1, dans l'ordre écrit", () => {
|
|||
|
|
const p = { ...fichierParfait(), siegesAttribues: true };
|
|||
|
|
p.placement[0].sieges[0] = [3, 1, 2];
|
|||
|
|
const lignes = formeNommee(p, chargePetite()).filter(({ tour, table: id }) => tour === 1 && id === 1);
|
|||
|
|
assert.deepStrictEqual(lignes, [
|
|||
|
|
ligne(1, 1, 1, 1, 3, 'Chardon', 'Céleste'),
|
|||
|
|
ligne(1, 1, 1, 2, 1, 'Aubépine', 'Agathe'),
|
|||
|
|
ligne(1, 1, 1, 3, 2, 'Bruyère', 'Basile'),
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("lignes rangées par tour, numéro de table, rang : numéros égaux départagés par l'identifiant, une table supprimée après les autres sans numéro, la réserve en dernier, une personne supprimée sans nom", () => {
|
|||
|
|
const p = fichierPermute();
|
|||
|
|
p.participants.push(13);
|
|||
|
|
for (const tour of p.placement) tour.reserve = [13];
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
charge.participants.push(personne(13, ...TREIZIEME));
|
|||
|
|
charge.participants[6].prenom = null;
|
|||
|
|
charge.participants.splice(11, 1);
|
|||
|
|
charge.prochainsIds.participant = 14;
|
|||
|
|
charge.tables = [table(1, 3), table(2, 1), table(3, 1)];
|
|||
|
|
const lignes = formeNommee(p, charge);
|
|||
|
|
assert.equal(lignes.length, 52);
|
|||
|
|
assert.deepStrictEqual(lignes.filter(({ tour }) => tour === 1), [
|
|||
|
|
ligne(1, 2, 1, null, 4, 'Doucette', 'Désiré'),
|
|||
|
|
ligne(1, 2, 1, null, 5, 'Églantine', 'Edmée'),
|
|||
|
|
ligne(1, 2, 1, null, 6, 'Fougère', 'Firmin'),
|
|||
|
|
ligne(1, 3, 1, null, 7, 'Giroflée', null),
|
|||
|
|
ligne(1, 3, 1, null, 8, 'Houblon', 'Honoré'),
|
|||
|
|
ligne(1, 3, 1, null, 9, 'Isatis', 'Ida'),
|
|||
|
|
ligne(1, 1, 3, null, 1, 'Aubépine', 'Agathe'),
|
|||
|
|
ligne(1, 1, 3, null, 2, 'Bruyère', 'Basile'),
|
|||
|
|
ligne(1, 1, 3, null, 3, 'Chardon', 'Céleste'),
|
|||
|
|
ligne(1, 4, null, null, 10, 'Jonquille', 'Joachim'),
|
|||
|
|
ligne(1, 4, null, null, 11, 'Lavande', 'Lazare'),
|
|||
|
|
ligne(1, 4, null, null, 12, null, null),
|
|||
|
|
ligne(1, null, null, null, 13, 'Nigelle', 'Noémie'),
|
|||
|
|
]);
|
|||
|
|
assert.deepStrictEqual(
|
|||
|
|
lignes.map(({ tour }) => tour),
|
|||
|
|
[1, 2, 3, 4].flatMap((tour) => Array(13).fill(tour)),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la réserve suit les tables de son tour, ses personnes dans l'ordre écrit", () => {
|
|||
|
|
const lignes = formeNommee(fichierAReserve(), chargePetite()).filter(({ tour, table: id }) => tour === 1 && id === null);
|
|||
|
|
assert.deepStrictEqual(lignes, [
|
|||
|
|
ligne(1, null, null, null, 6, 'Fougère', 'Firmin'),
|
|||
|
|
ligne(1, null, null, null, 3, 'Chardon', 'Céleste'),
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("planDepuisFichier, derive et formeNommee refusent une proposition fautive par TypeError, au lieu d'en rendre une lecture fausse ; fautes ne lève jamais", () => {
|
|||
|
|
const charge = chargePetite();
|
|||
|
|
const abimee = fichierParfait();
|
|||
|
|
abimee.placement[0].sieges[0].pop();
|
|||
|
|
for (const [nom, appel] of [
|
|||
|
|
['planDepuisFichier', () => planDepuisFichier(abimee)],
|
|||
|
|
['derive', () => derive(abimee, charge)],
|
|||
|
|
['formeNommee', () => formeNommee(abimee, charge)],
|
|||
|
|
]) {
|
|||
|
|
leveTypeError(appel, /"MANQUANT"/, nom);
|
|||
|
|
}
|
|||
|
|
for (const illisible of [42, undefined, { ...fichierParfait(), placement: 'x' }]) {
|
|||
|
|
leveTypeError(() => planDepuisFichier(illisible), /"FORME"/, String(illisible));
|
|||
|
|
leveTypeError(() => derive(illisible, charge), /"FORME"/, String(illisible));
|
|||
|
|
leveTypeError(() => formeNommee(illisible, charge), /"FORME"/, String(illisible));
|
|||
|
|
}
|
|||
|
|
assert.deepStrictEqual(fautes(undefined), [{ code: 'FORME', chemin: '' }]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Exécute fonction pendant que l'horloge et les sources d'aléa lèvent à leur
|
|||
|
|
// lecture, puis les rétablit telles qu'elles étaient, propres ou héritées ;
|
|||
|
|
// rend ce que rend fonction.
|
|||
|
|
function sansHorlogeNiAlea(fonction) {
|
|||
|
|
const interdite = (nom) => () => {
|
|||
|
|
throw new Error(`${nom} lu`);
|
|||
|
|
};
|
|||
|
|
const remplacees = [
|
|||
|
|
[Math, 'random'],
|
|||
|
|
[Date, 'now'],
|
|||
|
|
[performance, 'now'],
|
|||
|
|
[crypto, 'getRandomValues'],
|
|||
|
|
[crypto, 'randomUUID'],
|
|||
|
|
].map(([objet, nom]) => ({ objet, nom, propre: Object.hasOwn(objet, nom), valeur: objet[nom] }));
|
|||
|
|
const DateReelle = globalThis.Date;
|
|||
|
|
for (const { objet, nom } of remplacees) objet[nom] = interdite(nom);
|
|||
|
|
function DateInterdite() {
|
|||
|
|
throw new Error('Date lu');
|
|||
|
|
}
|
|||
|
|
DateInterdite.now = interdite('Date.now');
|
|||
|
|
globalThis.Date = DateInterdite;
|
|||
|
|
try {
|
|||
|
|
return fonction();
|
|||
|
|
} finally {
|
|||
|
|
globalThis.Date = DateReelle;
|
|||
|
|
for (const { objet, nom, propre, valeur } of remplacees) {
|
|||
|
|
if (propre) objet[nom] = valeur;
|
|||
|
|
else delete objet[nom];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// La valeur refaite à toute profondeur, les clés de chaque objet à rebours.
|
|||
|
|
function aRebours(valeur) {
|
|||
|
|
if (Array.isArray(valeur)) return valeur.map(aRebours);
|
|||
|
|
if (typeof valeur !== 'object' || valeur === null) return valeur;
|
|||
|
|
const copie = {};
|
|||
|
|
for (const cle of Object.keys(valeur).reverse()) copie[cle] = aRebours(valeur[cle]);
|
|||
|
|
return copie;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('déterminisme (§ 8.8, § 14.7)', () => {
|
|||
|
|
// Les résultats de chaque fonction du module sur une charge où se trouvent
|
|||
|
|
// une proposition fautive, une dérive et un retenu, en un seul texte.
|
|||
|
|
function resultats(charge) {
|
|||
|
|
const configuration = petite();
|
|||
|
|
const [, coherente] = charge.propositions;
|
|||
|
|
const examen = examiner(charge);
|
|||
|
|
return JSON.stringify([
|
|||
|
|
versFichier(GENERATION[0], configuration, { produitVersion: VERSION.affichee, attribuerSieges: false, decalage: 0 }),
|
|||
|
|
charge.propositions.map((proposition) => fautes(proposition)),
|
|||
|
|
derive(coherente, charge),
|
|||
|
|
formeNommee(coherente, charge),
|
|||
|
|
planDepuisFichier(coherente),
|
|||
|
|
[examen.fautives, examen.derives, examen.retenu, examen.charge.propositions.map(({ id }) => id)],
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
function chargeMelee() {
|
|||
|
|
const charge = chargeAvecGeneration();
|
|||
|
|
charge.propositions[0].placement[0].reserve.push(13, 2);
|
|||
|
|
charge.propositions[2].placement[3].sieges[1].pop();
|
|||
|
|
charge.retenu = retenuDe(charge.propositions[1]);
|
|||
|
|
charge.participants[4].exclu = true;
|
|||
|
|
return charge;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test('ni horloge ni aléa : la sortie de chaque fonction ne dépend que de ses arguments', () => {
|
|||
|
|
// Le filet est éprouvé d'abord : chaque source lève pendant qu'il est tendu.
|
|||
|
|
for (const lecture of [
|
|||
|
|
() => Math.random(),
|
|||
|
|
() => Date.now(),
|
|||
|
|
() => new Date(),
|
|||
|
|
() => performance.now(),
|
|||
|
|
() => crypto.getRandomValues(new Uint8Array(1)),
|
|||
|
|
() => crypto.randomUUID(),
|
|||
|
|
]) {
|
|||
|
|
assert.throws(() => sansHorlogeNiAlea(lecture), / lu$/);
|
|||
|
|
}
|
|||
|
|
assert.equal(typeof Date.now(), 'number');
|
|||
|
|
assert.equal(sansHorlogeNiAlea(() => resultats(chargeMelee())), resultats(chargeMelee()));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("l'ordre des clés reçues n'entre dans aucun résultat, ni dans la clé inconnue nommée", () => {
|
|||
|
|
const charge = chargeMelee();
|
|||
|
|
const inversee = aRebours(charge);
|
|||
|
|
assert.notEqual(JSON.stringify(inversee), JSON.stringify(charge));
|
|||
|
|
assert.equal(resultats(inversee), resultats(charge));
|
|||
|
|
const p = fichierParfait();
|
|||
|
|
const zetaPuisAlpha = { ...p, zeta: 1, alpha: 2 };
|
|||
|
|
const alphaPuisZeta = { ...p, alpha: 2, zeta: 1 };
|
|||
|
|
assert.deepStrictEqual(fautes(zetaPuisAlpha), [{ code: 'FORME', chemin: 'alpha' }]);
|
|||
|
|
assert.deepStrictEqual(fautes(alphaPuisZeta), [{ code: 'FORME', chemin: 'alpha' }]);
|
|||
|
|
});
|
|||
|
|
});
|