erplibre/test/test_analyse_custom_field.py

213 lines
8 KiB
Python
Raw 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)
"""Champs hors module : l'attribution et le rendu, sans base.
Ce qui décide d'un blocage vit dans `collect()`, qui a besoin de PostgreSQL.
Il s'éprouve sur une base synthétique portant les cinq cas qui comptent :
* un champ Studio dont la colonne existe — rien à signaler ;
* un champ fait main SANS colonne — bloquant, le registre ne chargerait pas ;
* un one2many non stocké — PAS un bloquant, il n'a jamais de colonne ;
* un champ sur `ir.actions.act_window`, dont la table est `ir_act_window` —
`replace('.', '_')` crierait à la colonne manquante ;
* un many2one vers un modèle absent — bloquant.
Attendu : 6 champs, 2 bloquants, 1 modèle manuel. Rejoué ensuite sur la forme
12.0 — `company_dependent` retirée, `field_description` en text, pas de
`ir_model_fields_selection` — pour la même sortie.
"""
import unittest
from script.analyse import analyse_custom_field as A
from script.todo import todo_i18n
def field(**override):
row = {
"id": 1,
"model": "res.partner",
"name": "x_champ",
"ttype": "char",
"relation": None,
"store": True,
"xmlid_modules": None,
"origin": "handmade",
"blocker": None,
}
row.update(override)
return row
class TestFieldOrigin(unittest.TestCase):
def test_no_xmlid_at_all_means_hand_made(self):
# Un champ créé en mode développeur n'a AUCUNE ligne ir_model_data.
# C'est ce qui le distingue, pas son nom : il s'appelle « x_… » lui
# aussi.
self.assertEqual(A.field_origin(field(xmlid_modules=None)), "handmade")
self.assertEqual(A.field_origin(field(xmlid_modules=[])), "handmade")
def test_studio_module_means_studio(self):
self.assertEqual(
A.field_origin(field(xmlid_modules=["studio_customization"])),
"studio",
)
def test_studio_seen_among_several_xmlids(self):
# Un champ peut porter plusieurs identifiants externes. Une jointure
# plate n'en rendrait qu'un, choisi au hasard : Studio passerait
# inaperçu une fois sur deux. D'où l'agrégat côté SQL.
self.assertEqual(
A.field_origin(
field(xmlid_modules=["aaa", "studio_customization"])
),
"studio",
)
def test_a_module_xmlid_means_a_module(self):
self.assertEqual(
A.field_origin(field(xmlid_modules=["mon_module"])), "module"
)
def test_the_x_studio_prefix_is_never_the_only_signal(self):
# Le préfixe ressemble à Studio mais ne prouve rien : sans identifiant
# externe, le champ a été fait à la main.
self.assertEqual(
A.field_origin(field(name="x_studio_faux", xmlid_modules=None)),
"handmade",
)
class TestLabels(unittest.TestCase):
def setUp(self):
# PAS `set_lang()` : il PERSISTE la langue dans ./env_var.sh, un
# fichier suivi par git. Un test qui l'appelle modifie l'arbre de
# travail et laisse la langue changée pour tout ce qui suit —
# `_current_lang = None` ne défait que la mémoïsation, pas le
# fichier, et la résolution suivante relit celui-ci. On écrit donc
# la mémoïsation directement, et on rend la valeur trouvée.
self.addCleanup(
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
)
todo_i18n._current_lang = "en"
def test_every_origin_has_a_label(self):
for name in ("studio", "handmade", "module"):
self.assertNotEqual(A.origin_label(name), name)
def test_every_blocker_has_a_label(self):
for name in (
"missing_column",
"dangling_relation",
"model_gone",
"table_unknown",
):
self.assertNotEqual(A.blocker_label(name), name)
def test_unknown_key_falls_back_to_itself(self):
self.assertEqual(A.origin_label("inconnu"), "inconnu")
class TestNoColumnTypes(unittest.TestCase):
def test_to_many_fields_have_no_column_by_design(self):
# Le faux positif à ne pas produire : un one2many n'a jamais de
# colonne, il vit dans une table de relation. Le compter manquant
# ferait un bloquant sur chaque relation d'une base ordinaire.
self.assertIn("one2many", A.NO_COLUMN_TYPES)
self.assertIn("many2many", A.NO_COLUMN_TYPES)
self.assertNotIn("char", A.NO_COLUMN_TYPES)
self.assertNotIn("many2one", A.NO_COLUMN_TYPES)
class TestRender(unittest.TestCase):
def setUp(self):
# PAS `set_lang()` : il PERSISTE la langue dans ./env_var.sh, un
# fichier suivi par git. Un test qui l'appelle modifie l'arbre de
# travail et laisse la langue changée pour tout ce qui suit —
# `_current_lang = None` ne défait que la mémoïsation, pas le
# fichier, et la résolution suivante relit celui-ci. On écrit donc
# la mémoïsation directement, et on rend la valeur trouvée.
self.addCleanup(
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
)
todo_i18n._current_lang = "en"
def data(self, **override):
fields = [
field(id=1, name="x_studio_code", origin="studio"),
field(
id=2,
name="x_fait_main",
origin="handmade",
blocker="missing_column",
),
]
data = {
"tool": "analyse_custom_field",
"version": 1,
"database": "prod_18",
"odoo_version": "18.0.1.3",
"n_fields": 2,
"n_models": 1,
"counts": {
"studio": 1,
"handmade": 1,
"module": 0,
"blockers": 1,
"models": 1,
},
"fields": fields,
"models": [{"model": "x_contrat", "description": "Contrat"}],
"blockers": [fields[1]],
}
data.update(override)
return data
def test_blockers_come_first_and_say_why(self):
out = A.render(self.data())
self.assertIn("Blocking (1)", out)
self.assertIn("stored, but its column is missing", out)
self.assertLess(out.index("Blocking"), out.index("To carry over"))
def test_says_the_registry_will_not_load(self):
self.assertIn("stops the registry from loading", A.render(self.data()))
def test_no_blocker_block_when_there_is_none(self):
data = self.data(blockers=[])
data["counts"]["blockers"] = 0
self.assertNotIn("Blocking", A.render(data))
def test_counts_say_how_many_of_the_list_are_blocking(self):
# Les bloquants sont AUSSI dans la liste à reporter : le dire, sinon
# deux populations dont les comptes ne s'additionnent pas.
self.assertIn("(2, 1 blocking)", A.render(self.data()))
def test_clean_database_says_so(self):
data = self.data(
n_fields=0, n_models=0, fields=[], models=[], blockers=[]
)
out = A.render(data)
self.assertIn("No field or model was added outside a module.", out)
def test_says_nothing_will_recreate_them(self):
# La raison d'être de l'outil : ces champs ne sont dans aucun fichier.
self.assertIn("no module will recreate", A.render(self.data()))
def test_hint_is_absent_in_verbose_and_in_the_menu(self):
self.assertIn("Use -v", A.render(self.data()))
self.assertNotIn("Use -v", A.render(self.data(), verbose=True))
self.assertNotIn("Use -v", A.render(self.data(), hints=False))
def test_french_differs(self):
english = A.render(self.data())
todo_i18n._current_lang = "fr" # cf. setUp : pas de persistance
french = A.render(self.data())
self.assertIn("Fait à la main", french)
self.assertNotEqual(english, french)
if __name__ == "__main__":
unittest.main()