diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index da1a738..6f4464a 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -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?", diff --git a/script/todo/todo_upgrade.py b/script/todo/todo_upgrade.py index 8956ead..fa69554 100755 --- a/script/todo/todo_upgrade.py +++ b/script/todo/todo_upgrade.py @@ -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')} : ") diff --git a/test/test_prompt_defaults.py b/test/test_prompt_defaults.py index fb0248d..f9f6f71 100644 --- a/test/test_prompt_defaults.py +++ b/test/test_prompt_defaults.py @@ -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