[FIX] matrix: UTF-8 locale for Chromium, skip ps reading when absent

On Linux, Chromium converts origin-private file names through the process
locale. The debian:13 and fedora:44 images set no LANG, so every accented
name failed with TypeMismatchError there, while Arch, which sets C.UTF-8,
passed. The container steps now run under LANG=C.UTF-8, and a browser test
names that cause when a Chromium cannot write an accented name. The long
test that reads the real ps skips with its reason when ps is missing.
Checked: debian-13 and fedora-44 test_navigateur and test_long green in
containers; 36 browser and 26 node-long tests locally.

--- FR ---

[FIX] matrice : locale UTF-8 pour Chromium, lecture ps sautée sans ps

Sous Linux, Chromium convertit les noms de fichiers de l'origine privée par
la locale du processus. Les images debian:13 et fedora:44 ne posent aucun
LANG : chaque nom accentué y échouait (TypeMismatchError), alors qu'Arch,
qui pose C.UTF-8, passait. Les étapes des conteneurs tournent sous
LANG=C.UTF-8, et une épreuve du navigateur nomme cette cause quand un
Chromium n'écrit pas un nom accentué. L'épreuve longue qui lit le vrai ps se
saute, raison dite, quand ps manque.
Vérifié : debian-13 et fedora-44 au vert en conteneur ; 36 + 26 en local.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-07 02:14:19 -04:00
parent 8d03d5ba41
commit 63efdb21a1
3 changed files with 47 additions and 7 deletions

View file

