[ADD] migration state: how long the migration took

From the first write to the LAST one. The progression file is rewritten
after every move, so its update date is the end — or the present moment
while the migration is still running. Both dates were already shown; what
was missing was the one number one actually wants.

The statistics screen already computed this. It is reused rather than
rewritten: two formulas would give two durations for the same migration
depending on which screen one opens.

--- FR ---

[ADD] état de migration : combien de temps elle a duré

Du premier écrit au DERNIER. Le fichier de progression est réécrit après
chaque geste, donc sa date de mise à jour est la fin — ou l'instant
présent tant que la migration tourne. Les deux dates étaient déjà
affichées ; il manquait le seul chiffre qu'on cherche.

L'écran de statistiques portait déjà ce calcul. Il est réutilisé plutôt
que réécrit : deux formules donneraient deux durées pour la même
migration selon l'écran qu'on ouvre.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 06:37:14 -04:00
parent c0c5144680
commit e981500c94
4 changed files with 87 additions and 2 deletions

View file

@ -288,9 +288,28 @@ def failures(dct):
return list(reversed(events(dct, kind="command")))
def elapsed(dct):
"""Combien de temps la migration a duré, ou dure encore.
Du premier écrit à la DERNIÈRE écriture du journal : la progression est
réécrite après chaque geste, donc sa date de mise à jour EST la fin —
ou l'instant présent si la migration tourne toujours.
On délègue à `migration_stats.fmt_delay`, qui porte déjà ce calcul pour
l'écran de statistiques. Deux formules donneraient deux durées pour la
même migration selon l'écran qu'on ouvre.
"""
try:
from script.todo.migration_stats import fmt_delay
except Exception:
return "?"
return fmt_delay(dct.get("date_create"), dct.get("date_update"))
def overview(dct):
"""L'en-tête : de quelle migration parle-t-on."""
return {
"elapsed": elapsed(dct),
"file": os.path.basename(dct.get("migration_file") or "?"),
"database": dct.get("config_database_name") or "?",
"target": dct.get("target_odoo_version") or "?",
@ -340,7 +359,8 @@ def render_text(dct, limit_cmd=12, colour=None):
f" {t('database')} : {info['database']}",
f" {t('image')} : {info['file']}",
f" {t('started')} : {info['started']}",
f" {t('last written')} : {info['updated']}",
f" {t('finished')} : {info['updated']}",
f" {t('duration')} : {info['elapsed']}",
f" {t('current step')} : {info['step']}",
]

View file

@ -48,7 +48,8 @@ def head_text(dct):
return (
f"{info['database']} · {info['file']}\n"
f"{t('current step')} : {info['step']}\n"
f"{t('last written')} : {info['updated']}"
f"{t('finished')} : {info['updated']}"
f" · {t('duration')} {info['elapsed']}"
f" · {len(status.failures(dct))} {t('failed')}"
f" · {len(casse)}/{len(lst_test)} {t('Test results')}"
)

View file

@ -5667,6 +5667,14 @@ TRANSLATIONS = {
"fr": "Rétrécir",
"en": "Narrower",
},
"finished": {
"fr": "terminée",
"en": "finished",
},
"duration": {
"fr": "durée",
"en": "duration",
},
"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

@ -958,6 +958,62 @@ class TestTheFullScreenColoursToo(Base):
self.assertIn("colour=True", source)
class TestHowLongItTook(Base):
"""Du premier écrit à la dernière écriture du journal.
La progression est réécrite après chaque geste, donc sa date de mise à
jour EST la fin — ou l'instant présent si la migration tourne encore.
"""
def dct(self, debut, fin):
return progression(date_create=debut, date_update=fin)
def test_it_counts_from_the_start_to_the_last_write(self):
info = status.overview(
self.dct("2026-08-19 09:06:27", "2026-08-19 10:19:18")
)
self.assertEqual(info["elapsed"], "1 h 12 min")
def test_a_short_run_is_given_in_minutes(self):
info = status.overview(
self.dct("2026-08-19 09:06:00", "2026-08-19 09:41:00")
)
self.assertEqual(info["elapsed"], "35 min")
def test_a_long_run_is_given_in_days(self):
info = status.overview(
self.dct("2026-08-17 09:00:00", "2026-08-19 11:30:00")
)
self.assertIn("j", info["elapsed"])
def test_a_missing_date_never_invents_a_duration(self):
self.assertEqual(status.overview(self.dct(None, None))["elapsed"], "?")
def test_the_report_shows_it(self):
texte = status.render_text(
self.dct("2026-08-19 09:06:27", "2026-08-19 10:19:18"),
colour=False,
)
self.assertIn("1 h 12 min", texte)
def test_the_full_screen_shows_it_too(self):
texte = tui.head_text(
self.dct("2026-08-19 09:06:27", "2026-08-19 10:19:18")
)
self.assertIn("1 h 12 min", texte)
def test_it_is_computed_in_ONE_place(self):
# L'écran de statistiques porte déjà ce calcul. Deux formules
# donneraient deux durées pour la même migration selon l'écran
# qu'on ouvre.
import inspect
source = inspect.getsource(status.elapsed)
self.assertIn(
"from script.todo.migration_stats import fmt_delay", source
)
class TestSeparatingCommandsFromLogs(Base):
"""Les deux mélangés dans un même panneau se confondent.