gestion_table_tournante_libre/src/application/executeur_travailleur.test.js

175 lines
8.1 KiB
JavaScript
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves de l'exécuteur du travailleur (§ 5.10) : creerExecuteurTravailleur
// sur une fabrique de travailleurs factices. Un travailleur factice garde
// chaque message reçu et compte ses terminate() ; l'épreuve lui fait poster
// les messages du contrat de données par son onmessage, comme le ferait le
// vrai travailleur. Aucun calcul n'a lieu : les propositions et la
// configuration sont des objets d'épreuve.
import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js';
import { creerExecuteurTravailleur } from './executeur_travailleur.js';
import { ErreurAnnulee, ErreurGeneration } from './generation.js';
const CONFIGURATION = Object.freeze({ marque: 'configuration d’épreuve', tours: 3 });
const REGLAGES = Object.freeze({ graine: 11, nombre: 2, arret: 40_000, historique: 1000 });
// Une fabrique de travailleurs factices, et les travailleurs qu'elle a
// rendus, dans l'ordre. Chacun : recus, les messages reçus, clonés ;
// terminaisons, le compte de terminate() ; poster(data), un message du
// travailleur, remis à son onmessage courant.
function fabriqueFactice() {
const travailleurs = [];
const fabrique = () => {
const travailleur = {
recus: [],
terminaisons: 0,
onmessage: null,
postMessage(message) {
this.recus.push(structuredClone(message));
},
terminate() {
this.terminaisons += 1;
},
poster(data) {
this.onmessage?.({ data });
},
};
travailleurs.push(travailleur);
return travailleur;
};
return { fabrique, travailleurs };
}
// L'issue d'une promesse, sans attendre plus d'un tour de la file des
// microtâches : { etat: 'attente' }, { etat: 'resolue', valeur } ou
// { etat: 'rejetee', erreur }.
async function issue(promesse) {
let lue = { etat: 'attente' };
promesse.then(
(valeur) => {
lue = { etat: 'resolue', valeur };
},
(erreur) => {
lue = { etat: 'rejetee', erreur };
},
);
await Promise.resolve();
await Promise.resolve();
return lue;
}
describe('exécuteur du travailleur (§ 5.10)', () => {
test('un travailleur neuf par lancer, dont le premier message est { type: lancer, configuration, reglages }', () => {
const { fabrique, travailleurs } = fabriqueFactice();
const executeur = creerExecuteurTravailleur(fabrique);
assert.equal(travailleurs.length, 0, 'aucun travailleur avant le premier lancer');
const premiere = executeur.lancer(CONFIGURATION, REGLAGES);
const seconde = executeur.lancer(CONFIGURATION, { ...REGLAGES, graine: 12 });
premiere.resultat.catch(() => undefined);
seconde.resultat.catch(() => undefined);
assert.equal(travailleurs.length, 2);
assert.notEqual(travailleurs[0], travailleurs[1]);
assert.deepEqual(travailleurs[0].recus, [{ type: 'lancer', configuration: CONFIGURATION, reglages: REGLAGES }]);
assert.deepEqual(travailleurs[1].recus, [
{ type: 'lancer', configuration: CONFIGURATION, reglages: { ...REGLAGES, graine: 12 } },
]);
assert.deepEqual(
travailleurs.map(({ terminaisons }) => terminaisons),
[0, 0],
);
});
test('deux avancements parviennent à surAvancement dans leur ordre ; un abonné retiré ne reçoit plus rien', () => {
const { fabrique, travailleurs } = fabriqueFactice();
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
execution.resultat.catch(() => undefined);
const recus = [];
const retires = [];
execution.surAvancement((fait, total) => recus.push([fait, total]));
const desabonner = execution.surAvancement((fait, total) => retires.push([fait, total]));
const [travailleur] = travailleurs;
travailleur.poster({ type: 'avancement', fait: 50_000, total: 80_000 });
desabonner();
travailleur.poster({ type: 'avancement', fait: 80_000, total: 80_000 });
assert.deepEqual(recus, [
[50_000, 80_000],
[80_000, 80_000],
]);
assert.deepEqual(retires, [[50_000, 80_000]]);
});
test('resultat : la promesse se résout aux propositions reçues, puis le travailleur est terminé ; un message tardif ne change rien', async () => {
const { fabrique, travailleurs } = fabriqueFactice();
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
const avancements = [];
execution.surAvancement((fait) => avancements.push(fait));
const [travailleur] = travailleurs;
const propositions = [
{ id: 1, graine: 101, arret: 40_000, historique: 1000, plan: [[[1, 2]]] },
{ id: 2, graine: 102, arret: 40_000, historique: 1000, plan: [[[2, 1]]] },
];
assert.equal((await issue(execution.resultat)).etat, 'attente');
travailleur.poster({ type: 'resultat', propositions });
const lue = await issue(execution.resultat);
assert.equal(lue.etat, 'resolue');
assert.deepEqual(lue.valeur, propositions);
assert.equal(travailleur.terminaisons, 1);
travailleur.poster({ type: 'avancement', fait: 1, total: 2 });
execution.annuler();
assert.deepEqual(avancements, []);
assert.equal(travailleur.terminaisons, 1, 'annuler après le résultat ne termine pas une seconde fois');
});
test('erreur { code, details } : rejet par une ErreurGeneration de ce code et de ces détails, le travailleur terminé', async () => {
const { fabrique, travailleurs } = fabriqueFactice();
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
const [travailleur] = travailleurs;
travailleur.poster({ type: 'erreur', code: 'PLACES_MANQUANTES', details: { placesManquantes: 1 } });
const lue = await issue(execution.resultat);
assert.equal(lue.etat, 'rejetee');
assert.ok(lue.erreur instanceof ErreurGeneration, String(lue.erreur));
assert.equal(lue.erreur.code, 'PLACES_MANQUANTES');
assert.deepEqual(lue.erreur.details, { placesManquantes: 1 });
assert.equal(travailleur.terminaisons, 1);
});
test("annuler : terminate() aussitôt, rejet par ErreurAnnulee ; un avancement ou un résultat posté ensuite n'atteint plus rien", async () => {
const { fabrique, travailleurs } = fabriqueFactice();
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
const avancements = [];
execution.surAvancement((fait, total) => avancements.push([fait, total]));
const [travailleur] = travailleurs;
// Le gestionnaire posé au lancement : un message déjà en file chez le
// navigateur au moment de l'annulation l'atteindrait encore.
const gestionnaire = travailleur.onmessage;
travailleur.poster({ type: 'avancement', fait: 50_000, total: 80_000 });
execution.annuler();
assert.equal(travailleur.terminaisons, 1, 'terminate() dans le même tour que annuler()');
gestionnaire({ data: { type: 'avancement', fait: 80_000, total: 80_000 } });
gestionnaire({ data: { type: 'resultat', propositions: [] } });
travailleur.poster({ type: 'avancement', fait: 80_000, total: 80_000 });
const lue = await issue(execution.resultat);
assert.equal(lue.etat, 'rejetee');
assert.ok(lue.erreur instanceof ErreurAnnulee, String(lue.erreur));
assert.deepEqual(avancements, [[50_000, 80_000]]);
execution.annuler();
assert.equal(travailleur.terminaisons, 1, 'une seconde annulation ne termine rien de plus');
});
test('le canal d’erreur du travailleur : rejet par une erreur sans code, le travailleur terminé', async () => {
const { fabrique, travailleurs } = fabriqueFactice();
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
const [travailleur] = travailleurs;
assert.equal(typeof travailleur.onerror, 'function');
travailleur.onerror({ message: 'faute d’épreuve' });
const lue = await issue(execution.resultat);
assert.equal(lue.etat, 'rejetee');
assert.ok(lue.erreur instanceof Error);
assert.equal(lue.erreur.code, undefined);
assert.match(lue.erreur.message, /faute d’épreuve/);
assert.equal(travailleur.terminaisons, 1);
});
});