Le lanceur ne prenait que sept préfixes de noms, « le reste demandant une base de données » : les 3703 tests du répertoire passent avec PostgreSQL injoignable, et la suite n'en exécutait que 1131. Il balaie test/test_*.py. Deux formes rendent un fichier muet sans erreur : unittest.main() posé en plein milieu, qui sort avant que la seconde moitié soit définie (quatre fichiers, 87 tests), et l'absence de bloc __main__, qui compte zéro (huit fichiers, 174 tests). Une garde refuse ces deux formes et le retour d'une liste de préfixes. --- EN --- The runner took seven filename prefixes only, "the rest needing a database": all 3703 tests in the directory pass with PostgreSQL unreachable, and the suite ran 1131 of them. It now globs test/test_*.py. Two shapes make a file silent without error: unittest.main() placed mid-file, which exits before the second half is defined (four files, 87 tests), and no __main__ block at all, which counts zero (eight files, 174 tests). A guard now refuses both shapes and the return of a prefix list. Assisted-by: Claude Opus 5 (cherry picked from commit db71b333cc08ae0fce4ddaaf037357648b10236b)
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
"""Un test qui ne tourne pas est pire qu'un test absent.
|
|
|
|
Absent, on le sait. Vert sans avoir tourné, il rassure. Le dépôt a
|
|
rencontré les deux formes de ce silence le même jour :
|
|
|
|
· `unittest.main()` posé au MILIEU d'un fichier. Python exécute de haut
|
|
en bas : l'appel part, découvre ce qui est défini jusque-là, et sort.
|
|
Tout ce qui suit n'existe pas encore. Quatre fichiers, 87 tests.
|
|
|
|
· Aucun bloc `__main__` du tout. Le fichier ne fait rien quand on le
|
|
lance, le lanceur compte zéro test, et le total ne bouge pas. Huit
|
|
fichiers, 174 tests.
|
|
|
|
À quoi s'ajoutait une liste de préfixes dans le lanceur, qui laissait 2400
|
|
tests hors de la suite. Trois façons différentes d'obtenir le même
|
|
résultat : du vert qui ne prouve rien.
|
|
|
|
Ce fichier est la garde. Il ne lit pas ce que les tests vérifient — il
|
|
vérifie qu'ils PEUVENT être vérifiés.
|
|
"""
|
|
|
|
import ast
|
|
import glob
|
|
import io
|
|
import os
|
|
import unittest
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
LANCEUR = os.path.join(REPO, "script", "test", "run_unit_test.sh")
|
|
|
|
|
|
def fichiers_de_test():
|
|
return sorted(glob.glob(os.path.join(REPO, "test", "test_*.py")))
|
|
|
|
|
|
def bloc_main(arbre):
|
|
"""L'indice du `if __name__ == "__main__":` du module, ou None."""
|
|
for rang, noeud in enumerate(arbre.body):
|
|
if (
|
|
isinstance(noeud, ast.If)
|
|
and isinstance(noeud.test, ast.Compare)
|
|
and getattr(noeud.test.left, "id", "") == "__name__"
|
|
):
|
|
return rang
|
|
return None
|
|
|
|
|
|
class TestEveryFileCanRun(unittest.TestCase):
|
|
def setUp(self):
|
|
self.fichiers = fichiers_de_test()
|
|
self.assertTrue(self.fichiers, "aucun fichier de test trouvé")
|
|
|
|
def test_every_file_has_a_way_to_run_itself(self):
|
|
# Le lanceur exécute chaque fichier comme un programme. Sans ce
|
|
# bloc, il ne fait rien et le lanceur compte zéro — sans erreur.
|
|
for chemin in self.fichiers:
|
|
arbre = ast.parse(io.open(chemin, encoding="utf-8").read())
|
|
self.assertIsNotNone(
|
|
bloc_main(arbre),
|
|
f"{os.path.basename(chemin)} : pas de bloc __main__",
|
|
)
|
|
|
|
def test_the_run_call_is_the_last_thing_in_the_file(self):
|
|
# Au milieu, il coupe le fichier en deux et la seconde moitié
|
|
# n'est jamais définie au moment où les tests sont découverts.
|
|
for chemin in self.fichiers:
|
|
arbre = ast.parse(io.open(chemin, encoding="utf-8").read())
|
|
rang = bloc_main(arbre)
|
|
if rang is None:
|
|
continue
|
|
suivants = arbre.body[rang + 1 :]
|
|
self.assertEqual(
|
|
[],
|
|
[type(n).__name__ for n in suivants],
|
|
f"{os.path.basename(chemin)} : du code après unittest.main()",
|
|
)
|
|
|
|
def test_every_file_actually_declares_a_test(self):
|
|
for chemin in self.fichiers:
|
|
arbre = ast.parse(io.open(chemin, encoding="utf-8").read())
|
|
combien = sum(
|
|
1
|
|
for noeud in ast.walk(arbre)
|
|
if isinstance(noeud, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
and noeud.name.startswith("test_")
|
|
)
|
|
self.assertTrue(
|
|
combien, f"{os.path.basename(chemin)} : aucun test dedans"
|
|
)
|
|
|
|
|
|
class TestTheRunnerLeavesNobodyOut(unittest.TestCase):
|
|
"""Une liste de préfixes oublie ; un glob, non."""
|
|
|
|
def setUp(self):
|
|
self.source = io.open(LANCEUR, encoding="utf-8").read()
|
|
|
|
def test_it_takes_the_whole_directory(self):
|
|
self.assertIn("ls test/test_*.py", self.source)
|
|
|
|
def test_it_does_not_pick_families_by_name(self):
|
|
# Le défaut d'origine : un fichier hors préfixe tombait dans le
|
|
# même silence qu'un fichier absent.
|
|
for prefixe in ("test/test_qemu_*.py", "test/test_todo_*.py"):
|
|
self.assertNotIn(prefixe, self.source, prefixe)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|