[FIX] migration: rebâtir le clone annule aussi sa préparation

Quand OpenUpgrade échoue, le pilote remet le drapeau de clonage à zéro
pour que la base intermédiaire soit refaite depuis la version d'avant.
Mais les étapes qui avaient préparé CE clone gardaient le leur : le SQL
de pré-migration, les désinstallations, les installations. La base neuve
repartait sans sa préparation, et OpenUpgrade retombait sur le problème
même que ce SQL existe pour écarter.

Vu sur test_neutralize_upgrade_18 : clone à refaire, et pourtant
fix_migration_odoo170_to_odoo180.sql déjà consigné comme appliqué.
Les trois drapeaux tombent maintenant avec le clone.

--- EN ---

When OpenUpgrade fails, the driver clears the clone flag so the
intermediate database is rebuilt from the previous version. But the
steps that had prepared THAT clone kept theirs: the pre-migration SQL,
the uninstalls, the installs. The fresh database started without its
preparation, and OpenUpgrade met the very problem that SQL exists to
prevent.

Seen on test_neutralize_upgrade_18: clone pending, yet
fix_migration_odoo170_to_odoo180.sql already recorded as applied.
The three flags now fall with the clone.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-23 02:09:06 -04:00
parent 24f38c79ad
commit e3b1fd48be
2 changed files with 89 additions and 0 deletions

View file

@ -2665,6 +2665,24 @@ class TodoUpgrade:
self.dct_progression["state_4_clone_odoo_lst"] = (
lst_clone_odoo
)
# Tout ce qui a été fait À ce clone meurt avec lui : la
# base reconstruite est une copie neuve de la version
# précédente et réclame la même préparation. Laisser ces
# drapeaux debout sautait le SQL de pré-migration sur le
# clone neuf, et OpenUpgrade retombait sur le problème
# même que ce SQL existe pour écarter.
lst_fix_migration_odoo[index] = []
self.dct_progression["state_4_fix_migration_odoo_lst"] = (
lst_fix_migration_odoo
)
lst_module_uninstall_module[index] = False
self.dct_progression["state_4_uninstall_module"] = (
lst_module_uninstall_module
)
lst_module_install_module[index] = False
self.dct_progression["state_4_install_module"] = (
lst_module_install_module
)
self.write_config()
print(
f"\n❌ -> {t('Database migration to Odoo')}"

View file

@ -278,5 +278,76 @@ class TestAFailedOpenUpgradeStaysUnrecorded(unittest.TestCase):
self.assertEqual(len(self.lignes_affectation()), 1)
class TestDiscardingTheCloneDiscardsItsPreparation(unittest.TestCase):
"""Rebâtir le clone annule tout ce qu'on lui avait fait.
Quand OpenUpgrade échoue, le pilote remet le drapeau de clonage à zéro
pour que la base intermédiaire soit refaite depuis la version
précédente. Mais les drapeaux des étapes qui avaient préparé CE
clone — le SQL de pré-migration, les désinstallations, les
installations — restaient debout. La base neuve repartait donc sans
sa préparation, et OpenUpgrade retombait sur le problème même que le
SQL existe pour écarter.
"""
PAR_CLONE = (
"lst_fix_migration_odoo",
"lst_module_uninstall_module",
"lst_module_install_module",
)
def bloc_abandon(self):
with io.open(SOURCE, encoding="utf-8") as handle:
arbre = ast.parse(handle.read())
for noeud in ast.walk(arbre):
if isinstance(
noeud, ast.If
) and TestAFailedOpenUpgradeStaysUnrecorded._remet_le_clone_a_zero(
noeud
):
return noeud
return None
def remis_a_zero(self):
"""Les listes que ce bloc remet à leur valeur vide."""
noms = set()
for petit in ast.walk(self.bloc_abandon()):
if not isinstance(petit, ast.Assign):
continue
if not (
isinstance(petit.value, ast.Constant)
and petit.value.value is False
) and not (
isinstance(petit.value, ast.List) and not petit.value.elts
):
continue
for cible in petit.targets:
if isinstance(cible, ast.Subscript) and isinstance(
cible.value, ast.Name
):
noms.add(cible.value.id)
return noms
def test_the_failure_block_is_found(self):
self.assertIsNotNone(self.bloc_abandon())
def test_every_per_clone_flag_is_reset(self):
remis = self.remis_a_zero()
for nom in self.PAR_CLONE:
self.assertIn(nom, remis, f"{nom} survit à son clone")
def test_each_reset_is_persisted(self):
# Remettre la liste à zéro sans l'écrire ne survit pas au
# processus : c'est la reprise qui relit le fichier.
corps = ast.dump(self.bloc_abandon())
for cle in (
"state_4_fix_migration_odoo_lst",
"state_4_uninstall_module",
"state_4_install_module",
):
self.assertIn(cle, corps)
self.assertIn("write_config", corps)
if __name__ == "__main__":
unittest.main()