The event list showed no outcome, so comparing events meant opening each. Each line now shows, for the retained plan only, the cumulated collisions (pair and tour) and the redundancy gap |target - sum of r(p)|, with an explanation tooltip; an event without a measurable retained plan shows a dash. The target is set per event in Propositions, 1000 by default: an optional settings field, absent in every existing file and read as 1000, written through one undoable command refused in reading mode. Listing stays read-only. Checked: 2909 node, 604 browser, 62 long tests. --- FR --- [ADD] accueil : collisions et Diff de redondance du retenu par événement La liste des événements ne montrait aucun résultat : comparer obligeait à ouvrir chacun. Chaque ligne montre, pour le retenu seul, les collisions cumulées (paire et tour) et la Diff somme d'appartenance, |cible - somme de r(p)|, avec une infobulle ; sans retenu mesurable, un tiret. La cible se règle par événement dans Propositions, 1000 par défaut : champ facultatif des réglages, absent de tout fichier existant et lu 1000, écrit par une commande qui se défait et se refuse en lecture. La liste reste en lecture seule. Vérifié : 2909 node, 604 navigateur, 62 longues. Assisted-by: Claude Opus 5.5
426 lines
22 KiB
JavaScript
426 lines
22 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Épreuves de la table d'évaluation (§ 5.4, § 5.5, § 5.7, § 8.9, § 12.6,
|
|
// § 13.2, § 15.1, § 15.3, § 15.4, § 17) : la mesure qui fait foi, le
|
|
// classement, les placements en dérive ou fautifs, les imposés. Les charges
|
|
// partent des démonstrations livrées, et leurs propositions s'enregistrent
|
|
// par la commande ; les plans construits à la main sont écrits ici, sur la
|
|
// petite démonstration au conflit inévitable. La valeur attendue vient des
|
|
// fonctions du moteur, sollicitées une seconde fois (§ 19.1).
|
|
import assert from 'node:assert/strict';
|
|
import { describe, test } from '../../test/lanceur.js';
|
|
import { texteLivre } from '../demo/livrees.js';
|
|
import { classer } from '../moteur/classement.js';
|
|
import { indexerPlan, normaliser } from '../moteur/configuration.js';
|
|
import { diagnostiquer } from '../moteur/diagnostic.js';
|
|
import { ErreurConfiguration } from '../moteur/erreurs.js';
|
|
import { mesurer } from '../moteur/indicateurs.js';
|
|
import { ecartsAuPlafondAPriori } from '../moteur/manque.js';
|
|
import { plafondsAPriori } from '../moteur/plafond.js';
|
|
import { rechercher } from '../moteur/recherche.js';
|
|
import { analyser, capacite, configurationDepuisCharge } from '../stockage/document.js';
|
|
import { planDepuisFichier } from '../stockage/placements.js';
|
|
import { VERSION } from '../version.genere.js';
|
|
import { appliquerCommande } from './commandes.js';
|
|
import { CIBLE_REDONDANCE_PAR_DEFAUT, cibleRedondance, evaluer, resumeDuRetenu } from './evaluation.js';
|
|
|
|
const V1 = VERSION.affichee;
|
|
|
|
const chargeDemo = (cle) => analyser(texteLivre(cle)).charge;
|
|
|
|
// La charge, une génération du moteur de plus, enregistrée par la commande.
|
|
function generee(charge, reglages) {
|
|
const configuration = configurationDepuisCharge(charge);
|
|
const propositions = rechercher(configuration, reglages);
|
|
return appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: V1 }).charge;
|
|
}
|
|
|
|
const executer = (charge, nom, args) => appliquerCommande(nom, charge, args).charge;
|
|
|
|
// Une proposition écrite à la main, sièges non attribués, sur les tables de
|
|
// la charge dans leur ordre ; ses listes et réserves telles que données.
|
|
function propositionDe(charge, id, tours, reserves = tours.map(() => [])) {
|
|
const participants = charge.participants.filter(({ exclu }) => !exclu).map(({ id: p }) => p);
|
|
return {
|
|
id,
|
|
graine: id,
|
|
arret: 40,
|
|
historique: 1000,
|
|
produitVersion: V1,
|
|
siegesAttribues: false,
|
|
tables: charge.tables.map(({ id: t }) => t),
|
|
capacites: charge.tables.map((table) => capacite(charge, table)),
|
|
tours: tours.length,
|
|
participants,
|
|
placement: tours.map((listes, rang) => ({ sieges: listes.map((liste) => [...liste].sort((a, b) => a - b)), reserve: reserves[rang] })),
|
|
};
|
|
}
|
|
|
|
// Le retenu tiré d'une proposition.
|
|
function retenuDe(proposition) {
|
|
const { id, siegesAttribues, tables, capacites, tours, participants, placement } = structuredClone(proposition);
|
|
return { proposition: id, siegesAttribues, tables, capacites, tours, participants, placement };
|
|
}
|
|
|
|
// Gèle en profondeur : evaluer qui modifierait sa charge lèverait.
|
|
function figer(valeur) {
|
|
if (valeur !== null && typeof valeur === 'object' && !Object.isFrozen(valeur)) {
|
|
Object.freeze(valeur);
|
|
for (const element of Object.values(valeur)) figer(element);
|
|
}
|
|
return valeur;
|
|
}
|
|
|
|
// Chaque nombre atteint depuis la valeur, à toute profondeur.
|
|
function nombres(valeur, trouves = []) {
|
|
if (typeof valeur === 'number') trouves.push(valeur);
|
|
else if (valeur !== null && typeof valeur === 'object') for (const element of Object.values(valeur)) nombres(element, trouves);
|
|
return trouves;
|
|
}
|
|
|
|
// Les plans écrits à la main sur la petite démonstration au conflit
|
|
// inévitable — appartenances de tailles 5, 4 et 3 —, nommés par leurs
|
|
// collisions (cumulées, distinctes). Les trois premiers ont le même plus
|
|
// grand écart au plafond a priori, 3 ; le quatrième garde ensemble, aux
|
|
// quatre tours, les personnes 1, 2 et 3, d'appartenances distinctes, et ses
|
|
// quatre collisions tombent sur quatre paires.
|
|
const PLANS_CONFLIT = [
|
|
{
|
|
nom: '(4, 1)',
|
|
tours: [
|
|
[[2, 3, 10], [6, 8, 9], [4, 7, 12], [1, 5, 11]],
|
|
[[2, 7, 10], [4, 6, 8], [3, 11, 12], [1, 5, 9]],
|
|
[[7, 9, 12], [3, 8, 11], [4, 6, 10], [1, 2, 5]],
|
|
[[2, 7, 10], [3, 8, 9], [6, 11, 12], [1, 4, 5]],
|
|
],
|
|
collisions: [4, 1],
|
|
},
|
|
{
|
|
nom: '(4, 2)',
|
|
tours: [
|
|
[[2, 6, 10], [3, 8, 9], [7, 11, 12], [1, 4, 5]],
|
|
[[4, 7, 10], [2, 6, 8], [1, 5, 11], [3, 9, 12]],
|
|
[[2, 6, 12], [4, 8, 10], [1, 3, 11], [5, 7, 9]],
|
|
[[2, 3, 12], [4, 8, 10], [1, 6, 9], [5, 7, 11]],
|
|
],
|
|
collisions: [4, 2],
|
|
},
|
|
{
|
|
nom: '(6, 6)',
|
|
tours: [
|
|
[[3, 4, 12], [7, 8, 9], [2, 6, 10], [1, 5, 11]],
|
|
[[4, 6, 12], [2, 8, 10], [1, 7, 11], [3, 5, 9]],
|
|
[[2, 4, 10], [6, 8, 11], [1, 3, 12], [5, 7, 9]],
|
|
[[2, 7, 10], [5, 6, 8], [9, 11, 12], [1, 3, 4]],
|
|
],
|
|
collisions: [6, 6],
|
|
},
|
|
{
|
|
nom: 'trio',
|
|
tours: [
|
|
[[1, 2, 3], [6, 9, 10], [7, 11, 12], [4, 5, 8]],
|
|
[[1, 2, 3], [5, 6, 9], [7, 11, 12], [4, 8, 10]],
|
|
[[1, 2, 3], [7, 8, 9], [10, 11, 12], [4, 5, 6]],
|
|
[[1, 2, 3], [7, 8, 9], [6, 10, 11], [4, 5, 12]],
|
|
],
|
|
collisions: [4, 4],
|
|
},
|
|
];
|
|
|
|
describe("évaluation : la mesure qui fait foi (§ 5.10, § 13.2)", () => {
|
|
test('chaque ligne comparable porte mesurer, les plafonds, les trois chiffres ; le classement est celui de classer', () => {
|
|
const charge = figer(generee(chargeDemo('petite-conflit'), { graine: 5, nombre: 3, arret: 400 }));
|
|
const table = evaluer(charge);
|
|
const instance = normaliser(configurationDepuisCharge(charge));
|
|
const aPriori = plafondsAPriori(instance);
|
|
assert.deepEqual(table.population, instance.ids);
|
|
assert.equal(table.propositions.length, 3);
|
|
const entrees = [];
|
|
for (const proposition of charge.propositions) {
|
|
const ligne = table.propositions.find(({ id }) => id === proposition.id);
|
|
const mesures = mesurer(instance, indexerPlan(instance, planDepuisFichier(proposition)));
|
|
assert.equal(ligne.statut, 'comparable');
|
|
assert.deepEqual(ligne.raisons, []);
|
|
assert.deepEqual(ligne.violations, []);
|
|
assert.deepEqual(ligne.refusDessin, []);
|
|
assert.deepEqual(ligne.mesures, mesures);
|
|
assert.equal(ligne.plafondsRealises.length, 12);
|
|
assert.equal(ligne.minimumAtteint, false);
|
|
assert.deepEqual(Object.keys(ligne.troisChiffres).sort(), ['ecartItineraireMax', 'effectifManque', 'manqueMax']);
|
|
assert.equal(ligne.ecartAuPlafondAPrioriMax, Math.max(...ecartsAuPlafondAPriori(mesures, aPriori)));
|
|
entrees.push({ id: proposition.id, mesures, plafondsAPriori: aPriori });
|
|
}
|
|
assert.deepEqual(table.classement, classer(entrees));
|
|
assert.deepEqual(table.propositions.map(({ id }) => id), table.classement.ordre);
|
|
assert.equal(table.retenu, null);
|
|
});
|
|
|
|
test("trois plans au même premier critère se rangent par l'excédent, puis les cumulées ; le trio vient en dernier", () => {
|
|
const charge = structuredClone(chargeDemo('petite-conflit'));
|
|
charge.propositions = PLANS_CONFLIT.map(({ tours }, rang) => propositionDe(charge, rang + 1, tours));
|
|
charge.prochainsIds.proposition = 5;
|
|
charge.evenement.etat = 'propose';
|
|
const table = evaluer(figer(charge));
|
|
const parId = new Map(table.propositions.map((ligne) => [ligne.id, ligne]));
|
|
PLANS_CONFLIT.forEach(({ nom, collisions }, rang) => {
|
|
const { mesures } = parId.get(rang + 1);
|
|
assert.deepEqual([mesures.collisionsCumulees, mesures.pairesDistinctes], collisions, nom);
|
|
});
|
|
assert.deepEqual([1, 2, 3].map((id) => parId.get(id).ecartAuPlafondAPrioriMax), [3, 3, 3]);
|
|
assert.ok(parId.get(4).ecartAuPlafondAPrioriMax > 3);
|
|
assert.deepEqual(table.classement.ordre, [3, 2, 1, 4]);
|
|
assert.deepEqual(table.classement.criteresAppliques[0], 'ecartAuPlafondAPrioriMax');
|
|
for (const ligne of table.propositions) {
|
|
const ecarts = ecartsAuPlafondAPriori(ligne.mesures, plafondsAPriori(normaliser(configurationDepuisCharge(charge))));
|
|
assert.equal(ligne.ecartAuPlafondAPrioriMax, Math.max(...ecarts));
|
|
}
|
|
});
|
|
|
|
test('evaluer ne modifie pas la charge, et deux appels rendent la même table', () => {
|
|
const charge = figer(generee(chargeDemo('petite'), { graine: 2, nombre: 2, arret: 300 }));
|
|
assert.deepEqual(evaluer(charge), evaluer(charge));
|
|
});
|
|
});
|
|
|
|
describe('évaluation : une réservation que le plan ne tient pas (§ 5.8, § 12.6)', () => {
|
|
test("une réservation posée après la génération laisse la ligne comparable et classée, la violation RESERVATION nommée", () => {
|
|
const avant = generee(chargeDemo('petite'), { graine: 1, nombre: 1, arret: 200 });
|
|
const [proposition] = avant.propositions;
|
|
// La table de la personne 1 au premier tour, puis une autre où la réserver.
|
|
const listes = planDepuisFichier(proposition).tours[0];
|
|
const sienne = avant.tables[listes.findIndex((liste) => liste.includes(1))].id;
|
|
const autre = avant.tables.find(({ id }) => id !== sienne).id;
|
|
const charge = executer(avant, 'reserver', { participant: 1, table: autre, siege: null, portee: 'tour', tour: 1 });
|
|
const table = evaluer(figer(charge));
|
|
const [ligne] = table.propositions;
|
|
assert.equal(ligne.id, proposition.id);
|
|
assert.equal(ligne.statut, 'comparable');
|
|
assert.deepEqual(ligne.raisons, []);
|
|
assert.deepEqual(ligne.violations, [{ code: 'RESERVATION', participant: 1, table: autre, tour: 1 }]);
|
|
assert.notEqual(ligne.mesures, null);
|
|
assert.deepEqual(table.classement.ordre, [proposition.id]);
|
|
});
|
|
});
|
|
|
|
describe('évaluation : les formes closes ne corrigent rien (§ 5.4)', () => {
|
|
test("un retenu qui laisse une personne en réserve à trois tours mesure moins de collisions que le plancher, et la table porte la mesure", () => {
|
|
const charge = structuredClone(chargeDemo('petite-conflit'));
|
|
// Le plan parfait de la petite démonstration, la personne 10 en réserve
|
|
// aux tours 2 à 4 : elle seule fait deux membres d'une même appartenance
|
|
// à une table, au premier tour.
|
|
const tours = [
|
|
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
|
|
[[6, 9, 12], [3, 8, 11], [2, 5], [1, 4, 7]],
|
|
[[4, 8], [2, 7, 12], [1, 6, 11], [3, 5, 9]],
|
|
[[5, 7, 11], [1, 9], [3, 4, 12], [2, 6, 8]],
|
|
];
|
|
const proposition = propositionDe(charge, 1, tours, [[], [10], [10], [10]]);
|
|
charge.propositions = [proposition];
|
|
charge.retenu = retenuDe(proposition);
|
|
charge.prochainsIds.proposition = 2;
|
|
charge.evenement.etat = 'retenu';
|
|
const table = evaluer(figer(charge));
|
|
const plancher = diagnostiquer(configurationDepuisCharge(charge)).collisions.reduce((total, { plancherCumulees }) => total + plancherCumulees, 0);
|
|
assert.equal(plancher, 4);
|
|
assert.equal(table.retenu.statut, 'comparable');
|
|
assert.equal(table.retenu.id, 1);
|
|
assert.equal(table.retenu.mesures.collisionsCumulees, 1);
|
|
assert.ok(table.retenu.mesures.collisionsCumulees < plancher);
|
|
});
|
|
});
|
|
|
|
describe('évaluation : les populations différentes (§ 9, § 12.6)', () => {
|
|
test("générer, exclure une personne, générer encore : les premières sont en dérive, hors classement, leurs raisons nomment la personne", () => {
|
|
let charge = generee(chargeDemo('petite'), { graine: 1, nombre: 2, arret: 200 });
|
|
charge = executer(charge, 'exclureParticipant', { id: 4 });
|
|
charge = generee(charge, { graine: 2, nombre: 2, arret: 200 });
|
|
const table = evaluer(figer(charge));
|
|
assert.deepEqual(table.population, [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12]);
|
|
assert.deepEqual(table.classement.ordre.slice().sort(), [3, 4]);
|
|
for (const id of [1, 2]) {
|
|
const ligne = table.propositions.find((candidate) => candidate.id === id);
|
|
assert.equal(ligne.statut, 'derive');
|
|
assert.deepEqual(ligne.raisons, [{ code: 'PARTICIPANT_EXCLU', participant: 4 }]);
|
|
assert.deepEqual([ligne.mesures, ligne.plafondsRealises, ligne.troisChiffres, ligne.ecartAuPlafondAPrioriMax, ligne.minimumAtteint], [null, null, null, null, null]);
|
|
assert.deepEqual([ligne.violations, ligne.refusDessin], [[], []]);
|
|
}
|
|
assert.deepEqual(table.propositions.map(({ id }) => id), [...table.classement.ordre, 1, 2]);
|
|
});
|
|
|
|
test('une exclusion et un ajout, à effectif égal, mettent en dérive ; une table ajoutée aussi, TABLE_AJOUTEE', () => {
|
|
let charge = generee(chargeDemo('petite'), { graine: 1, nombre: 1, arret: 200 });
|
|
charge = executer(charge, 'exclureParticipant', { id: 4 });
|
|
charge = executer(charge, 'ajouterParticipant', { champs: { nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles' } });
|
|
const echange = evaluer(figer(charge));
|
|
assert.equal(echange.population.length, 12);
|
|
assert.equal(echange.propositions[0].statut, 'derive');
|
|
assert.deepEqual(echange.propositions[0].raisons.map(({ code }) => code), ['PARTICIPANT_EXCLU', 'PARTICIPANT_NON_PLACE']);
|
|
assert.deepEqual(echange.classement.ordre, []);
|
|
let ajout = generee(chargeDemo('petite'), { graine: 1, nombre: 1, arret: 200 });
|
|
ajout = executer(ajout, 'creerTable', { forme: 'ronde' });
|
|
const table = evaluer(figer(ajout));
|
|
assert.equal(table.propositions[0].statut, 'derive');
|
|
assert.deepEqual(table.propositions[0].raisons, [{ code: 'TABLE_AJOUTEE', table: 5 }]);
|
|
});
|
|
});
|
|
|
|
describe('évaluation : un placement fautif (§ 8.9, point 3)', () => {
|
|
test("un retenu dont une liste dépasse sa longueur déclarée est fautif, non mesuré, et les propositions se classent sans lui", () => {
|
|
const base = generee(chargeDemo('petite'), { graine: 3, nombre: 2, arret: 300 });
|
|
const attendue = evaluer(figer(structuredClone(base)));
|
|
const charge = structuredClone(base);
|
|
const retenu = retenuDe(charge.propositions[1]);
|
|
const [premiere, seconde] = retenu.placement[0].sieges;
|
|
premiere.push(seconde.shift());
|
|
charge.retenu = retenu;
|
|
charge.evenement.etat = 'retenu';
|
|
const table = evaluer(figer(charge));
|
|
assert.equal(table.retenu.statut, 'fautif');
|
|
assert.equal(table.retenu.id, charge.propositions[1].id);
|
|
assert.equal(table.retenu.mesures, null);
|
|
assert.ok(table.retenu.raisons.length > 0);
|
|
assert.ok(table.retenu.raisons.every(({ code }) => code === 'LONGUEUR'), JSON.stringify(table.retenu.raisons));
|
|
assert.deepEqual(table.propositions, attendue.propositions);
|
|
assert.deepEqual(table.classement, attendue.classement);
|
|
});
|
|
});
|
|
|
|
describe('évaluation : le plafond a priori inconnu (§ 17, point 4)', () => {
|
|
test("plafondsAPriori null : le premier critère est sauté et nommé ; écart d'itinéraire et certificat inconnus", () => {
|
|
const charge = figer(generee(chargeDemo('petite-conflit'), { graine: 5, nombre: 2, arret: 300 }));
|
|
const table = evaluer(charge, { plafondsAPriori: null });
|
|
assert.equal(table.classement.critereSaute, 'ecartAuPlafondAPrioriMax');
|
|
assert.equal(table.classement.criteresAppliques[0], 'excedentCollisions');
|
|
for (const ligne of table.propositions) {
|
|
assert.equal(ligne.troisChiffres.ecartItineraireMax, null);
|
|
assert.equal(ligne.minimumAtteint, null);
|
|
assert.equal(ligne.ecartAuPlafondAPrioriMax, null);
|
|
}
|
|
assert.throws(() => evaluer(charge, { plafondsAPriori: [] }), TypeError);
|
|
});
|
|
});
|
|
|
|
describe('évaluation : les imposés (§ 5.4, § 15.1)', () => {
|
|
test('la grande démonstration : 99 retours imposés, 33 ancrages qui en imposent, ceux de diagnostiquer', () => {
|
|
const charge = chargeDemo('grande');
|
|
const table = evaluer(figer(charge));
|
|
const diagnostic = diagnostiquer(configurationDepuisCharge(charge));
|
|
assert.deepEqual(table.imposes, { retours: 99, ancrages: 33 });
|
|
assert.deepEqual(table.imposes, { retours: diagnostic.retoursImposes, ancrages: diagnostic.ancragesImposantRetours });
|
|
});
|
|
|
|
test("deux personnes d'appartenances distinctes réservées ensemble : une rencontre répétée imposée par ligne, hors des choisies", () => {
|
|
const charge = structuredClone(chargeDemo('petite'));
|
|
charge.reservations = [1, 2].map((participant) => ({ participant, table: 1, siege: null, portee: 'tous', tour: null }));
|
|
const avec = generee(charge, { graine: 6, nombre: 3, arret: 500 });
|
|
const table = evaluer(figer(avec));
|
|
assert.equal(table.propositions.length, 3);
|
|
for (const ligne of table.propositions) {
|
|
assert.equal(ligne.mesures.rencontresRepetees.imposees, 1);
|
|
// Les paires réunies deux fois au moins, comptées sur le plan : celle
|
|
// des deux réservés n'est pas parmi les choisies.
|
|
const plan = planDepuisFichier(avec.propositions.find(({ id }) => id === ligne.id));
|
|
const fois = new Map();
|
|
for (const listes of plan.tours) {
|
|
for (const liste of listes) {
|
|
for (const a of liste) for (const b of liste) if (a < b) fois.set(`${a}-${b}`, (fois.get(`${a}-${b}`) ?? 0) + 1);
|
|
}
|
|
}
|
|
const repetees = [...fois.values()].filter((n) => n >= 2).length;
|
|
assert.equal(fois.get('1-2'), 4);
|
|
assert.equal(ligne.mesures.rencontresRepetees.choisies, repetees - 1);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('évaluation : une table, un tour (§ 15.4)', () => {
|
|
test('six personnes à une table de 6, un tour : rien de répété, aucun NaN ni infini', () => {
|
|
const charge = structuredClone(chargeDemo('petite'));
|
|
charge.participants = charge.participants.slice(0, 6);
|
|
charge.tables = charge.tables.slice(0, 1).map((table) => ({ ...table, sieges: 6 }));
|
|
charge.evenement.tours = 1;
|
|
const table = evaluer(figer(generee(charge, { graine: 1, nombre: 1, arret: 10 })));
|
|
const [ligne] = table.propositions;
|
|
assert.equal(ligne.statut, 'comparable');
|
|
assert.deepEqual(ligne.mesures.rencontresRepetees, { choisies: 0, imposees: 0 });
|
|
assert.equal(ligne.mesures.excedentCollisions, 0);
|
|
assert.equal(ligne.mesures.totalRetoursChoisis, 0);
|
|
const tous = nombres(table);
|
|
assert.ok(tous.length > 0);
|
|
assert.ok(tous.every(Number.isFinite), 'NaN ou infini dans la table');
|
|
});
|
|
});
|
|
|
|
describe("évaluation : une configuration qui ne se tient pas", () => {
|
|
test("evaluer lève l'ErreurConfiguration de normaliser", () => {
|
|
const charge = structuredClone(chargeDemo('petite'));
|
|
charge.reservations = [1, 2, 3, 4].map((participant) => ({ participant, table: 1, siege: null, portee: 'tous', tour: null }));
|
|
assert.throws(() => evaluer(figer(charge)), (erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'SURRESERVATION');
|
|
});
|
|
});
|
|
|
|
describe('resumeDuRetenu : les mesures du retenu que lit la liste des événements (§ 5.4, § 8.1, § 8.9)', () => {
|
|
// La petite démonstration, trois propositions générées, la deuxième retenue.
|
|
function retenue() {
|
|
const generees = generee(chargeDemo('petite-conflit'), { graine: 3, nombre: 3, arret: 300 });
|
|
return executer(generees, 'retenirProposition', { id: generees.propositions[1].id });
|
|
}
|
|
|
|
test("les collisions cumulées et Σ r(p) du retenu, celles de la ligne du retenu d'evaluer, et la cible, 1000 par défaut", () => {
|
|
const charge = figer(retenue());
|
|
const resume = resumeDuRetenu(charge);
|
|
const { mesures } = evaluer(charge).retenu;
|
|
assert.ok(mesures.collisionsCumulees > 0, 'la petite démonstration en conflit compte des collisions');
|
|
assert.deepEqual(resume, {
|
|
collisionsCumulees: mesures.collisionsCumulees,
|
|
totalRedondance: mesures.totalRedondance,
|
|
cibleRedondance: 1000,
|
|
diffRedondance: Math.abs(1000 - mesures.totalRedondance),
|
|
});
|
|
const instance = normaliser(configurationDepuisCharge(charge));
|
|
const attendues = mesurer(instance, indexerPlan(instance, planDepuisFichier(charge.retenu)));
|
|
assert.equal(resume.totalRedondance, attendues.totalRedondance);
|
|
assert.equal(resume.collisionsCumulees, attendues.collisionsCumulees);
|
|
assert.equal(CIBLE_REDONDANCE_PAR_DEFAUT, 1000);
|
|
assert.equal(cibleRedondance(charge), 1000);
|
|
});
|
|
|
|
test("une cible propre à l'événement : la différence se lit contre elle, en valeur absolue, des deux côtés", () => {
|
|
const charge = retenue();
|
|
const total = resumeDuRetenu(charge).totalRedondance;
|
|
for (const cible of [0, total, total + 7, 5_000]) {
|
|
const avecCible = executer(charge, 'changerCibleRedondance', { cible: String(cible) });
|
|
assert.equal(cibleRedondance(avecCible), cible);
|
|
const resume = resumeDuRetenu(avecCible);
|
|
assert.equal(resume.cibleRedondance, cible);
|
|
assert.equal(resume.diffRedondance, Math.abs(cible - total), String(cible));
|
|
}
|
|
const dessous = executer(charge, 'changerCibleRedondance', { cible: '0' });
|
|
assert.equal(resumeDuRetenu(dessous).diffRedondance, total);
|
|
});
|
|
|
|
test('sans retenu, null ; un retenu fautif ou en dérive, null, sans lever', () => {
|
|
const sansRetenu = generee(chargeDemo('petite-conflit'), { graine: 3, nombre: 1, arret: 200 });
|
|
assert.equal(sansRetenu.retenu, null);
|
|
assert.equal(resumeDuRetenu(sansRetenu), null);
|
|
assert.equal(resumeDuRetenu(chargeDemo('petite')), null);
|
|
|
|
const fautif = structuredClone(retenue());
|
|
fautif.retenu.placement[0].reserve.push(fautif.retenu.participants[0]);
|
|
assert.equal(evaluer(fautif).retenu.statut, 'fautif');
|
|
assert.equal(resumeDuRetenu(fautif), null);
|
|
|
|
const enDerive = executer(retenue(), 'changerTours', { tours: '3' });
|
|
assert.equal(evaluer(enDerive).retenu.statut, 'derive');
|
|
assert.equal(resumeDuRetenu(enDerive), null);
|
|
});
|
|
|
|
test('une configuration qui ne se normalise pas rend null là où evaluer lève', () => {
|
|
const charge = structuredClone(retenue());
|
|
const table = charge.tables[0].id;
|
|
charge.reservations = [1, 2, 3, 4].map((participant) => ({ participant, table, siege: null, portee: 'tous', tour: null }));
|
|
assert.throws(() => evaluer(charge), (erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'SURRESERVATION');
|
|
assert.equal(resumeDuRetenu(charge), null);
|
|
});
|
|
});
|