erplibre/test/test_check_cow_views_render.py
Mathieu Benoit 32f62fda04 [ADD] migration: prédire la copie de site qui rendra 500 après le palier
`check_cow_views` ne voyait que la rupture qui ARRÊTE la migration : la
forme que l'arch doit avoir change. Deux autres la laissent finir sans un
mot — un ancrage qu'une vue héritière de la cible réclame, un t-call vers
un gabarit que la cible ne livre plus — et ne se voient qu'à l'ouverture
de la page, c'est-à-dire jamais avant la fin.

Rejouée sur la chaîne 12 → 18, la prédiction nomme le défaut de /contact
dès le palier 13 → 14, en 0,8 s. Il avait attendu six paliers.

Ces copies ne sont PAS neutralisées : chacune porte une page écrite par
quelqu'un. Le pilote appelle la réparation après OpenUpgrade, là où la
vue module porte enfin l'arch de la cible.

--- EN ---

`check_cow_views` only saw the break that STOPS the migration: the shape
the arch must have changes. Two others let it finish without a word — an
anchor a target child view requires, a t-call to a template the target no
longer ships — and only show when the page is opened, which is to say
never before the end.

Replayed on the 12 → 18 chain, the prediction names /contact's defect as
early as the 13 → 14 step, in 0.8 s. It had waited six steps.

Those copies are NOT neutralized: each holds a page someone wrote. The
driver calls the repair after OpenUpgrade, where the module view finally
carries the target's arch.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00

