[ADD] test: what the tooling imports must be declared
textual, urwid and lxml were already in the tooling requirements; the earlier table read as if they were missing, when it only compared against the system python. psycopg2 is the one absent, and stays so: nothing under script/ imports it, the analysis tools query PostgreSQL through psql, and the Odoo venvs declare it where Odoo needs it. A test now answers this by reading the imports instead of the venv. It bites both ways: a removed declaration, and a new undeclared import. Also chmod +X -> +x in the Dockerfile: uppercase only adds the bit to directories, or to files that already carry one. On what COPY had just laid down in 644 it did nothing. --- FR --- [ADD] test : ce que l'outillage importe doit être déclaré textual, urwid et lxml y étaient déjà ; le tableau précédent laissait croire le contraire, alors qu'il comparait au python du système. psycopg2 est le seul absent, et le reste : rien sous script/ ne l'importe, les outils d'analyse passent par psql, et les venvs Odoo le déclarent là où Odoo en a besoin. Un test répond désormais à la question en lisant les imports plutôt que le venv. Il mord des deux côtés : déclaration retirée, et import non déclaré. Aussi chmod +X -> +x dans le Dockerfile : la majuscule n'ajoute le bit qu'aux répertoires, ou aux fichiers qui en portent déjà un. Sur ce que COPY venait de déposer en 644, elle ne faisait rien. Assisted-by: Claude Opus 5
This commit is contained in:
parent
75498384c9
commit
f7f5e800fe
2 changed files with 160 additions and 1 deletions
|
|
@ -86,7 +86,11 @@ ENV ODOO_RC /etc/odoo/odoo.conf
|
||||||
|
|
||||||
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
||||||
|
|
||||||
RUN chmod +X /usr/local/bin/wait-for-psql.py
|
# « +X » en majuscule n'ajoute le bit que si l'un des trois est DÉJÀ posé,
|
||||||
|
# ou sur un répertoire. Sur le fichier que COPY vient de déposer en 644, il
|
||||||
|
# ne faisait rien : le script restait non exécutable dans l'image, malgré
|
||||||
|
# sa place dans /usr/local/bin.
|
||||||
|
RUN chmod +x /usr/local/bin/wait-for-psql.py
|
||||||
|
|
||||||
# Set default user when running the container
|
# Set default user when running the container
|
||||||
USER odoo
|
USER odoo
|
||||||
|
|
|
||||||
155
test/test_tooling_requirements.py
Executable file
155
test/test_tooling_requirements.py
Executable file
|
|
@ -0,0 +1,155 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
"""Ce que l'outillage importe doit être déclaré, pas seulement installé.
|
||||||
|
|
||||||
|
Un paquet peut vivre dans `.venv.erplibre` sans figurer nulle part : arrivé
|
||||||
|
par la dépendance d'un autre, ou posé à la main un jour. Rien ne le réclame,
|
||||||
|
donc rien ne le réinstalle — il disparaît au prochain venv neuf, et l'outil
|
||||||
|
qui s'en servait tombe chez le suivant, pas chez celui qui l'a écrit.
|
||||||
|
|
||||||
|
Le fichier porte déjà ce raisonnement en commentaire pour lxml. Ce test le
|
||||||
|
rend vérifiable au lieu de le laisser à la vigilance.
|
||||||
|
|
||||||
|
Portée : le venv d'OUTILS. Les venvs Odoo ont leurs propres requirements, et
|
||||||
|
psycopg2 y est déclaré parce qu'Odoo en a besoin — l'outillage, lui, parle à
|
||||||
|
PostgreSQL par psql en sous-processus.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
REQUIREMENTS = os.path.join(REPO, "requirement", "erplibre_require-ments.txt")
|
||||||
|
|
||||||
|
PATTERNS = (
|
||||||
|
"script/analyse/*.py",
|
||||||
|
"script/analyse/*/*.py",
|
||||||
|
"script/odoo/migration/*.py",
|
||||||
|
"script/todo/*.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ce que l'on écrit pour importer n'est pas ce que l'on écrit pour installer.
|
||||||
|
# La table est explicite : un nouveau décalage doit être ajouté ici en
|
||||||
|
# connaissance de cause, plutôt que d'être toléré par une correspondance
|
||||||
|
# approximative qui laisserait passer une vraie absence.
|
||||||
|
PACKAGE_OF_MODULE = {
|
||||||
|
"dotenv": "python-dotenv",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Modules fournis par l'environnement, jamais par ce fichier.
|
||||||
|
PROVIDED_ELSEWHERE = {
|
||||||
|
# Le venv Odoo, pas celui des outils : ces scripts tournent sous
|
||||||
|
# `odoo-bin shell`, qui apporte son propre interpréteur.
|
||||||
|
"odoo",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tooling_files():
|
||||||
|
return sorted(
|
||||||
|
{p for m in PATTERNS for p in glob.glob(os.path.join(REPO, m))}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def third_party_imports():
|
||||||
|
"""{module: {fichiers qui l'importent}}, hors stdlib et hors modules locaux."""
|
||||||
|
files = tooling_files()
|
||||||
|
local = {os.path.basename(p)[:-3] for p in files}
|
||||||
|
found = {}
|
||||||
|
for path in files:
|
||||||
|
with open(path) as handle:
|
||||||
|
tree = ast.parse(handle.read())
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
names = []
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
names = [alias.name.split(".")[0] for alias in node.names]
|
||||||
|
elif (
|
||||||
|
isinstance(node, ast.ImportFrom)
|
||||||
|
and node.module
|
||||||
|
and not node.level
|
||||||
|
):
|
||||||
|
names = [node.module.split(".")[0]]
|
||||||
|
for name in names:
|
||||||
|
if (
|
||||||
|
name in sys.stdlib_module_names
|
||||||
|
or name in local
|
||||||
|
or name in PROVIDED_ELSEWHERE
|
||||||
|
or name == "script"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
found.setdefault(name, set()).add(os.path.relpath(path, REPO))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def declared():
|
||||||
|
with open(REQUIREMENTS) as handle:
|
||||||
|
return handle.read()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEveryImportIsDeclared(unittest.TestCase):
|
||||||
|
def test_the_scan_finds_something(self):
|
||||||
|
# Un test qui n'inspecte rien passe toujours.
|
||||||
|
self.assertGreater(len(tooling_files()), 20)
|
||||||
|
self.assertGreater(len(third_party_imports()), 5)
|
||||||
|
|
||||||
|
def test_no_third_party_import_is_undeclared(self):
|
||||||
|
text = declared()
|
||||||
|
missing = []
|
||||||
|
for module, users in sorted(third_party_imports().items()):
|
||||||
|
package = PACKAGE_OF_MODULE.get(module, module)
|
||||||
|
# En début de ligne : « lxml » ne doit pas être satisfait par
|
||||||
|
# une mention en commentaire ni par « python-lxml-stubs ».
|
||||||
|
if not re.search(
|
||||||
|
rf"^\s*{re.escape(package)}\b", text, re.M | re.I
|
||||||
|
):
|
||||||
|
missing.append(f"{module} ({package}) <- {sorted(users)[0]}")
|
||||||
|
self.assertEqual(missing, [])
|
||||||
|
|
||||||
|
def test_the_alias_table_has_no_dead_entry(self):
|
||||||
|
# Une correspondance qui ne sert plus masque le jour où le module
|
||||||
|
# revient sous un autre nom.
|
||||||
|
imported = set(third_party_imports())
|
||||||
|
for module in PACKAGE_OF_MODULE:
|
||||||
|
with self.subTest(module=module):
|
||||||
|
self.assertIn(module, imported)
|
||||||
|
|
||||||
|
def test_the_check_would_notice_a_removal(self):
|
||||||
|
# La preuve que la recherche mord : un paquet réellement déclaré,
|
||||||
|
# retiré du texte, doit être vu comme manquant.
|
||||||
|
# `assertRegex` cherche SANS re.M : « ^ » n'y vaut qu'au tout début
|
||||||
|
# de la chaîne, et la vérification ci-dessus, elle, lit ligne à ligne.
|
||||||
|
# Un motif compilé fait dire au test la même chose qu'au code.
|
||||||
|
motif = re.compile(r"^\s*lxml\b", re.M)
|
||||||
|
text = declared()
|
||||||
|
self.assertRegex(text, motif)
|
||||||
|
without = motif.sub("", text)
|
||||||
|
self.assertNotRegex(without, motif)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPsycopg2StaysOutOfTheToolingVenv(unittest.TestCase):
|
||||||
|
"""Décision prise, et la raison avec — sinon elle se reprend à l'aveugle.
|
||||||
|
|
||||||
|
Les outils d'analyse interrogent PostgreSQL par `psql` en sous-processus :
|
||||||
|
un choix délibéré, qui porte la garantie de lecture seule côté serveur
|
||||||
|
(PGOPTIONS). Déclarer psycopg2 ici ferait compiler une extension C à
|
||||||
|
chaque installation neuve, sans que rien ne l'importe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_nothing_in_the_tooling_imports_psycopg2(self):
|
||||||
|
self.assertNotIn("psycopg2", third_party_imports())
|
||||||
|
|
||||||
|
def test_the_analysis_tools_query_through_psql(self):
|
||||||
|
path = os.path.join(REPO, "script", "analyse", "lib_analyse.py")
|
||||||
|
with open(path) as handle:
|
||||||
|
source = handle.read()
|
||||||
|
self.assertIn('"psql"', source)
|
||||||
|
self.assertIn("default_transaction_read_only=on", source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in a new issue