[UPD] invites : 15 secondes pour décider, et le fichier nomme la base
Cinq secondes ne suffisaient pas à LIRE la question. Le compte à rebours n'existe pas pour aller vite mais pour qu'on puisse s'absenter ; trop court, il fait l'inverse — on répond par réflexe, ou l'on subit un défaut non lu. Et « test » ne disait rien de ce qu'on migrait : des migrations successives portaient toutes le même nom, quand le fichier de sauvegarde en porte déjà un parlant. Le nom est assaini — il finit dans un createdb — et borné à 41 caractères : le pilote ajoute « _neutralize_upgrade_18 » et PostgreSQL tronque à 63, ce qui ferait finir deux paliers sur le même nom. Un téléchargement distant garde celui que le serveur a donné. --- EN --- Five seconds were not enough to READ the question. The countdown does not exist to go fast but so you can step away; too short, it does the opposite — you answer by reflex, or you get a default you never read. And "test" said nothing about what was being migrated: successive migrations all carried the same name, when the backup file already carries a telling one. The name is sanitised — it ends up in a createdb — and capped at 41 characters: the driver appends "_neutralize_upgrade_18" and PostgreSQL truncates at 63, which would land two steps on one name. A remote download keeps the name the server gave. Assisted-by: Claude Opus 5 (cherry picked from commit dad0f6b0ef4551898fe2964e177ff5c1c888b5dc)
This commit is contained in:
parent
f6aa2575c3
commit
7ba3c6504c
3 changed files with 146 additions and 1 deletions
|
|
@ -37,7 +37,7 @@ except Exception: # pragma: no cover - repli si i18n indisponible
|
|||
|
||||
ENV_ENABLED = "ERPLIBRE_AUTO_EXECUTE"
|
||||
ENV_DELAY = "ERPLIBRE_AUTO_DELAY"
|
||||
DEFAULT_DELAY = 5
|
||||
DEFAULT_DELAY = 15
|
||||
|
||||
|
||||
def enabled():
|
||||
|
|
|
|||
|
|
@ -164,6 +164,39 @@ class MigrationRewind(Exception):
|
|||
"""
|
||||
|
||||
|
||||
# Le pilote suffixe le nom choisi : « _neutralize » à l'étape 1, puis
|
||||
# « _upgrade_<version> » à chaque palier. PostgreSQL tronque un
|
||||
# identifiant à 63 octets — deux paliers finiraient alors sur le MÊME
|
||||
# nom, et le second écraserait le premier sans rien dire.
|
||||
SUFFIXE_MAX = len("_neutralize_upgrade_18")
|
||||
NOM_BASE_MAX = 63 - SUFFIXE_MAX
|
||||
|
||||
|
||||
def database_name_from_file(chemin, defaut="test", limite=NOM_BASE_MAX):
|
||||
"""Le nom de base que suggère le fichier de sauvegarde.
|
||||
|
||||
« test » ne disait rien de ce qu'on migrait : trois migrations de
|
||||
suite portaient le même nom, et retrouver laquelle avait échoué
|
||||
demandait de relire le journal. Le fichier, lui, porte déjà le nom du
|
||||
client — c'est la seule information qu'on ait à ce moment-là.
|
||||
|
||||
Le nom est ASSAINI, car il finit dans un `createdb` et dans des noms
|
||||
de fichiers : minuscules, et tout ce qui n'est ni lettre ni chiffre
|
||||
devient un « _ ». Un nom qui commence par un chiffre reçoit un
|
||||
préfixe — PostgreSQL refuse un identifiant nu qui en commence un.
|
||||
"""
|
||||
nom = os.path.basename(chemin or "")
|
||||
if nom.lower().endswith(".zip"):
|
||||
nom = nom[: -len(".zip")]
|
||||
nom = re.sub(r"[^a-z0-9]+", "_", nom.lower())
|
||||
nom = re.sub(r"_{2,}", "_", nom).strip("_")
|
||||
if not nom:
|
||||
return defaut
|
||||
if nom[0].isdigit():
|
||||
nom = "db_" + nom
|
||||
return nom[:limite].rstrip("_") or defaut
|
||||
|
||||
|
||||
class TodoUpgrade:
|
||||
def __init__(self, todo):
|
||||
self.file_path = None
|
||||
|
|
@ -1370,6 +1403,13 @@ class TodoUpgrade:
|
|||
self.dct_progression["migration_file"] = self.file_path
|
||||
self.write_config()
|
||||
|
||||
# Le téléchargement distant a déjà nommé la base — le serveur sait
|
||||
# mieux que le nom du fichier. Sinon, c'est le fichier qui parle.
|
||||
if default_database_name == "test" and self.file_path:
|
||||
default_database_name = database_name_from_file(
|
||||
self.file_path, default_database_name
|
||||
)
|
||||
|
||||
print(f"✅ {t('Open file')} {self.file_path}")
|
||||
with zipfile.ZipFile(self.file_path, "r") as zip_ref:
|
||||
manifest_file_1 = zip_ref.open("manifest.json")
|
||||
|
|
|
|||
|
|
@ -224,6 +224,111 @@ class TestTheCountdownSpeaksThePromptsLanguage(EnvCase):
|
|||
self.assertIn("Entrée", texte)
|
||||
|
||||
|
||||
class TestHowLongTheCountdownLasts(EnvCase):
|
||||
"""Cinq secondes ne suffisaient pas à LIRE la question.
|
||||
|
||||
Le compte à rebours n'existe pas pour aller vite : il existe pour
|
||||
qu'on puisse s'absenter. Trop court, il fait l'inverse — on répond par
|
||||
réflexe, ou l'on subit un défaut qu'on n'a pas eu le temps de lire.
|
||||
"""
|
||||
|
||||
def test_the_default_leaves_time_to_read_and_decide(self):
|
||||
self.assertGreaterEqual(auto_ask.DEFAULT_DELAY, 15)
|
||||
|
||||
def test_the_environment_still_wins(self):
|
||||
# C'est le seul canal qui traverse un fork : une valeur posée par
|
||||
# le pilote doit primer sur celle écrite ici.
|
||||
os.environ[auto_ask.ENV_DELAY] = "3"
|
||||
self.assertEqual(3, auto_ask.delay())
|
||||
|
||||
|
||||
class TestTheDatabaseNameTheFileSuggests(unittest.TestCase):
|
||||
"""« test » ne disait rien de ce qu'on migrait.
|
||||
|
||||
Trois migrations de suite portaient le même nom, et retrouver
|
||||
laquelle avait échoué demandait de relire le journal. Le fichier de
|
||||
sauvegarde porte déjà le nom du client — c'est la seule information
|
||||
disponible au moment où la question se pose.
|
||||
"""
|
||||
|
||||
def nom(self, chemin, **kw):
|
||||
from script.todo.todo_upgrade import database_name_from_file
|
||||
|
||||
return database_name_from_file(chemin, **kw)
|
||||
|
||||
def test_the_zip_extension_goes_away(self):
|
||||
self.assertEqual("chezlepro3", self.nom("image_db/chezlepro3.zip"))
|
||||
|
||||
def test_an_uppercase_extension_goes_away_too(self):
|
||||
self.assertEqual("client", self.nom("image_db/CLIENT.ZIP"))
|
||||
|
||||
def test_a_real_backup_name_survives_whole(self):
|
||||
self.assertEqual(
|
||||
"technolibre_2026_08_26_02h41m23s",
|
||||
self.nom("image_db/technolibre_2026-08-26_02h41m23s.zip"),
|
||||
)
|
||||
|
||||
def test_a_file_without_extension_still_names_a_base(self):
|
||||
# Une sauvegarde peut être posée sans « .zip » ; elle s'ouvre
|
||||
# quand même comme une archive.
|
||||
self.assertEqual("technolibre", self.nom("image_db/technolibre"))
|
||||
|
||||
def test_spaces_and_punctuation_become_underscores(self):
|
||||
# Le nom finit dans un createdb et dans des noms de fichiers.
|
||||
self.assertEqual(
|
||||
"client_final_2026", self.nom("image_db/Client Final (2026).zip")
|
||||
)
|
||||
|
||||
def test_it_never_starts_with_a_digit(self):
|
||||
# PostgreSQL refuse un identifiant nu qui commence par un chiffre.
|
||||
nom = self.nom("image_db/2026-backup.zip")
|
||||
self.assertFalse(nom[0].isdigit(), nom)
|
||||
|
||||
def test_nothing_usable_falls_back_to_the_default(self):
|
||||
for chemin in ("", None, "image_db/.zip", "image_db/---.zip"):
|
||||
self.assertEqual("test", self.nom(chemin))
|
||||
|
||||
def test_a_long_name_leaves_room_for_the_suffixes(self):
|
||||
# Le pilote ajoute « _neutralize » puis « _upgrade_<version> ».
|
||||
# PostgreSQL tronque à 63 octets : sans marge, deux paliers
|
||||
# finiraient sur le MÊME nom et le second écraserait le premier.
|
||||
from script.todo.todo_upgrade import database_name_from_file
|
||||
|
||||
nom = database_name_from_file("image_db/" + "a" * 90 + ".zip")
|
||||
complet = nom + "_neutralize_upgrade_18"
|
||||
self.assertLessEqual(len(complet), 63, complet)
|
||||
|
||||
def test_it_does_not_end_on_a_separator(self):
|
||||
nom = self.nom("image_db/" + "a" * 40 + "-suite.zip")
|
||||
self.assertFalse(nom.endswith("_"), nom)
|
||||
|
||||
|
||||
class TestTheMigrationUsesThatName(unittest.TestCase):
|
||||
def test_the_prompt_offers_what_the_file_suggests(self):
|
||||
import inspect
|
||||
|
||||
from script.todo.todo_upgrade import TodoUpgrade
|
||||
|
||||
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||||
pose = source.index("database_name_from_file(")
|
||||
invite = source.index("Which database name do you want to work with?")
|
||||
# Il faut le calculer AVANT de le proposer.
|
||||
self.assertLess(pose, invite)
|
||||
|
||||
def test_a_remote_download_keeps_the_name_the_server_gave(self):
|
||||
# Le serveur sait mieux que le nom du fichier téléchargé.
|
||||
import inspect
|
||||
|
||||
from script.todo.todo_upgrade import TodoUpgrade
|
||||
|
||||
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
||||
fenetre = source[
|
||||
source.index("database_name_from_file(")
|
||||
- 400 : source.index("database_name_from_file(")
|
||||
]
|
||||
self.assertIn('default_database_name == "test"', fenetre)
|
||||
|
||||
|
||||
class TestTheQuestionThatCannotAnswerItself(EnvCase):
|
||||
"""Celle-là engage toutes les suivantes : elle ne se prend pas seule.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue