[FIX] migration: a skipped back office must not read as a healthy one
The back-office pass was already there, and it works. What did not work was the way it declined: one discreet line at the end of a long report, saying « the database was not neutralized ». On six databases of a real migration the test user is present up to the 15 bump and GONE at 17 and 18 — so the pass stopped silently exactly where a migration does the most damage, and said something that was not even true. The migration knows what it neutralized, so it now asks for the back office by name. A missing test user on a database it neutralized is a finding, printed loudly and counted as a failure. A missing tool is too: returning None made the whole pass vanish without a word. And it says UP FRONT which passes will run, on that database, by name. --- FR --- [FIX] migration : un back-office sauté ne doit pas se lire comme un sain La passe back-office était déjà là et elle fonctionne. Ce qui ne fonctionnait pas, c'est sa façon de renoncer : une ligne discrète en fin d'un long rapport, disant « la base n'a pas été neutralisée ». Sur les six bases d'une vraie migration, l'utilisateur test est présent jusqu'au palier 15 et ABSENT en 17 et 18 — la passe s'arrêtait donc sans bruit là où une migration fait le plus de dégâts, en disant quelque chose de faux. La migration sait ce qu'elle a neutralisé : elle réclame désormais le back-office. Un utilisateur test manquant sur une base qu'elle a neutralisée est une trouvaille, affichée fort et comptée comme un échec. Un outil absent aussi : rendre None faisait disparaître la passe entière. Et elle annonce AVANT de lancer ce qui sera parcouru. Assisted-by: Claude Opus 5
This commit is contained in:
parent
1c7664409e
commit
fc23bd6f96
4 changed files with 186 additions and 6 deletions
|
|
@ -445,6 +445,14 @@ def render_internal(internal):
|
|||
if internal is None:
|
||||
return False
|
||||
if "skipped" in internal:
|
||||
if internal.get("loud"):
|
||||
# Un saut ATTENDU se dit à voix basse ; un saut qui trahit une
|
||||
# panne doit compter comme un échec, sinon le code de sortie
|
||||
# annonce que tout va bien.
|
||||
print(
|
||||
f"\n⚠️ {t('Back office NOT browsed')} : {internal['skipped']}"
|
||||
)
|
||||
return True
|
||||
print(f"\nℹ️ {t('Back office not browsed')} : {internal['skipped']}")
|
||||
return False
|
||||
import smoke_internal_ui
|
||||
|
|
@ -481,6 +489,7 @@ def internal_phase(
|
|||
limit=20,
|
||||
every_menu=False,
|
||||
lst_portal=None,
|
||||
required=False,
|
||||
):
|
||||
"""Le back-office, si la base a été neutralisée. Rend None sinon.
|
||||
|
||||
|
|
@ -498,14 +507,32 @@ def internal_phase(
|
|||
try:
|
||||
import smoke_internal_ui
|
||||
except ImportError:
|
||||
return None
|
||||
# Se taire ici ferait disparaître la passe ENTIÈRE sans un mot, et
|
||||
# l'on croirait le back-office testé. Un outil absent est une
|
||||
# panne d'installation, pas une base saine.
|
||||
return {"skipped": t("smoke_internal_ui.py is missing"), "loud": True}
|
||||
etat = smoke_internal_ui.user_state(database, login, run_psql=run_psql)
|
||||
if etat == "absent":
|
||||
# `required` dit que la migration a NEUTRALISÉ cette base : le
|
||||
# compte devrait donc y être. Mesuré — il survit jusqu'au palier
|
||||
# 15 puis disparaît, et la passe s'arrêtait sans bruit exactement
|
||||
# là où une migration fait le plus de dégâts.
|
||||
if required:
|
||||
return {
|
||||
"skipped": t(
|
||||
"the test user is gone from a neutralized"
|
||||
" database: the back office was NOT checked"
|
||||
),
|
||||
"loud": True,
|
||||
}
|
||||
return {"skipped": t("no test user: the database was not neutralized")}
|
||||
if etat != "present":
|
||||
# « je ne sais pas » n'est pas « tout va bien » : le dire autrement
|
||||
# ferait passer un back-office jamais ouvert pour un back-office sain.
|
||||
return {"skipped": t("could not tell whether the test user exists")}
|
||||
return {
|
||||
"skipped": t("could not tell whether the test user exists"),
|
||||
"loud": True,
|
||||
}
|
||||
try:
|
||||
lst_result, lst_failure = smoke_internal_ui.run(
|
||||
base_url,
|
||||
|
|
@ -541,6 +568,7 @@ def run(
|
|||
internal_limit=20,
|
||||
every_menu=False,
|
||||
portal=None,
|
||||
internal_required=False,
|
||||
):
|
||||
"""Démarrer, interroger, arrêter, LIRE, éventuellement corriger, revérifier.
|
||||
|
||||
|
|
@ -585,6 +613,7 @@ def run(
|
|||
limit=internal_limit,
|
||||
every_menu=every_menu,
|
||||
lst_portal=portal,
|
||||
required=internal_required,
|
||||
)
|
||||
finally:
|
||||
stop_server(server)
|
||||
|
|
@ -720,6 +749,11 @@ def main(argv=None):
|
|||
action="store_true",
|
||||
help="open every menu with an action, not just each app's first page",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--internal-required",
|
||||
action="store_true",
|
||||
help="the database WAS neutralized: a missing test user is a failure",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--portal",
|
||||
default="/my",
|
||||
|
|
@ -748,6 +782,7 @@ def main(argv=None):
|
|||
internal_password=config.password,
|
||||
internal_limit=config.record_limit,
|
||||
every_menu=config.all_menus,
|
||||
internal_required=config.internal_required,
|
||||
portal=[
|
||||
path.strip()
|
||||
for path in (config.portal or "").split(",")
|
||||
|
|
|
|||
|
|
@ -5498,6 +5498,38 @@ TRANSLATIONS = {
|
|||
"fr": "appuyez pour continuer",
|
||||
"en": "press to continue",
|
||||
},
|
||||
"smoke_internal_ui.py is missing": {
|
||||
"fr": "smoke_internal_ui.py est absent",
|
||||
"en": "smoke_internal_ui.py is missing",
|
||||
},
|
||||
"the test user is gone from a neutralized database: the back office was NOT checked": {
|
||||
"fr": "l'utilisateur test a disparu d'une base neutralisée : le back-office n'a PAS été vérifié",
|
||||
"en": "the test user is gone from a neutralized database: the back office was NOT checked",
|
||||
},
|
||||
"Back office NOT browsed": {
|
||||
"fr": "Back-office NON parcouru",
|
||||
"en": "Back office NOT browsed",
|
||||
},
|
||||
"Public pages, then the back office and /my as the": {
|
||||
"fr": "Les pages publiques, puis le back-office et /my avec",
|
||||
"en": "Public pages, then the back office and /my as the",
|
||||
},
|
||||
"test user (neutralized database).": {
|
||||
"fr": "l'utilisateur test (base neutralisée).",
|
||||
"en": "test user (neutralized database).",
|
||||
},
|
||||
"Public pages only:": {
|
||||
"fr": "Pages publiques seulement :",
|
||||
"en": "Public pages only:",
|
||||
},
|
||||
"was not neutralized, so there is no test user to": {
|
||||
"fr": "n'a pas été neutralisée, il n'y a donc pas d'utilisateur test pour",
|
||||
"en": "was not neutralized, so there is no test user to",
|
||||
},
|
||||
"sign in with.": {
|
||||
"fr": "s'y connecter.",
|
||||
"en": "sign in with.",
|
||||
},
|
||||
"Clean the database before testing the pages?": {
|
||||
"fr": "Nettoyer la base avant de tester les pages ?",
|
||||
"en": "Clean the database before testing the pages?",
|
||||
|
|
|
|||
|
|
@ -3058,14 +3058,27 @@ class TodoUpgrade:
|
|||
)
|
||||
if answer != "y":
|
||||
return
|
||||
print(
|
||||
f" {t('Public pages, then the back office as the test user')}"
|
||||
f" {t('if the database was neutralized.')}"
|
||||
)
|
||||
# DIRE ce qui sera parcouru, et le dire AVANT. « si la base a été
|
||||
# neutralisée » laissait la question ouverte pendant tout le
|
||||
# parcours, et un saut annoncé en une ligne à la fin d'un long
|
||||
# rapport ne se voit pas : on croit alors le back-office testé.
|
||||
neutralise = "_neutralize" in database_name
|
||||
if neutralise:
|
||||
print(
|
||||
f" {t('Public pages, then the back office and /my as the')}"
|
||||
f" {t('test user (neutralized database).')}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f" {t('Public pages only:')} '{database_name}'"
|
||||
f" {t('was not neutralized, so there is no test user to')}"
|
||||
f" {t('sign in with.')}"
|
||||
)
|
||||
self.run_on_terminal(
|
||||
f"{PYTHON_BIN}"
|
||||
" ./script/odoo/migration/smoke_public_url.py"
|
||||
f" -d {database_name}"
|
||||
+ (" --internal-required" if neutralise else "")
|
||||
)
|
||||
|
||||
def show_cow_drift(self, database_name, next_version, mode="diff"):
|
||||
|
|
|
|||
|
|
@ -404,6 +404,106 @@ class TestTheViewsAreRenderedServerSide(unittest.TestCase):
|
|||
self.assertEqual(appels, ["get_views", "load_views", "load_views"])
|
||||
|
||||
|
||||
class TestASkipThatHidesAFailure(unittest.TestCase):
|
||||
"""Tous les sauts ne se valent pas, et c'est mesuré.
|
||||
|
||||
L'utilisateur `test` survit aux paliers 12 à 15 puis DISPARAÎT : relevé
|
||||
sur les six bases d'une vraie migration, présent jusqu'à 15, absent en
|
||||
17 et 18. La passe back-office s'arrêtait donc sans bruit exactement là
|
||||
où une migration fait le plus de dégâts, et le rapport disait
|
||||
tranquillement « la base n'a pas été neutralisée » — ce qui était faux.
|
||||
|
||||
`required` porte ce que la migration SAIT : elle a neutralisé cette
|
||||
base, le compte devrait y être. Son absence devient alors une
|
||||
trouvaille, pas une formalité.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from script.todo import todo_i18n
|
||||
|
||||
self.addCleanup(
|
||||
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
||||
)
|
||||
todo_i18n._current_lang = "en"
|
||||
import smoke_public_url
|
||||
|
||||
self.public = smoke_public_url
|
||||
original = smoke_public_url.run_psql
|
||||
self.addCleanup(setattr, smoke_public_url, "run_psql", original)
|
||||
|
||||
def phase(self, rows, **kw):
|
||||
self.public.run_psql = lambda db, sql: rows
|
||||
return self.public.internal_phase("http://x", "db", enabled=True, **kw)
|
||||
|
||||
def test_no_user_on_a_plain_database_is_a_quiet_skip(self):
|
||||
rapport = self.phase([["0"]])
|
||||
self.assertIn("not neutralized", rapport["skipped"])
|
||||
self.assertFalse(rapport.get("loud"))
|
||||
|
||||
def test_no_user_on_a_NEUTRALIZED_database_is_a_finding(self):
|
||||
rapport = self.phase([["0"]], required=True)
|
||||
self.assertIn("NOT checked", rapport["skipped"])
|
||||
self.assertTrue(rapport["loud"])
|
||||
|
||||
def test_a_finding_counts_as_a_failure(self):
|
||||
# Sinon le code de sortie annonce que tout va bien alors que le
|
||||
# back-office n'a jamais été ouvert.
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
echec = self.public.render_internal(
|
||||
{"skipped": "disparu", "loud": True}
|
||||
)
|
||||
self.assertTrue(echec)
|
||||
self.assertIn("NOT browsed", out.getvalue())
|
||||
|
||||
def test_a_quiet_skip_does_not(self):
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertFalse(
|
||||
self.public.render_internal({"skipped": "pas neutralisée"})
|
||||
)
|
||||
|
||||
def test_an_unreadable_database_is_never_silent(self):
|
||||
rapport = self.phase([])
|
||||
self.assertTrue(rapport["loud"])
|
||||
|
||||
def test_a_missing_tool_is_never_silent(self):
|
||||
# `except ImportError: return None` faisait disparaître la passe
|
||||
# ENTIÈRE sans un mot, et l'on croyait le back-office testé.
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(self.public.internal_phase)
|
||||
debut = source.index("except ImportError")
|
||||
self.assertIn("loud", source[debut : debut + 400])
|
||||
|
||||
def test_the_migration_asks_for_it_when_it_neutralized(self):
|
||||
import inspect
|
||||
|
||||
from script.todo.todo_upgrade import TodoUpgrade
|
||||
|
||||
source = inspect.getsource(TodoUpgrade.prompt_smoke_public_url)
|
||||
self.assertIn("--internal-required", source)
|
||||
self.assertIn('"_neutralize" in database_name', source)
|
||||
|
||||
def test_the_migration_says_UP_FRONT_what_will_be_browsed(self):
|
||||
# Un saut annoncé en une ligne à la fin d'un long rapport ne se
|
||||
# voit pas : on croit alors le back-office testé.
|
||||
import inspect
|
||||
|
||||
from script.todo.todo_upgrade import TodoUpgrade
|
||||
|
||||
source = inspect.getsource(TodoUpgrade.prompt_smoke_public_url)
|
||||
self.assertLess(
|
||||
source.index("Public pages only:"),
|
||||
source.index("run_on_terminal"),
|
||||
)
|
||||
|
||||
|
||||
class TestThePortalPage(unittest.TestCase):
|
||||
"""/my n'est ni le site public ni le back-office : c'est un troisième
|
||||
rendu, en QWeb frontend, avec ses compteurs qui interrogent chacun leur
|
||||
|
|
|
|||
Loading…
Reference in a new issue