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
66 lines
3 KiB
JavaScript
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 });
|
|
}
|