erplibre/test/test_openupgrade_analysis.py

187 lines
7.1 KiB
Python
Raw Permalink 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)
"""Lire ce qu'OpenUpgrade déclare, sans le déformer.
La catégorie qui porte tout l'outil est « unstored » : le champ EXISTE
encore, il n'a plus de colonne parce qu'il est calculé. Le ranger avec
les suppressions ferait crier au loup à chaque palier ; l'oublier
masquerait une vraie perte.
"""
import os
import shutil
import sys
import tempfile
import unittest
sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
)
from script.analyse import openupgrade_analysis as oa # noqa: E402
EXEMPLE = """---Models in module 'account'---
obsolete model account.unreconcile [transient]
new model account.code.mapping [sql_view]
new model product.combo (renamed from pos.combo in module point_of_sale)
---Fields in module 'account'---
account / account.account / _order : _order is now 'code' ('id')
account / account.account / code (char) : not stored anymore
account / account.account / code_store (char) : NEW
account / account.journal / secure_sequence_id (many2one) : DEL relation: ir.sequence
account / account.account / company_id (many2one) : is now stored
account / account.cash.rounding / loss_account_id (many2one) : needs conversion to v18-style company dependent
account / pos.combo / base_price (float) : module is now 'product' ('point_of_sale')
account / account.journal / type (selection) : selection_keys added: [credit]
---XML records in module 'account'---
NEW ir.ui.view: account.view_x
DEL ir.ui.view: account.view_y
DEL ir.ui.view: account.view_z
"""
class TestParsing(unittest.TestCase):
def setUp(self):
self.index = oa.parse(EXEMPLE)
def test_obsolete_and_new_models(self):
self.assertIn("account.unreconcile", self.index["models_obsolete"])
self.assertIn("account.code.mapping", self.index["models_new"])
def test_a_rename_is_read_from_the_NEW_side(self):
# OpenUpgrade ne note le renommage que sur le nouveau modèle ;
# l'ancien n'apparaît jamais comme « obsolete ». Sans cette
# lecture, sa disparition passerait pour une perte sèche.
self.assertEqual(
self.index["models_renamed"].get("pos.combo"), "product.combo"
)
def test_each_field_lands_in_its_category(self):
for cle, categorie in (
("account.account.code", "fields_unstored"),
("account.account.code_store", "fields_new"),
("account.journal.secure_sequence_id", "fields_del"),
("account.account.company_id", "fields_stored"),
(
"account.cash.rounding.loss_account_id",
"fields_company_dependent",
),
):
self.assertIn(cle, self.index[categorie], f"{cle} → {categorie}")
def test_a_moved_field_keeps_its_new_module(self):
self.assertEqual(
self.index["fields_moved"].get("pos.combo.base_price"), "product"
)
def test_pseudo_fields_are_not_fields(self):
# `_order` n'est pas dans `ir_model_fields` : le compter fausserait
# le rapprochement avec la base.
for cle in list(self.index["fields_other"]) + list(
self.index["fields_new"]
):
self.assertNotIn("._order", cle)
def test_an_unclassified_change_is_kept_not_dropped(self):
# « selection_keys added » n'a pas de catégorie : le perdre
# silencieusement ferait mentir les totaux.
self.assertIn("account.journal.type", self.index["fields_other"])
def test_xml_records_are_counted(self):
self.assertEqual(self.index["xml_new"], 1)
self.assertEqual(self.index["xml_del"], 2)
class TestClassify(unittest.TestCase):
def test_new_with_details_is_still_new(self):
self.assertEqual(
oa.classify("NEW relation: res.company, required"), "new"
)
def test_not_stored_is_not_confused_with_stored(self):
# « not stored anymore » CONTIENT « stored » : tester par
# appartenance de sous-chaîne inverserait le sens.
self.assertEqual(oa.classify("not stored anymore"), "unstored")
self.assertEqual(oa.classify("is now stored"), "stored")
def test_an_unknown_wording_falls_back_to_other(self):
self.assertEqual(oa.classify("something brand new"), "other")
class TestFindingTheFiles(unittest.TestCase):
def setUp(self):
self.racine = tempfile.mkdtemp()
base = os.path.join(
self.racine,
"odoo18.0",
"OCA_OpenUpgrade",
"openupgrade_scripts",
"scripts",
)
for module, release in (
("account", "18.0.1.0"),
("account", "17.0.1.0"),
("sale", "18.0.2.0"),
("tests", "18.0.1.0"),
):
dossier = os.path.join(base, module, release)
os.makedirs(dossier)
with open(
os.path.join(dossier, "upgrade_analysis.txt"),
"w",
encoding="utf-8",
) as handle:
handle.write(EXEMPLE)
def tearDown(self):
shutil.rmtree(self.racine)
def test_only_the_target_version_is_read(self):
# À côté des `18.0.x` vivent des dossiers d'autres paliers : les
# mélanger attribuerait à la 18 des changements de la 17.
trouves = oa.analysis_files(18, self.racine)
self.assertTrue(all("18.0" in c for c in trouves), trouves)
self.assertEqual(len(trouves), 3)
def test_a_missing_checkout_is_empty_not_a_crash(self):
self.assertEqual(oa.analysis_files(13, self.racine), [])
self.assertEqual(oa.load(13, self.racine)["modules"], 0)
def test_the_module_list_comes_from_the_directories(self):
modules = oa.analysed_modules(18, self.racine)
self.assertIn("account", modules)
self.assertIn("sale", modules)
def test_loading_counts_the_modules_it_read(self):
self.assertEqual(oa.load(18, self.racine)["modules"], 3)
class TestTheRealCheckout(unittest.TestCase):
"""Le dépôt lui-même, quand il est là. Sinon on ne prétend rien."""
def setUp(self):
if not oa.analysis_files(18):
self.skipTest("OCA_OpenUpgrade absent du checkout 18.0")
def test_it_reads_hundreds_of_core_modules(self):
index = oa.load(18)
self.assertGreater(index["modules"], 300)
def test_it_finds_the_company_dependent_conversion(self):
# C'est la refonte qui a fait disparaître `ir_property` en 18 :
# si l'outil ne la voit pas, il ne sert à rien sur ce palier-là.
index = oa.load(18)
self.assertTrue(
any(
cle.startswith("account.cash.rounding.")
for cle in index["fields_company_dependent"]
),
sorted(index["fields_company_dependent"])[:5],
)
if __name__ == "__main__":
unittest.main()