175 lines
8.1 KiB
JavaScript
175 lines
8.1 KiB
JavaScript
|
|
// © 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);
|
|||
|
|
});
|
|||
|
|
});
|