erplibre/test/test_install_swig_compat.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

197 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""L'installation Poetry survit à SWIG >= 4.3.
pykcs11 — tiré par endesive, donc dans les locks Odoo 14, 15 et 17 — ne livre
aucun wrapper pré-généré : SWIG tourne à chaque installation. Son
« requires = ["setuptools", "swig"] » n'étant pas borné, Poetry prend la
DERNIÈRE version de PyPI. SWIG 4.3 a retiré les alias Python 2 que les
versions antérieures écrivaient dans le code généré, et le typemap CK_RV de
pykcs11 en utilise un : « ‘PyInt_FromLong’ was not declared in this scope »,
55 fois, et install_odoo_17 s'arrête.
Ces tests gardent les trois choses que l'enquête a coûté :
- CPPFLAGS, pas CFLAGS. Un .cpp est compilé par « compiler_so_cxx », qui lit
CXXFLAGS et CPPFLAGS ; CFLAGS ne l'atteint JAMAIS. Le premier correctif a
échoué exactement là.
- La définition doit être identique TOKEN POUR TOKEN à celle de SWIG 4.2,
sinon les hôtes qui ont encore un vieux SWIG récoltent un avertissement de
redéfinition à chaque fichier.
- Le drapeau doit être posé AVANT « poetry install », et s'ajouter à un
CPPFLAGS existant au lieu de l'écraser.
"""
import os
import re
import shutil
import subprocess
import sysconfig
import tempfile
import unittest
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
SCRIPT = RACINE / "script/install/install_locally.sh"
SOURCE = SCRIPT.read_text(encoding="utf-8")
# La ligne que SWIG <= 4.2 plaçait lui-même dans le wrapper généré.
SWIG_42 = "#define PyInt_FromLong(x) PyLong_FromLong(x)"
# Ce que produit SWIG >= 4.3 : l'appel, sans l'alias.
APPEL_SWIG_43 = """
#include <Python.h>
PyObject *sonde(long v) { return PyInt_FromLong(v); }
"""
def _drapeau():
"""Le -D tel qu'il est écrit dans le script — source unique."""
m = re.search(
r"export CPPFLAGS=\"[^\"]*?(-DPyInt_FromLong[^\" ]*)\"", SOURCE
)
return m.group(1) if m else ""
def _compilateur():
return shutil.which("c++") or shutil.which("g++")
def _compile(source, drapeaux):
"""(succès, sortie) d'une compilation de syntaxe seule."""
inc = sysconfig.get_paths()["include"]
if not Path(inc, "Python.h").exists():
return None, "Python.h absent"
with tempfile.TemporaryDirectory() as tmp:
fichier = Path(tmp, "sonde.cpp")
fichier.write_text(source, encoding="utf-8")
res = subprocess.run(
[
_compilateur(),
"-fsyntax-only",
*drapeaux,
"-I",
inc,
str(fichier),
],
capture_output=True,
text=True,
timeout=120,
)
return res.returncode == 0, res.stdout + res.stderr
class TestLeDrapeauDansLeScript(unittest.TestCase):
def test_it_is_exported(self):
self.assertTrue(_drapeau(), "aucun -DPyInt_FromLong dans le script")
def test_it_is_set_before_poetry_install(self):
"""Posé après, il n'atteindrait rien : c'est « poetry install » qui
lance les compilations."""
export = SOURCE.index("export CPPFLAGS=")
install = SOURCE.index("install --no-root ${POETRY_VERBOSE}")
self.assertLess(export, install)
def test_it_appends_instead_of_replacing(self):
"""Écraser CPPFLAGS jetterait ce que l'appelant y avait mis — sur
s390x et openSUSE, des chemins d'en-têtes y passent."""
self.assertIn("${CPPFLAGS:+${CPPFLAGS} }", SOURCE)
def test_it_is_cppflags_and_not_cflags(self):
"""Le piège qui a fait échouer le premier correctif : le fichier fautif
est un .cpp, et CFLAGS ne va qu'aux .c."""
self.assertNotIn("export CFLAGS=", SOURCE)
def test_the_shell_builds_the_value_it_claims(self):
"""Les parenthèses du -D sont des métacaractères du shell : mal
protégées, la variable serait tronquée ou le script casserait."""
for depart, attendu in (
("", "-DPyInt_FromLong(x)=PyLong_FromLong(x)"),
("-DDEJA=1", "-DDEJA=1 -DPyInt_FromLong(x)=PyLong_FromLong(x)"),
):
res = subprocess.run(
[
"bash",
"-c",
f'export CPPFLAGS="{depart}"; '
'export CPPFLAGS="${CPPFLAGS:+${CPPFLAGS} }'
f'{_drapeau()}"; printf %s "$CPPFLAGS"',
],
capture_output=True,
text=True,
timeout=30,
)
self.assertEqual(attendu, res.stdout)
def test_the_script_still_parses(self):
res = subprocess.run(
["bash", "-n", str(SCRIPT)], capture_output=True, text=True
)
self.assertEqual(0, res.returncode, res.stderr)
@unittest.skipUnless(_compilateur(), "aucun compilateur C++")
class TestLeDrapeauCompile(unittest.TestCase):
"""Le drapeau fait-il ce qu'on lui prête ? Compilé, pas supposé."""
def test_swig_43_output_fails_without_it(self):
"""Sans quoi le test suivant ne prouverait rien : c'est bien CE
symptôme que le drapeau soigne."""
ok, sortie = _compile(APPEL_SWIG_43, [])
if ok is None:
self.skipTest(sortie)
self.assertFalse(ok)
self.assertIn("PyInt_FromLong", sortie)
def test_the_same_source_compiles_with_it(self):
ok, sortie = _compile(APPEL_SWIG_43, [_drapeau()])
if ok is None:
self.skipTest(sortie)
self.assertTrue(ok, sortie[-400:])
def test_it_does_not_clash_with_an_older_swig(self):
"""Un hôte dont le SWIG écrit encore l'alias verrait, sinon, un
avertissement de redéfinition par fichier compilé. En -Werror, ce
serait un échec."""
ok, sortie = _compile(SWIG_42 + APPEL_SWIG_43, ["-Werror", _drapeau()])
if ok is None:
self.skipTest(sortie)
self.assertTrue(ok, sortie[-400:])
self.assertNotIn("redefined", sortie)
class TestHypotheseSetuptools(unittest.TestCase):
"""L'hypothèse dont dépend le choix de CPPFLAGS, écrite noir sur blanc.
Si un setuptools futur change de câblage, c'est ici qu'on l'apprend — pas
au milieu d'une installation de VM.
"""
def test_cppflags_reaches_the_cxx_compiler_but_cflags_does_not(self):
try:
from setuptools._distutils.ccompiler import new_compiler
from setuptools._distutils.sysconfig import customize_compiler
except ImportError as exc: # pragma: no cover - setuptools trop vieux
self.skipTest(f"distutils vendu introuvable : {exc}")
garde = {v: os.environ.get(v) for v in ("CFLAGS", "CPPFLAGS")}
os.environ["CFLAGS"] = "-DVU_PAR_CFLAGS=1"
os.environ["CPPFLAGS"] = "-DVU_PAR_CPPFLAGS=1"
try:
compilateur = new_compiler()
customize_compiler(compilateur)
cxx = getattr(compilateur, "compiler_so_cxx", None)
if not cxx:
self.skipTest("pas de compiler_so_cxx dans ce setuptools")
self.assertIn("-DVU_PAR_CPPFLAGS=1", cxx)
self.assertNotIn("-DVU_PAR_CFLAGS=1", cxx)
finally:
for var, val in garde.items():
if val is None:
os.environ.pop(var, None)
else:
os.environ[var] = val
if __name__ == "__main__":
unittest.main(verbosity=1)