114 lines
4.1 KiB
Python
114 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()
|