[FIX] migration: Enter exports the backup at the end

After several hours of migration the archive is what one wanted anyway,
and the prompt asked for it with an empty default: pressing Enter — or
letting auto-run answer — walked past the only artifact of the whole run.

The three-way answer moved into a function of its own, because the third
way is the one that traps. « n » refuses, « y » or Enter takes the
timestamped name, and EVERYTHING ELSE is a filename. So « non.zip » is a
file, not a refusal: comparing the whole answer rather than its first
letter is what keeps it one.

--- FR ---

[FIX] migration : Entrée exporte la sauvegarde à la fin

Au bout de plusieurs heures de migration, l'archive est ce qu'on voulait
de toute façon, et l'invite la proposait avec un défaut vide : appuyer sur
Entrée — ou laisser l'auto-exécution répondre — passait à côté du seul
artefact de toute la course.

La réponse à trois sens est devenue une fonction à part, car c'est le
troisième qui piège. « n » refuse, « y » ou Entrée prend le nom horodaté,
et TOUT LE RESTE est un nom de fichier. « non.zip » est donc un fichier et
non un refus : comparer la réponse entière plutôt que sa première lettre
est ce qui le garantit.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 04:20:13 -04:00
parent 3a182f2b0d
commit 566070b4f8
3 changed files with 86 additions and 5 deletions

View file

@ -5607,6 +5607,14 @@ TRANSLATIONS = {
"fr": "État de la migration (plein écran)",
"en": "Migration state (full screen)",
},
"Export a backup?": {
"fr": "Exporter une sauvegarde ?",
"en": "Export a backup?",
},
"or type filename.zip": {
"fr": "ou tapez nomfichier.zip",
"en": "or type filename.zip",
},
"Clean the database before testing the pages?": {
"fr": "Nettoyer la base avant de tester les pages ?",
"en": "Clean the database before testing the pages?",

View file

@ -851,6 +851,27 @@ class TodoUpgrade:
# Renoncer au retour en arrière ne doit pas arrêter la migration :
# on revient à la même invite, exactement là où l'on était.
@staticmethod
def backup_command(answer, template, default_cmd):
"""La commande de sauvegarde à lancer, ou « » pour ne rien faire.
Trois réponses, trois sens, et le troisième est celui qui piège :
« n » refuse, « y » ou Entrée prend le nom horodaté, et TOUT LE
RESTE est un nom de fichier. Une réponse comme « non.zip » est donc
un fichier, pas un refus — d'où la comparaison exacte plutôt qu'un
début de mot.
Entrée sauvegarde : au bout d'une migration de plusieurs heures,
l'archive est ce qu'on voulait de toute façon. Mais refuser doit
rester possible, et avoir un mot pour le dire.
"""
reponse = (answer or "").strip()
if reponse.lower() in ("", "n"):
return ""
if reponse.lower() == "y":
return default_cmd
return f"{template} {reponse}"
def show_migration_status(self):
"""Ouvrir l'état de la migration, en plein écran.
@ -2766,12 +2787,14 @@ class TodoUpgrade:
cmd_backup = f"{cmd_backup_template} {database_name_upgrade}_finish_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
print(f"✨ {t('A backup can be created')} :\n{cmd_backup}")
status = self.ask(
f"💬 {t('Press y/Y or type filename.zip to export, or')}"
f" {t('enter to continue')} : "
f"💬 {t('Export a backup?')}"
f" (Y/n, {t('or type filename.zip')}) : ",
default="y",
).strip()
if status.lower():
if status.lower() != "y":
cmd_backup = f"{cmd_backup_template} {status}"
cmd_backup = self.backup_command(
status, cmd_backup_template, cmd_backup
)
if cmd_backup:
self.todo_upgrade_execute(cmd_backup)
status = self.ask(f"💬 {t('Test the migration, press y/Y')} : ")

View file

@ -295,6 +295,56 @@ class TestWhatEnterDoesIsWritten(unittest.TestCase):
self.assertIn(refus, self.source(nom), nom)
class TestTheBackupAtTheEnd(unittest.TestCase):
"""Au bout de plusieurs heures, l'archive est ce qu'on voulait.
Trois réponses, trois sens, et le troisième est celui qui piège : « n »
refuse, « y » ou Entrée prend le nom horodaté, et TOUT LE RESTE est un
nom de fichier. « non.zip » est donc un fichier, pas un refus.
"""
TPL = "./odoo_bin.sh db --backup --database x --restore_image"
DEFAUT = "./odoo_bin.sh db --backup --database x --restore_image x_f_2026"
def cmd(self, answer):
return TodoUpgrade.backup_command(answer, self.TPL, self.DEFAUT)
def test_yes_takes_the_timestamped_name(self):
self.assertEqual(self.cmd("y"), self.DEFAUT)
def test_case_does_not_matter(self):
self.assertEqual(self.cmd("Y"), self.DEFAUT)
def test_n_refuses(self):
# Le défaut ne retire pas le choix : il ne fait qu'en proposer un.
self.assertEqual(self.cmd("n"), "")
self.assertEqual(self.cmd("N"), "")
def test_anything_else_is_a_filename(self):
self.assertTrue(self.cmd("archive.zip").endswith(" archive.zip"))
def test_a_filename_that_STARTS_with_n_is_still_a_filename(self):
# Le piège : comparer un début de mot au lieu de la réponse
# entière ferait passer « non.zip » pour un refus, et l'on
# perdrait l'archive d'une migration de plusieurs heures.
self.assertTrue(self.cmd("non.zip").endswith(" non.zip"))
def test_blanks_are_not_a_filename(self):
self.assertEqual(self.cmd(" "), "")
def test_none_does_not_crash(self):
self.assertEqual(self.cmd(None), "")
def test_enter_backs_up_because_the_default_says_so(self):
import inspect
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
debut = source.index("Export a backup?")
fenetre = source[debut : debut + 200]
self.assertIn('default="y"', fenetre)
self.assertIn("(Y/n,", fenetre)
class TestTheToolsLaunchedApart(unittest.TestCase):
def test_the_leftovers_default_to_deleting(self):
import theme_leftover