erplibre/test/test_reset_stale_cow_tui.py

176 lines
6.2 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)
"""Lire une copie COW avant d'accepter de la perdre, en plein écran.
Le rapport texte met à mille lignes d'écart les deux choses qui décident :
ce que la copie porte en propre — la seule chose qu'une réinitialisation
abandonne — et quel enfant ne trouve plus son ancrage, qui est la raison
pour laquelle quoi que ce soit casse. L'espace bascule entre les deux.
La commande de réparation prend une CLÉ. La recopier à la main depuis un
diff défilé est l'endroit où un caractère se perd, et une clé sans
correspondance n'est pas une erreur pour l'outil : il tourne et ne fait
rien. D'où « c ».
"""
import os
import sys
import unittest
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "script", "odoo", "migration"))
import reset_stale_cow_tui as tui # noqa: E402
import reset_stale_cow_views as reset # noqa: E402
def finding(**override):
cow = {
"id": 2841,
"key": "website_blog.blog_post_complete",
"website_id": 1,
"arch": "<t>\n <div/>\n <span>vieux</span>\n</t>",
}
module = {
"id": 1826,
"key": "website_blog.blog_post_complete",
"website_id": None,
"arch": "<t>\n <div/>\n <section id='o_wblog_post_footer'/>\n</t>",
}
broken = [(3288, "//section[@id='o_wblog_post_footer']")]
if "cow" in override:
cow.update(override["cow"])
return cow, override.get("module", module), override.get("broken", broken)
class TestBothScreens(unittest.TestCase):
def setUp(self):
from script.todo import todo_i18n
self.addCleanup(
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
)
todo_i18n._current_lang = "en"
def test_the_diff_shows_what_the_copy_holds(self):
cow, module, _broken = finding()
text = reset.render_diff(module, cow, indent=" ")
self.assertIn("vieux", text)
self.assertIn("o_wblog_post_footer", text)
def test_the_why_screen_names_the_failing_child(self):
# Sans lui, on lit un diff sans savoir pourquoi il compte.
text = tui.render_broken(*finding())
self.assertIn("3288", text)
self.assertIn("//section[@id='o_wblog_post_footer']", text)
def test_the_why_screen_says_what_a_reset_costs(self):
text = tui.render_broken(*finding())
self.assertIn("INHERITING", text)
def test_a_copy_without_broken_child_is_said_so(self):
# Elle a dérivé sans rien casser ENCORE : le taire ferait croire à
# une erreur de détection.
cow, module, _ = finding()
text = tui.render_broken(cow, module, [])
self.assertIn("without", text)
def test_the_two_screens_use_the_same_diff_as_the_cli(self):
# Deux rendus séparés dériveraient sans que rien ne le signale.
import inspect
source = inspect.getsource(tui)
self.assertIn("from reset_stale_cow_views import", source)
self.assertIn("render_diff", source)
class TestTheTriageColumn(unittest.TestCase):
def test_the_weight_counts_both_directions(self):
cow, module, _ = finding()
self.assertEqual(tui.weight(cow, module), "+1/-1")
def test_no_module_twin_has_no_weight(self):
cow, _module, _ = finding()
self.assertEqual(tui.weight(cow, None), "—")
class TestTheRefusalIsNeverSilent(unittest.TestCase):
"""Un refus muet ferait réafficher le rapport texte sans le dire."""
def test_a_pipe_is_explained(self):
import io
from contextlib import redirect_stdout
out = io.StringIO()
with redirect_stdout(out):
result = tui.run_tui([finding()], "db")
self.assertFalse(result)
self.assertTrue(out.getvalue().strip(), "refus muet")
def test_nothing_to_show_stays_silent(self):
import io
from contextlib import redirect_stdout
out = io.StringIO()
with redirect_stdout(out):
self.assertFalse(tui.run_tui([], "db"))
self.assertEqual(out.getvalue(), "")
class TestTheToolAndTheMigrationOfferIt(unittest.TestCase):
def test_the_tool_has_a_tui_flag(self):
import subprocess
done = subprocess.run(
[
sys.executable,
"./script/odoo/migration/reset_stale_cow_views.py",
"--help",
],
capture_output=True,
text=True,
cwd=REPO,
)
self.assertEqual(done.returncode, 0)
self.assertIn("--tui", done.stdout)
def test_the_error_prompt_offers_it_as_option_four(self):
import inspect
from script.todo.todo_upgrade import TodoUpgrade
[ADD] migration: repair views whose stored type contradicts their parent Odoo refused to load a database: it validated a <search> arch with tree rules. The view is not a COW copy -- the COW tools were right to say they had nothing to reset -- its stored `type` simply lies. Nothing would ever have fixed it. In ir_ui_view.py, `type` is filled in `create`, and only when absent; `write` never recomputes it. A wrong value stays wrong, and `-u module` fails on the validation it causes before it could rewrite anything. Plain SQL, no ORM: the registry is what will not load, and a repair that needed Odoo to fix what stops Odoo would be useless. The rule is Odoo's own -- an inherited view takes its parent's type -- and it holds: zero disagreement on a pristine 18 install and on four migrated databases, exactly one on the database that refused to load. --- FR --- Odoo refusait de charger une base : il validait un arch <search> avec les règles d'un tree. La vue n'est pas une copie COW — les outils COW avaient raison de dire qu'ils n'avaient rien à réinitialiser — c'est son `type` stocké qui ment. Rien ne l'aurait jamais réparé. Dans ir_ui_view.py, `type` est rempli dans `create`, et seulement s'il est absent ; `write` ne le recalcule jamais. Une valeur fausse le reste, et `-u module` échoue sur la validation qu'elle provoque avant de pouvoir réécrire quoi que ce soit. En SQL, sans ORM : c'est le registre qui ne charge plus, et une réparation qui aurait besoin d'Odoo ne servirait à rien. La règle est celle d'Odoo — une vue héritée prend le type de son parent — et elle tient : zéro écart sur une 18 neuve et sur quatre bases migrées, exactement un sur celle qui refusait de charger. Assisted-by: Claude Opus 5
2026-08-22 00:46:42 -04:00
# Le menu d'erreur vit dans `_prompt_on_error`, extrait de
# `todo_upgrade_execute` quand celui-ci a passé le seuil de
# complexité. Lire les deux : c'est le CHEMIN d'erreur qu'on
# éprouve, pas une méthode en particulier.
source = inspect.getsource(
TodoUpgrade.todo_upgrade_execute
) + inspect.getsource(TodoUpgrade._prompt_on_error)
self.assertIn("Browse the differences full screen", source)
self.assertIn('wait_status == "4"', source)
def test_it_gets_a_real_terminal(self):
# Un plein écran lancé par l'exécuteur qui capture retomberait sur
# le rapport texte, sans que rien ne distingue les deux.
import inspect
from script.todo.todo_upgrade import TodoUpgrade
[ADD] migration: repair views whose stored type contradicts their parent Odoo refused to load a database: it validated a <search> arch with tree rules. The view is not a COW copy -- the COW tools were right to say they had nothing to reset -- its stored `type` simply lies. Nothing would ever have fixed it. In ir_ui_view.py, `type` is filled in `create`, and only when absent; `write` never recomputes it. A wrong value stays wrong, and `-u module` fails on the validation it causes before it could rewrite anything. Plain SQL, no ORM: the registry is what will not load, and a repair that needed Odoo to fix what stops Odoo would be useless. The rule is Odoo's own -- an inherited view takes its parent's type -- and it holds: zero disagreement on a pristine 18 install and on four migrated databases, exactly one on the database that refused to load. --- FR --- Odoo refusait de charger une base : il validait un arch <search> avec les règles d'un tree. La vue n'est pas une copie COW — les outils COW avaient raison de dire qu'ils n'avaient rien à réinitialiser — c'est son `type` stocké qui ment. Rien ne l'aurait jamais réparé. Dans ir_ui_view.py, `type` est rempli dans `create`, et seulement s'il est absent ; `write` ne le recalcule jamais. Une valeur fausse le reste, et `-u module` échoue sur la validation qu'elle provoque avant de pouvoir réécrire quoi que ce soit. En SQL, sans ORM : c'est le registre qui ne charge plus, et une réparation qui aurait besoin d'Odoo ne servirait à rien. La règle est celle d'Odoo — une vue héritée prend le type de son parent — et elle tient : zéro écart sur une 18 neuve et sur quatre bases migrées, exactement un sur celle qui refusait de charger. Assisted-by: Claude Opus 5
2026-08-22 00:46:42 -04:00
# Le menu d'erreur vit dans `_prompt_on_error`, extrait de
# `todo_upgrade_execute` quand celui-ci a passé le seuil de
# complexité. Lire les deux : c'est le CHEMIN d'erreur qu'on
# éprouve, pas une méthode en particulier.
source = inspect.getsource(
TodoUpgrade.todo_upgrade_execute
) + inspect.getsource(TodoUpgrade._prompt_on_error)
start = source.index('wait_status == "4"')
window = source[start : start + 620]
self.assertIn("run_on_terminal", window)
self.assertIn("--tui", window)
if __name__ == "__main__":
unittest.main()