266 lines
8.7 KiB
Python

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Prédire, AVANT le palier, la copie qui rendra 500 après.
`check_cow_views` cherchait la rupture qui ARRÊTE la migration : la forme
que l'arch doit avoir change. Deux autres la laissent finir et ne se
voient qu'à l'ouverture de la page — et personne n'ouvre les pages avant
la fin.
Mesuré sur une chaîne 12 → 18 réelle : /contact rendait 500 depuis le
palier 14 → 15. Rejouée avec ce contrôle, la prédiction nomme le défaut
dès le palier 13 → 14, cinq paliers avant que quiconque s'en aperçoive.
Ces copies-là ne se NEUTRALISENT pas : chacune porte une page écrite par
quelqu'un, et la mettre de côté l'effacerait du site. Elles se réparent.
"""
import os
import shutil
import sys
import tempfile
import unittest
sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
)
sys.path.append(
os.path.normpath(
os.path.join(
os.path.dirname(__file__), "..", "script", "odoo", "migration"
)
)
)
import check_cow_views as cow # noqa: E402
# La copie du client : sa page, sans l'ancrage que la cible attend, et
# avec un appel vers un gabarit que la cible ne livre plus.
ARCH_COPIE = (
"<t t-name='website.contactus'>"
"<t t-call='website.layout'>"
"<div id='wrap'><h1>Parlons de votre projet</h1>"
"<t t-call='website.company_description'/></div>"
"</t></t>"
)
MODULE_ENFANT = """<odoo>
<template id="contactus_form" inherit_id="website.contactus">
<xpath expr="//t[@t-set='contactus_form_values']" position="after">
<t t-set="autre" t-value="2"/>
</xpath>
</template>
</odoo>
"""
MODULE_PARENT = """<odoo>
<template id="contactus" name="Contact">
<t t-call="website.layout"><div id="wrap"/></t>
</template>
<template id="layout" name="Layout"><div/></template>
</odoo>
"""
class TestFullKey(unittest.TestCase):
def test_a_bare_id_takes_its_module(self):
self.assertEqual(
cow.full_key("website", "contactus"), "website.contactus"
)
def test_a_qualified_id_is_left_alone(self):
self.assertEqual(
cow.full_key("website_crm", "website.contactus"),
"website.contactus",
)
def test_nothing_stays_nothing(self):
self.assertIsNone(cow.full_key("website", ""))
self.assertIsNone(cow.full_key("website", None))
class TestScanningTheTargetSources(unittest.TestCase):
def setUp(self):
self.racine = tempfile.mkdtemp(prefix="el_cow_")
self.addCleanup(shutil.rmtree, self.racine, True)
for module, contenu in (
("website", MODULE_PARENT),
("website_crm", MODULE_ENFANT),
):
chemin = os.path.join(self.racine, "odoo", "addons", module)
os.makedirs(chemin)
with open(
os.path.join(chemin, "views.xml"), "w", encoding="utf-8"
) as f:
f.write(contenu)
def scan(self, modules=("website", "website_crm")):
return cow.scan_target_views(self.racine, list(modules))
def test_it_lists_what_the_target_ships(self):
declares, _ = self.scan()
self.assertIn("website.contactus", declares)
self.assertIn("website.layout", declares)
def test_it_maps_a_child_xpath_to_the_parent_it_needs(self):
# C'est LE renseignement qui manquait : quelle vue de la cible
# exige quel ancrage, et dans quel parent.
_, heritages = self.scan()
self.assertEqual(
heritages["website.contactus"],
["//t[@t-set='contactus_form_values']"],
)
def test_a_module_absent_from_the_target_is_skipped(self):
declares, _ = self.scan(["website", "jamais_livre"])
self.assertIn("website.contactus", declares)
def test_a_template_without_xpath_adds_no_requirement(self):
_, heritages = self.scan(["website"])
self.assertEqual(heritages, {})
class TestWillNotRender(unittest.TestCase):
"""Le verdict, en remplaçant ce qui touche la base et le disque."""
def setUp(self):
self.vrai = (
cow.query_cow_archs,
cow.installed_modules,
cow.scan_target_views,
)
def tearDown(self):
(
cow.query_cow_archs,
cow.installed_modules,
cow.scan_target_views,
) = self.vrai
def poser(self, copies, declares, heritages):
cow.query_cow_archs = lambda d: copies
cow.installed_modules = lambda d: ["website"]
cow.scan_target_views = lambda v, m: (declares, heritages)
def copie(self, arch=ARCH_COPIE, langues=None):
return [
(
1228,
"website.contactus",
"primary",
1,
langues or {"en_US": arch},
)
]
def test_it_names_the_anchor_a_target_child_will_need(self):
self.poser(
self.copie(),
{
"website.contactus",
"website.layout",
"website.company_description",
},
{"website.contactus": ["//t[@t-set='contactus_form_values']"]},
)
risques = cow.will_not_render("db", "odoo18.0")
self.assertEqual(1, len(risques), risques)
self.assertIn("contactus_form_values", risques[0][5])
def test_an_anchor_the_copy_already_has_is_not_reported(self):
arch = ARCH_COPIE.replace(
"<div id='wrap'>",
"<t t-set='contactus_form_values' t-value='1'/><div id='wrap'>",
)
self.poser(
self.copie(arch),
{
"website.contactus",
"website.layout",
"website.company_description",
},
{"website.contactus": ["//t[@t-set='contactus_form_values']"]},
)
self.assertEqual([], cow.will_not_render("db", "odoo18.0"))
def test_it_names_a_template_the_target_no_longer_ships(self):
self.poser(
self.copie(),
{"website.contactus", "website.layout"},
{},
)
risques = cow.will_not_render("db", "odoo18.0")
self.assertEqual(1, len(risques), risques)
self.assertIn("company_description", risques[0][5])
def test_another_copy_counts_as_a_known_template(self):
# Un gabarit peut n'exister qu'en base : une copie de site qui en
# appelle une autre n'est pas cassée pour autant.
copies = self.copie() + [
(
99,
"website.company_description",
"primary",
1,
{"en_US": "<t/>"},
)
]
self.poser(copies, {"website.contactus", "website.layout"}, {})
self.assertEqual([], cow.will_not_render("db", "odoo18.0"))
def test_a_copy_broken_in_one_language_only_is_still_reported(self):
# Le site rend dans SA langue : réparer l'anglais et laisser le
# français cassé donne une page en 500 et un rapport vert.
sain = ARCH_COPIE.replace(
"<div id='wrap'>",
"<t t-set='contactus_form_values' t-value='1'/><div id='wrap'>",
)
self.poser(
self.copie(langues={"en_US": sain, "fr_CA": ARCH_COPIE}),
{
"website.contactus",
"website.layout",
"website.company_description",
},
{"website.contactus": ["//t[@t-set='contactus_form_values']"]},
)
risques = cow.will_not_render("db", "odoo18.0")
self.assertEqual(1, len(risques), risques)
def test_a_copy_without_a_key_is_left_alone(self):
self.poser(
[(7, "", "primary", 1, {"en_US": ARCH_COPIE})],
{"website.company_description"},
{},
)
self.assertEqual([], cow.will_not_render("db", "odoo18.0"))
def test_no_copy_at_all_is_not_an_error(self):
self.poser([], set(), {})
self.assertEqual([], cow.will_not_render("db", "odoo18.0"))
class TestItStaysOutOfTheNeutralizer(unittest.TestCase):
"""Ces copies se réparent ; les neutraliser effacerait la page."""
def test_the_neutralizer_only_acts_on_the_shape_bucket(self):
with open(
os.path.join(
os.path.dirname(__file__),
"..",
"script",
"odoo",
"migration",
"neutralize_cow_views.py",
),
encoding="utf-8",
) as handle:
source = handle.read()
self.assertIn("lst_at_risk, _, _ = analyse(", source)
self.assertNotIn("will_not_render", source)
if __name__ == "__main__":
unittest.main()