gestion_table_tournante_libre/scripts/mutation/crochets.js
Mathieu Benoit 8bdc959183 [ADD] mutation: run one mutant through node --test with a loader hook
A mutant counts only when the tests that import its module actually run
against the mutated source. A loader hook (module.registerHooks) serves the
mutated text in place of the module; the import graph picks the tests that
reach it, and node --test runs them in a child with a deadline. The verdict
is killed, survived, timed out or error; an empty request, a missing test
or a non-positive deadline is refused before anything starts.
Checked: red first; 3418 node and 76 long tests green from the index,
including a real run on a small invented module.

--- FR ---

[ADD] mutation : jouer un mutant sous node --test, crochet de chargement

Un mutant ne compte que si les épreuves qui importent son module jouent
vraiment contre le source muté. Un crochet de chargement
(module.registerHooks) sert le texte muté à la place du module ; le graphe
des imports choisit les épreuves qui l'atteignent, et node --test les joue
dans un enfant sous délai. Le verdict est tué, survivant, délai ou erreur ;
une demande vide, une épreuve absente ou un délai non positif sont refusés
avant tout lancement.
Vérifié : rouge d'abord ; 3418 node et 76 longues vertes depuis l'index,
dont un vrai lancement sur un petit module inventé.

Assisted-by: Claude Opus 5.5
2026-10-09 13:04:14 -04:00

66 lines
3 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Les crochets de chargement de l'audit par mutation (§ 14.13). executer.js
// charge ce module par --import dans le processus de node --test, qui le
// transmet à chaque processus d'épreuve. Deux substitutions, par URL :
//
// test/lanceur.js sert « export { test, describe } from 'node:test' » :
// les épreuves tournent sous le repli du § 14.8, sans
// qu'un seul fichier d'épreuve change ;
// le module muté sert le source muté à la place de celui du disque,
// que rien n'écrit.
//
// Les données arrivent par la variable d'environnement VARIABLE, que les
// processus d'épreuve héritent : le chemin d'un fichier JSON
// {fichier, remplacement} — fichier, chemin absolu du module muté ;
// remplacement, son source muté entier —, ou la chaîne vide pour la série non
// mutée. Sans la variable, importer ce module ne pose aucun crochet.
//
// Les crochets sont synchrones et posés dans le fil du module
// (module.registerHooks) : module.register, qui les poserait dans un fil à
// part, est déprécié par Node.
import { readFileSync, realpathSync } from 'node:fs';
import { registerHooks } from 'node:module';
import { fileURLToPath, pathToFileURL } from 'node:url';
export const VARIABLE = 'GTT_MUTATION';
// Les URL se comparent après résolution des liens : Node charge un module
// sous son chemin réel.
const urlReelle = (chemin) => pathToFileURL(realpathSync(chemin)).href;
const LANCEUR = urlReelle(fileURLToPath(new URL('../../test/lanceur.js', import.meta.url)));
const LANCEUR_DE_REPLI = "export { test, describe } from 'node:test';\n";
// Le module muté : { url, remplacement }, ou null pour la série non mutée.
let cible = null;
/**
* Désigne le module muté et son source muté. Lève sur un fichier absent.
* @param {{fichier: string, remplacement: string}} donnees fichier : chemin absolu
*/
export function initialize({ fichier, remplacement }) {
if (typeof fichier !== 'string' || typeof remplacement !== 'string') {
throw new TypeError('initialize attend { fichier, remplacement }, deux chaînes');
}
cible = { url: urlReelle(fichier), remplacement };
}
/**
* Crochet load de module.registerHooks : le lanceur de repli pour
* test/lanceur.js, le source muté pour le module désigné, le chargement
* ordinaire sinon. Le format du module muté reste celui que Node lui
* reconnaît.
*/
export function load(url, contexte, suivant) {
if (url === LANCEUR) return { format: 'module', source: LANCEUR_DE_REPLI, shortCircuit: true };
if (cible !== null && url === cible.url) return { ...suivant(url, contexte), source: cible.remplacement };
return suivant(url, contexte);
}
const donnees = process.env[VARIABLE];
if (donnees !== undefined) {
if (donnees !== '') initialize(JSON.parse(readFileSync(donnees, 'utf8')));
registerHooks({ load });
}