@ -119,8 +119,15 @@ preparation() {
# Étapes sous le compte dev, chacune annoncée par une ligne « === étape # Étapes sous le compte dev, chacune annoncée par une ligne « === étape
# statut » que le bilan relit. L'essai et le serveur de Vite ne se jouent # statut » que le bilan relit. L'essai et le serveur de Vite ne se jouent
# que sur une installation réussie. # que sur une installation réussie. Elles tournent sous LANG=C.UTF-8, des
# paramètres régionaux UTF-8 comme ceux d'un poste de développement, que la
# plupart des images ne posent pas : sous le « C » qu'elles laissent,
# Chromium ne convertit plus un nom hors ASCII de l'OPFS, et les épreuves
# du navigateur n'éprouvent plus le projet. C.UTF-8 vient avec la glibc,
# sans paquet de langue ; une image qui ne le porterait pas retomberait sur
# « C », comme sans la variable.
ETAPES_DEV=' ETAPES_DEV='
export LANG=C.UTF-8
etape() { local nom="$1"; shift; if "$@"; then echo "=== $nom ok"; else echo "=== $nom ÉCHEC"; return 1; fi; } etape() { local nom="$1"; shift; if "$@"; then echo "=== $nom ok"; else echo "=== $nom ÉCHEC"; return 1; fi; }
cd "$HOME/projet" cd "$HOME/projet"
etape install_dev ./install_dev.sh || exit 1 etape install_dev ./install_dev.sh || exit 1

View file

@ -124,6 +124,27 @@ describe('fichiers web : la plateforme et sa racine (§ 8.8, § 13.1)', () => {
expect(await fs.typeSupport(DOCUMENTS)).toBe('inconnu'); expect(await fs.typeSupport(DOCUMENTS)).toBe('inconnu');
}); });
// Sous Linux, Chromium convertit le nom d'une entrée de l'OPFS par les
// paramètres régionaux de son processus. Hors UTF-8 — LANG absent,
// LC_ALL=C —, un nom hors ASCII ne se convertit pas et désigne le dossier
// qui le porte : un fichier y lève TypeMismatchError, un dossier y rend son
// parent. Les épreuves qui écrivent un nom accentué n'éprouvent alors plus
// le module ; celle-ci, sur l'OPFS seul, nomme la cause.
test("l'OPFS de ce Chromium nomme une entrée hors ASCII : un fichier et un dossier accentués se créent et se listent sous leur nom", async () => {
const racine = await opfs();
const cause = 'Chromium tourne sous des paramètres régionaux qui ne sont pas UTF-8 : lancer les épreuves sous LANG=C.UTF-8';
let refus = null;
try {
await racine.getFileHandle('\u{E9}t\u{E9}.txt', { create: true });
} catch (erreur) {
refus = erreur.name;
}
expect(refus, cause).toBeNull();
const dossier = await racine.getDirectoryHandle('\u{E9}', { create: true });
expect(await dossier.isSameEntry(racine), cause).toBe(false);
expect(await nomsOpfs(), cause).toStrictEqual(['\u{E9}', '\u{E9}t\u{E9}.txt']);
});
test("la racine documents est le dossier gestion_table_tournante_libre de l'OPFS : ce qui s'y écrit s'y lit en UTF-8 sans marque d'ordre d'octets, et ce qui s'y pose se lit et se liste", async () => { test("la racine documents est le dossier gestion_table_tournante_libre de l'OPFS : ce qui s'y écrit s'y lit en UTF-8 sans marque d'ordre d'octets, et ce qui s'y pose se lit et se liste", async () => {
const fs = await preparer(); const fs = await preparer();
await fs.creerDossier(DOCUMENTS, 'corbeille'); await fs.creerDossier(DOCUMENTS, 'corbeille');

View file

@ -57,18 +57,23 @@ function dossierTemporaire() {
} }
// test du lanceur, dont chaque épreuve retire à sa fin les dossiers // test du lanceur, dont chaque épreuve retire à sa fin les dossiers
// temporaires qu'elle a ouverts. // temporaires qu'elle a ouverts ; le corps reçoit le contexte de l'épreuve.
function testNettoye(nom, ...suite) { function testNettoye(nom, ...suite) {
const corps = suite.pop(); const corps = suite.pop();
test(nom, ...suite, async () => { test(nom, ...suite, async (contexte) => {
try { try {
await corps(); await corps(contexte);
} finally { } finally {
for (const dossier of ouverts.splice(0)) rmSync(dossier, { recursive: true, force: true }); for (const dossier of ouverts.splice(0)) rmSync(dossier, { recursive: true, force: true });
} }
}); });
} }
// Ce que dit une commande lancée par spawnSync : l'exception qui l'a
// empêchée de démarrer, sinon sa sortie d'erreur. Toujours une chaîne,
// seul message que node:assert accepte.
const ditPar = (lancee) => String(lancee.error?.message ?? lancee.stderr ?? '');
// Charge electron/preload.cjs comme un rendu en bac à sable, contre un // Charge electron/preload.cjs comme un rendu en bac à sable, contre un
// electron dont invoke porte chaque appel au gestionnaire que servir a posé // electron dont invoke porte chaque appel au gestionnaire que servir a posé
// sur ce canal ; arguments et réponse sont copiés comme l'IPC les copie, et // sur ce canal ; arguments et réponse sont copiés comme l'IPC les copie, et
@ -350,7 +355,14 @@ describe('coquille sur un vrai disque', () => {
testNettoye( testNettoye(
"la lecture par ps (macOS) passe TZ=UTC et LC_ALL=C à la commande : le démarrage inscrit est en temps universel, et un processus d'un autre fuseau lit la séance vivante", "la lecture par ps (macOS) passe TZ=UTC et LC_ALL=C à la commande : le démarrage inscrit est en temps universel, et un processus d'un autre fuseau lit la séance vivante",
{ skip: SOUS_WINDOWS }, { skip: SOUS_WINDOWS },
async () => { async (contexte) => {
// L'épreuve joue macOS avec le ps de l'hôte. Sous Linux, la coquille
// lit /proc et ne demande pas ps, qu'une image de système minimale ne
// porte pas : sans lui, l'épreuve se saute et dit pourquoi.
if (spawnSync('ps', ['-p', String(process.pid)]).error?.code === 'ENOENT') {
contexte.skip("ps introuvable sur ce système : la lecture du démarrage par ps ne s'y éprouve pas");
return;
}
const documents = join(dossierTemporaire(), 'Documents'); const documents = join(dossierTemporaire(), 'Documents');
mkdirSync(documents, { recursive: true }); mkdirSync(documents, { recursive: true });
const fichiers = creerFichiers({ const fichiers = creerFichiers({
@ -366,7 +378,7 @@ describe('coquille sur un vrai disque', () => {
env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' }, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' },
encoding: 'utf8', encoding: 'utf8',
}); });
assert.equal(enUtc.status, 0, enUtc.stderr); assert.equal(enUtc.status, 0, ditPar(enUtc));
assert.equal(pose.demarrage, enUtc.stdout.trim()); assert.equal(pose.demarrage, enUtc.stdout.trim());
// Un second processus, dans un fuseau qui n'est pas celui de // Un second processus, dans un fuseau qui n'est pas celui de
@ -387,7 +399,7 @@ describe('coquille sur un vrai disque', () => {
}, },
encoding: 'utf8', encoding: 'utf8',
}); });
assert.equal(relu.status, 0, relu.stderr); assert.equal(relu.status, 0, ditPar(relu));
assert.deepEqual(JSON.parse(relu.stdout), { assert.deepEqual(JSON.parse(relu.stdout), {
fuseau: autre, fuseau: autre,
tenu: { pris: false, seance: 'seance-a', depuis: pose.depuis, vivant: true }, tenu: { pris: false, seance: 'seance-a', depuis: pose.depuis, vivant: true },