[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:
parent
3a182f2b0d
commit
566070b4f8
3 changed files with 86 additions and 5 deletions
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -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')} : ")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue