[FIX] migration: exit 1 means findings, not failure, for the view fixer

Entry [5] never asked "Correct them?". The tool ran, printed its
diagnosis, exited 1 -- and todo_upgrade_execute read that as a failure
and reopened ITS error menu on top of ours. The menu looped and nothing
was ever corrected.

wait_at_error=False on both calls, as the COW tool next door already
does. The fake in the test now models the real contract: it refuses to
return a non-zero status when the flag is missing, exactly as the real
one refuses by recursing into its prompt.

--- FR ---

L'entrée [5] ne posait jamais la question « Les corriger ? ». L'outil
tournait, affichait son diagnostic, sortait 1 — et todo_upgrade_execute
y lisait un échec et rouvrait SON menu d'erreur par-dessus le nôtre. Le
menu tournait en rond et rien n'était corrigé.

wait_at_error=False sur les deux appels, comme le fait déjà l'outil COW
juste à côté. Le faux du test modèle désormais le vrai contrat : il
refuse de rendre un statut non nul quand le drapeau manque, comme le
vrai refuse en repartant dans son invite.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-22 00:57:46 -04:00
parent 068022db82
commit 89ef6a97fd
2 changed files with 78 additions and 2 deletions

View file

@ -2956,8 +2956,14 @@ class TodoUpgrade:
pour réparer ce qui empêche Odoo de démarrer ne servirait à rien.
"""
outil = os.path.join(PATH_MIGRATION_GLOBAL, "fix_view_type.py")
# `wait_at_error=False` est OBLIGATOIRE : pour cet outil, 1 veut
# dire « des trouvailles », pas « échec ». Sans ce drapeau,
# `todo_upgrade_execute` y lit une panne et rouvre SON menu
# d'erreur par-dessus le nôtre — mesuré : la question « Les
# corriger ? » n'était jamais posée et le menu tournait en rond.
status, _cmd = self.todo_upgrade_execute(
f"{PYTHON_BIN} ./{outil} -d {database}"
f"{PYTHON_BIN} ./{outil} -d {database}",
wait_at_error=False,
)
if status != 1:
# 0 : rien à corriger. 2 : l'outil a échoué. Ni l'un ni
@ -2971,7 +2977,8 @@ class TodoUpgrade:
if reponse not in ("y", "yes", "o"):
return False
status, _cmd = self.todo_upgrade_execute(
f"{PYTHON_BIN} ./{outil} -d {database} --apply"
f"{PYTHON_BIN} ./{outil} -d {database} --apply",
wait_at_error=False,
)
return status == 0

View file

@ -241,6 +241,75 @@ class TestTheWiring(unittest.TestCase):
)
class TestTheToolIsCalledWithTheRightContract(unittest.TestCase):
"""Pour cet outil, 1 veut dire « trouvailles », pas « échec ».
`todo_upgrade_execute` ouvre SON menu d'erreur sur tout statut non
nul. Sans `wait_at_error=False`, ce menu se superpose au nôtre : la
question « Les corriger ? » n'est jamais posée, et l'on tourne en
rond. C'est arrivé en vrai, sur une migration bloquée.
"""
def setUp(self):
from script.todo import auto_ask
from script.todo.todo_upgrade import TodoUpgrade
self.auto_ask = auto_ask
self.vrai_ask = auto_ask.ask
self.obj = TodoUpgrade.__new__(TodoUpgrade)
self.appels = []
def faux(cmd, **kw):
self.appels.append((cmd, kw))
statut = 0 if "--apply" in cmd else 1
if statut and kw.get("wait_at_error", True):
# Ce que fait le VRAI : il n'en revient pas avec le
# statut, il repart dans son propre menu.
raise AssertionError(
"menu d'erreur imbriqué : wait_at_error manquant"
)
return statut, cmd
self.obj.todo_upgrade_execute = faux
self.obj.ask = lambda prompt, default="": "y"
def tearDown(self):
self.auto_ask.ask = self.vrai_ask
def test_the_report_call_does_not_reopen_the_error_menu(self):
with redirect_stdout(io.StringIO()):
self.obj.prompt_fix_view_type("db")
self.assertTrue(self.appels)
self.assertIs(self.appels[0][1].get("wait_at_error"), False)
def test_the_apply_call_does_not_either(self):
with redirect_stdout(io.StringIO()):
self.obj.prompt_fix_view_type("db")
self.assertEqual(len(self.appels), 2)
self.assertIn("--apply", self.appels[1][0])
self.assertIs(self.appels[1][1].get("wait_at_error"), False)
def test_it_returns_true_only_when_the_apply_succeeded(self):
with redirect_stdout(io.StringIO()):
self.assertTrue(self.obj.prompt_fix_view_type("db"))
def test_refusing_runs_no_apply(self):
self.obj.ask = lambda prompt, default="": "n"
with redirect_stdout(io.StringIO()):
self.assertFalse(self.obj.prompt_fix_view_type("db"))
self.assertEqual(len(self.appels), 1)
def test_nothing_to_fix_asks_nothing(self):
demandes = []
self.obj.ask = (
lambda prompt, default="": demandes.append(prompt) or "y"
)
self.obj.todo_upgrade_execute = lambda cmd, **kw: (0, cmd)
with redirect_stdout(io.StringIO()):
self.assertFalse(self.obj.prompt_fix_view_type("db"))
self.assertEqual(demandes, [])
class TestTranslations(unittest.TestCase):
def test_every_key_exists(self):
with io.open(fvt.__file__, encoding="utf-8") as handle: