erplibre/test/test_suite_reaches_every_file.py

114 lines
4.1 KiB
Python
Raw Permalink Normal View History

#!/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()