gestion_table_tournante_libre/test/fils.test.js

124 lines
5.4 KiB
JavaScript
Raw Permalink Normal View History

[IMP] tests: watched series adapts to the core count, threads below 4 Below four cores the runner carried the watched series in one process, and a relaunch after a base module took 2.5 to 3 s, over the 2 s budget of § 14.14. The configuration now reads the cores the process may use: from four, full processes and the whole series, as before; below, threads on every core, and the end-to-end test moves to the long series. No test disappears or changes behaviour. A guard refuses, in these series, what a thread refuses and a process accepts; the reporter names the mode. Checked: relaunch on 2 cores 1.0 to 1.3 s, was 2.5 to 3.0; on 4 cores 1.2 to 1.5 s; process.env is copied per thread, measured. --- FR --- [IMP] épreuves : la série surveillée suit les cœurs, threads sous 4 Sous quatre cœurs, le lanceur portait la série surveillée dans un seul processus, et une relance après un module de base prenait 2,5 à 3 s, au-delà du budget de 2 s du § 14.14. La configuration lit les cœurs que le processus peut occuper : à partir de quatre, processus complets et série entière, comme avant ; en deçà, threads sur tous les cœurs, et l'épreuve de bout en bout passe dans la série longue. Aucune épreuve ne disparaît ni ne change. Une garde refuse dans ces séries ce qu'un thread refuse et qu'un processus accepte ; le rapporteur nomme le mode. Vérifié : relance sur 2 cœurs en 1,0 à 1,3 s, contre 2,5 à 3,0 ; sur 4 cœurs en 1,2 à 1,5 s ; process.env est copié par thread, mesuré. Assisted-by: Claude Opus 5.5
2026-10-06 04:54:01 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Sur une petite machine, les séries node et node-long se jouent en threads
// (test/machine.js). Un thread de Node refuse ce qui changerait le processus
// entier — process.chdir, process.umask avec un masque, les changements
// d'identité — là où un processus complet l'accepte : une épreuve qui s'en
// servirait passerait sur une grande machine et échouerait sur une petite.
// La garde les refuse dans tout le code que ces séries exécutent : les
// épreuves de src/ et de scripts/, les modules sous test/, et les scripts
// qu'elles importent. L'environnement, lui, est copié dans chaque thread :
// une épreuve qui écrit process.env ne trouble pas sa voisine, et la garde ne
// le refuse pas.
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, test } from './lanceur.js';
const RACINE = fileURLToPath(new URL('..', import.meta.url));
// Appels qu'un thread de Node refuse. Un motif exige la parenthèse de
// l'appel : une phrase qui nomme process.chdir n'en est pas un. umask sans
// argument lit le masque, ce qu'un thread accepte.
const REFUSES_DANS_UN_THREAD = [
['process.chdir', /\bprocess\s*\??\.\s*chdir\s*(?:\?\.)?\s*\(/g],
['process.umask(masque)', /\bprocess\s*\??\.\s*umask\s*(?:\?\.)?\s*\(\s*[^)\s]/g],
['process.set…id', /\bprocess\s*\??\.\s*(?:set(?:e?[ug]id|groups)|initgroups)\s*(?:\?\.)?\s*\(/g],
];
// Ce fichier nomme ces appels dans ses motifs et dans ses données d'épreuve :
// il est hors de son propre balayage.
const CE_FICHIER = fileURLToPath(import.meta.url);
// Fichiers JavaScript que les séries node exécutent : les épreuves de src/ et
// de scripts/, hors épreuves du navigateur, tout module sous test/, et les
// scripts de scripts/, que des épreuves importent.
function fichiersExecutes(racine) {
const sous = (dossier) => {
try {
return readdirSync(join(racine, dossier), { recursive: true, withFileTypes: true })
.filter((e) => e.isFile() && /\.[cm]?js$/.test(e.name))
.map((e) => join(e.parentPath, e.name));
} catch {
return [];
}
};
return [
...sous('src').filter((f) => /\.test\.[cm]?js$/.test(f) && !/\.navigateur\.test\.[cm]?js$/.test(f)),
...sous('scripts'),
...sous('test').filter((f) => !f.includes(`${join('test', 'fixtures')}`) && f !== CE_FICHIER),
].sort();
}
// Relevé « fichier:ligne appel » de ces appels, par fichier puis par ligne.
// Lève quand aucun fichier n'est examiné (§ 14.2).
function releverAppelsRefuses(racine) {
const fichiers = fichiersExecutes(racine);
assert.ok(fichiers.length > 0, 'aucun fichier examiné');
return fichiers.flatMap((fichier) => {
const texte = readFileSync(fichier, 'utf8');
return REFUSES_DANS_UN_THREAD.flatMap(([appel, motif]) =>
[...texte.matchAll(motif)].map(({ index }) => ({ ligne: texte.slice(0, index).split('\n').length, appel })),
)
.sort((a, b) => a.ligne - b.ligne)
.map(({ ligne, appel }) => `${relative(racine, fichier)}:${ligne} ${appel}`);
});
}
// Écrit les fichiers donnés dans une racine temporaire, rend le relevé, et
// efface la racine.
function avecArbre(fichiers) {
const racine = mkdtempSync(join(tmpdir(), 'fils-'));
try {
for (const [chemin, texte] of Object.entries(fichiers)) {
mkdirSync(dirname(join(racine, chemin)), { recursive: true });
writeFileSync(join(racine, chemin), texte);
}
return releverAppelsRefuses(racine);
} finally {
rmSync(racine, { recursive: true, force: true });
}
}
describe('fils : ce qu’un thread refuse, hors des séries node (test/machine.js)', () => {
test('aucune épreuve, aucun module sous test/ ni aucun script n’appelle ce qu’un thread refuse', () => {
assert.deepEqual(releverAppelsRefuses(RACINE), []);
});
test('la garde relève chaque appel par sa ligne, dans les épreuves, les modules sous test/ et les scripts, et pas ses voisins', () => {
const fichiers = {
'src/moteur/a.test.js': [
"process.chdir('/tmp');",
'process.umask(0o022);',
'process.setuid(1000);',
'process?.chdir?.(ici);',
].join('\n'),
'scripts/outil.js': 'process.setgroups([]);\n',
'test/aide.js': 'process.initgroups("x", 1);\n',
'src/moteur/voisins.test.js': [
'const ici = process.cwd();',
'const masque = process.umask();',
"process.env.SONDE = 'copie par thread';",
'// process.chdir changerait le processus entier.',
].join('\n'),
'src/moteur/module.js': "process.chdir('/tmp');\n",
'src/interface/App.navigateur.test.js': "process.chdir('/tmp');\n",
};
assert.deepEqual(avecArbre(fichiers), [
'scripts/outil.js:1 process.set…id',
'src/moteur/a.test.js:1 process.chdir',
'src/moteur/a.test.js:2 process.umask(masque)',
'src/moteur/a.test.js:3 process.set…id',
'src/moteur/a.test.js:4 process.chdir',
'test/aide.js:1 process.set…id',
]);
});
test('un arbre sans fichier exécuté par ces séries fait échouer la garde', () => {
assert.throws(() => avecArbre({ 'src/moteur/module.js': 'export const a = 1;\n' }), /aucun fichier examiné/);
});
});