erplibre/test/test_todo_i18n.py
Mathieu Benoit cfbdd9406e [REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.

--- EN ---

This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.

Assisted-by: Claude Opus 5
2026-09-24 14:40:38 -04:00

320 lines
10 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import ast
import collections
import os
import tempfile
import unittest
from unittest.mock import patch
from script.todo import todo_i18n
class TestTranslations(unittest.TestCase):
"""Test TRANSLATIONS dictionary integrity."""
def test_all_entries_have_fr_and_en(self):
for key, entry in todo_i18n.TRANSLATIONS.items():
self.assertIn("fr", entry, f"Key '{key}' missing 'fr' translation")
self.assertIn("en", entry, f"Key '{key}' missing 'en' translation")
def test_no_empty_translations(self):
for key, entry in todo_i18n.TRANSLATIONS.items():
for lang in ("fr", "en"):
self.assertTrue(
len(entry[lang]) > 0,
f"Key '{key}' has empty '{lang}' translation",
)
def test_translations_not_empty(self):
self.assertGreater(len(todo_i18n.TRANSLATIONS), 0)
class TestT(unittest.TestCase):
"""Test t() translation function."""
def setUp(self):
todo_i18n._current_lang = None
def tearDown(self):
todo_i18n._current_lang = None
def test_returns_french_when_lang_fr(self):
todo_i18n._current_lang = "fr"
result = todo_i18n.t("Quit")
self.assertEqual(result, "Quitter")
def test_returns_english_when_lang_en(self):
todo_i18n._current_lang = "en"
result = todo_i18n.t("Quit")
self.assertEqual(result, "Quit")
def test_unknown_key_returns_key(self):
todo_i18n._current_lang = "fr"
result = todo_i18n.t("nonexistent_key_xyz")
self.assertEqual(result, "nonexistent_key_xyz")
def test_fallback_to_fr_if_lang_missing(self):
todo_i18n._current_lang = "de"
result = todo_i18n.t("Quit")
self.assertEqual(result, "Quitter")
class TestGetLang(unittest.TestCase):
"""Test get_lang() function."""
def setUp(self):
todo_i18n._current_lang = None
def tearDown(self):
todo_i18n._current_lang = None
def test_returns_cached_lang(self):
todo_i18n._current_lang = "en"
result = todo_i18n.get_lang()
self.assertEqual(result, "en")
def test_reads_from_env_var_sh(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write('EL_LANG="en"\n')
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
result = todo_i18n.get_lang()
self.assertEqual(result, "en")
finally:
os.unlink(f.name)
def test_reads_unquoted_lang(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write("EL_LANG=fr\n")
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
result = todo_i18n.get_lang()
self.assertEqual(result, "fr")
finally:
os.unlink(f.name)
def test_env_variable_fallback(self):
with (
patch.object(
todo_i18n,
"ENV_VAR_FILE",
"/nonexistent/path",
),
patch.dict(os.environ, {"EL_LANG": "en"}),
):
result = todo_i18n.get_lang()
self.assertEqual(result, "en")
def test_default_is_fr(self):
with (
patch.object(
todo_i18n,
"ENV_VAR_FILE",
"/nonexistent/path",
),
patch.dict(os.environ, {}, clear=True),
):
result = todo_i18n.get_lang()
self.assertEqual(result, "fr")
def test_invalid_lang_in_file_falls_through(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write('EL_LANG="de"\n')
f.flush()
try:
with (
patch.object(todo_i18n, "ENV_VAR_FILE", f.name),
patch.dict(os.environ, {}, clear=True),
):
result = todo_i18n.get_lang()
self.assertEqual(result, "fr")
finally:
os.unlink(f.name)
class TestSetLang(unittest.TestCase):
"""Test set_lang() function."""
def setUp(self):
todo_i18n._current_lang = None
def tearDown(self):
todo_i18n._current_lang = None
def test_sets_current_lang(self):
# Détourner ENV_VAR_FILE comme le font les trois tests suivants :
# `set_lang()` PERSISTE, et sans ce détournement celui-ci écrivait
# dans le ./env_var.sh du dépôt, suivi par git.
with patch.object(todo_i18n, "ENV_VAR_FILE", "/nonexistent/path"):
todo_i18n.set_lang("en")
self.assertEqual(todo_i18n._current_lang, "en")
def test_persists_to_file_update(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write('EL_LANG="fr"\nOTHER=value\n')
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
todo_i18n.set_lang("en")
with open(f.name) as rf:
content = rf.read()
self.assertIn('EL_LANG="en"', content)
self.assertIn("OTHER=value", content)
finally:
os.unlink(f.name)
def test_persists_to_file_append(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write("SOME_VAR=123\n")
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
todo_i18n.set_lang("en")
with open(f.name) as rf:
content = rf.read()
self.assertIn('EL_LANG="en"', content)
self.assertIn("SOME_VAR=123", content)
finally:
os.unlink(f.name)
def test_nonexistent_file_no_crash(self):
with patch.object(
todo_i18n,
"ENV_VAR_FILE",
"/nonexistent/path",
):
todo_i18n.set_lang("en")
self.assertEqual(todo_i18n._current_lang, "en")
class TestLangIsConfigured(unittest.TestCase):
"""Test lang_is_configured() function."""
def test_returns_true_when_configured(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write('EL_LANG="fr"\n')
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
result = todo_i18n.lang_is_configured()
self.assertTrue(result)
finally:
os.unlink(f.name)
def test_returns_false_when_not_configured(self):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False
) as f:
f.write("SOME_VAR=123\n")
f.flush()
try:
with patch.object(todo_i18n, "ENV_VAR_FILE", f.name):
result = todo_i18n.lang_is_configured()
self.assertFalse(result)
finally:
os.unlink(f.name)
def test_returns_false_when_file_missing(self):
with patch.object(
todo_i18n,
"ENV_VAR_FILE",
"/nonexistent/path",
):
result = todo_i18n.lang_is_configured()
self.assertFalse(result)
class TestAucuneCleRepetee(unittest.TestCase):
"""Une clé répétée écrase la précédente EN SILENCE.
Le dictionnaire est un littéral Python de plusieurs milliers d'entrées :
rien n'avertit, rien ne lève, et la deuxième définition gagne. Le prix
s'est déjà payé une fois en étiquette de menu principal — la traduction
lue n'était pas celle qu'on venait d'écrire, et le fichier montrait la
bonne à qui la cherchait.
Le contrôle lit l'ARBRE et non le dictionnaire construit : une fois
construit, le doublon a déjà disparu, et il n'y a plus rien à voir. C'est
la seule façon de poser la question.
"""
def _cles(self):
chemin = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"script",
"todo",
"todo_i18n.py",
)
with open(chemin, encoding="utf-8") as fichier:
arbre = ast.parse(fichier.read())
litteral = None
for noeud in ast.walk(arbre):
if isinstance(noeud, ast.Assign) and any(
isinstance(c, ast.Name) and c.id == "TRANSLATIONS"
for c in noeud.targets
):
litteral = noeud.value
self.assertIsInstance(litteral, ast.Dict)
return [
c.value
for c in litteral.keys
if isinstance(c, ast.Constant) and isinstance(c.value, str)
]
def test_the_literal_was_actually_read(self):
"""Sinon un dictionnaire vide passerait le test suivant."""
self.assertGreater(len(self._cles()), 2000)
def test_no_key_is_declared_twice(self):
compte = collections.Counter(self._cles())
doublons = sorted(k for k, n in compte.items() if n > 1)
self.assertEqual(
doublons,
[],
"clés déclarées deux fois : la seconde écrase la première",
)
def test_every_key_survives_the_build(self):
"""Le compte du littéral et celui du dictionnaire s'accordent.
C'est la même vérité dite autrement, et elle tombe d'elle-même le
jour où une clé se répète."""
self.assertEqual(len(self._cles()), len(todo_i18n.TRANSLATIONS))
class TestChaqueCleDitSesDeuxLangues(unittest.TestCase):
"""Une clé sans « fr » ou sans « en » ne lève rien au chargement.
t() retombe alors sur la clé elle-même, si bien que l'écran montre
l'anglais à qui a choisi le français, ou l'inverse, sans que rien ne le
signale.
"""
def test_every_key_carries_fr_and_en(self):
manques = sorted(
k
for k, v in todo_i18n.TRANSLATIONS.items()
if not isinstance(v, dict) or "fr" not in v or "en" not in v
)
self.assertEqual(manques, [], "traductions incomplètes")
if __name__ == "__main__":
unittest.main()