[REM] résidus : retirer le contrôle res_lang.active à NULL
Le contrôle jugeait cassée une langue dont `active` est NULL : listée
nulle part, plus réactivable. Faux dans la source 18 — un domaine
('active','=',False) compile en (IS NULL OR = FALSE) (models.py:3217),
l'action du menu Langues porte active_test: False, le tri passe par
COALESCE(active, FALSE) et la lecture rend bool(value).
Odoo écrit ce NULL : active = fields.Boolean() sans défaut
(res_lang.py:64) et un res.lang.csv sans la colonne, soit un NULL par
langue ajoutée au catalogue. « Zéro avant, non nul après » ne suffit
donc pas. Un test refuse désormais une clé que plus rien ne définit.
--- EN ---
The check called a language broken when `active` is NULL: listed
nowhere, no longer re-enablable. False in the 18 source — an
('active','=',False) domain compiles to (IS NULL OR = FALSE)
(models.py:3217), the Languages menu action carries active_test: False,
sorting goes through COALESCE(active, FALSE), reading returns bool(value).
Odoo writes that NULL itself: active = fields.Boolean() with no default
(res_lang.py:64) and a res.lang.csv without the column, one NULL per
language added to the catalogue. So "zero before, nonzero after" is not
enough. A test now refuses a key that nothing defines any more.
Assisted-by: Claude Opus 5
(cherry picked from commit ee932334cfde63c1af831316549a432c3d382721)
This commit is contained in:
parent
c95cf2e9d8
commit
33cf04c7f9
2 changed files with 61 additions and 18 deletions
|
|
@ -27,7 +27,6 @@ finit par être ignoré en entier.
|
|||
Ne restent ici que les constats qui se jugent SANS point de comparaison,
|
||||
parce qu'ils sont faux en eux-mêmes. Les mêmes bases, mêmes mesures :
|
||||
|
||||
res_lang.active à NULL 0 → 9 un booléen NULL est un bug
|
||||
ir_model_relation sans table 0 → 68 la table m2m est nommée,
|
||||
elle n'existe pas
|
||||
index doublés convention 17 0 → 414 Odoo 17 renomme, sans
|
||||
|
|
@ -35,9 +34,30 @@ parce qu'ils sont faux en eux-mêmes. Les mêmes bases, mêmes mesures :
|
|||
liste de prix par défaut absente 0 → 1 `product` installé, son
|
||||
xmlid pas là
|
||||
|
||||
Zéro avant, non nul après : aucun de ces trois ne peut s'expliquer
|
||||
Zéro avant, non nul après : aucun de ceux-là ne peut s'expliquer
|
||||
autrement que par la migration.
|
||||
|
||||
« Zéro avant, non nul après » ne suffit pourtant pas
|
||||
----------------------------------------------------
|
||||
Un quatrième contrôle a vécu ici et n'y est plus : `res_lang.active` à
|
||||
NULL, 0 avant et 9 après. Le chiffre était juste, la conclusion fausse.
|
||||
|
||||
Mesuré palier par palier : 0, 1, 2, 3, 5, 8, 9 — les NULL arrivent avec
|
||||
les langues que CHAQUE version ajoute au catalogue. Et c'est Odoo
|
||||
lui-même qui les écrit : `active = fields.Boolean()` sans défaut
|
||||
(res_lang.py:64) et un `res.lang.csv` sans colonne `active` — l'INSERT ne
|
||||
porte pas la colonne, PostgreSQL y met NULL.
|
||||
|
||||
Aucune conséquence, vérifiée dans la source de la 18 : un domaine
|
||||
`('active','=',False)` compile en `(IS NULL OR = FALSE)`
|
||||
(models.py:3217-3222), l'action du menu Langues porte `active_test: False`
|
||||
(res_lang_views.xml:136), le tri passe par `COALESCE(active, FALSE)`
|
||||
(models.py:5692) et la lecture rend `bool(value)` (fields.py:1515). NULL
|
||||
et FALSE sont indiscernables partout.
|
||||
|
||||
La leçon : la croissance mesurée doit AUSSI être inexplicable autrement.
|
||||
Ici elle s'expliquait très bien.
|
||||
|
||||
Un quatrième a été RETIRÉ après vérification
|
||||
--------------------------------------------
|
||||
« ir_model_relation nomme une table absente » : 0 avant, 68 après, le
|
||||
|
|
@ -107,15 +127,6 @@ CONTROLES = (
|
|||
"gravity": "broken",
|
||||
"repair": "script/todo/todo_upgrade.py (uninstall_one_by_one)",
|
||||
},
|
||||
{
|
||||
"key": "lang_active_null",
|
||||
"title": "Languages whose active flag is NULL",
|
||||
"why": "A boolean that is neither true nor false: the language is"
|
||||
" listed nowhere and cannot be re-enabled from the interface.",
|
||||
"sql": "SELECT count(*) FROM res_lang WHERE active IS NULL",
|
||||
"gravity": "broken",
|
||||
"repair": None,
|
||||
},
|
||||
{
|
||||
"key": "duplicate_index",
|
||||
"title": "Indexes duplicated by the Odoo 17 renaming",
|
||||
|
|
@ -189,7 +200,7 @@ CONTROLES = (
|
|||
"sql": "SELECT count(*) FROM ir_model_data d WHERE NOT EXISTS"
|
||||
" (SELECT 1 FROM ir_model m WHERE m.model = d.model)",
|
||||
"gravity": "broken",
|
||||
"repair": "script/analyse/database_cleanup.py",
|
||||
"repair": "script/odoo/migration/database_cleanup.py",
|
||||
},
|
||||
{
|
||||
"key": "attachment_field_gone",
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
)
|
||||
REPO = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, REPO)
|
||||
|
||||
from script.analyse import check_migration_residue as residue # noqa: E402
|
||||
from script.analyse import monitoring # noqa: E402
|
||||
|
|
@ -190,9 +189,9 @@ class TestWhatAMigrationLeftBehind(unittest.TestCase):
|
|||
def test_broken_is_read_before_watch(self):
|
||||
resultats = {c["key"]: 0 for c in residue.CONTROLES}
|
||||
resultats["duplicate_index"] = 400 # watch
|
||||
resultats["lang_active_null"] = 1 # broken
|
||||
resultats["stuck_modules"] = 1 # broken
|
||||
trouve, _ = residue.judge(resultats)
|
||||
self.assertEqual(trouve[0][0]["key"], "lang_active_null")
|
||||
self.assertEqual(trouve[0][0]["key"], "stuck_modules")
|
||||
|
||||
def test_a_check_that_could_not_run_is_not_a_check_that_found_nothing(
|
||||
self,
|
||||
|
|
@ -247,7 +246,7 @@ class TestWhatAMigrationLeftBehind(unittest.TestCase):
|
|||
def test_the_exit_code_separates_nothing_from_something(self):
|
||||
vide = {c["key"]: 0 for c in residue.CONTROLES}
|
||||
self.assertEqual(residue.judge(vide)[0], [])
|
||||
plein = dict(vide, lang_active_null=3)
|
||||
plein = dict(vide, stuck_modules=3)
|
||||
self.assertTrue(residue.judge(plein)[0])
|
||||
|
||||
|
||||
|
|
@ -451,6 +450,39 @@ def verdict(**champs):
|
|||
return brut
|
||||
|
||||
|
||||
class TestTheControlsThemselves(unittest.TestCase):
|
||||
"""Un contrôle retiré ne doit pas survivre dans les tests.
|
||||
|
||||
« res_lang.active à NULL » a vécu ici avec la gravité « broken », et
|
||||
c'était un faux constat : Odoo écrit lui-même ce NULL, faute de défaut
|
||||
sur `active = fields.Boolean()`. Deux tests le nommaient encore après
|
||||
son retrait — ils passaient, en construisant un dictionnaire avec une
|
||||
clé inconnue de personne.
|
||||
"""
|
||||
|
||||
def test_every_key_a_test_names_still_exists(self):
|
||||
import re
|
||||
|
||||
connues = {c["key"] for c in residue.CONTROLES}
|
||||
source = io.open(__file__, encoding="utf-8").read()
|
||||
nommees = set(re.findall(r'resultats\["([a-z_]+)"\]', source))
|
||||
nommees |= set(re.findall(r"dict\(vide, ([a-z_]+)=", source))
|
||||
self.assertTrue(nommees)
|
||||
self.assertEqual(set(), nommees - connues)
|
||||
|
||||
def test_no_control_promises_a_repair_tool_that_is_missing(self):
|
||||
import os
|
||||
|
||||
for controle in residue.CONTROLES:
|
||||
morceaux = (controle["repair"] or "").split()
|
||||
chemin = morceaux[0] if morceaux else ""
|
||||
if chemin.startswith("script/"):
|
||||
self.assertTrue(
|
||||
os.path.isfile(os.path.join(REPO, chemin)),
|
||||
f"{controle['key']} → {chemin}",
|
||||
)
|
||||
|
||||
|
||||
class TestTheVerdictsSection(unittest.TestCase):
|
||||
"""Ce que les contrôles SQL ne peuvent structurellement pas voir.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue