[ADD] generation: a pool of workers over the CPUs, cores setting
Generation ran in a single worker, one proposition after another, using one core. A pool of workers now takes propositions by rank, as many as the cores set: automatic by default (every logical CPU), or a count from 1 to that number. Each proposition is still computed from its own derived seed, so the result is byte-identical whatever the core count, ordered by identifier. One progress bar sums the workers; Cancel stops them all; the duration estimate counts the cores. The setting is per session and never written. Checked: 2872 node, 586 browser, 62 long; 5 workers in Electron. --- FR --- [ADD] génération : réserve de travailleurs sur les processeurs, cœurs La génération tournait dans un seul travailleur, une proposition après l'autre, sur un seul cœur. Une réserve de travailleurs prend maintenant les propositions par rang, autant que de cœurs réglés : automatique par défaut (tous les processeurs logiques), ou un nombre de 1 à ce total. Chaque proposition se calcule toujours depuis sa graine dérivée : le résultat est identique à l'octet quel que soit le nombre de cœurs, rangé par identifiant. Une seule barre ; Annuler arrête tout ; l'estimation compte les cœurs. Réglage de séance, jamais écrit. Vérifié : 2872, 586, 62. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
8afa53ebba
commit
b969304c2a
15 changed files with 1458 additions and 271 deletions
36
spec.md
36
spec.md
|
|
@ -767,8 +767,8 @@ d'écran (§ 19.4) tomberait avec. L'interdit de `performance.now` dans le moteu
|
|||
|
||||
**Le compte est un réglage, et son plafond d'usage est de trente minutes.** Il
|
||||
est exposé dans les réglages de génération, avec son unité ; le logiciel affiche
|
||||
à côté la **durée réellement observée du dernier calcul de la séance**, qui est
|
||||
la seule façon d'étalonner un compte. Elle vaut pour la séance et ne se garde
|
||||
à côté la **durée réellement observée du dernier calcul de la séance**, et les
|
||||
cœurs qu'il a employés, qui est la seule façon d'étalonner un compte. Elle vaut pour la séance et ne se garde
|
||||
pas au-delà — rien ne survit à la séance hors du dossier de travail (§ 8.6) : au
|
||||
premier calcul d'un lancement, aucune mesure n'existe, et l'estimation se dit
|
||||
inconnue. Le réglage livré par défaut est celui qui, sur la machine de
|
||||
|
|
@ -777,9 +777,11 @@ référence, reste bien en deçà de ce plafond.
|
|||
**Une génération longue s'annonce avant de partir.** À partir de la dernière
|
||||
durée mesurée, le logiciel énonce l'ordre de grandeur attendu — « environ vingt
|
||||
minutes à ce réglage » — et, **au-delà d'une minute estimée**, demande, par une
|
||||
question posée en place, sans fenêtre. Sans mesure, il demande dès que les
|
||||
mouvements demandés — propositions × compte d'arrêt — dépassent dix fois ceux du
|
||||
réglage par défaut. Une recherche d'une demi-heure lancée par mégarde est une
|
||||
question posée en place, sans fenêtre. L'estimation compte les mouvements qui se
|
||||
suivent sur un même cœur — le compte d'arrêt pour chacune des ⌈propositions /
|
||||
cœurs⌉ vagues de propositions calculées ensemble —, ceux de la dernière mesure
|
||||
ramenés de même aux cœurs qu'elle a employés. Sans mesure, il demande dès que ces
|
||||
mouvements dépassent dix fois ceux du réglage par défaut. Une recherche d'une demi-heure lancée par mégarde est une
|
||||
demi-heure de soirée perdue, et aucun réglage par défaut ne protège de cela.
|
||||
|
||||
**Pendant la recherche, l'application reste vivante.** Le calcul ne tient pas le
|
||||
|
|
@ -790,6 +792,23 @@ logiciel — ce qui perd exactement ce que la recherche avait trouvé. Le compte
|
|||
d'inactivité du § 8.4 est suspendu tant qu'un calcul tourne, donc le retour
|
||||
automatique en lecture ne tombe jamais au milieu d'une génération.
|
||||
|
||||
**Les propositions d'une génération se calculent en parallèle, sur les cœurs du
|
||||
poste.** Chaque proposition ne dépend que de sa graine dérivée (§ 5.7) : un
|
||||
groupe de fils d'exécution, hors de celui de l'interface, s'en partage les rangs,
|
||||
chacun prenant le suivant dès qu'il a rendu le sien, et le résultat, rangé par
|
||||
identifiant, est octet pour octet celui d'un calcul sur un seul fil, quel que
|
||||
soit le nombre de cœurs. Ce nombre est un **réglage du poste, non de
|
||||
l'événement** : une case « Cœurs automatiques », cochée par défaut, en emploie un
|
||||
par processeur logique, hyperthreads compris ; décochée, elle laisse saisir un
|
||||
« Nombre de cœurs », un entier de 1 au nombre de processeurs logiques, la valeur
|
||||
automatique affichée à côté, et refuse toute autre saisie là où elle a eu lieu.
|
||||
Le réglage vaut pour la séance du logiciel, comme la durée observée : il ne
|
||||
s'écrit ni dans le fichier d'état ni dans le journal, et se règle en lecture
|
||||
comme en écriture. Une génération n'emploie jamais plus de cœurs que de
|
||||
propositions demandées. L'avancement reste une seule barre, la somme des
|
||||
mouvements évalués de toutes les propositions ; l'annulation arrête tous les fils
|
||||
à la fois, et l'échec de l'un les arrête tous, sans résultat partiel.
|
||||
|
||||
**Une commande annule une génération en cours, et elle écarte le résultat
|
||||
partiel.** L'annulation est immédiate — elle n'attend pas la fin d'une itération
|
||||
longue. Une proposition issue d'une recherche tronquée porterait la même graine
|
||||
|
|
@ -3071,6 +3090,13 @@ d'inscriptibilité **en cours de séance**, et un support amovible. Sans elle, l
|
|||
la règle à cinq issues du § 8.6 reste le code le moins éprouvé du logiciel alors
|
||||
qu'elle décide de l'endroit où vit la soirée.
|
||||
|
||||
**Les fils de la génération sont un paramètre, comme le système de fichiers.**
|
||||
L'exécuteur qui répartit les propositions entre ses fils (§ 5.10) reçoit de quoi
|
||||
en faire naître et le nombre de processeurs logiques du poste ; seul le point
|
||||
d'entrée de la page lit ce nombre de la plateforme. Les épreuves donnent le leur,
|
||||
et des fils simulés qui s'achèvent dans l'ordre qu'elles choisissent : c'est
|
||||
ainsi que s'éprouve un résultat indépendant de l'ordre d'achèvement (§ 5.7).
|
||||
|
||||
**La frontière des couches est gardée mécaniquement**, et pas seulement par
|
||||
discipline : un test du **graphe d'imports** refuse qu'un module de `src/moteur`
|
||||
ou de `src/geometrie` importe de `src/interface`, de `src/application`, de
|
||||
|
|
|
|||
|
|
@ -1,57 +1,85 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// L'exécuteur de production de la génération (§ 5.10, § 13.4) : la
|
||||
// recherche tourne dans un travailleur, hors du fil de l'interface, et
|
||||
// l'exécuteur adapte son protocole — celui de generation_travailleur.js —
|
||||
// au contrat Executeur du contrôleur de génération (generation.js).
|
||||
// L'exécuteur de production de la génération (§ 5.7, § 5.10, § 13.4) : la
|
||||
// recherche tourne dans un groupe de travailleurs, hors du fil de
|
||||
// l'interface, et l'exécuteur adapte leur protocole — celui de
|
||||
// generation_travailleur.js — au contrat Executeur du contrôleur de
|
||||
// génération (generation.js).
|
||||
//
|
||||
// → { type: 'lancer', configuration, reglages } le premier et seul message
|
||||
// ← { type: 'avancement', fait, total } remis aux abonnés
|
||||
// ← { type: 'resultat', propositions } résout le résultat
|
||||
// ← { type: 'erreur', code, details } rejette par ErreurGeneration
|
||||
// ← { type: 'faute', message, pile } rejette par une Error, une faute
|
||||
// ✕ canal d'erreur du travailleur rejette par TRAVAILLEUR_EN_ECHEC
|
||||
// Un lancer fait naître W = min(nombre, coeurs) travailleurs. Les rangs des
|
||||
// propositions, 1 à nombre, se donnent dans l'ordre croissant : le
|
||||
// travailleur k reçoit d'abord le rang k, puis chaque rang suivant va au
|
||||
// premier travailleur qui rend sa proposition. Celui qui n'a plus de rang à
|
||||
// recevoir est terminé aussitôt. Chaque proposition se calcule de sa graine
|
||||
// dérivée, la même quel que soit le travailleur qui la calcule (proposerUne) :
|
||||
// le résultat, rangé par identifiant, est octet pour octet celui de
|
||||
// rechercher, quel que soit W et quel que soit l'ordre d'achèvement (§ 5.7).
|
||||
//
|
||||
// Le travailleur poste ses fautes de calcul en message : son canal d'erreur
|
||||
// → { type: 'proposer', configuration, reglages, rang } un rang à la fois
|
||||
// ← { type: 'avancement', fait, total } fait dans la proposition en
|
||||
// cours de ce travailleur
|
||||
// ← { type: 'proposition', proposition } range la proposition, puis
|
||||
// le rang suivant ou la fin
|
||||
// ← { type: 'erreur', code, details } rejette par ErreurGeneration
|
||||
// ← { type: 'faute', message, pile } rejette par une Error, une faute
|
||||
// ✕ canal d'erreur d'un travailleur rejette par TRAVAILLEUR_EN_ECHEC
|
||||
//
|
||||
// L'avancement remis aux abonnés est la somme, sur toutes les propositions,
|
||||
// des mouvements évalués — le dernier avancement reçu pour une proposition
|
||||
// en cours, son arret entier pour une proposition rendue —, sur
|
||||
// nombre × arret ; il ne décroît jamais.
|
||||
//
|
||||
// Un travailleur poste ses fautes de calcul en message : son canal d'erreur
|
||||
// ne porte que l'échec de son script, qui ne se charge ou ne se compile pas.
|
||||
//
|
||||
// Chaque lancer prend un travailleur neuf de la fabrique et le termine à
|
||||
// l'issue : résultat, erreur, faute, échec du script ou annulation. Terminé,
|
||||
// le travailleur ne remet plus rien : un message déjà en file à cet instant
|
||||
// ne parvient ni aux abonnés ni au résultat. L'annulation est terminate(),
|
||||
// dans le même tour : immédiate, sans résultat partiel. La fabrique est un
|
||||
// paramètre : src/main.js la construit sur le travailleur inclus dans la
|
||||
// page, une épreuve sur un travailleur factice. Ce module ne touche aucune
|
||||
// plateforme.
|
||||
import { ErreurAnnulee, ErreurGeneration } from './generation.js';
|
||||
// L'issue — résultat, erreur, faute, échec d'un script ou annulation —
|
||||
// termine tous les travailleurs du lancer, dans le même tour. Terminé, un
|
||||
// travailleur ne remet plus rien : un message déjà en file à cet instant ne
|
||||
// parvient ni aux abonnés ni au résultat. L'annulation est terminate() de
|
||||
// chacun : immédiate, sans résultat partiel. La fabrique est un paramètre :
|
||||
// src/main.js la construit sur le travailleur inclus dans la page, avec le
|
||||
// nombre de processeurs logiques du poste ; une épreuve, sur des
|
||||
// travailleurs factices. Ce module ne touche aucune plateforme.
|
||||
import { ErreurAnnulee, ErreurGeneration, coeursDe, exigerReglages } from './generation.js';
|
||||
|
||||
/**
|
||||
* L'exécuteur du travailleur.
|
||||
* L'exécuteur du groupe de travailleurs.
|
||||
*
|
||||
* @param {() => {postMessage: (message: Object) => void, terminate: () => void,
|
||||
* onmessage: ((evenement: {data: Object}) => void)|null,
|
||||
* onerror?: ((evenement: {message?: string}) => void)|null}} fabrique
|
||||
* rend un travailleur neuf à chaque appel
|
||||
* @param {{coeursDisponibles?: unknown}} [options] le nombre de processeurs
|
||||
* logiques du poste, que l'exécuteur rend par coeursDe : 1 quand il n'est
|
||||
* pas un entier d'au moins 1
|
||||
* @returns {import('./generation.js').Executeur}
|
||||
* lancer(configuration, reglages) rend une Execution : avancement →
|
||||
* surAvancement ; resultat → la promesse résolue aux propositions reçues,
|
||||
* puis terminate() ; erreur {code, details} → rejet par
|
||||
* ErreurGeneration(code, details) ; faute {message, pile} → rejet par
|
||||
* une Error sans code, dont le message porte celui du travailleur et la
|
||||
* pile la sienne : elle remonte comme une faute, comme sous l'exécuteur
|
||||
* direct ; canal d'erreur du travailleur — son script ne se charge ou ne
|
||||
* coeursDisponibles ; lancer(configuration, reglages, { coeurs = 1 }) rend
|
||||
* une Execution, coeurs étant le nombre de travailleurs qu'elle emploie.
|
||||
* Un réglage hors de son domaine la rejette par REGLAGE_HORS_DOMAINE
|
||||
* (exigerReglages), sans travailleur, coeurs valant 0 ; un nombre de
|
||||
* cœurs qui n'est pas un entier d'au moins 1 lève une RangeError, une
|
||||
* faute de l'appelant. avancement → surAvancement, la somme de l'en-tête ;
|
||||
* la dernière proposition rangée → la promesse résolue aux propositions
|
||||
* par identifiant croissant ; erreur {code, details} → rejet par
|
||||
* ErreurGeneration(code, details) ; faute {message, pile} → rejet par une
|
||||
* Error sans code, dont le message porte celui du travailleur et la pile
|
||||
* la sienne : elle remonte comme une faute, comme sous l'exécuteur
|
||||
* direct ; canal d'erreur d'un travailleur — son script ne se charge ou ne
|
||||
* se compile pas —, → rejet par ErreurGeneration('TRAVAILLEUR_EN_ECHEC',
|
||||
* { remede: null }), l'événement traité : aucun geste de la page ne
|
||||
* relance un travailleur que le navigateur refuse, et le texte du refus
|
||||
* nomme sa cause (§ 5.9) ;
|
||||
* annuler() → terminate() aussitôt, rejet par ErreurAnnulee, plus aucun
|
||||
* avancement. Après l'issue, annuler() ne fait rien.
|
||||
* nomme sa cause (§ 5.9) ; annuler() → terminate() de chacun aussitôt,
|
||||
* rejet par ErreurAnnulee, plus aucun avancement. Après l'issue, annuler()
|
||||
* ne fait rien.
|
||||
*/
|
||||
export function creerExecuteurTravailleur(fabrique) {
|
||||
export function creerExecuteurTravailleur(fabrique, { coeursDisponibles } = {}) {
|
||||
return {
|
||||
lancer(configuration, reglages) {
|
||||
const travailleur = fabrique();
|
||||
coeursDisponibles: coeursDe(coeursDisponibles),
|
||||
lancer(configuration, reglages, { coeurs = 1 } = {}) {
|
||||
if (!Number.isInteger(coeurs) || coeurs < 1) {
|
||||
throw new RangeError(`coeurs : entier d'au moins 1 attendu, reçu ${String(coeurs)}`);
|
||||
}
|
||||
const abonnes = new Set();
|
||||
let termine = false;
|
||||
let resoudre;
|
||||
|
|
@ -60,59 +88,132 @@ export function creerExecuteurTravailleur(fabrique) {
|
|||
resoudre = ok;
|
||||
rejeter = ko;
|
||||
});
|
||||
const surAvancement = (fn) => {
|
||||
abonnes.add(fn);
|
||||
return () => {
|
||||
abonnes.delete(fn);
|
||||
};
|
||||
};
|
||||
|
||||
// L'issue : le travailleur se termine, ses gestionnaires sont retirés,
|
||||
// et rien de ce qu'il remettrait encore ne passe.
|
||||
function terminer() {
|
||||
termine = true;
|
||||
try {
|
||||
exigerReglages(reglages);
|
||||
} catch (erreur) {
|
||||
rejeter(erreur);
|
||||
return { surAvancement, resultat, annuler() {}, coeurs: 0 };
|
||||
}
|
||||
|
||||
const { nombre, arret } = reglages;
|
||||
const total = nombre * arret;
|
||||
// propositions[r − 1] : la proposition de rang r, rangée ; faits[r − 1] :
|
||||
// les mouvements évalués de ce rang ; fait, leur somme.
|
||||
const propositions = new Array(nombre);
|
||||
const faits = new Array(nombre).fill(0);
|
||||
let fait = 0;
|
||||
let rangees = 0;
|
||||
let prochain = 1;
|
||||
const travailleurs = [];
|
||||
|
||||
function publier() {
|
||||
for (const fn of [...abonnes]) fn(fait, total);
|
||||
}
|
||||
|
||||
// Porte les mouvements évalués du rang à valeur, s'ils croissent, et
|
||||
// publie la somme nouvelle.
|
||||
function avancer(rang, valeur) {
|
||||
if (valeur <= faits[rang - 1]) return;
|
||||
fait += valeur - faits[rang - 1];
|
||||
faits[rang - 1] = valeur;
|
||||
publier();
|
||||
}
|
||||
|
||||
// Un travailleur arrêté : ses gestionnaires retirés, il ne remet plus rien.
|
||||
function arreter(travailleur) {
|
||||
travailleur.onmessage = null;
|
||||
travailleur.onerror = null;
|
||||
travailleur.terminate();
|
||||
}
|
||||
|
||||
travailleur.onmessage = ({ data }) => {
|
||||
if (termine) return;
|
||||
// L'issue : chaque travailleur encore en vie est arrêté.
|
||||
function terminer() {
|
||||
termine = true;
|
||||
for (const suivi of travailleurs) {
|
||||
if (suivi.vivant) {
|
||||
suivi.vivant = false;
|
||||
arreter(suivi.travailleur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function echouer(erreur) {
|
||||
rejeter(erreur);
|
||||
terminer();
|
||||
}
|
||||
|
||||
// Donne au travailleur suivi le rang suivant, ou l'arrête quand il
|
||||
// n'en reste plus.
|
||||
function confier(suivi) {
|
||||
if (prochain > nombre) {
|
||||
suivi.vivant = false;
|
||||
suivi.rang = null;
|
||||
arreter(suivi.travailleur);
|
||||
return;
|
||||
}
|
||||
suivi.rang = prochain;
|
||||
prochain += 1;
|
||||
suivi.travailleur.postMessage({ type: 'proposer', configuration, reglages, rang: suivi.rang });
|
||||
}
|
||||
|
||||
function recevoir(suivi, data) {
|
||||
if (termine || !suivi.vivant) return;
|
||||
if (data.type === 'avancement') {
|
||||
for (const fn of [...abonnes]) fn(data.fait, data.total);
|
||||
} else if (data.type === 'resultat') {
|
||||
resoudre(data.propositions);
|
||||
terminer();
|
||||
avancer(suivi.rang, data.fait);
|
||||
} else if (data.type === 'proposition') {
|
||||
const { rang } = suivi;
|
||||
propositions[rang - 1] = data.proposition;
|
||||
rangees += 1;
|
||||
avancer(rang, arret);
|
||||
if (rangees === nombre) {
|
||||
resoudre(propositions);
|
||||
terminer();
|
||||
} else {
|
||||
confier(suivi);
|
||||
}
|
||||
} else if (data.type === 'erreur') {
|
||||
rejeter(new ErreurGeneration(data.code, data.details));
|
||||
terminer();
|
||||
echouer(new ErreurGeneration(data.code, data.details));
|
||||
} else if (data.type === 'faute') {
|
||||
const faute = new Error(`travailleur de génération : ${data.message}`);
|
||||
faute.stack = `${faute.stack}\n${data.pile}`;
|
||||
rejeter(faute);
|
||||
terminer();
|
||||
echouer(faute);
|
||||
}
|
||||
};
|
||||
// Le script du travailleur ne se charge ou ne se compile pas : le refus
|
||||
// TRAVAILLEUR_EN_ECHEC, que la commande « Générer » pose à côté
|
||||
// d'elle, jamais le message du moteur d'exécution. L'événement est
|
||||
// traité : le navigateur ne le signale pas en plus comme une faute de
|
||||
// la page. Une faute du calcul arrive en message, jamais ici.
|
||||
travailleur.onerror = (evenement) => {
|
||||
evenement?.preventDefault?.();
|
||||
if (termine) return;
|
||||
rejeter(new ErreurGeneration('TRAVAILLEUR_EN_ECHEC', { remede: null }));
|
||||
terminer();
|
||||
};
|
||||
travailleur.postMessage({ type: 'lancer', configuration, reglages });
|
||||
}
|
||||
|
||||
const paralleles = Math.min(nombre, coeurs);
|
||||
for (let k = 0; k < paralleles; k += 1) {
|
||||
const suivi = { travailleur: fabrique(), rang: null, vivant: true };
|
||||
travailleurs.push(suivi);
|
||||
suivi.travailleur.onmessage = ({ data }) => recevoir(suivi, data);
|
||||
// Le script du travailleur ne se charge ou ne se compile pas : le
|
||||
// refus TRAVAILLEUR_EN_ECHEC, que la commande « Générer » pose à côté
|
||||
// d'elle, jamais le message du moteur d'exécution. L'événement est
|
||||
// traité : le navigateur ne le signale pas en plus comme une faute de
|
||||
// la page. Une faute du calcul arrive en message, jamais ici.
|
||||
suivi.travailleur.onerror = (evenement) => {
|
||||
evenement?.preventDefault?.();
|
||||
if (termine || !suivi.vivant) return;
|
||||
echouer(new ErreurGeneration('TRAVAILLEUR_EN_ECHEC', { remede: null }));
|
||||
};
|
||||
}
|
||||
for (const suivi of travailleurs) confier(suivi);
|
||||
|
||||
return {
|
||||
surAvancement(fn) {
|
||||
abonnes.add(fn);
|
||||
return () => {
|
||||
abonnes.delete(fn);
|
||||
};
|
||||
},
|
||||
surAvancement,
|
||||
resultat,
|
||||
annuler() {
|
||||
if (termine) return;
|
||||
terminer();
|
||||
rejeter(new ErreurAnnulee());
|
||||
},
|
||||
coeurs: paralleles,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
// © 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.
|
||||
// Épreuves de l'exécuteur du travailleur (§ 5.7, § 5.10) : le groupe de
|
||||
// travailleurs de 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
|
||||
// par son onmessage, comme le ferait le vrai travailleur. Les travailleurs
|
||||
// calculants, eux, répondent par brancherTravailleur, le vrai côté
|
||||
// travailleur, dans l'ordre que l'épreuve choisit : la valeur attendue est
|
||||
// alors celle de rechercher, sollicité une seconde fois.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { texteLivre } from '../demo/livrees.js';
|
||||
import { rechercher } from '../moteur/recherche.js';
|
||||
import { analyser, configurationDepuisCharge } from '../stockage/document.js';
|
||||
import { creerExecuteurTravailleur } from './executeur_travailleur.js';
|
||||
import { ErreurAnnulee, ErreurGeneration } from './generation.js';
|
||||
import { brancherTravailleur } from './generation_travailleur.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 proposition d'épreuve du rang donné, sans calcul.
|
||||
const propositionDe = (rang) => ({ id: rang, graine: 100 + rang, arret: 40_000, historique: 1000, plan: [[[rang]]] });
|
||||
|
||||
// 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.
|
||||
// travailleur, remis à son onmessage courant ; rang(), celui du dernier
|
||||
// proposer reçu.
|
||||
function fabriqueFactice() {
|
||||
const travailleurs = [];
|
||||
const fabrique = () => {
|
||||
|
|
@ -35,6 +45,13 @@ function fabriqueFactice() {
|
|||
poster(data) {
|
||||
this.onmessage?.({ data });
|
||||
},
|
||||
rang() {
|
||||
return this.recus.at(-1)?.rang ?? null;
|
||||
},
|
||||
// Poste la proposition du rang en cours.
|
||||
rendre() {
|
||||
this.poster({ type: 'proposition', proposition: propositionDe(this.rang()) });
|
||||
},
|
||||
};
|
||||
travailleurs.push(travailleur);
|
||||
return travailleur;
|
||||
|
|
@ -42,6 +59,42 @@ function fabriqueFactice() {
|
|||
return { fabrique, travailleurs };
|
||||
}
|
||||
|
||||
// Une fabrique de travailleurs calculants : chaque proposer reçu attend que
|
||||
// l'épreuve le serve ; servir(choisir) traite les demandes en attente une à
|
||||
// une, choisir désignant chaque fois l'indice de la suivante parmi elles, et
|
||||
// remet au travailleur ce que brancherTravailleur a posté. Un travailleur
|
||||
// terminé ne reçoit plus rien.
|
||||
function fabriqueCalculante() {
|
||||
const enAttente = [];
|
||||
let crees = 0;
|
||||
const fabrique = () => {
|
||||
crees += 1;
|
||||
const travailleur = {
|
||||
onmessage: null,
|
||||
termine: false,
|
||||
postMessage(message) {
|
||||
enAttente.push({ travailleur, message: structuredClone(message) });
|
||||
},
|
||||
terminate() {
|
||||
travailleur.termine = true;
|
||||
},
|
||||
};
|
||||
return travailleur;
|
||||
};
|
||||
function servir(choisir) {
|
||||
while (enAttente.length > 0) {
|
||||
const [{ travailleur, message }] = enAttente.splice(choisir(enAttente.length), 1);
|
||||
if (travailleur.termine) continue;
|
||||
const postes = [];
|
||||
const portee = { onmessage: null, postMessage: (m) => postes.push(structuredClone(m)) };
|
||||
brancherTravailleur(portee);
|
||||
portee.onmessage({ data: message });
|
||||
for (const data of postes) if (!travailleur.termine) travailleur.onmessage?.({ data });
|
||||
}
|
||||
}
|
||||
return { fabrique, servir, crees: () => crees };
|
||||
}
|
||||
|
||||
// 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 }.
|
||||
|
|
@ -60,28 +113,141 @@ async function issue(promesse) {
|
|||
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 configurationDe = (cle) => configurationDepuisCharge(analyser(texteLivre(cle)).charge);
|
||||
|
||||
describe('exécuteur du travailleur : le groupe (§ 5.7, § 5.10)', () => {
|
||||
test('coeursDisponibles : un entier d’au moins 1 tel quel, toute autre valeur 1 ; 1 sans option', () => {
|
||||
const { fabrique } = fabriqueFactice();
|
||||
const cas = [
|
||||
[8, 8],
|
||||
[1, 1],
|
||||
[undefined, 1],
|
||||
[0, 1],
|
||||
[2.5, 1],
|
||||
['4', 1],
|
||||
[null, 1],
|
||||
];
|
||||
assert.equal(cas.length, 7);
|
||||
for (const [donnee, attendue] of cas) {
|
||||
assert.equal(creerExecuteurTravailleur(fabrique, { coeursDisponibles: donnee }).coeursDisponibles, attendue, String(donnee));
|
||||
}
|
||||
assert.equal(creerExecuteurTravailleur(fabrique).coeursDisponibles, 1);
|
||||
});
|
||||
|
||||
test('min(nombre, coeurs) travailleurs, nés au lancer ; le travailleur k reçoit d’abord le rang k ; sans option, un seul', () => {
|
||||
const cas = [
|
||||
{ coeurs: 3, nombre: 5, attendus: 3 },
|
||||
{ coeurs: 8, nombre: 3, attendus: 3 },
|
||||
{ coeurs: 1, nombre: 5, attendus: 1 },
|
||||
{ coeurs: undefined, nombre: 4, attendus: 1 },
|
||||
];
|
||||
assert.equal(cas.length, 4);
|
||||
for (const { coeurs, nombre, attendus } of cas) {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const executeur = creerExecuteurTravailleur(fabrique, { coeursDisponibles: 16 });
|
||||
assert.equal(travailleurs.length, 0, 'aucun travailleur avant le lancer');
|
||||
const reglages = { ...REGLAGES, nombre };
|
||||
const execution = executeur.lancer(CONFIGURATION, reglages, coeurs === undefined ? undefined : { coeurs });
|
||||
execution.resultat.catch(() => undefined);
|
||||
assert.equal(travailleurs.length, attendus, JSON.stringify({ coeurs, nombre }));
|
||||
assert.equal(execution.coeurs, attendus);
|
||||
assert.equal(new Set(travailleurs).size, attendus);
|
||||
travailleurs.forEach((travailleur, k) => {
|
||||
assert.deepEqual(travailleur.recus, [{ type: 'proposer', configuration: CONFIGURATION, reglages, rang: k + 1 }]);
|
||||
assert.equal(travailleur.terminaisons, 0);
|
||||
});
|
||||
execution.annuler();
|
||||
}
|
||||
});
|
||||
|
||||
test('le rang suivant va au premier travailleur libéré ; celui qui n’a plus rien à faire est terminé ; le résultat suit les identifiants, non l’ordre d’achèvement', async () => {
|
||||
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 } },
|
||||
]);
|
||||
const reglages = { ...REGLAGES, nombre: 5 };
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, reglages, { coeurs: 2 });
|
||||
const [premier, second] = travailleurs;
|
||||
second.rendre(); // rang 2
|
||||
assert.equal(second.rang(), 3);
|
||||
second.rendre(); // rang 3
|
||||
assert.equal(second.rang(), 4);
|
||||
premier.rendre(); // rang 1
|
||||
assert.equal(premier.rang(), 5);
|
||||
premier.rendre(); // rang 5 : plus rien pour le premier
|
||||
assert.equal(premier.terminaisons, 1);
|
||||
assert.equal(second.terminaisons, 0);
|
||||
assert.equal((await issue(execution.resultat)).etat, 'attente');
|
||||
second.rendre(); // rang 4
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'resolue');
|
||||
assert.deepEqual(lue.valeur, [1, 2, 3, 4, 5].map(propositionDe));
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
);
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ recus }) => recus.map(({ rang }) => rang)),
|
||||
[
|
||||
[1, 5],
|
||||
[2, 3, 4],
|
||||
],
|
||||
);
|
||||
execution.annuler();
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1],
|
||||
'annuler après le résultat ne termine rien de plus',
|
||||
);
|
||||
});
|
||||
|
||||
test('deux avancements parviennent à surAvancement dans leur ordre ; un abonné retiré ne reçoit plus rien', () => {
|
||||
test('travailleurs calculants : pour 1, 2 et 3 cœurs et 5 propositions, servis dans trois ordres, le résultat sérialisé égale celui de rechercher, octet pour octet', async () => {
|
||||
const configuration = configurationDe('petite-conflit');
|
||||
const reglages = { graine: 77, nombre: 5, arret: 3_000, historique: 1000 };
|
||||
const attendu = JSON.stringify(rechercher(configuration, reglages));
|
||||
const ordres = {
|
||||
fifo: () => 0,
|
||||
lifo: (n) => n - 1,
|
||||
milieu: (n) => Math.floor(n / 2),
|
||||
};
|
||||
const coeurs = [1, 2, 3];
|
||||
let comparaisons = 0;
|
||||
for (const w of coeurs) {
|
||||
for (const [nom, choisir] of Object.entries(ordres)) {
|
||||
const { fabrique, servir, crees } = fabriqueCalculante();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(configuration, reglages, { coeurs: w });
|
||||
assert.equal(crees(), w);
|
||||
servir(choisir);
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'resolue', `${w} cœurs, ${nom}`);
|
||||
assert.equal(JSON.stringify(lue.valeur), attendu, `${w} cœurs, ${nom}`);
|
||||
comparaisons += 1;
|
||||
}
|
||||
}
|
||||
assert.equal(comparaisons, 9);
|
||||
});
|
||||
|
||||
test('l’avancement est la somme des avancements de chaque proposition, sur nombre × arret ; une proposition rendue compte pour son arret entier', () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const reglages = { ...REGLAGES, nombre: 3, arret: 100 };
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, reglages, { coeurs: 2 });
|
||||
execution.resultat.catch(() => undefined);
|
||||
const recus = [];
|
||||
execution.surAvancement((fait, total) => recus.push([fait, total]));
|
||||
const [premier, second] = travailleurs;
|
||||
premier.poster({ type: 'avancement', fait: 40, total: 100 });
|
||||
second.poster({ type: 'avancement', fait: 30, total: 100 });
|
||||
premier.poster({ type: 'proposition', proposition: propositionDe(1) });
|
||||
premier.poster({ type: 'avancement', fait: 50, total: 100 });
|
||||
second.poster({ type: 'avancement', fait: 90, total: 100 });
|
||||
assert.deepEqual(recus, [
|
||||
[40, 300],
|
||||
[70, 300],
|
||||
[130, 300],
|
||||
[180, 300],
|
||||
[240, 300],
|
||||
]);
|
||||
execution.annuler();
|
||||
});
|
||||
|
||||
test('un abonné retiré ne reçoit plus rien', () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
|
||||
execution.resultat.catch(() => undefined);
|
||||
|
|
@ -90,79 +256,79 @@ describe('exécuteur du travailleur (§ 5.10)', () => {
|
|||
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 });
|
||||
travailleur.poster({ type: 'avancement', fait: 20_000, total: 40_000 });
|
||||
desabonner();
|
||||
travailleur.poster({ type: 'avancement', fait: 80_000, total: 80_000 });
|
||||
travailleur.poster({ type: 'avancement', fait: 30_000, total: 40_000 });
|
||||
assert.deepEqual(recus, [
|
||||
[50_000, 80_000],
|
||||
[80_000, 80_000],
|
||||
[20_000, 80_000],
|
||||
[30_000, 80_000],
|
||||
]);
|
||||
assert.deepEqual(retires, [[50_000, 80_000]]);
|
||||
assert.deepEqual(retires, [[20_000, 80_000]]);
|
||||
execution.annuler();
|
||||
});
|
||||
|
||||
test('resultat : la promesse se résout aux propositions reçues, puis le travailleur est terminé ; un message tardif ne change rien', async () => {
|
||||
test("annuler : chaque travailleur terminé aussitôt, rejet par ErreurAnnulee ; rien de ce qu'un travailleur poste ensuite n'atteint plus rien", async () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, { ...REGLAGES, nombre: 5 }, { coeurs: 3 });
|
||||
const avancements = [];
|
||||
execution.surAvancement((fait, total) => avancements.push([fait, total]));
|
||||
// Les gestionnaires posés au lancement : un message déjà en file chez
|
||||
// le navigateur au moment de l'annulation les atteindrait encore.
|
||||
const gestionnaires = travailleurs.map(({ onmessage }) => onmessage);
|
||||
travailleurs[1].poster({ type: 'avancement', fait: 10_000, total: 40_000 });
|
||||
execution.annuler();
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1, 1],
|
||||
'terminate() de chacun dans le même tour que annuler()',
|
||||
);
|
||||
for (const gestionnaire of gestionnaires) {
|
||||
gestionnaire({ data: { type: 'avancement', fait: 40_000, total: 40_000 } });
|
||||
gestionnaire({ data: { type: 'proposition', proposition: propositionDe(1) } });
|
||||
}
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'rejetee');
|
||||
assert.ok(lue.erreur instanceof ErreurAnnulee, String(lue.erreur));
|
||||
assert.deepEqual(avancements, [[10_000, 200_000]]);
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ recus }) => recus.length),
|
||||
[1, 1, 1],
|
||||
'aucun rang donné après l’annulation',
|
||||
);
|
||||
execution.annuler();
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1, 1],
|
||||
'une seconde annulation ne termine rien de plus',
|
||||
);
|
||||
});
|
||||
|
||||
test('erreur { code, details } d’un travailleur : rejet par une ErreurGeneration de ce code et de ces détails, chaque travailleur terminé, la suite ignorée', async () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, { ...REGLAGES, nombre: 4 }, { coeurs: 3 });
|
||||
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 } });
|
||||
travailleurs[0].rendre();
|
||||
travailleurs[1].poster({ type: 'erreur', code: 'PLACES_MANQUANTES', details: { placesManquantes: 1 } });
|
||||
travailleurs[2].rendre();
|
||||
travailleurs[0].poster({ type: 'avancement', fait: 1, total: 2 });
|
||||
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);
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1, 1],
|
||||
);
|
||||
assert.deepEqual(avancements, [40_000]);
|
||||
});
|
||||
|
||||
test("annuler : terminate() aussitôt, rejet par ErreurAnnulee ; un avancement ou un résultat posté ensuite n'atteint plus rien", async () => {
|
||||
test('le canal d’erreur d’un travailleur — un script qui ne se charge ou ne se compile pas : rejet par ErreurGeneration TRAVAILLEUR_EN_ECHEC, remède null, sans le message ; l’événement est traité, chaque travailleur terminé', 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 — un script qui ne se charge ou ne se compile pas : rejet par ErreurGeneration TRAVAILLEUR_EN_ECHEC, remède null, sans le message ; l’événement est traité, le travailleur terminé', async () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
|
||||
const [travailleur] = travailleurs;
|
||||
assert.equal(typeof travailleur.onerror, 'function');
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES, { coeurs: 2 });
|
||||
assert.equal(travailleurs.length, 2);
|
||||
assert.ok(travailleurs.every(({ onerror }) => typeof onerror === 'function'));
|
||||
const evenement = {
|
||||
message: 'faute d’épreuve',
|
||||
traite: false,
|
||||
|
|
@ -170,7 +336,7 @@ describe('exécuteur du travailleur (§ 5.10)', () => {
|
|||
this.traite = true;
|
||||
},
|
||||
};
|
||||
travailleur.onerror(evenement);
|
||||
travailleurs[1].onerror(evenement);
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'rejetee');
|
||||
assert.ok(lue.erreur instanceof ErreurGeneration, String(lue.erreur));
|
||||
|
|
@ -178,22 +344,25 @@ describe('exécuteur du travailleur (§ 5.10)', () => {
|
|||
assert.deepEqual(lue.erreur.details, { remede: null });
|
||||
assert.doesNotMatch(JSON.stringify(lue.erreur.details), /épreuve/);
|
||||
assert.equal(evenement.traite, true);
|
||||
assert.equal(travailleur.terminaisons, 1);
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1],
|
||||
);
|
||||
// Un événement d'erreur sans preventDefault, comme le rendrait une portée factice, ne lève pas.
|
||||
const seconde = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
|
||||
travailleurs[1].onerror({});
|
||||
travailleurs[2].onerror({});
|
||||
assert.equal((await issue(seconde.resultat)).erreur.code, 'TRAVAILLEUR_EN_ECHEC');
|
||||
});
|
||||
|
||||
test('une faute pendant le calcul, postée { type: faute, message, pile } : rejet par une Error sans code, qui porte le message et la pile du travailleur ; aucun refus, le travailleur terminé, la suite ignorée', async () => {
|
||||
test('une faute pendant le calcul, postée { type: faute, message, pile } : rejet par une Error sans code, qui porte le message et la pile du travailleur ; aucun refus, chaque travailleur terminé, la suite ignorée', async () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES);
|
||||
const execution = creerExecuteurTravailleur(fabrique).lancer(CONFIGURATION, REGLAGES, { coeurs: 2 });
|
||||
const avancements = [];
|
||||
execution.surAvancement((fait, total) => avancements.push([fait, total]));
|
||||
const [travailleur] = travailleurs;
|
||||
const [travailleur, autre] = travailleurs;
|
||||
travailleur.poster({ type: 'faute', message: 'TypeError: invariant d’épreuve rompu', pile: 'TypeError: invariant d’épreuve rompu\n at calcul (épreuve.js:1:1)' });
|
||||
travailleur.poster({ type: 'avancement', fait: 1, total: 2 });
|
||||
travailleur.poster({ type: 'resultat', propositions: [] });
|
||||
autre.rendre();
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'rejetee');
|
||||
assert.ok(lue.erreur instanceof Error, String(lue.erreur));
|
||||
|
|
@ -203,6 +372,25 @@ describe('exécuteur du travailleur (§ 5.10)', () => {
|
|||
assert.match(lue.erreur.message, /TypeError: invariant d’épreuve rompu/);
|
||||
assert.match(lue.erreur.stack, /at calcul \(épreuve\.js:1:1\)/);
|
||||
assert.deepEqual(avancements, []);
|
||||
assert.equal(travailleur.terminaisons, 1);
|
||||
assert.deepEqual(
|
||||
travailleurs.map(({ terminaisons }) => terminaisons),
|
||||
[1, 1],
|
||||
);
|
||||
});
|
||||
|
||||
test('un réglage hors de son domaine rejette REGLAGE_HORS_DOMAINE sans faire naître de travailleur ; un nombre de cœurs qui n’est pas un entier d’au moins 1 lève une RangeError', async () => {
|
||||
const { fabrique, travailleurs } = fabriqueFactice();
|
||||
const executeur = creerExecuteurTravailleur(fabrique, { coeursDisponibles: 4 });
|
||||
const execution = executeur.lancer(CONFIGURATION, { ...REGLAGES, nombre: 0 }, { coeurs: 2 });
|
||||
const lue = await issue(execution.resultat);
|
||||
assert.equal(lue.etat, 'rejetee');
|
||||
assert.equal(lue.erreur.code, 'REGLAGE_HORS_DOMAINE');
|
||||
assert.deepEqual(lue.erreur.details, { reglage: 'nombre', remede: null });
|
||||
assert.equal(execution.coeurs, 0);
|
||||
assert.doesNotThrow(() => execution.annuler());
|
||||
for (const coeurs of [0, 1.5, '2']) {
|
||||
assert.throws(() => executeur.lancer(CONFIGURATION, REGLAGES, { coeurs }), RangeError, String(coeurs));
|
||||
}
|
||||
assert.equal(travailleurs.length, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@
|
|||
// et s'annule par terminate(). Une erreur de la recherche devient la même
|
||||
// dans les deux, par versErreurGeneration.
|
||||
//
|
||||
// Le contrôleur tient aussi le réglage des cœurs de calcul, un réglage du
|
||||
// poste qui vaut pour la séance de l'application et ne s'écrit nulle part :
|
||||
// automatique, la génération emploie autant de cœurs que l'exécuteur annonce
|
||||
// de processeurs logiques (coeursDisponibles, que src/main.js lit de la
|
||||
// plateforme) ; sinon le nombre saisi, de 1 à ces disponibles. Un lancement
|
||||
// en emploie min(nombre de propositions, cœurs réglés), et l'estimation
|
||||
// compte en vagues de propositions calculées ensemble.
|
||||
//
|
||||
// Le contrôleur ne numérote rien et ne mesure aucun plan. Il refuse avant
|
||||
// tout lancement un réglage hors de son domaine (REGLAGE_HORS_DOMAINE), puis
|
||||
// ce que l'enregistrement refuserait — les refus de la séance, lus dans son
|
||||
|
|
@ -104,6 +112,18 @@ export function exigerReglages(reglages) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Le nombre de processeurs logiques d'un poste, tel que la plateforme le
|
||||
* donne — hyperthreads compris —, rendu utilisable : un entier d'au moins 1
|
||||
* tel quel, 1 pour toute autre valeur, absente comprise.
|
||||
*
|
||||
* @param {unknown} valeur
|
||||
* @returns {number}
|
||||
*/
|
||||
export function coeursDe(valeur) {
|
||||
return Number.isInteger(valeur) && valeur >= 1 ? valeur : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* L'erreur d'une recherche, la même dans les deux exécuteurs : ErreurAnnulee
|
||||
* telle quelle ; une ErreurConfiguration devient ErreurGeneration de son code
|
||||
|
|
@ -127,10 +147,16 @@ export function versErreurGeneration(erreur) {
|
|||
* abonne fn aux avancements ; rend la fonction qui désabonne
|
||||
* @property {Promise<import('../moteur/recherche.js').Proposition[]>} resultat
|
||||
* rejetée par ErreurAnnulee à l'annulation, par ErreurGeneration sur
|
||||
* une erreur du moteur
|
||||
* une erreur du moteur ; les propositions par identifiant croissant
|
||||
* @property {() => void} annuler sans résultat partiel
|
||||
* @property {number} [coeurs] les cœurs que le calcul emploie ; 1 s'il
|
||||
* ne le dit pas
|
||||
*
|
||||
* @typedef {{lancer: (configuration: Object, reglages: Object) => Execution}} Executeur
|
||||
* @typedef {Object} Executeur
|
||||
* @property {(configuration: Object, reglages: Object, options?: {coeurs?: number}) => Execution} lancer
|
||||
* options.coeurs : les cœurs que le calcul peut employer, 1 par défaut
|
||||
* @property {number} [coeursDisponibles] les processeurs logiques du poste ;
|
||||
* 1 s'il ne le dit pas
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -139,12 +165,17 @@ export function versErreurGeneration(erreur) {
|
|||
* ou depuis un rappel d'avancement, fait lever ErreurAnnulee à la lecture
|
||||
* suivante du signal par le moteur : avant la proposition suivante, au plus
|
||||
* 1 000 mouvements plus tard. Chaque avancement est celui du moteur, toutes
|
||||
* les 1 000 itérations.
|
||||
* les 1 000 itérations. Il calcule une proposition après l'autre quel que
|
||||
* soit options.coeurs, et chaque exécution dit n'employer qu'un cœur ; il
|
||||
* annonce les cœurs qu'on lui donne, par coeursDe, pour qu'une épreuve règle
|
||||
* ce que le contrôleur en lit.
|
||||
*
|
||||
* @param {{coeursDisponibles?: unknown}} [options]
|
||||
* @returns {Executeur}
|
||||
*/
|
||||
export function creerExecuteurDirect() {
|
||||
export function creerExecuteurDirect({ coeursDisponibles } = {}) {
|
||||
return {
|
||||
coeursDisponibles: coeursDe(coeursDisponibles),
|
||||
lancer(configuration, reglages) {
|
||||
const signal = { aborted: false };
|
||||
const abonnes = new Set();
|
||||
|
|
@ -170,6 +201,7 @@ export function creerExecuteurDirect() {
|
|||
annuler() {
|
||||
signal.aborted = true;
|
||||
},
|
||||
coeurs: 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -189,23 +221,35 @@ function planRange(proposition, configuration, produitVersion) {
|
|||
* @param {Object} parametres
|
||||
* @param {Object} parametres.seance la séance d'A1 et A2 : etat,
|
||||
* subscribe, surAnnulation, executer, debuterCalcul, finirCalcul
|
||||
* @param {Executeur} parametres.executeur
|
||||
* @param {Executeur} parametres.executeur coeursDisponibles, qu'il annonce,
|
||||
* borne le réglage des cœurs
|
||||
* @param {import('./horloge.js').Horloge} parametres.horloge instant() mesure
|
||||
* la durée d'un calcul
|
||||
* @param {string} parametres.produitVersion
|
||||
* @returns {Object} le contrôleur : grainePropose, reglagesProposes, valider,
|
||||
* estimer, derniereGeneration, generer, annuler, subscribe
|
||||
* estimer, derniereGeneration, generer, annuler, coeurs,
|
||||
* reglerCoeurs, subscribe
|
||||
*/
|
||||
export function creerControleurGeneration({ seance, executeur, horloge, produitVersion }) {
|
||||
// graine : la graine proposée ; base : celle de l'événement ouvert, null
|
||||
// sans événement ; mesure : le dernier calcul abouti, null avant lui ;
|
||||
// execution : celle en cours, null hors génération ; enCours vaut vrai du
|
||||
// lancement à la fin de l'enregistrement.
|
||||
// coeurs : le réglage des cœurs de calcul, que l'observable porte —
|
||||
// automatique, le nombre saisi (manuel), les disponibles, et nombre, les
|
||||
// cœurs réglés.
|
||||
let graine = 1;
|
||||
let base = null;
|
||||
let mesure = null;
|
||||
let execution = null;
|
||||
let observable = { enCours: false, fait: 0, total: 0, annulee: null };
|
||||
const disponibles = coeursDe(executeur.coeursDisponibles);
|
||||
let reglageCoeurs = { automatique: true, manuel: disponibles };
|
||||
const coeurs = () => ({
|
||||
...reglageCoeurs,
|
||||
disponibles,
|
||||
nombre: reglageCoeurs.automatique ? disponibles : reglageCoeurs.manuel,
|
||||
});
|
||||
let observable = { enCours: false, fait: 0, total: 0, annulee: null, coeurs: coeurs() };
|
||||
const abonnes = new Set();
|
||||
|
||||
function publier(changements) {
|
||||
|
|
@ -232,22 +276,37 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV
|
|||
courante.annuler();
|
||||
}
|
||||
|
||||
// Les cœurs qu'un lancement de nombre propositions emploie.
|
||||
const coeursPour = (nombre) => Math.min(nombre, coeurs().nombre);
|
||||
|
||||
// Les mouvements d'une génération qui se suivent sur un même cœur : arret
|
||||
// pour chacune des ⌈nombre / cœurs⌉ vagues de propositions calculées
|
||||
// ensemble.
|
||||
const mouvementsParCoeur = (nombre, arret, parallele) => Math.ceil(nombre / parallele) * arret;
|
||||
|
||||
// La durée d'une génération, proportionnelle aux mouvements qui se suivent
|
||||
// sur un même cœur, ceux de la mesure ramenés aux cœurs qu'elle a employés.
|
||||
function estimer({ nombre, arret }) {
|
||||
const mouvements = nombre * arret;
|
||||
if (mesure === null) return { inconnue: true };
|
||||
const secondes = (mouvements * mesure.dureeMs) / (mesure.mouvements * 1000);
|
||||
const parallele = coeursPour(nombre);
|
||||
const secondes =
|
||||
(mouvementsParCoeur(nombre, arret, parallele) * mesure.dureeMs) /
|
||||
(mouvementsParCoeur(mesure.nombre, mesure.arret, mesure.coeurs) * 1000);
|
||||
return {
|
||||
secondes,
|
||||
longue: secondes > SEUIL_GENERATION_LONGUE_S,
|
||||
auDelaDuPlafond: secondes > PLAFOND_USAGE_S,
|
||||
mesure: { ...mesure },
|
||||
coeurs: parallele,
|
||||
};
|
||||
}
|
||||
|
||||
// Longue : au-delà du seuil estimé, ou, sans mesure, au-delà de dix fois
|
||||
// le réglage par défaut.
|
||||
// Longue : au-delà du seuil estimé, ou, sans mesure, quand les mouvements
|
||||
// qui se suivent sur un même cœur dépassent dix fois le réglage par défaut.
|
||||
function estLongue(reglages, estimation) {
|
||||
if (estimation.inconnue === true) return reglages.nombre * reglages.arret > MOUVEMENTS_SANS_MESURE;
|
||||
if (estimation.inconnue === true) {
|
||||
return mouvementsParCoeur(reglages.nombre, reglages.arret, coeursPour(reglages.nombre)) > MOUVEMENTS_SANS_MESURE;
|
||||
}
|
||||
return estimation.longue;
|
||||
}
|
||||
|
||||
|
|
@ -290,13 +349,14 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV
|
|||
// résultat entre-temps.
|
||||
async function calculer(configuration, reglages) {
|
||||
const debut = horloge.instant();
|
||||
execution = executeur.lancer(configuration, reglages);
|
||||
execution = executeur.lancer(configuration, reglages, { coeurs: coeursPour(reglages.nombre) });
|
||||
const employes = coeursDe(execution.coeurs);
|
||||
const desabonner = execution.surAvancement((fait, total) => publier({ fait, total }));
|
||||
const desannuler = seance.surAnnulation(() => annulerAvec('seance'));
|
||||
try {
|
||||
const propositions = await execution.resultat;
|
||||
if (observable.annulee !== null) throw new ErreurAnnulee();
|
||||
return { propositions, dureeMs: horloge.instant() - debut };
|
||||
return { propositions, dureeMs: horloge.instant() - debut, coeurs: employes };
|
||||
} finally {
|
||||
desabonner();
|
||||
desannuler();
|
||||
|
|
@ -349,13 +409,14 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV
|
|||
publier({ enCours: true, fait: 0, total: reglages.nombre * reglages.arret, annulee: null });
|
||||
seance.debuterCalcul();
|
||||
try {
|
||||
const { propositions, dureeMs } = await calculer(configuration, reglages);
|
||||
const { propositions, dureeMs, coeurs: employes } = await calculer(configuration, reglages);
|
||||
mesure = {
|
||||
dureeMs,
|
||||
mouvements: reglages.nombre * reglages.arret,
|
||||
participants: instance.N,
|
||||
nombre: reglages.nombre,
|
||||
arret: reglages.arret,
|
||||
coeurs: employes,
|
||||
};
|
||||
const { gardees, identiques } = trier(propositions, configuration, instance);
|
||||
if (gardees.length === 0) return { ajoutees: [], identiques, dureeMs };
|
||||
|
|
@ -404,12 +465,19 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV
|
|||
return { reglages: refus.length === 0 ? reglages : null, refus };
|
||||
},
|
||||
/**
|
||||
* L'estimation des réglages, proportionnelle aux mouvements de la
|
||||
* dernière mesure : { inconnue: true } sans elle, sinon { secondes,
|
||||
* longue, auDelaDuPlafond, mesure }.
|
||||
* L'estimation des réglages, proportionnelle aux mouvements qui se
|
||||
* suivent sur un même cœur — arret × ⌈nombre / cœurs⌉ —, ceux de la
|
||||
* dernière mesure ramenés de même aux cœurs qu'elle a employés :
|
||||
* { inconnue: true } sans elle, sinon { secondes, longue,
|
||||
* auDelaDuPlafond, mesure, coeurs }, coeurs étant ceux que ces réglages
|
||||
* emploieraient.
|
||||
*/
|
||||
estimer,
|
||||
/** La mesure du dernier calcul abouti de ce contrôleur, en mémoire ; null avant lui. */
|
||||
/**
|
||||
* La mesure du dernier calcul abouti de ce contrôleur, en mémoire ; null
|
||||
* avant lui : { dureeMs, mouvements, participants, nombre, arret,
|
||||
* coeurs }, coeurs étant ceux que l'exécution a employés.
|
||||
*/
|
||||
derniereGeneration: () => (mesure === null ? null : { ...mesure }),
|
||||
/**
|
||||
* Lance une génération et l'enregistre : { ajoutees, identiques, dureeMs }.
|
||||
|
|
@ -426,9 +494,41 @@ export function creerControleurGeneration({ seance, executeur, horloge, produitV
|
|||
/** Annule la génération en cours, sans résultat partiel ; annulee vaut 'commande'. */
|
||||
annuler: () => annulerAvec('commande'),
|
||||
/**
|
||||
* Le contrat des magasins de Svelte sur { enCours, fait, total, annulee } :
|
||||
* annulee, la cause de la dernière annulation, 'commande' ou 'seance',
|
||||
* null dès le lancement suivant.
|
||||
* Le réglage des cœurs de calcul : { automatique, manuel, disponibles,
|
||||
* nombre } — automatique vrai à la création ; manuel, le dernier nombre
|
||||
* saisi, les disponibles avant toute saisie ; disponibles, ceux
|
||||
* qu'annonce l'exécuteur, par coeursDe ; nombre, les cœurs réglés,
|
||||
* disponibles en automatique, manuel sinon.
|
||||
*/
|
||||
coeurs,
|
||||
/**
|
||||
* Règle les cœurs de calcul, pour la séance de l'application : rien ne
|
||||
* s'écrit, ni état, ni journal. saisie, quand elle est donnée, se lit
|
||||
* par lireEntier (NON_ENTIER {saisie}), puis se borne de 1 aux
|
||||
* disponibles (HORS_DOMAINE {min: 1, max}), remède null ; refusée, rien
|
||||
* ne change, automatique compris. Rend { refus: null } ou { refus:
|
||||
* {code, details} } ; un réglage accepté se publie aux abonnés.
|
||||
*/
|
||||
reglerCoeurs({ automatique, saisie } = {}) {
|
||||
const suivant = { ...reglageCoeurs };
|
||||
if (saisie !== undefined) {
|
||||
const lu = lireEntier(saisie);
|
||||
if (lu.refus !== undefined) return { refus: { code: lu.refus.code, details: lu.refus.details } };
|
||||
if (lu.valeur < 1 || lu.valeur > disponibles) {
|
||||
return { refus: { code: 'HORS_DOMAINE', details: { min: 1, max: disponibles, remede: null } } };
|
||||
}
|
||||
suivant.manuel = lu.valeur;
|
||||
}
|
||||
if (automatique !== undefined) suivant.automatique = automatique === true;
|
||||
reglageCoeurs = suivant;
|
||||
publier({ coeurs: coeurs() });
|
||||
return { refus: null };
|
||||
},
|
||||
/**
|
||||
* Le contrat des magasins de Svelte sur { enCours, fait, total, annulee,
|
||||
* coeurs } : annulee, la cause de la dernière annulation, 'commande' ou
|
||||
* 'seance', null dès le lancement suivant ; coeurs, le réglage des
|
||||
* cœurs de calcul, celui que rend coeurs().
|
||||
*/
|
||||
subscribe(fn) {
|
||||
abonnes.add(fn);
|
||||
|
|
|
|||
|
|
@ -24,15 +24,18 @@ import { SUFFIXES } from '../stockage/noms.js';
|
|||
import { derive, planDepuisFichier, versFichier } from '../stockage/placements.js';
|
||||
import { VERSION } from '../version.genere.js';
|
||||
import { ErreurCommande } from './erreurs.js';
|
||||
import { creerExecuteurTravailleur } from './executeur_travailleur.js';
|
||||
import {
|
||||
ErreurAnnulee,
|
||||
ErreurGeneration,
|
||||
PLAFOND_USAGE_S,
|
||||
SEUIL_GENERATION_LONGUE_S,
|
||||
creerControleurGeneration,
|
||||
coeursDe,
|
||||
creerExecuteurDirect,
|
||||
versErreurGeneration,
|
||||
} from './generation.js';
|
||||
import { brancherTravailleur } from './generation_travailleur.js';
|
||||
import { creerHorlogePilotee } from './horloge.js';
|
||||
import { libelle } from './libelles.js';
|
||||
import { INACTIVITE_MS, creerSeance } from './seance.js';
|
||||
|
|
@ -85,14 +88,16 @@ function chargeDe({ n = 0, exclus = [], tables = 0, sieges = 3, tours = 2, nom =
|
|||
|
||||
// --- Les exécuteurs d'épreuve ---------------------------------------------------
|
||||
|
||||
// Un exécuteur qui note chaque lancement avant de le confier à interne.
|
||||
// Un exécuteur qui note chaque lancement, options comprises, avant de le
|
||||
// confier à interne ; il porte les cœurs disponibles d'interne.
|
||||
function espionner(interne = creerExecuteurDirect()) {
|
||||
const lancements = [];
|
||||
return {
|
||||
lancements,
|
||||
lancer(configuration, reglages) {
|
||||
lancements.push({ configuration, reglages });
|
||||
return interne.lancer(configuration, reglages);
|
||||
coeursDisponibles: interne.coeursDisponibles,
|
||||
lancer(configuration, reglages, options) {
|
||||
lancements.push({ configuration, reglages, options });
|
||||
return interne.lancer(configuration, reglages, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -102,11 +107,14 @@ function espionner(interne = creerExecuteurDirect()) {
|
|||
// rejette ErreurAnnulee, comme le travailleur qu'on termine ; differer
|
||||
// vrai, elle ne fait que compter, et l'épreuve rejette quand elle veut,
|
||||
// comme l'exécuteur direct qui n'arrête qu'à la lecture suivante du signal.
|
||||
function executeurManuel({ differer = false } = {}) {
|
||||
// coeursDisponibles : ceux qu'il annonce ; chaque exécution emploie les
|
||||
// cœurs que le lancer lui demande (options.coeurs).
|
||||
function executeurManuel({ differer = false, coeursDisponibles } = {}) {
|
||||
const executions = [];
|
||||
return {
|
||||
executions,
|
||||
lancer(configuration, reglages) {
|
||||
coeursDisponibles,
|
||||
lancer(configuration, reglages, options) {
|
||||
const abonnes = new Set();
|
||||
let resoudre;
|
||||
let rejeter;
|
||||
|
|
@ -117,6 +125,7 @@ function executeurManuel({ differer = false } = {}) {
|
|||
const execution = {
|
||||
configuration,
|
||||
reglages,
|
||||
coeurs: options?.coeurs,
|
||||
annulations: 0,
|
||||
resultat,
|
||||
resoudre,
|
||||
|
|
@ -226,6 +235,8 @@ const planRange = (proposition, configuration) =>
|
|||
|
||||
const ids = (charge) => charge.propositions.map(({ id }) => id);
|
||||
const COURT = { graine: 1, nombre: 1, arret: 50, historique: 1000 };
|
||||
// Le réglage des cœurs d'un contrôleur dont l'exécuteur n'en annonce pas.
|
||||
const UN_COEUR = Object.freeze({ automatique: true, manuel: 1, disponibles: 1, nombre: 1 });
|
||||
|
||||
// --- valider (§ 5.7) ------------------------------------------------------------
|
||||
|
||||
|
|
@ -807,7 +818,7 @@ describe("génération : l'annulation (§ 5.10, § 8.4)", () => {
|
|||
montage.controleur.subscribe((etatControleur) => {
|
||||
vu = etatControleur;
|
||||
});
|
||||
assert.deepEqual(vu, { enCours: false, fait: 0, total: COURT.nombre * COURT.arret, annulee: 'seance' });
|
||||
assert.deepEqual(vu, { enCours: false, fait: 0, total: COURT.nombre * COURT.arret, annulee: 'seance', coeurs: UN_COEUR });
|
||||
assert.deepEqual(montage.journal(), journal);
|
||||
assert.equal(montage.etatDisque(), etat);
|
||||
});
|
||||
|
|
@ -849,17 +860,17 @@ describe("génération : l'annulation (§ 5.10, § 8.4)", () => {
|
|||
const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel });
|
||||
const vus = [];
|
||||
montage.controleur.subscribe((etat) => vus.push(etat));
|
||||
assert.deepEqual(vus[0], { enCours: false, fait: 0, total: 0, annulee: null });
|
||||
assert.deepEqual(vus[0], { enCours: false, fait: 0, total: 0, annulee: null, coeurs: UN_COEUR });
|
||||
const enCours = montage.controleur.generer(COURT);
|
||||
await Promise.resolve();
|
||||
manuel.executions[0].avancer(20, 50);
|
||||
assert.deepEqual(vus.at(-1), { enCours: true, fait: 20, total: 50, annulee: null });
|
||||
assert.deepEqual(vus.at(-1), { enCours: true, fait: 20, total: 50, annulee: null, coeurs: UN_COEUR });
|
||||
montage.controleur.annuler();
|
||||
await rejet(enCours);
|
||||
assert.deepEqual(vus.at(-1), { enCours: false, fait: 20, total: 50, annulee: 'commande' });
|
||||
assert.deepEqual(vus.at(-1), { enCours: false, fait: 20, total: 50, annulee: 'commande', coeurs: UN_COEUR });
|
||||
const suivante = montage.controleur.generer(COURT);
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(vus.at(-1), { enCours: true, fait: 0, total: 50, annulee: null });
|
||||
assert.deepEqual(vus.at(-1), { enCours: true, fait: 0, total: 50, annulee: null, coeurs: UN_COEUR });
|
||||
manuel.executions[1].resoudreParLeMoteur();
|
||||
await suivante;
|
||||
assert.equal(vus.at(-1).enCours, false);
|
||||
|
|
@ -971,6 +982,7 @@ describe('génération : la durée observée, en mémoire (§ 5.10)', () => {
|
|||
participants: 12,
|
||||
nombre: 5,
|
||||
arret: 200000,
|
||||
coeurs: 1,
|
||||
});
|
||||
const neuf = creerControleurGeneration({
|
||||
seance: montage.seance,
|
||||
|
|
@ -1009,12 +1021,14 @@ describe("génération : l'estimation et la génération longue (§ 5.10)", () =
|
|||
longue: true,
|
||||
auDelaDuPlafond: false,
|
||||
mesure,
|
||||
coeurs: 1,
|
||||
});
|
||||
assert.deepEqual(controleur.estimer({ graine: 1, nombre: 5, arret: 200000, historique: 1000 }), {
|
||||
secondes: 2,
|
||||
longue: false,
|
||||
auDelaDuPlafond: false,
|
||||
mesure,
|
||||
coeurs: 1,
|
||||
});
|
||||
assert.equal(controleur.estimer({ graine: 1, nombre: 5000, arret: 200000, historique: 1000 }).auDelaDuPlafond, true);
|
||||
assert.equal(controleur.estimer({ graine: 1, nombre: 150, arret: 200000, historique: 1000 }).longue, false);
|
||||
|
|
@ -1050,6 +1064,268 @@ describe("génération : l'estimation et la génération longue (§ 5.10)", () =
|
|||
});
|
||||
});
|
||||
|
||||
// --- Les cœurs de calcul (§ 5.7, § 5.10) -------------------------------------------------
|
||||
|
||||
// Une fabrique de travailleurs qui calculent par brancherTravailleur, le
|
||||
// vrai côté travailleur : la proposition de rang r rend sa réponse après
|
||||
// (8 − r) tours de la file des microtâches, et les derniers rangs
|
||||
// s'achèvent ainsi avant les premiers.
|
||||
function travailleursCalculants() {
|
||||
return () => {
|
||||
const travailleur = {
|
||||
onmessage: null,
|
||||
termine: false,
|
||||
postMessage(message) {
|
||||
const copie = structuredClone(message);
|
||||
let attente = 8 - copie.rang;
|
||||
const tour = () => {
|
||||
if (attente > 0) {
|
||||
attente -= 1;
|
||||
return Promise.resolve().then(tour);
|
||||
}
|
||||
if (travailleur.termine) return undefined;
|
||||
const portee = { onmessage: null, postMessage: (data) => !travailleur.termine && travailleur.onmessage?.({ data: structuredClone(data) }) };
|
||||
brancherTravailleur(portee);
|
||||
portee.onmessage({ data: copie });
|
||||
return undefined;
|
||||
};
|
||||
Promise.resolve().then(tour);
|
||||
},
|
||||
terminate() {
|
||||
travailleur.termine = true;
|
||||
},
|
||||
};
|
||||
return travailleur;
|
||||
};
|
||||
}
|
||||
|
||||
describe('génération : les cœurs de calcul, un réglage du poste pour la séance (§ 5.7, § 5.10)', () => {
|
||||
test("automatiques à la création : autant que l'exécuteur en annonce ; 1 quand il n'en annonce pas", async () => {
|
||||
const montage = await monter({ charges: [chargeDemo('petite')], executeur: creerExecuteurDirect({ coeursDisponibles: 6 }) });
|
||||
assert.deepEqual(montage.controleur.coeurs(), { automatique: true, manuel: 6, disponibles: 6, nombre: 6 });
|
||||
const sans = await monter({ charges: [chargeDemo('petite')], executeur: executeurManuel() });
|
||||
assert.deepEqual(sans.controleur.coeurs(), UN_COEUR);
|
||||
});
|
||||
|
||||
test('reglerCoeurs : une saisie de 1 à disponibles, ou le refus de lireEntier ou HORS_DOMAINE {min: 1, max}, remède null, sans rien changer ; automatique rend les disponibles et garde le manuel', async () => {
|
||||
const montage = await monter({ charges: [chargeDemo('petite')], executeur: creerExecuteurDirect({ coeursDisponibles: 6 }) });
|
||||
const { controleur } = montage;
|
||||
const vus = [];
|
||||
controleur.subscribe((etat) => vus.push(etat.coeurs));
|
||||
assert.deepEqual(controleur.reglerCoeurs({ automatique: false }), { refus: null });
|
||||
assert.deepEqual(controleur.coeurs(), { automatique: false, manuel: 6, disponibles: 6, nombre: 6 });
|
||||
assert.deepEqual(controleur.reglerCoeurs({ saisie: ' 3 ' }), { refus: null });
|
||||
assert.deepEqual(controleur.coeurs(), { automatique: false, manuel: 3, disponibles: 6, nombre: 3 });
|
||||
const refusees = [
|
||||
['0', 'HORS_DOMAINE', { min: 1, max: 6, remede: null }],
|
||||
['7', 'HORS_DOMAINE', { min: 1, max: 6, remede: null }],
|
||||
['x', 'NON_ENTIER', { saisie: 'x', remede: null }],
|
||||
['1,5', 'NON_ENTIER', { saisie: '1,5', remede: null }],
|
||||
['-1', 'NON_ENTIER', { saisie: '-1', remede: null }],
|
||||
['', 'NON_ENTIER', { saisie: '', remede: null }],
|
||||
];
|
||||
assert.equal(refusees.length, 6);
|
||||
for (const [saisie, code, details] of refusees) {
|
||||
const { refus } = controleur.reglerCoeurs({ saisie, automatique: true });
|
||||
assert.deepEqual(refus, { code, details }, saisie);
|
||||
assert.equal(typeof libelle(code, details), 'string');
|
||||
assert.deepEqual(controleur.coeurs(), { automatique: false, manuel: 3, disponibles: 6, nombre: 3 }, saisie);
|
||||
}
|
||||
assert.deepEqual(controleur.reglerCoeurs({ saisie: '6' }), { refus: null });
|
||||
assert.deepEqual(controleur.reglerCoeurs({ saisie: '1' }), { refus: null });
|
||||
assert.deepEqual(controleur.reglerCoeurs({ automatique: true }), { refus: null });
|
||||
assert.deepEqual(controleur.coeurs(), { automatique: true, manuel: 1, disponibles: 6, nombre: 6 });
|
||||
assert.deepEqual(vus, [
|
||||
{ automatique: true, manuel: 6, disponibles: 6, nombre: 6 },
|
||||
{ automatique: false, manuel: 6, disponibles: 6, nombre: 6 },
|
||||
{ automatique: false, manuel: 3, disponibles: 6, nombre: 3 },
|
||||
{ automatique: false, manuel: 6, disponibles: 6, nombre: 6 },
|
||||
{ automatique: false, manuel: 1, disponibles: 6, nombre: 1 },
|
||||
{ automatique: true, manuel: 1, disponibles: 6, nombre: 6 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("régler les cœurs n'écrit rien : ni état, ni entrée du journal, ni révision, en écriture comme en lecture", async () => {
|
||||
for (const ecriture of [true, false]) {
|
||||
const montage = await monter({ charges: [chargeDemo('petite')], executeur: creerExecuteurDirect({ coeursDisponibles: 4 }), ecriture });
|
||||
const ecritures = montage.fs.compteurs.ecritures;
|
||||
const journal = montage.journal();
|
||||
const etat = montage.etatDisque();
|
||||
const revision = montage.seance.etat().evenement.revision;
|
||||
montage.controleur.reglerCoeurs({ automatique: false });
|
||||
assert.deepEqual(montage.controleur.reglerCoeurs({ saisie: '2' }), { refus: null });
|
||||
montage.controleur.reglerCoeurs({ saisie: '9' });
|
||||
montage.controleur.reglerCoeurs({ automatique: true });
|
||||
await montage.seance.attendre();
|
||||
assert.equal(montage.fs.compteurs.ecritures, ecritures, `écriture ${ecriture}`);
|
||||
assert.deepEqual(montage.journal(), journal);
|
||||
assert.equal(montage.etatDisque(), etat);
|
||||
assert.equal(montage.seance.etat().evenement.revision, revision);
|
||||
}
|
||||
});
|
||||
|
||||
test('le réglage vaut pour la séance : il survit à la fermeture et à l’ouverture d’un autre événement', async () => {
|
||||
const montage = await monter({
|
||||
charges: [chargeDemo('petite'), chargeDemo('petite-conflit', AUTRE)],
|
||||
executeur: creerExecuteurDirect({ coeursDisponibles: 4 }),
|
||||
});
|
||||
montage.controleur.reglerCoeurs({ automatique: false });
|
||||
montage.controleur.reglerCoeurs({ saisie: '2' });
|
||||
await montage.seance.fermer();
|
||||
await montage.seance.ouvrir(AUTRE);
|
||||
assert.deepEqual(montage.controleur.coeurs(), { automatique: false, manuel: 2, disponibles: 4, nombre: 2 });
|
||||
});
|
||||
|
||||
test('generer confie à lancer coeurs = min(nombre de propositions, cœurs réglés)', async () => {
|
||||
const montage = await monter({ charges: [chargeDemo('petite')], executeur: creerExecuteurDirect({ coeursDisponibles: 4 }) });
|
||||
const { controleur, espion } = montage;
|
||||
const cas = [
|
||||
{ reglage: { automatique: true }, nombre: 3, attendus: 3 },
|
||||
{ reglage: { automatique: true }, nombre: 6, attendus: 4 },
|
||||
{ reglage: { automatique: false, saisie: '2' }, nombre: 5, attendus: 2 },
|
||||
{ reglage: { automatique: false, saisie: '1' }, nombre: 2, attendus: 1 },
|
||||
];
|
||||
assert.equal(cas.length, 4);
|
||||
for (const [k, { reglage, nombre, attendus }] of cas.entries()) {
|
||||
controleur.reglerCoeurs(reglage);
|
||||
await controleur.generer({ graine: 10 + k, nombre, arret: 50, historique: 1000 });
|
||||
assert.deepEqual(espion.lancements.at(-1).options, { coeurs: attendus }, JSON.stringify(reglage));
|
||||
}
|
||||
});
|
||||
|
||||
test("par le groupe de travailleurs, 1 cœur ou 3 : le même état écrit, octet pour octet, et les propositions de rechercher", async () => {
|
||||
const etats = [];
|
||||
for (const coeurs of [1, 3]) {
|
||||
const executeur = creerExecuteurTravailleur(travailleursCalculants(), { coeursDisponibles: 3 });
|
||||
const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur });
|
||||
montage.controleur.reglerCoeurs({ automatique: false });
|
||||
montage.controleur.reglerCoeurs({ saisie: String(coeurs) });
|
||||
const reglages = { graine: 5, nombre: 5, arret: 2000, historique: 1000 };
|
||||
const bilan = await montage.controleur.generer(reglages);
|
||||
assert.deepEqual(bilan.ajoutees, [1, 2, 3, 4, 5]);
|
||||
assert.deepEqual(montage.espion.lancements.at(-1).options, { coeurs });
|
||||
const attendues = rechercher(configurationDepuisCharge(montage.charge()), reglages);
|
||||
assert.deepEqual(
|
||||
montage.charge().propositions.map(({ graine }) => graine),
|
||||
attendues.map(({ graine }) => graine),
|
||||
);
|
||||
assert.deepEqual(montage.charge().propositions.map(planDepuisFichier), attendues.map((p) => planRange(p, configurationDepuisCharge(montage.charge()))));
|
||||
assert.equal(montage.controleur.derniereGeneration().coeurs, coeurs);
|
||||
etats.push(montage.etatDisque());
|
||||
}
|
||||
assert.equal(etats.length, 2);
|
||||
assert.equal(etats[1], etats[0]);
|
||||
});
|
||||
|
||||
test("la mesure porte les cœurs que l'exécution a employés ; 1 quand elle ne le dit pas", async () => {
|
||||
const manuel = executeurManuel({ coeursDisponibles: 4 });
|
||||
const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel });
|
||||
await genererEn(montage, manuel, { graine: 1, nombre: 5, arret: 200000, historique: 1000 }, 2000);
|
||||
assert.equal(montage.controleur.derniereGeneration().coeurs, 4);
|
||||
const muet = executeurTransforme((propositions) => propositions);
|
||||
const autre = await monter({ charges: [chargeDemo('petite-conflit')], executeur: muet });
|
||||
await autre.controleur.generer(COURT);
|
||||
assert.equal(autre.controleur.derniereGeneration().coeurs, 1);
|
||||
});
|
||||
|
||||
test("l'estimation compte en vagues : coût d'une proposition × ⌈nombre / cœurs⌉, la mesure ramenée à ses propres vagues", async () => {
|
||||
const manuel = executeurManuel({ coeursDisponibles: 8 });
|
||||
const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel });
|
||||
const { controleur } = montage;
|
||||
controleur.reglerCoeurs({ automatique: false });
|
||||
controleur.reglerCoeurs({ saisie: '1' });
|
||||
// 5 × 200 000 mouvements en 2 s sur 1 cœur : 0,4 s par proposition.
|
||||
await genererEn(montage, manuel, { graine: 1, nombre: 5, arret: 200000, historique: 1000 }, 2000);
|
||||
const sur1 = controleur.derniereGeneration();
|
||||
assert.equal(sur1.coeurs, 1);
|
||||
controleur.reglerCoeurs({ saisie: '2' });
|
||||
const deux = controleur.estimer({ graine: 1, nombre: 5, arret: 200000, historique: 1000 });
|
||||
assert.equal(deux.coeurs, 2);
|
||||
assert.ok(Math.abs(deux.secondes - 1.2) < 1e-9, String(deux.secondes));
|
||||
assert.deepEqual(deux.mesure, sur1);
|
||||
assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 4, arret: 200000 }).secondes - 0.8) < 1e-9);
|
||||
controleur.reglerCoeurs({ automatique: true });
|
||||
const automatique = controleur.estimer({ graine: 1, nombre: 5, arret: 200000 });
|
||||
assert.equal(automatique.coeurs, 5);
|
||||
assert.ok(Math.abs(automatique.secondes - 0.4) < 1e-9, String(automatique.secondes));
|
||||
// Longue au-delà de 60 s : 3 000 propositions, 1 200 s sur 1 cœur, 300 s
|
||||
// sur 4, et 0,4 × ⌈3 000 / 8⌉ = 150 s sur 8.
|
||||
controleur.reglerCoeurs({ automatique: false });
|
||||
controleur.reglerCoeurs({ saisie: '4' });
|
||||
const quatre = controleur.estimer({ graine: 1, nombre: 3000, arret: 200000 });
|
||||
assert.ok(Math.abs(quatre.secondes - 300) < 1e-9, String(quatre.secondes));
|
||||
assert.equal(quatre.longue, true);
|
||||
controleur.reglerCoeurs({ automatique: true });
|
||||
assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 3000, arret: 200000 }).secondes - 150) < 1e-9);
|
||||
// Une mesure prise sur 5 cœurs, 5 propositions : une seule vague de
|
||||
// 200 000 mouvements en 2 s ; sur 1 cœur, 5 vagues, 10 s.
|
||||
await genererEn(montage, manuel, { graine: 1, nombre: 5, arret: 200000, historique: 1000 }, 2000);
|
||||
assert.equal(controleur.derniereGeneration().coeurs, 5);
|
||||
controleur.reglerCoeurs({ automatique: false });
|
||||
controleur.reglerCoeurs({ saisie: '1' });
|
||||
assert.ok(Math.abs(controleur.estimer({ graine: 1, nombre: 5, arret: 200000 }).secondes - 10) < 1e-9);
|
||||
});
|
||||
|
||||
test('sans mesure, la question part au-delà de 10 000 000 mouvements par cœur : ⌈nombre / cœurs⌉ × arret', async () => {
|
||||
const manuel = executeurManuel({ coeursDisponibles: 2 });
|
||||
const montage = await monter({ charges: [chargeDemo('petite-conflit')], executeur: manuel });
|
||||
const { controleur, espion } = montage;
|
||||
await refuseSansRien(montage, 'CONFIRMATION_REQUISE', { estimation: { inconnue: true }, remede: null }, {
|
||||
graine: 1,
|
||||
nombre: 10,
|
||||
arret: 2000001,
|
||||
historique: 1000,
|
||||
});
|
||||
const juste = controleur.generer({ graine: 1, nombre: 10, arret: 2000000, historique: 1000 });
|
||||
await Promise.resolve();
|
||||
assert.equal(espion.lancements.length, 1);
|
||||
controleur.annuler();
|
||||
await rejet(juste);
|
||||
});
|
||||
|
||||
test('le texte de CONFIRMATION_REQUISE dit les cœurs de l’estimation et ceux de la mesure, seulement au-delà d’un', () => {
|
||||
const mesure = { dureeMs: 2000, mouvements: 1000000, participants: 12, nombre: 5, arret: 200000, coeurs: 2 };
|
||||
const plusieurs = libelle('CONFIRMATION_REQUISE', {
|
||||
estimation: { secondes: 1200, longue: true, auDelaDuPlafond: false, mesure, coeurs: 4 },
|
||||
remede: null,
|
||||
});
|
||||
assert.match(plusieurs, /environ vingt minutes, sur 4 cœurs, d'après/);
|
||||
assert.match(plusieurs, /12 participants, sur 2 cœurs/);
|
||||
const un = libelle('CONFIRMATION_REQUISE', {
|
||||
estimation: { secondes: 1200, longue: true, auDelaDuPlafond: false, mesure: { ...mesure, coeurs: 1 }, coeurs: 1 },
|
||||
remede: null,
|
||||
});
|
||||
assert.doesNotMatch(un, /cœur/);
|
||||
});
|
||||
|
||||
test("l'exécuteur direct annonce ses cœurs par coeursDe, calcule dans le fil quel que soit coeurs, et n'emploie qu'un cœur", async () => {
|
||||
assert.equal(creerExecuteurDirect().coeursDisponibles, 1);
|
||||
assert.equal(creerExecuteurDirect({ coeursDisponibles: 12 }).coeursDisponibles, 12);
|
||||
assert.equal(creerExecuteurDirect({ coeursDisponibles: 0 }).coeursDisponibles, 1);
|
||||
const configuration = configurationDepuisCharge(chargeDemo('petite-conflit'));
|
||||
const reglages = { graine: 4, nombre: 3, arret: 2000 };
|
||||
const execution = creerExecuteurDirect({ coeursDisponibles: 4 }).lancer(configuration, reglages, { coeurs: 3 });
|
||||
assert.equal(execution.coeurs, 1);
|
||||
assert.deepEqual(await execution.resultat, rechercher(configuration, reglages));
|
||||
});
|
||||
|
||||
test('coeursDe : un entier d’au moins 1 tel quel, toute autre valeur 1', () => {
|
||||
const cas = [
|
||||
[4, 4],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[-2, 1],
|
||||
[1.5, 1],
|
||||
['8', 1],
|
||||
[undefined, 1],
|
||||
[null, 1],
|
||||
[Number.NaN, 1],
|
||||
];
|
||||
assert.equal(cas.length, 9);
|
||||
for (const [valeur, attendue] of cas) assert.equal(coeursDe(valeur), attendue, String(valeur));
|
||||
});
|
||||
});
|
||||
|
||||
// --- Une table, un tour (§ 15.4) ---------------------------------------------------------
|
||||
|
||||
describe('génération : une table, un tour (§ 15.4)', () => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Le côté travailleur de la génération (§ 5.10) : la recherche du moteur
|
||||
// hors du fil de l'interface. Le travailleur reçoit un message, calcule, et
|
||||
// poste ses avancements puis son résultat ou son erreur :
|
||||
// Le côté travailleur de la génération (§ 5.7, § 5.10) : la recherche du
|
||||
// moteur hors du fil de l'interface, une proposition par message. Un
|
||||
// travailleur reçoit le rang d'une proposition, la calcule par proposerUne,
|
||||
// et poste ses avancements puis la proposition ou son erreur ; il attend
|
||||
// ensuite le rang suivant, que l'exécuteur lui donne ou non :
|
||||
//
|
||||
// → { type: 'lancer', configuration, reglages }
|
||||
// → { type: 'proposer', configuration, reglages, rang }
|
||||
// ← { type: 'avancement', fait, total } au plus une fois toutes les
|
||||
// CADENCE_AVANCEMENT progressions
|
||||
// du moteur, fait croissant
|
||||
// ← { type: 'resultat', propositions } celles de rechercher, telles quelles
|
||||
// du moteur, fait croissant ; fait
|
||||
// et total comptent dans la seule
|
||||
// proposition, total valant arret
|
||||
// ← { type: 'proposition', proposition } celle de rang rang de rechercher,
|
||||
// telle quelle
|
||||
// ← { type: 'erreur', code, details } REGLAGE_HORS_DOMAINE {reglage}
|
||||
// d'exigerReglages, avant tout
|
||||
// calcul ; puis, par
|
||||
|
|
@ -17,17 +22,20 @@
|
|||
// les détails d'une
|
||||
// ErreurConfiguration
|
||||
// ← { type: 'faute', message, pile } une erreur que versErreurGeneration
|
||||
// ne convertit pas : une faute de
|
||||
// programmation, son texte et sa
|
||||
// ne convertit pas — un rang hors
|
||||
// de 1 à nombre compris : une faute
|
||||
// de programmation, son texte et sa
|
||||
// pile, que l'exécuteur relève
|
||||
// comme une faute
|
||||
//
|
||||
// L'annulation est terminate() du côté de l'interface : immédiate, sans
|
||||
// résultat partiel ; le travailleur ne lit aucun signal. Un message d'un
|
||||
// autre type ne lance rien. Le gestionnaire ne lève jamais : le canal
|
||||
// d'erreur du travailleur ne porte ainsi que l'échec de son script — qui ne
|
||||
// se charge ou ne se compile pas —, et l'exécuteur ne confond pas les deux.
|
||||
import { rechercher } from '../moteur/recherche.js';
|
||||
// Le travailleur ne garde rien d'un message à l'autre : chaque proposition
|
||||
// se calcule à partir du seul message qui la demande. L'annulation est
|
||||
// terminate() du côté de l'interface : immédiate, sans résultat partiel ; le
|
||||
// travailleur ne lit aucun signal. Un message d'un autre type ne lance rien.
|
||||
// Le gestionnaire ne lève jamais : le canal d'erreur du travailleur ne porte
|
||||
// ainsi que l'échec de son script — qui ne se charge ou ne se compile pas —,
|
||||
// et l'exécuteur ne confond pas les deux.
|
||||
import { proposerUne } from '../moteur/recherche.js';
|
||||
import { ErreurGeneration, exigerReglages, versErreurGeneration } from './generation.js';
|
||||
|
||||
/** Progressions du moteur — une toutes les 1 000 itérations — entre deux messages d'avancement. */
|
||||
|
|
@ -35,13 +43,13 @@ export const CADENCE_AVANCEMENT = 50;
|
|||
|
||||
/**
|
||||
* Branche une portée de travailleur — { onmessage, postMessage } — sur
|
||||
* rechercher, selon les messages de l'en-tête du module.
|
||||
* proposerUne, selon les messages de l'en-tête du module.
|
||||
*
|
||||
* @param {{onmessage: ((evenement: {data: unknown}) => void)|null, postMessage: (message: Object) => void}} portee
|
||||
*/
|
||||
export function brancherTravailleur(portee) {
|
||||
portee.onmessage = ({ data }) => {
|
||||
if (data === null || typeof data !== 'object' || data.type !== 'lancer') return;
|
||||
if (data === null || typeof data !== 'object' || data.type !== 'proposer') return;
|
||||
let progressions = 0;
|
||||
const progression = (fait, total) => {
|
||||
progressions += 1;
|
||||
|
|
@ -49,8 +57,8 @@ export function brancherTravailleur(portee) {
|
|||
};
|
||||
try {
|
||||
exigerReglages(data.reglages);
|
||||
const propositions = rechercher(data.configuration, data.reglages, { progression });
|
||||
portee.postMessage({ type: 'resultat', propositions });
|
||||
const proposition = proposerUne(data.configuration, data.reglages, data.rang, { progression });
|
||||
portee.postMessage({ type: 'proposition', proposition });
|
||||
} catch (erreur) {
|
||||
const convertie = versErreurGeneration(erreur);
|
||||
if (convertie instanceof ErreurGeneration) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuves du travailleur de génération (§ 5.10) : brancherTravailleur sur
|
||||
// une portée factice, qui garde chaque message posté. Les configurations
|
||||
// viennent des démonstrations livrées et d'une charge aux noms inventés.
|
||||
// Épreuves du travailleur de génération (§ 5.7, § 5.10) : brancherTravailleur
|
||||
// sur une portée factice, qui garde chaque message posté. Le travailleur
|
||||
// calcule une proposition par message, celle du rang demandé ; la valeur
|
||||
// attendue est la proposition de même rang de rechercher. Les
|
||||
// configurations viennent des démonstrations livrées.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { texteLivre } from '../demo/livrees.js';
|
||||
|
|
@ -25,41 +27,58 @@ function porteeFactice() {
|
|||
return { postes, envoyer: (data) => portee.onmessage({ data }) };
|
||||
}
|
||||
|
||||
describe('travailleur de génération (§ 5.10)', () => {
|
||||
test("après lancer, des avancements à fait croissant, au plus un tous les CADENCE_AVANCEMENT, puis le résultat de rechercher", () => {
|
||||
describe('travailleur de génération (§ 5.7, § 5.10)', () => {
|
||||
test("après proposer, des avancements de la seule proposition, au plus un tous les CADENCE_AVANCEMENT, puis la proposition de ce rang de rechercher", () => {
|
||||
assert.equal(CADENCE_AVANCEMENT, 50);
|
||||
const configuration = configurationDe('petite-conflit');
|
||||
const reglages = { graine: 9, nombre: 2, arret: 60000, historique: 1000 };
|
||||
const reglages = { graine: 9, nombre: 3, arret: 120000, historique: 1000 };
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
envoyer({ type: 'lancer', configuration, reglages });
|
||||
envoyer({ type: 'proposer', configuration, reglages, rang: 2 });
|
||||
const avancements = postes.filter(({ type }) => type === 'avancement');
|
||||
assert.ok(avancements.length > 0, 'aucun avancement');
|
||||
assert.ok(avancements.length <= Math.ceil((reglages.nombre * reglages.arret) / 1000 / CADENCE_AVANCEMENT));
|
||||
assert.ok(avancements.length <= Math.ceil(reglages.arret / 1000 / CADENCE_AVANCEMENT));
|
||||
assert.deepEqual(avancements, [
|
||||
{ type: 'avancement', fait: 50000, total: 120000 },
|
||||
{ type: 'avancement', fait: 100000, total: 120000 },
|
||||
]);
|
||||
assert.deepEqual(postes.at(-1), { type: 'resultat', propositions: rechercher(configuration, reglages) });
|
||||
const attendue = rechercher(configuration, reglages)[1];
|
||||
assert.equal(attendue.id, 2);
|
||||
assert.deepEqual(postes.at(-1), { type: 'proposition', proposition: attendue });
|
||||
assert.equal(postes.length, avancements.length + 1);
|
||||
});
|
||||
|
||||
test('le même travailleur répond à chaque proposer, dans un ordre quelconque des rangs', () => {
|
||||
const configuration = configurationDe('petite');
|
||||
const reglages = { graine: 31, nombre: 4, arret: 2000, historique: 1000 };
|
||||
const attendues = rechercher(configuration, reglages);
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
const rangs = [3, 1, 4];
|
||||
for (const rang of rangs) envoyer({ type: 'proposer', configuration, reglages, rang });
|
||||
const rendues = postes.filter(({ type }) => type === 'proposition').map(({ proposition }) => proposition);
|
||||
assert.equal(rendues.length, rangs.length);
|
||||
assert.deepEqual(
|
||||
rendues,
|
||||
rangs.map((rang) => attendues[rang - 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('des places manquantes postent erreur PLACES_MANQUANTES, les détails du moteur', () => {
|
||||
const configuration = configurationDe('petite');
|
||||
configuration.tables = configuration.tables.slice(1);
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
envoyer({ type: 'lancer', configuration, reglages: { graine: 1, nombre: 1, arret: 10 } });
|
||||
envoyer({ type: 'proposer', configuration, reglages: { graine: 1, nombre: 1, arret: 10 }, rang: 1 });
|
||||
assert.deepEqual(postes, [{ type: 'erreur', code: 'PLACES_MANQUANTES', details: { placesManquantes: 3 } }]);
|
||||
});
|
||||
|
||||
test('une graine de 2^32 poste erreur REGLAGE_HORS_DOMAINE, qui nomme le réglage, sans le message du moteur', () => {
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
envoyer({ type: 'lancer', configuration: configurationDe('petite'), reglages: { graine: 2 ** 32, nombre: 1, arret: 10 } });
|
||||
envoyer({ type: 'proposer', configuration: configurationDe('petite'), reglages: { graine: 2 ** 32, nombre: 1, arret: 10 }, rang: 1 });
|
||||
assert.deepEqual(postes, [{ type: 'erreur', code: 'REGLAGE_HORS_DOMAINE', details: { reglage: 'graine', remede: null } }]);
|
||||
});
|
||||
|
||||
test('une faute que versErreurGeneration ne convertit pas poste { type: faute, message, pile }, sans lever : le canal d’erreur du travailleur reste au script qui ne se charge pas', () => {
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
assert.doesNotThrow(() => envoyer({ type: 'lancer', configuration: null, reglages: { graine: 1, nombre: 1, arret: 10 } }));
|
||||
assert.doesNotThrow(() => envoyer({ type: 'proposer', configuration: null, reglages: { graine: 1, nombre: 1, arret: 10 }, rang: 1 }));
|
||||
assert.equal(postes.length, 1);
|
||||
const [faute] = postes;
|
||||
assert.deepEqual(Object.keys(faute).sort(), ['message', 'pile', 'type']);
|
||||
|
|
@ -69,9 +88,18 @@ describe('travailleur de génération (§ 5.10)', () => {
|
|||
assert.match(faute.pile, /TypeError/);
|
||||
});
|
||||
|
||||
test('un rang hors de 1 à nombre est une faute de l’exécuteur, jamais un refus de réglage', () => {
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
envoyer({ type: 'proposer', configuration: configurationDe('petite'), reglages: { graine: 1, nombre: 2, arret: 10 }, rang: 3 });
|
||||
assert.equal(postes.length, 1);
|
||||
assert.equal(postes[0].type, 'faute');
|
||||
assert.match(postes[0].message, /^RangeError: rang :/);
|
||||
});
|
||||
|
||||
test("un message d'un autre type ne lance rien", () => {
|
||||
const { postes, envoyer } = porteeFactice();
|
||||
envoyer({ type: 'inconnu' });
|
||||
envoyer({ type: 'lancer', configuration: configurationDe('petite'), reglages: { graine: 1, nombre: 1, arret: 10 } });
|
||||
envoyer(null);
|
||||
assert.deepEqual(postes, []);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,8 +17,15 @@ const NOMS_DES_REGLAGES = new Map([
|
|||
['historique', "Historique d'acceptation"],
|
||||
]);
|
||||
|
||||
// L'estimation d'une génération longue, en mots : l'ordre de grandeur et la
|
||||
// mesure dont il vient, ou le seuil d'une génération sans mesure.
|
||||
// Les cœurs d'un calcul, dits quand il en emploie plus d'un : « , sur
|
||||
// 4 cœurs » ; rien pour un seul, ou quand le nombre n'est pas donné.
|
||||
function surCoeurs(coeurs, texte) {
|
||||
return typeof coeurs === 'number' && coeurs > 1 ? `, sur ${creerFormat(texte).nombre(coeurs)} cœurs` : '';
|
||||
}
|
||||
|
||||
// L'estimation d'une génération longue, en mots : l'ordre de grandeur et les
|
||||
// cœurs qu'elle emploierait, puis la mesure dont il vient ; ou le seuil d'une
|
||||
// génération sans mesure.
|
||||
function phraseEstimation(estimation, texte) {
|
||||
if (estimation.inconnue === true) {
|
||||
return (
|
||||
|
|
@ -30,9 +37,10 @@ function phraseEstimation(estimation, texte) {
|
|||
const { mesure } = estimation;
|
||||
const plafond = estimation.auDelaDuPlafond ? ", au-delà du plafond d'usage de trente minutes" : '';
|
||||
return (
|
||||
`Ce réglage prendrait ${format.ordreDeGrandeur(estimation.secondes)}${plafond}, d'après le dernier calcul, ` +
|
||||
`Ce réglage prendrait ${format.ordreDeGrandeur(estimation.secondes)}${surCoeurs(estimation.coeurs, texte)}` +
|
||||
`${plafond}, d'après le dernier calcul, ` +
|
||||
`${format.duree(mesure.dureeMs)} pour ${format.nombre(mesure.mouvements)} mouvements ` +
|
||||
`sur ${compte(mesure.participants, 'participant', 'participants')}`
|
||||
`sur ${compte(mesure.participants, 'participant', 'participants')}${surCoeurs(mesure.coeurs, texte)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
// La section de la génération dans un vrai moteur de rendu (§ 5.7, § 5.9,
|
||||
// § 5.10, § 2.1 étape 9 et « ce qui doit avoir eu lieu », § 15.4, § 8.4,
|
||||
// § 9) : ses réglages, leur libellé et leur unité ; la graine, saisie de la
|
||||
// commande ; le travailleur, qui rend ce que rend rechercher ; l'application
|
||||
// commande ; les cœurs de calcul, un réglage du poste qui n'écrit rien ; le
|
||||
// groupe de travailleurs, qui rend ce que rend rechercher, sur un cœur comme
|
||||
// sur deux ; l'application
|
||||
// vivante pendant le calcul ; l'annulation ; les refus posés à côté de
|
||||
// « Générer » avec leur remède ; la durée observée ; la génération longue,
|
||||
// annoncée en place.
|
||||
|
|
@ -63,25 +65,30 @@ afterEach(() => {
|
|||
|
||||
// --- Les exécuteurs ---------------------------------------------------------------
|
||||
|
||||
// L'exécuteur du travailleur, celui de src/main.js ; travailleurs compte les
|
||||
// travailleurs créés.
|
||||
function executeurTravailleur() {
|
||||
// L'exécuteur du travailleur, celui de src/main.js, qui annonce
|
||||
// coeursDisponibles ; travailleurs compte les travailleurs créés.
|
||||
function executeurTravailleur({ coeursDisponibles } = {}) {
|
||||
const suivi = { travailleurs: 0 };
|
||||
const executeur = creerExecuteurTravailleur(() => {
|
||||
suivi.travailleurs += 1;
|
||||
return new Travailleur();
|
||||
});
|
||||
const executeur = creerExecuteurTravailleur(
|
||||
() => {
|
||||
suivi.travailleurs += 1;
|
||||
return new Travailleur();
|
||||
},
|
||||
{ coeursDisponibles },
|
||||
);
|
||||
return { executeur, suivi };
|
||||
}
|
||||
|
||||
// Un exécuteur dont chaque lancer est relevé : configuration, reglages, et
|
||||
// ce que rend son résultat. lancements en garde la liste.
|
||||
// Un exécuteur dont chaque lancer est relevé : configuration, reglages,
|
||||
// options, et ce que rend son résultat. lancements en garde la liste. Il
|
||||
// annonce les cœurs de base.
|
||||
function releve(base) {
|
||||
const lancements = [];
|
||||
const executeur = {
|
||||
lancer(configuration, reglages) {
|
||||
const execution = base.lancer(configuration, reglages);
|
||||
const lancement = { configuration, reglages, propositions: null };
|
||||
coeursDisponibles: base.coeursDisponibles,
|
||||
lancer(configuration, reglages, options) {
|
||||
const execution = base.lancer(configuration, reglages, options);
|
||||
const lancement = { configuration, reglages, options, propositions: null };
|
||||
lancements.push(lancement);
|
||||
execution.resultat.then(
|
||||
(propositions) => {
|
||||
|
|
@ -96,11 +103,14 @@ function releve(base) {
|
|||
}
|
||||
|
||||
// Un exécuteur que l'épreuve résout à la main : chaque lancer est relevé,
|
||||
// avec resoudre(propositions), avancer(fait, total) et annulee.
|
||||
function executeurAMain() {
|
||||
// avec resoudre(propositions), avancer(fait, total) et annulee. Il annonce
|
||||
// coeursDisponibles, et chaque exécution dit employer les cœurs que le
|
||||
// lancer lui demande.
|
||||
function executeurAMain({ coeursDisponibles } = {}) {
|
||||
const executions = [];
|
||||
const executeur = {
|
||||
lancer(configuration, reglages) {
|
||||
coeursDisponibles,
|
||||
lancer(configuration, reglages, options) {
|
||||
const abonnes = new Set();
|
||||
let resoudre;
|
||||
let rejeter;
|
||||
|
|
@ -128,6 +138,7 @@ function executeurAMain() {
|
|||
execution.annulee = true;
|
||||
rejeter(new ErreurAnnulee());
|
||||
},
|
||||
coeurs: options?.coeurs,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -147,6 +158,8 @@ const fenetre = () => document.querySelector('dialog[open]');
|
|||
const duree = (m) => section(m).querySelector('[data-duree]');
|
||||
const question = (m) => section(m).querySelector('[data-question="longue"]');
|
||||
const barre = (m) => section(m)?.querySelector('[data-avancement] [role="progressbar"]') ?? null;
|
||||
const champCoeurs = (m) => section(m).querySelector('input[data-champ="coeurs"]');
|
||||
const disponiblesAffiches = (m) => section(m).querySelector('[data-coeurs-disponibles]');
|
||||
|
||||
const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)];
|
||||
const versSection = (m, cible) => dans(m, `[data-commande="section"][data-cible="${cible}"]`);
|
||||
|
|
@ -385,7 +398,7 @@ describe('les contraintes et l’attribution des sièges (§ 5.3)', () => {
|
|||
test('direct : cinq bascules sous leur nom du § 5.3 ; chacune, une entrée, son réglage seul', async () => {
|
||||
const m = await demonstration('petite', { executeur: creerExecuteurDirect() });
|
||||
const cases = [...section(m).querySelectorAll('input[type="checkbox"]')];
|
||||
expect(cases.map((caseACocher) => caseACocher.dataset.champ)).toEqual(BASCULES);
|
||||
expect(cases.map((caseACocher) => caseACocher.dataset.champ)).toEqual([...BASCULES, 'coeursAutomatiques']);
|
||||
BASCULES.forEach((cle, rang) => {
|
||||
expect(etiquette(m, cle).textContent).toBe(t(`generation.contrainte.${cle}`));
|
||||
expect(t(`generation.contrainte.${cle}`)).toBe(NOMS_DU_SPEC[rang]);
|
||||
|
|
@ -522,8 +535,8 @@ describe('le travailleur (§ 5.7, § 5.10, § 15.5)', () => {
|
|||
expect(charge(m).propositions.map(({ graine }) => graine)).toEqual(attendues.map(({ graine }) => graine));
|
||||
}, 60_000);
|
||||
|
||||
test('exécuteur du travailleur : grande démonstration, compte élevé — pendant le calcul, la navigation et le changement du tour affiché mettent le DOM à jour ; l’avancement croît, aria-busy suit le calcul', async () => {
|
||||
const { executeur } = executeurTravailleur();
|
||||
test('exécuteur du travailleur, deux cœurs : grande démonstration, compte élevé — pendant le calcul, la navigation et le changement du tour affiché mettent le DOM à jour ; une seule barre, dont l’avancement croît ; aria-busy suit le calcul', async () => {
|
||||
const { executeur, suivi } = executeurTravailleur({ coeursDisponibles: 2 });
|
||||
const m = await demonstration('grande', { executeur });
|
||||
await regler(m, { nombre: 4, arret: 400000 });
|
||||
// Une réservation du seul tour 1, à un siège : une marque et une ligne
|
||||
|
|
@ -579,13 +592,14 @@ describe('le travailleur (§ 5.7, § 5.10, § 15.5)', () => {
|
|||
}
|
||||
expect(valeurs.length).toBeGreaterThanOrEqual(2);
|
||||
for (let i = 1; i < valeurs.length; i += 1) expect(valeurs[i]).toBeGreaterThan(valeurs[i - 1]);
|
||||
expect(suivi.travailleurs).toBe(2);
|
||||
await attendre(() => expect(section(m).getAttribute('aria-busy')).toBe('false'));
|
||||
expect(barre(m)).toBeNull();
|
||||
await attendre(() => expect(charge(m).propositions.length).toBeGreaterThan(0));
|
||||
}, 120_000);
|
||||
|
||||
test('exécuteur du travailleur : annuler est immédiat ; plus aucun avancement ; l’état prêt revient ; propositions, journal et état sur le disque inchangés', async () => {
|
||||
const { executeur } = executeurTravailleur();
|
||||
test('exécuteur du travailleur, deux cœurs : annuler est immédiat ; plus aucun avancement ; l’état prêt revient ; propositions, journal et état sur le disque inchangés', async () => {
|
||||
const { executeur } = executeurTravailleur({ coeursDisponibles: 2 });
|
||||
const m = await demonstration('grande', { executeur });
|
||||
await regler(m, { nombre: 4, arret: 400000 });
|
||||
const avant = { propositions: charge(m).propositions, journal: journalDe(m), etat: etatSurDisque(m) };
|
||||
|
|
@ -609,6 +623,149 @@ describe('le travailleur (§ 5.7, § 5.10, § 15.5)', () => {
|
|||
}, 60_000);
|
||||
});
|
||||
|
||||
describe('les cœurs de calcul, un réglage du poste (§ 5.7, § 5.10)', () => {
|
||||
test('quatre cœurs annoncés : « Cœurs automatiques » cochée, aucun champ de nombre ; la valeur automatique et la note affichées', async () => {
|
||||
const m = await demonstration('petite', { executeur: creerExecuteurDirect({ coeursDisponibles: 4 }) });
|
||||
const automatique = bascule(m, 'coeursAutomatiques');
|
||||
expect(automatique).not.toBeNull();
|
||||
expect(automatique.checked).toBe(true);
|
||||
expect(etiquette(m, 'coeursAutomatiques').textContent).toBe(t('generation.coeurs.automatique'));
|
||||
expect(champCoeurs(m)).toBeNull();
|
||||
expect(disponiblesAffiches(m).textContent).toBe(t('generation.coeurs.disponibles', { disponibles: 4 }));
|
||||
expect(disponiblesAffiches(m).textContent).toContain('4 cœurs');
|
||||
expect(section(m).querySelector('[data-coeurs]').textContent).toContain(t('generation.coeurs.note'));
|
||||
expect(controleur(m).coeurs()).toEqual({ automatique: true, manuel: 4, disponibles: 4, nombre: 4 });
|
||||
});
|
||||
|
||||
test('décocher montre « Nombre de cœurs », pré-rempli ; hors de 1 à 4 ou non entier, refus contre le champ ; 2 accepté ; recocher le cache et garde 2 ; rien n’est écrit, aucune entrée du journal', async () => {
|
||||
const m = await demonstration('petite', { executeur: creerExecuteurDirect({ coeursDisponibles: 4 }) });
|
||||
const ecritures = m.trace.length;
|
||||
const entrees = journalDe(m).entrees.length;
|
||||
const r0 = revision(m);
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
expect(bascule(m, 'coeursAutomatiques').checked).toBe(false);
|
||||
expect(controleur(m).coeurs().automatique).toBe(false);
|
||||
expect(champCoeurs(m)).not.toBeNull();
|
||||
expect(champCoeurs(m).value).toBe('4');
|
||||
expect(etiquette(m, 'coeurs').textContent).toBe(t('generation.coeurs.nombre', { disponibles: 4 }));
|
||||
expect(disponiblesAffiches(m).textContent).toBe(t('generation.coeurs.disponibles', { disponibles: 4 }));
|
||||
const refusees = [
|
||||
['0', 'HORS_DOMAINE', { min: 1, max: 4, remede: null }],
|
||||
['5', 'HORS_DOMAINE', { min: 1, max: 4, remede: null }],
|
||||
['x', 'NON_ENTIER', { saisie: 'x', remede: null }],
|
||||
];
|
||||
expect(refusees.length).toBe(3);
|
||||
for (const [saisie, code, details] of refusees) {
|
||||
await saisir(m, champCoeurs(m), saisie);
|
||||
const refus = refusPres(champCoeurs(m), code);
|
||||
expect(refus, saisie).not.toBeNull();
|
||||
expect(refus.querySelector('p').textContent).toBe(t(code, details));
|
||||
expect(champCoeurs(m).getAttribute('aria-invalid')).toBe('true');
|
||||
expect(champCoeurs(m).value).toBe(saisie);
|
||||
expect(controleur(m).coeurs().nombre, saisie).toBe(4);
|
||||
}
|
||||
await saisir(m, champCoeurs(m), '2', { fin: 'Enter' });
|
||||
expect(champCoeurs(m).parentElement.querySelector('[data-refus]')).toBeNull();
|
||||
expect(champCoeurs(m).hasAttribute('aria-invalid')).toBe(false);
|
||||
expect(controleur(m).coeurs()).toEqual({ automatique: false, manuel: 2, disponibles: 4, nombre: 2 });
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
expect(champCoeurs(m)).toBeNull();
|
||||
expect(controleur(m).coeurs()).toEqual({ automatique: true, manuel: 2, disponibles: 4, nombre: 4 });
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
expect(champCoeurs(m).value).toBe('2');
|
||||
expect(m.trace.length).toBe(ecritures);
|
||||
expect(journalDe(m).entrees.length).toBe(entrees);
|
||||
expect(revision(m)).toBe(r0);
|
||||
expect(fenetre()).toBeNull();
|
||||
});
|
||||
|
||||
test('en lecture, le réglage se change sans refus ni écriture ; il survit au départ de la section', async () => {
|
||||
const m = await demonstration('petite', { executeur: creerExecuteurDirect({ coeursDisponibles: 3 }), ecriture: false });
|
||||
const ecritures = m.trace.length;
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
await saisir(m, champCoeurs(m), '1');
|
||||
expect(section(m).querySelector('[data-coeurs] [data-refus]')).toBeNull();
|
||||
expect(controleur(m).coeurs().nombre).toBe(1);
|
||||
await cliquer(m, versSection(m, 'participants'));
|
||||
expect(section(m)).toBeNull();
|
||||
await cliquer(m, versSection(m, 'generation'));
|
||||
expect(bascule(m, 'coeursAutomatiques').checked).toBe(false);
|
||||
expect(champCoeurs(m).value).toBe('1');
|
||||
expect(m.trace.length).toBe(ecritures);
|
||||
});
|
||||
|
||||
test('« Générer » quand le nombre de cœurs est refusé : rien ne part, le refus reste contre le champ ; corrigé, la génération part sur lui', async () => {
|
||||
const { executeur, lancements } = releve(creerExecuteurDirect({ coeursDisponibles: 2 }));
|
||||
const m = await demonstration('petite', { executeur });
|
||||
await regler(m, { nombre: 2, arret: 2000 });
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
// Saisi puis quitté : le refus est posé, et « Générer » ne bouge plus
|
||||
// sous le pointeur quand on le presse.
|
||||
await saisir(m, champCoeurs(m), '3');
|
||||
expect(refusPres(champCoeurs(m), 'HORS_DOMAINE')).not.toBeNull();
|
||||
await cliquer(m, generer(m));
|
||||
await images(2);
|
||||
expect(refusPres(champCoeurs(m), 'HORS_DOMAINE')).not.toBeNull();
|
||||
expect(lancements).toEqual([]);
|
||||
expect(charge(m).propositions).toEqual([]);
|
||||
await saisir(m, champCoeurs(m), '1');
|
||||
await cliquer(m, generer(m));
|
||||
await attendre(() => expect(charge(m).propositions.length).toBe(2));
|
||||
expect(lancements.map(({ options }) => options)).toEqual([{ coeurs: 1 }]);
|
||||
});
|
||||
|
||||
test('vrais travailleurs : deux cœurs, puis un seul — deux travailleurs, puis un ; les mêmes propositions, celles de rechercher', async () => {
|
||||
const reglages = { graine: 1, nombre: 3, arret: 20000, historique: 1000 };
|
||||
const propositions = [];
|
||||
for (const coeurs of [2, 1]) {
|
||||
const { executeur: travailleur, suivi } = executeurTravailleur({ coeursDisponibles: 2 });
|
||||
const { executeur, lancements } = releve(travailleur);
|
||||
const m = await demonstration('petite', { executeur });
|
||||
await regler(m, { nombre: reglages.nombre, arret: reglages.arret });
|
||||
if (coeurs === 1) {
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
await saisir(m, champCoeurs(m), '1');
|
||||
}
|
||||
await cliquer(m, generer(m));
|
||||
await attendre(() => expect(lancements[0]?.propositions ?? null).not.toBeNull());
|
||||
expect(lancements[0].options).toEqual({ coeurs });
|
||||
expect(suivi.travailleurs).toBe(coeurs);
|
||||
expect(lancements[0].reglages).toEqual(reglages);
|
||||
expect(lancements[0].propositions).toEqual(rechercher(lancements[0].configuration, reglages));
|
||||
await attendre(() => expect(charge(m).propositions.length).toBe(3));
|
||||
propositions.push(charge(m).propositions);
|
||||
}
|
||||
expect(propositions.length).toBe(2);
|
||||
expect(JSON.stringify(propositions[1])).toBe(JSON.stringify(propositions[0]));
|
||||
}, 60_000);
|
||||
|
||||
test('la durée et l’estimation disent les cœurs : mesure sur 4 cœurs, puis l’estimation sur 1 cœur, recalculée sans relancer', async () => {
|
||||
const { executeur, executions } = executeurAMain({ coeursDisponibles: 4 });
|
||||
const m = await demonstration('petite', { executeur });
|
||||
await cliquer(m, generer(m));
|
||||
expect(executions.length).toBe(1);
|
||||
m.horloge.avancer(2000);
|
||||
executions[0].resoudre([]);
|
||||
await attendre(() => expect(enCours(m)).toBe(false));
|
||||
const mesure = controleur(m).derniereGeneration();
|
||||
expect(mesure).toMatchObject({ dureeMs: 2000, nombre: 5, coeurs: 4 });
|
||||
expect(duree(m).textContent).toBe(t('generation.duree.observee', mesure));
|
||||
expect(duree(m).textContent).toContain('sur 4 cœurs');
|
||||
const estimation = () => section(m).querySelector('[data-estimation]');
|
||||
const reglages = charge(m).reglages.generation;
|
||||
expect(estimation().textContent).toBe(t('generation.duree.estimee', controleur(m).estimer(reglages)));
|
||||
expect(controleur(m).estimer(reglages).coeurs).toBe(4);
|
||||
await cliquer(m, bascule(m, 'coeursAutomatiques'));
|
||||
await saisir(m, champCoeurs(m), '1');
|
||||
const surUn = controleur(m).estimer(reglages);
|
||||
expect(surUn.coeurs).toBe(1);
|
||||
expect(surUn.secondes).toBeCloseTo(5, 9);
|
||||
await attendre(() => expect(estimation().textContent).toBe(t('generation.duree.estimee', surUn)));
|
||||
expect(estimation().textContent).not.toContain('cœur');
|
||||
expect(executions.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('les refus à la commande (§ 5.9, § 15.4, § 9)', () => {
|
||||
test('exécuteur direct, relevé : 13 participants, 4 tables de 3 — PLACES_MANQUANTES {placesManquantes: 1} près de « Générer », remède vers les tables ; rien de lancé ni d’écrit', async () => {
|
||||
const { executeur, lancements } = releve(creerExecuteurDirect());
|
||||
|
|
|
|||
|
|
@ -26,11 +26,30 @@
|
|||
// écrite ou inchangée. Un refus de la séance — lecture, plan
|
||||
// bloqué, support rompu — se pose à côté de la case, avec son remède, et
|
||||
// rien ne s'écrit.
|
||||
import { getContext } from 'svelte';
|
||||
//
|
||||
// Les cœurs de calcul sont un réglage du poste, que le contrôleur de
|
||||
// génération tient pour la séance du logiciel : ni le fichier ni le journal
|
||||
// ne les portent, et ils se règlent en lecture comme en écriture. La case
|
||||
// « Cœurs automatiques », cochée par défaut, emploie un cœur par
|
||||
// processeur logique ; décochée, elle montre « Nombre de cœurs », pré-rempli
|
||||
// du dernier nombre accepté. La saisie se valide à la sortie du champ ou
|
||||
// par Entrée, par controleur.reglerCoeurs ; son refus se pose contre le
|
||||
// champ, saisie gardée, et le parent le lit (refusCoeurs, lié) : un refus
|
||||
// posé, « Générer » ne part pas. Échap rend le dernier nombre accepté. La
|
||||
// valeur automatique du poste s'affiche à côté, dans les deux états.
|
||||
import { getContext, untrack } from 'svelte';
|
||||
import Champ from '../primitives/Champ.svelte';
|
||||
import Refus from '../primitives/Refus.svelte';
|
||||
|
||||
let { charge, mesure, estimation, graine = $bindable(), refusGraine = null, saisirGraine } = $props();
|
||||
let {
|
||||
charge,
|
||||
mesure,
|
||||
estimation,
|
||||
graine = $bindable(),
|
||||
refusGraine = null,
|
||||
saisirGraine,
|
||||
refusCoeurs = $bindable(null),
|
||||
} = $props();
|
||||
|
||||
const t = getContext('t');
|
||||
const seance = getContext('seance');
|
||||
|
|
@ -38,6 +57,37 @@
|
|||
const uid = $props.id();
|
||||
const idGraine = `${uid}-graine`;
|
||||
const idRefusGraine = `${uid}-graine-refus`;
|
||||
const idCoeursAutomatiques = `${uid}-coeurs-automatiques`;
|
||||
const idCoeurs = `${uid}-coeurs`;
|
||||
const idRefusCoeurs = `${uid}-coeurs-refus`;
|
||||
|
||||
// Le réglage des cœurs, tel que le contrôleur le publie, et le texte du
|
||||
// champ « Nombre de cœurs ».
|
||||
const coeurs = $derived($generation.coeurs);
|
||||
let saisieCoeurs = $state(untrack(() => String(generation.coeurs().manuel)));
|
||||
|
||||
// Rend au champ le dernier nombre accepté, sans refus.
|
||||
function rendreCoeurs() {
|
||||
saisieCoeurs = String(generation.coeurs().manuel);
|
||||
refusCoeurs = null;
|
||||
}
|
||||
|
||||
function basculerCoeurs(evenement) {
|
||||
generation.reglerCoeurs({ automatique: evenement.currentTarget.checked });
|
||||
rendreCoeurs();
|
||||
}
|
||||
|
||||
function validerCoeurs() {
|
||||
refusCoeurs = generation.reglerCoeurs({ saisie: saisieCoeurs }).refus;
|
||||
}
|
||||
|
||||
function toucheCoeurs(evenement) {
|
||||
if (evenement.key === 'Escape') rendreCoeurs();
|
||||
else if (evenement.key === 'Enter') {
|
||||
evenement.preventDefault();
|
||||
validerCoeurs();
|
||||
}
|
||||
}
|
||||
|
||||
// Les réglages de la charge, dans l'ordre de l'écran.
|
||||
const REGLAGES = ['nombre', 'arret', 'historique'];
|
||||
|
|
@ -130,11 +180,46 @@
|
|||
</div>
|
||||
{/each}
|
||||
</fieldset>
|
||||
<fieldset class="coeurs" data-coeurs>
|
||||
<legend>{t('generation.coeurs.titre')}</legend>
|
||||
<div class="bascule">
|
||||
<input
|
||||
id={idCoeursAutomatiques}
|
||||
type="checkbox"
|
||||
data-champ="coeursAutomatiques"
|
||||
checked={coeurs.automatique}
|
||||
onchange={basculerCoeurs}
|
||||
/>
|
||||
<label for={idCoeursAutomatiques}>{t('generation.coeurs.automatique')}</label>
|
||||
</div>
|
||||
{#if !coeurs.automatique}
|
||||
<div class="champ">
|
||||
<label for={idCoeurs}>{t('generation.coeurs.nombre', { disponibles: coeurs.disponibles })}</label>
|
||||
<input
|
||||
id={idCoeurs}
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
data-champ="coeurs"
|
||||
bind:value={saisieCoeurs}
|
||||
aria-invalid={refusCoeurs === null ? undefined : 'true'}
|
||||
aria-describedby={refusCoeurs === null ? undefined : idRefusCoeurs}
|
||||
oninput={() => (refusCoeurs = null)}
|
||||
onkeydown={toucheCoeurs}
|
||||
onblur={validerCoeurs}
|
||||
/>
|
||||
{#if refusCoeurs !== null}
|
||||
<Refus id={idRefusCoeurs} refus={refusCoeurs} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<p data-coeurs-disponibles>{t('generation.coeurs.disponibles', { disponibles: coeurs.disponibles })}</p>
|
||||
<p>{t('generation.coeurs.note')}</p>
|
||||
</fieldset>
|
||||
{#if mesure === null}
|
||||
<p data-duree>{t('generation.duree.inconnue')}</p>
|
||||
{:else}
|
||||
<p data-duree>{t('generation.duree.observee', mesure)}</p>
|
||||
<p data-estimation>{t('generation.duree.estimee', { secondes: estimation.secondes })}</p>
|
||||
<p data-estimation>{t('generation.duree.estimee', estimation)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -178,6 +263,17 @@
|
|||
border-color: var(--couleur-refus);
|
||||
}
|
||||
|
||||
.coeurs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--espace-2);
|
||||
margin: 0;
|
||||
padding: var(--espace-2) var(--espace-3);
|
||||
border: 1px solid var(--couleur-page-filet);
|
||||
border-radius: var(--rayon-1);
|
||||
}
|
||||
|
||||
.bascules {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
// frappe dans la graine, au clic de « Générer », et dès que la charge
|
||||
// change : elle ne vaut que pour les réglages qu'elle nomme.
|
||||
//
|
||||
// Le nombre de cœurs refusé — refusCoeurs, que Reglages pose contre son
|
||||
// champ — retient « Générer » : rien ne part tant qu'il est posé.
|
||||
//
|
||||
// La graine est pré-remplie de controleur.grainePropose() au montage, puis,
|
||||
// à chaque lancement, de la graine suivante. Pendant le calcul, la section
|
||||
// porte aria-busy="true" et montre l'avancement ; « Générer » reste offert,
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
let refusGraine = $state.raw(null);
|
||||
let question = $state.raw(null);
|
||||
let issue = $state.raw(null);
|
||||
let refusCoeurs = $state.raw(null);
|
||||
|
||||
// Un lancement — enCours qui passe à vrai — met dans le champ la graine
|
||||
// suivante et retire la question, le refus de la graine et l'issue d'avant.
|
||||
|
|
@ -69,15 +73,17 @@
|
|||
});
|
||||
});
|
||||
|
||||
// La mesure du dernier calcul de la séance, relue à chaque publication du
|
||||
// contrôleur, et l'estimation des réglages de la charge qu'elle donne.
|
||||
// La mesure du dernier calcul de la séance, et l'estimation des réglages
|
||||
// de la charge qu'elle donne sur les cœurs réglés, relues à chaque
|
||||
// publication du contrôleur — un changement des cœurs compris.
|
||||
const mesure = $derived.by(() => {
|
||||
void $generation;
|
||||
return generation.derniereGeneration();
|
||||
});
|
||||
const estimation = $derived(
|
||||
mesure === null || charge === null ? null : generation.estimer(charge.reglages.generation),
|
||||
);
|
||||
const estimation = $derived.by(() => {
|
||||
void $generation;
|
||||
return mesure === null || charge === null ? null : generation.estimer(charge.reglages.generation);
|
||||
});
|
||||
|
||||
function saisirGraine() {
|
||||
refusGraine = null;
|
||||
|
|
@ -90,6 +96,7 @@
|
|||
// trouve : un réglage quitté par le clic même s'écrit d'abord, et la
|
||||
// génération part sur lui, quelle que soit la lenteur du disque.
|
||||
async function lancer(confirmee) {
|
||||
if (refusCoeurs !== null) return;
|
||||
refusGraine = null;
|
||||
issue = null;
|
||||
if (!confirmee) question = null;
|
||||
|
|
@ -120,7 +127,7 @@
|
|||
<section data-section="generation" aria-busy={$generation.enCours ? 'true' : 'false'}>
|
||||
<h2>{t('coquille.section.generation')}</h2>
|
||||
{#if charge !== null}
|
||||
<Reglages {charge} {mesure} {estimation} bind:graine {refusGraine} {saisirGraine} />
|
||||
<Reglages {charge} {mesure} {estimation} bind:graine {refusGraine} {saisirGraine} bind:refusCoeurs />
|
||||
<div class="lancer">
|
||||
<Commande cle="generation.commande.generer" nom="generer" executer={() => lancer(false)} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Les libellés de la génération : réglages, durée observée, question d'une
|
||||
// génération longue, avancement et issue (§ 5.7, § 5.10, § 14.6). Chaque
|
||||
// Les libellés de la génération : réglages, cœurs de calcul, durée observée,
|
||||
// question d'une génération longue, avancement et issue (§ 5.7, § 5.10,
|
||||
// § 14.6). Chaque
|
||||
// clé commence par « generation. ». Le libellé d'un réglage porte son unité,
|
||||
// entre parenthèses : l'étiquette du champ la dit, et le nom accessible du
|
||||
// champ avec elle. Un nombre et une durée se mettent en forme par
|
||||
|
|
@ -11,12 +12,21 @@
|
|||
import { creerFormat } from '../../application/format.js';
|
||||
import { compte } from '../../application/libelles/aides.js';
|
||||
|
||||
// Les cœurs d'un calcul, dits quand il en emploie plus d'un : « , sur
|
||||
// 4 cœurs » ; rien pour un seul, ou quand le nombre n'est pas donné.
|
||||
function surCoeurs(coeurs, texte) {
|
||||
return typeof coeurs === 'number' && coeurs > 1 ? `, sur ${creerFormat(texte).nombre(coeurs)} cœurs` : '';
|
||||
}
|
||||
|
||||
// Ce que vaut un calcul mesuré : sa durée, puis ses réglages, nombre de
|
||||
// propositions et compte d'arrêt, dont le produit est le nombre de
|
||||
// mouvements évalués.
|
||||
function calculMesure({ dureeMs, nombre, arret }, texte) {
|
||||
// mouvements évalués, puis les cœurs qu'il a employés.
|
||||
function calculMesure({ dureeMs, nombre, arret, coeurs }, texte) {
|
||||
const format = creerFormat(texte);
|
||||
return `${format.duree(dureeMs)} pour ${format.nombre(nombre)} \u{00D7} ${format.nombre(arret)} mouvements`;
|
||||
return (
|
||||
`${format.duree(dureeMs)} pour ${format.nombre(nombre)} \u{00D7} ${format.nombre(arret)} mouvements` +
|
||||
surCoeurs(coeurs, texte)
|
||||
);
|
||||
}
|
||||
|
||||
export const TABLE = new Map([
|
||||
|
|
@ -34,21 +44,44 @@ export const TABLE = new Map([
|
|||
['generation.contrainte.varierAppartenances', 'Varier les appartenances rencontrées'],
|
||||
['generation.contrainte.attribuerSieges', 'Attribuer les sièges'],
|
||||
|
||||
// --- Les cœurs de calcul, un réglage du poste pour la séance (§ 5.10) --------
|
||||
// disponibles : les processeurs logiques du poste, hyperthreads compris.
|
||||
['generation.coeurs.titre', 'Cœurs de calcul'],
|
||||
['generation.coeurs.automatique', 'Cœurs automatiques (un par processeur logique du poste)'],
|
||||
[
|
||||
'generation.coeurs.nombre',
|
||||
({ disponibles }, texte) => `Nombre de cœurs (de 1 à ${creerFormat(texte).nombre(disponibles)})`,
|
||||
],
|
||||
[
|
||||
'generation.coeurs.disponibles',
|
||||
({ disponibles }, texte) =>
|
||||
`Valeur automatique sur ce poste : ${creerFormat(texte).nombre(disponibles)} ` +
|
||||
`${disponibles <= 1 ? 'cœur' : 'cœurs'}, un par processeur logique, hyperthreads compris.`,
|
||||
],
|
||||
[
|
||||
'generation.coeurs.note',
|
||||
"Une génération n'emploie jamais plus de cœurs que de propositions demandées. Ce réglage vaut pour cette " +
|
||||
"séance du logiciel sur ce poste : il ne s'enregistre pas dans l'événement.",
|
||||
],
|
||||
|
||||
// --- La durée du dernier calcul de cette séance (§ 5.10) --------------------
|
||||
[
|
||||
'generation.duree.inconnue',
|
||||
"Aucun calcul n'est encore mesuré sur ce poste pendant cette séance : la durée d'une génération est inconnue.",
|
||||
],
|
||||
['generation.duree.observee', (details, texte) => `Dernier calcul sur ce poste : ${calculMesure(details, texte)}.`],
|
||||
// secondes : l'estimation des réglages courants, d'après le dernier calcul.
|
||||
// secondes : l'estimation des réglages courants, d'après le dernier calcul ;
|
||||
// coeurs : ceux qu'ils emploieraient.
|
||||
[
|
||||
'generation.duree.estimee',
|
||||
({ secondes }, texte) => `À ces réglages, une génération prendrait ${creerFormat(texte).ordreDeGrandeur(secondes)}.`,
|
||||
({ secondes, coeurs }, texte) =>
|
||||
`À ces réglages, une génération prendrait ${creerFormat(texte).ordreDeGrandeur(secondes)}${surCoeurs(coeurs, texte)}.`,
|
||||
],
|
||||
|
||||
// --- La génération longue, annoncée avant de partir (§ 5.10) ----------------
|
||||
// estimation : celle de CONFIRMATION_REQUISE, { inconnue: true } sans
|
||||
// mesure, sinon { secondes, auDelaDuPlafond, mesure: { dureeMs, mouvements } }.
|
||||
// mesure, sinon { secondes, auDelaDuPlafond, mesure: { dureeMs, mouvements,
|
||||
// coeurs }, coeurs }.
|
||||
[
|
||||
'generation.longue.question',
|
||||
({ estimation }, texte) => {
|
||||
|
|
@ -62,9 +95,10 @@ export const TABLE = new Map([
|
|||
const { mesure } = estimation;
|
||||
const plafond = estimation.auDelaDuPlafond ? ", au-delà du plafond d'usage de trente minutes" : '';
|
||||
return (
|
||||
`Cette génération prendrait ${format.ordreDeGrandeur(estimation.secondes)} à ce réglage${plafond}, ` +
|
||||
`Cette génération prendrait ${format.ordreDeGrandeur(estimation.secondes)} à ce réglage` +
|
||||
`${surCoeurs(estimation.coeurs, texte)}${plafond}, ` +
|
||||
`d'après le dernier calcul : ${format.duree(mesure.dureeMs)} pour ${format.nombre(mesure.mouvements)} ` +
|
||||
'mouvements. La lancer quand même ?'
|
||||
`mouvements${surCoeurs(mesure.coeurs, texte)}. La lancer quand même ?`
|
||||
);
|
||||
},
|
||||
],
|
||||
|
|
|
|||
13
src/main.js
13
src/main.js
|
|
@ -14,8 +14,13 @@
|
|||
//
|
||||
// Il est le seul module qui importe le travailleur de génération, par
|
||||
// « ?worker&inline » : Vite l'inclut dans la page, que la coquille charge
|
||||
// par file://, sans fichier de travailleur à côté. Chaque génération en
|
||||
// prend un neuf, que l'exécuteur termine à son issue (§ 5.10).
|
||||
// par file://, sans fichier de travailleur à côté. Chaque génération fait
|
||||
// naître un groupe de travailleurs neufs, un par cœur employé, que
|
||||
// l'exécuteur termine à son issue (§ 5.10). Il est aussi le seul à lire le
|
||||
// nombre de processeurs logiques du poste, navigator.hardwareConcurrency,
|
||||
// hyperthreads compris : l'exécuteur l'annonce au contrôleur, qui en fait
|
||||
// la valeur des cœurs automatiques ; une épreuve donne le sien à
|
||||
// l'exécuteur qu'elle monte.
|
||||
import { mount } from 'svelte';
|
||||
import { creerExecuteurTravailleur } from './application/executeur_travailleur.js';
|
||||
import { creerHorlogeSysteme } from './application/horloge.js';
|
||||
|
|
@ -49,7 +54,9 @@ mount(App, {
|
|||
props: {
|
||||
fs,
|
||||
horloge: creerHorlogeSysteme(),
|
||||
executeur: creerExecuteurTravailleur(() => new Travailleur()),
|
||||
executeur: creerExecuteurTravailleur(() => new Travailleur(), {
|
||||
coeursDisponibles: navigator.hardwareConcurrency,
|
||||
}),
|
||||
pressePapiers,
|
||||
identifiant: () => crypto.randomUUID(),
|
||||
langue: 'fr',
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
// l'historique, trois réglages qu'elle porte : la proposition 2 d'une demande
|
||||
// de 3 est celle d'une demande de 5, quel que soit l'ordre dans lequel les
|
||||
// propositions s'achèvent, et regenerer en rend le plan à partir de ces trois
|
||||
// champs seuls (§ 8.9). Les deux passent par la même descente, proposer.
|
||||
// champs seuls (§ 8.9). proposerUne calcule la proposition d'un seul rang, ce
|
||||
// qui répartit une génération entre plusieurs fils. Les trois passent par la
|
||||
// même descente, proposer.
|
||||
//
|
||||
// Sièges. Chaque tour offre S = Σ c_t sièges, rangés table par table dans
|
||||
// l'ordre des tables. Les personnes que instance.fixe impose à un tour
|
||||
|
|
@ -627,6 +629,52 @@ export function rechercher(configuration, reglages, { signal, progression } = {}
|
|||
return propositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* La proposition de rang rang d'une génération (§ 5.7) : celle que
|
||||
* rechercher(configuration, reglages) rend à l'indice rang − 1, octet pour
|
||||
* octet. Sa graine dérivée est le rang-ième tirage de creerPcg32(graine,
|
||||
* FLUX.GRAINES), et sa descente celle de rechercher : le résultat ne dépend
|
||||
* ni de l'ordre dans lequel les rangs se calculent, ni du fil qui les
|
||||
* calcule. Plusieurs fils calculent ainsi les propositions d'une même
|
||||
* génération, chacun les siennes, et les rangent par identifiant.
|
||||
*
|
||||
* Toutes les 1 000 itérations, progression(fait, total) reçoit le rang du
|
||||
* mouvement en cours dans cette seule proposition, total valant arret. Le
|
||||
* signal se lit avant la descente et à chacun de ces instants : vrai, il
|
||||
* fait lever ErreurAnnulee, et rien n'est rendu (§ 5.10).
|
||||
*
|
||||
* Lève ce que rechercher lève : la RangeError de verifierReglages,
|
||||
* ErreurConfiguration de normaliser, PLACES_MANQUANTES ; et une RangeError
|
||||
* qui nomme rang, champ reglage compris, quand rang n'est pas un entier de
|
||||
* 1 à reglages.nombre.
|
||||
*
|
||||
* @param {import('./types.js').Configuration} configuration
|
||||
* @param {ReglagesGeneration} reglages
|
||||
* @param {number} rang 1 à reglages.nombre
|
||||
* @param {{signal?: {aborted: boolean}, progression?: function(number, number): void}} [options]
|
||||
* @returns {Proposition}
|
||||
*/
|
||||
export function proposerUne(configuration, reglages, rang, { signal, progression } = {}) {
|
||||
verifierReglages(reglages);
|
||||
const { graine, arret, nombre, historique = HISTORIQUE_PAR_DEFAUT } = reglages;
|
||||
exigerReglage('rang', rang, 1, nombre);
|
||||
const instance = normaliser(configuration);
|
||||
const structure = preparer(instance);
|
||||
const graines = creerPcg32(graine, FLUX.GRAINES);
|
||||
let graineDerivee = 0;
|
||||
for (let tirage = 1; tirage <= rang; tirage += 1) graineDerivee = graines.suivant();
|
||||
lireSignal(signal);
|
||||
const plan = proposer(instance, structure, graineDerivee, {
|
||||
arret,
|
||||
historique,
|
||||
dejaFaits: 0,
|
||||
total: arret,
|
||||
signal,
|
||||
progression,
|
||||
});
|
||||
return { id: rang, graine: graineDerivee, arret, historique, plan };
|
||||
}
|
||||
|
||||
/**
|
||||
* Le contrôle des réglages que rechercher applique avant tout calcul : graine
|
||||
* de 0 à 2^32 − 1, puis arret, historique et nombre ≥ 1, un historique absent
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
creerEtat,
|
||||
defaire,
|
||||
proposerEtAppliquer,
|
||||
proposerUne,
|
||||
rechercher,
|
||||
regenerer,
|
||||
scoreComplet,
|
||||
|
|
@ -471,6 +472,108 @@ describe('rechercher : reproductibilité et identifiants (§ 5.7, § 19.4)', ()
|
|||
});
|
||||
});
|
||||
|
||||
describe('proposerUne : une proposition par son rang (§ 5.7, § 5.10)', () => {
|
||||
test('pour chaque rang de 1 à nombre, la proposition de ce rang de rechercher, octet pour octet', () => {
|
||||
const cles = ['petite', 'petite-conflit'];
|
||||
assert.equal(cles.length, 2);
|
||||
for (const cle of cles) {
|
||||
const reglages = { graine: 4_242, arret: 3_000, nombre: 5, historique: 200 };
|
||||
const attendues = rechercher(demo(cle), reglages);
|
||||
assert.equal(attendues.length, 5);
|
||||
for (let rang = 1; rang <= reglages.nombre; rang += 1) {
|
||||
const une = proposerUne(demo(cle), reglages, rang);
|
||||
assert.equal(JSON.stringify(une), JSON.stringify(attendues[rang - 1]), `${cle}, rang ${rang}`);
|
||||
assert.deepStrictEqual(une, attendues[rang - 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("le rang dans un ordre quelconque, l'historique absent : la même proposition, à l'historique par défaut", () => {
|
||||
const reglages = { graine: 9, arret: 1_500, nombre: 4 };
|
||||
const attendues = rechercher(demo('petite'), reglages);
|
||||
const desordre = [3, 1, 4, 2].map((rang) => proposerUne(demo('petite'), reglages, rang));
|
||||
assert.deepStrictEqual(
|
||||
[...desordre].sort((a, b) => a.id - b.id),
|
||||
attendues,
|
||||
);
|
||||
assert.deepEqual(desordre.map(({ historique }) => historique), [1_000, 1_000, 1_000, 1_000]);
|
||||
});
|
||||
|
||||
test("l'avancement se compte dans la seule proposition : fait de 1 000 en 1 000, sur arret", () => {
|
||||
const etapes = [];
|
||||
proposerUne(
|
||||
demo('petite-conflit'),
|
||||
{ graine: 3, arret: 2_500, nombre: 3 },
|
||||
2,
|
||||
{ progression: (fait, total) => etapes.push([fait, total]) },
|
||||
);
|
||||
assert.deepEqual(etapes, [
|
||||
[1_000, 2_500],
|
||||
[2_000, 2_500],
|
||||
]);
|
||||
});
|
||||
|
||||
test('un signal levé avant le départ, ou à sa deuxième lecture, fait lever ErreurAnnulee sans rien rendre', () => {
|
||||
assert.throws(
|
||||
() => proposerUne(demo('petite'), { graine: 3, arret: 5_000, nombre: 2 }, 1, { signal: { aborted: true } }),
|
||||
ErreurAnnulee,
|
||||
);
|
||||
let lectures = 0;
|
||||
const signal = {
|
||||
get aborted() {
|
||||
lectures += 1;
|
||||
return lectures >= 2;
|
||||
},
|
||||
};
|
||||
const etapes = [];
|
||||
assert.throws(
|
||||
() =>
|
||||
proposerUne(
|
||||
demo('petite-conflit'),
|
||||
{ graine: 3, arret: 5_000, nombre: 2 },
|
||||
2,
|
||||
{ signal, progression: (fait, total) => etapes.push([fait, total]) },
|
||||
),
|
||||
ErreurAnnulee,
|
||||
);
|
||||
assert.equal(lectures, 2);
|
||||
assert.deepEqual(etapes, [[1_000, 5_000]]);
|
||||
});
|
||||
|
||||
test('un rang hors de 1 à nombre, ou non entier, lève une RangeError qui nomme rang ; un réglage hors de son domaine, celle de verifierReglages', () => {
|
||||
const reglages = { graine: 1, arret: 10, nombre: 3 };
|
||||
const rangs = [0, 4, 1.5, '1', -1];
|
||||
assert.equal(rangs.length, 5);
|
||||
for (const rang of rangs) {
|
||||
assert.throws(
|
||||
() => proposerUne(demo('petite'), reglages, rang),
|
||||
(erreur) => erreur instanceof RangeError && erreur.reglage === 'rang' && erreur.message.startsWith('rang :'),
|
||||
String(rang),
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => proposerUne(demo('petite'), { ...reglages, graine: 2 ** 32 }, 1),
|
||||
(erreur) => erreur instanceof RangeError && erreur.reglage === 'graine',
|
||||
);
|
||||
assert.throws(
|
||||
() => proposerUne(demo('petite'), { ...reglages, nombre: 0 }, 1),
|
||||
(erreur) => erreur instanceof RangeError && erreur.reglage === 'nombre',
|
||||
);
|
||||
});
|
||||
|
||||
test('ne modifie pas la configuration reçue ; une salle trop petite lève PLACES_MANQUANTES comme rechercher', () => {
|
||||
const configuration = salleReservee();
|
||||
proposerUne(configuration, { graine: 1, arret: 500, nombre: 2 }, 2);
|
||||
assert.deepStrictEqual(configuration, salleReservee());
|
||||
const petite = demo('petite');
|
||||
petite.tables = petite.tables.slice(1);
|
||||
assert.throws(
|
||||
() => proposerUne(petite, { graine: 1, arret: 10, nombre: 1 }, 1),
|
||||
(erreur) => erreur.code === 'PLACES_MANQUANTES' && erreur.details.placesManquantes === 3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regenerer : une proposition depuis ses seuls champs (§ 5.7, § 8.9)', () => {
|
||||
test("pour chaque proposition d'une génération, regenerer rend le même plan à partir de ses trois champs, sans lire ni son plan ni son rang", () => {
|
||||
// arret 5 000 n'est pas un multiple de l'historique 37 : une descente
|
||||
|
|
|
|||
Loading…
Reference in a new issue