[ADD] migration quality: tell Odoo's own redesigns from real losses
« 81 tables lost rows » buried the questions worth asking. The largest of them — ir_translation, 32 984 rows — is a redesign Odoo made in 16, when translations moved into jsonb columns. Putting redesigns and real losses on the same footing is the surest way not to see the second kind. A map now names what Odoo moves or retires, and the report splits the list in two: unexplained first, then what Odoo did on purpose. On a real 12 → 18 the split is 57 and 14, and the partition is exact — an explained loss is annotated, never removed, because removing it was the one thing that could hide a real one. Every entry was VERIFIED on that migration by counting both sides, not written from memory: crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. A map written from memory would explain losses that are not losses. And a merge whose target gained nothing says so: the explanation does not hold, and that is exactly what the map was meant to surface. --- FR --- [ADD] qualité de migration : distinguer les refontes d'Odoo des vraies pertes « 81 tables ont perdu des lignes » enterrait les questions qui valaient la peine. La plus grosse — ir_translation, 32 984 lignes — est une refonte d'Odoo 16, quand les traductions sont passées en colonnes jsonb. Une carte nomme désormais ce qu'Odoo déplace ou retire, et le rapport sépare la liste en deux : l'inexpliqué d'abord, puis ce qu'Odoo a fait exprès. Sur une vraie 12 → 18 : 57 et 14, et la partition est exacte — une perte expliquée est annotée, jamais retirée. Chaque entrée a été VÉRIFIÉE sur cette migration en comptant les deux côtés : crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. Et une fusion dont la table d'accueil n'a rien reçu le dit : l'explication ne tient pas, et c'est précisément ce que la carte devait révéler. Assisted-by: Claude Opus 5
This commit is contained in:
parent
951b88abab
commit
e75bc585ba
4 changed files with 384 additions and 23 deletions
|
|
@ -333,6 +333,158 @@ def render_missing(etat, colour=False, limit=60):
|
||||||
return "\n".join(lignes)
|
return "\n".join(lignes)
|
||||||
|
|
||||||
|
|
||||||
|
# Ce qu'Odoo déplace ou retire de lui-même, d'une version à l'autre.
|
||||||
|
#
|
||||||
|
# Sans cette carte, l'outil annonçait « 81 tables ont perdu des lignes »
|
||||||
|
# et la plus grosse d'entre elles — ir_translation, 32 984 lignes — était
|
||||||
|
# une refonte voulue par l'éditeur. Les vraies questions se noyaient dans
|
||||||
|
# les fausses, ce qui est la façon la plus sûre de ne pas les voir.
|
||||||
|
#
|
||||||
|
# Chaque entrée a été VÉRIFIÉE sur une migration réelle 12 → 18, en
|
||||||
|
# comptant les deux côtés. Une carte écrite de mémoire vaudrait moins que
|
||||||
|
# pas de carte : elle expliquerait des pertes qui n'en sont pas.
|
||||||
|
SEMANTIC_MAP = (
|
||||||
|
# Fusions : les enregistrements continuent, ailleurs et autrement.
|
||||||
|
{
|
||||||
|
"since": 13,
|
||||||
|
"table": "account_invoice",
|
||||||
|
"into": "account_move",
|
||||||
|
"kind": "merged",
|
||||||
|
"why": "invoices became journal entries",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 13,
|
||||||
|
"table": "account_invoice_line",
|
||||||
|
"into": "account_move_line",
|
||||||
|
"kind": "merged",
|
||||||
|
"why": "invoices became journal entries",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 13,
|
||||||
|
"table": "account_invoice_tax",
|
||||||
|
"into": "account_move_line",
|
||||||
|
"kind": "merged",
|
||||||
|
"why": "invoices became journal entries",
|
||||||
|
},
|
||||||
|
# Renommages : les mêmes enregistrements, sous un autre nom.
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "mail_channel",
|
||||||
|
"into": "discuss_channel",
|
||||||
|
"kind": "renamed",
|
||||||
|
"why": "Discuss was renamed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "mail_channel_partner",
|
||||||
|
"into": "discuss_channel_member",
|
||||||
|
"kind": "renamed",
|
||||||
|
"why": "Discuss was renamed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 13,
|
||||||
|
"table": "website_redirect",
|
||||||
|
"into": "website_rewrite",
|
||||||
|
"kind": "renamed",
|
||||||
|
"why": "redirections were reworked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 13,
|
||||||
|
"table": "crm_lead_tag",
|
||||||
|
"into": "crm_tag",
|
||||||
|
"kind": "renamed",
|
||||||
|
"why": "tags became shared",
|
||||||
|
},
|
||||||
|
# Retraits : la fonction a quitté la base pour du code.
|
||||||
|
{
|
||||||
|
"since": 16,
|
||||||
|
"table": "ir_translation",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "translations moved into jsonb columns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_tax_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_account_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_chart_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_fiscal_position_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_fiscal_position_tax_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 17,
|
||||||
|
"table": "account_fiscal_position_account_template",
|
||||||
|
"into": None,
|
||||||
|
"kind": "retired",
|
||||||
|
"why": "chart templates left the database",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 15,
|
||||||
|
"table": "stock_inventory",
|
||||||
|
"into": "stock_quant",
|
||||||
|
"kind": "merged",
|
||||||
|
"why": "inventory adjustments became quants",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"since": 15,
|
||||||
|
"table": "stock_inventory_line",
|
||||||
|
"into": "stock_quant",
|
||||||
|
"kind": "merged",
|
||||||
|
"why": "inventory adjustments became quants",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def as_version(etat):
|
||||||
|
"""« 18.0 » -> 18. None si l'on ne sait pas."""
|
||||||
|
try:
|
||||||
|
return int(float((etat or {}).get("odoo") or 0)) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def explain_loss(table, version):
|
||||||
|
"""Ce qu'Odoo a fait de cette table à cette version, ou None.
|
||||||
|
|
||||||
|
`version` est celle d'ARRIVÉE : une refonte de la 17 n'explique rien
|
||||||
|
d'un palier 13 → 14, et l'accepter ferait taire une vraie perte sous
|
||||||
|
prétexte qu'elle porte le nom d'une table refondue plus tard.
|
||||||
|
"""
|
||||||
|
if not version:
|
||||||
|
return None
|
||||||
|
for entree in SEMANTIC_MAP:
|
||||||
|
if entree["table"] == table and entree["since"] <= version:
|
||||||
|
return entree
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def compare(avant, apres):
|
def compare(avant, apres):
|
||||||
"""Ce qui a été gagné et ce qui a été perdu entre deux paliers."""
|
"""Ce qui a été gagné et ce qui a été perdu entre deux paliers."""
|
||||||
if not avant.get("exists") or not apres.get("exists"):
|
if not avant.get("exists") or not apres.get("exists"):
|
||||||
|
|
@ -341,18 +493,34 @@ def compare(avant, apres):
|
||||||
mod_avant, mod_apres = set(avant["model"]), set(apres["model"])
|
mod_avant, mod_apres = set(avant["model"]), set(apres["model"])
|
||||||
tbl_avant, tbl_apres = avant["table"], apres["table"]
|
tbl_avant, tbl_apres = avant["table"], apres["table"]
|
||||||
|
|
||||||
|
# TOUJOURS quatre éléments, le dernier étant l'explication ou None.
|
||||||
|
# Un tuple de taille variable obligerait chaque lecteur à s'en méfier.
|
||||||
lignes_perdues = []
|
lignes_perdues = []
|
||||||
for table, nombre in sorted(tbl_avant.items()):
|
for table, nombre in sorted(tbl_avant.items()):
|
||||||
reste = tbl_apres.get(table)
|
reste = tbl_apres.get(table)
|
||||||
if reste is None and nombre:
|
if reste is None and nombre:
|
||||||
lignes_perdues.append((table, nombre, 0))
|
lignes_perdues.append((table, nombre, 0, None))
|
||||||
elif reste is not None and reste < nombre:
|
elif reste is not None and reste < nombre:
|
||||||
lignes_perdues.append((table, nombre, reste))
|
lignes_perdues.append((table, nombre, reste, None))
|
||||||
lignes_gagnees = [
|
lignes_gagnees = [
|
||||||
(table, tbl_avant.get(table, 0), nombre)
|
(table, tbl_avant.get(table, 0), nombre)
|
||||||
for table, nombre in sorted(tbl_apres.items())
|
for table, nombre in sorted(tbl_apres.items())
|
||||||
if nombre > tbl_avant.get(table, 0)
|
if nombre > tbl_avant.get(table, 0)
|
||||||
]
|
]
|
||||||
|
version = as_version(apres)
|
||||||
|
for index, (table, debut, fin, _rien) in enumerate(lignes_perdues):
|
||||||
|
connu = explain_loss(table, version)
|
||||||
|
if not connu:
|
||||||
|
continue
|
||||||
|
# Une fusion qui n'a PAS grossi la table d'accueil n'explique
|
||||||
|
# rien : on garde l'explication et on dit qu'elle ne tient pas.
|
||||||
|
arrivee = connu["into"]
|
||||||
|
recue = (
|
||||||
|
apres["table"].get(arrivee, 0) - avant["table"].get(arrivee, 0)
|
||||||
|
if arrivee
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
lignes_perdues[index] = (table, debut, fin, {**connu, "gained": recue})
|
||||||
return {
|
return {
|
||||||
"modules_lost": sorted(inst_avant - inst_apres),
|
"modules_lost": sorted(inst_avant - inst_apres),
|
||||||
"modules_gained": sorted(inst_apres - inst_avant),
|
"modules_gained": sorted(inst_apres - inst_avant),
|
||||||
|
|
@ -384,7 +552,9 @@ def probable_renames(perdues, gagnees):
|
||||||
cherche un dégât là où il n'y en a pas.
|
cherche un dégât là où il n'y en a pas.
|
||||||
"""
|
"""
|
||||||
disparues = {
|
disparues = {
|
||||||
table: avant for table, avant, apres in perdues if apres == 0 and avant
|
table: avant
|
||||||
|
for table, avant, apres, _connu in perdues
|
||||||
|
if apres == 0 and avant
|
||||||
}
|
}
|
||||||
apparues = {
|
apparues = {
|
||||||
table: apres for table, avant, apres in gagnees if avant == 0 and apres
|
table: apres for table, avant, apres in gagnees if avant == 0 and apres
|
||||||
|
|
@ -528,21 +698,51 @@ def render_compare(diff, colour, limit=8):
|
||||||
# LE signal qui compte : un module en moins se voit, une table qui
|
# LE signal qui compte : un module en moins se voit, une table qui
|
||||||
# passe de quatre mille lignes à zéro ne se voit nulle part.
|
# passe de quatre mille lignes à zéro ne se voit nulle part.
|
||||||
#
|
#
|
||||||
# Une table probablement renommée reste dans la LISTE, annotée. L'en
|
# Les pertes EXPLIQUÉES sont séparées des autres, jamais retirées.
|
||||||
# retirer était le vrai danger : un rapprochement faux — et il y en
|
# Sans cette séparation on lisait « 81 tables ont perdu des lignes »
|
||||||
# a eu — aurait fait disparaître une perte réelle du rapport.
|
# dont la plus grosse — ir_translation, 32 984 lignes — était une
|
||||||
|
# refonte voulue par Odoo : les vraies questions se noyaient dans
|
||||||
|
# les fausses, ce qui est la façon la plus sûre de ne pas les voir.
|
||||||
vers = {a: b for a, b, _n in diff["renamed"]}
|
vers = {a: b for a, b, _n in diff["renamed"]}
|
||||||
lignes.append(
|
ouvertes = [item for item in perdues if not item[3]]
|
||||||
f" {paint('▼', 'fail', colour)} {len(perdues)}"
|
connues = [item for item in perdues if item[3]]
|
||||||
f" {t('table(s) lost rows')} :"
|
if ouvertes:
|
||||||
)
|
lignes.append(
|
||||||
for table, avant, apres in perdues[:limit]:
|
f" {paint('▼', 'fail', colour)} {len(ouvertes)}"
|
||||||
note = (
|
f" {t('table(s) lost rows, unexplained')} :"
|
||||||
f" ↻ {t('probably renamed to')} {vers[table]}"
|
|
||||||
if table in vers
|
|
||||||
else ""
|
|
||||||
)
|
)
|
||||||
lignes.append(f" {table:<40} {avant:>8} → {apres}{note}")
|
for table, avant, apres, _rien in ouvertes[:limit]:
|
||||||
|
note = (
|
||||||
|
f" ↻ {t('probably renamed to')} {vers[table]}"
|
||||||
|
if table in vers
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
lignes.append(f" {table:<40} {avant:>8} → {apres}{note}")
|
||||||
|
if len(ouvertes) > limit:
|
||||||
|
lignes.append(f" … {len(ouvertes) - limit} {t('more')}")
|
||||||
|
if connues:
|
||||||
|
lignes.append(
|
||||||
|
f" {paint('▽', 'dim', colour)} {len(connues)}"
|
||||||
|
f" {t('table(s) Odoo moved or retired')} :"
|
||||||
|
)
|
||||||
|
for table, avant, apres, connu in connues[:limit]:
|
||||||
|
if connu["into"]:
|
||||||
|
recue = connu.get("gained")
|
||||||
|
# Une fusion dont la table d'accueil n'a PAS grossi
|
||||||
|
# n'explique rien : le dire, plutôt que de classer la
|
||||||
|
# perte comme attendue et passer à autre chose.
|
||||||
|
accueil = (
|
||||||
|
f"+{recue} {t('there')}"
|
||||||
|
if recue and recue > 0
|
||||||
|
else paint(t("but it gained nothing"), "fail", colour)
|
||||||
|
)
|
||||||
|
ou = f"→ {connu['into']} ({accueil})"
|
||||||
|
else:
|
||||||
|
ou = t("retired from the database")
|
||||||
|
lignes.append(f" {table:<40} {avant:>8} → {apres} {ou}")
|
||||||
|
lignes.append(f" {connu['why']}")
|
||||||
|
if len(connues) > limit:
|
||||||
|
lignes.append(f" … {len(connues) - limit} {t('more')}")
|
||||||
delta = diff["delta"]
|
delta = diff["delta"]
|
||||||
lignes.append(
|
lignes.append(
|
||||||
" "
|
" "
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,13 @@ def rows(lst_snapshot):
|
||||||
# Le précédent voyage avec la ligne : le panneau en a besoin pour
|
# Le précédent voyage avec la ligne : le panneau en a besoin pour
|
||||||
# écrire « 171 (−43) », et le recalculer là-bas ferait deux
|
# écrire « 171 (−43) », et le recalculer là-bas ferait deux
|
||||||
# sources pour la même comparaison.
|
# sources pour la même comparaison.
|
||||||
|
# La colonne compte les pertes INEXPLIQUÉES : afficher 81 quand
|
||||||
|
# 79 sont des refontes voulues par Odoo ferait fuir le lecteur du
|
||||||
|
# seul chiffre qui demande une réponse.
|
||||||
perdu = (
|
perdu = (
|
||||||
0
|
0
|
||||||
if diff is None or diff.get("unavailable")
|
if diff is None or diff.get("unavailable")
|
||||||
else len(diff["rows_lost"])
|
else len([item for item in diff["rows_lost"] if not item[3]])
|
||||||
)
|
)
|
||||||
lst.append(
|
lst.append(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5783,6 +5783,26 @@ TRANSLATIONS = {
|
||||||
"fr": "Choisissez un palier pour voir ses fichiers absents.",
|
"fr": "Choisissez un palier pour voir ses fichiers absents.",
|
||||||
"en": "Pick a step to see its missing files.",
|
"en": "Pick a step to see its missing files.",
|
||||||
},
|
},
|
||||||
|
"table(s) lost rows, unexplained": {
|
||||||
|
"fr": "table(s) ont perdu des lignes, sans explication",
|
||||||
|
"en": "table(s) lost rows, unexplained",
|
||||||
|
},
|
||||||
|
"table(s) Odoo moved or retired": {
|
||||||
|
"fr": "table(s) qu'Odoo a déplacées ou retirées",
|
||||||
|
"en": "table(s) Odoo moved or retired",
|
||||||
|
},
|
||||||
|
"retired from the database": {
|
||||||
|
"fr": "retirée de la base",
|
||||||
|
"en": "retired from the database",
|
||||||
|
},
|
||||||
|
"but it gained nothing": {
|
||||||
|
"fr": "mais elle n'a rien reçu",
|
||||||
|
"en": "but it gained nothing",
|
||||||
|
},
|
||||||
|
"there": {
|
||||||
|
"fr": "là-bas",
|
||||||
|
"en": "there",
|
||||||
|
},
|
||||||
"Clean the database before testing the pages?": {
|
"Clean the database before testing the pages?": {
|
||||||
"fr": "Nettoyer la base avant de tester les pages ?",
|
"fr": "Nettoyer la base avant de tester les pages ?",
|
||||||
"en": "Clean the database before testing the pages?",
|
"en": "Clean the database before testing the pages?",
|
||||||
|
|
|
||||||
|
|
@ -122,17 +122,19 @@ class TestWhatIsGainedAndLost(Base):
|
||||||
def test_a_table_that_empties_is_reported(self):
|
def test_a_table_that_empties_is_reported(self):
|
||||||
# LE signal : un module en moins se voit, une table qui passe de
|
# LE signal : un module en moins se voit, une table qui passe de
|
||||||
# cent lignes à zéro ne se voit nulle part.
|
# cent lignes à zéro ne se voit nulle part.
|
||||||
|
# Une table NEUTRE : le mécanisme se teste sans la carte
|
||||||
|
# sémantique, qui a ses propres tests.
|
||||||
diff = quality.compare(
|
diff = quality.compare(
|
||||||
snapshot(table={"account_invoice": 651}),
|
snapshot(table={"ma_table": 651}),
|
||||||
snapshot(table={"account_invoice": 0}),
|
snapshot(table={"ma_table": 0}),
|
||||||
)
|
)
|
||||||
self.assertEqual(diff["rows_lost"], [("account_invoice", 651, 0)])
|
self.assertEqual(diff["rows_lost"], [("ma_table", 651, 0, None)])
|
||||||
|
|
||||||
def test_a_table_that_disappears_counts_as_emptied(self):
|
def test_a_table_that_disappears_counts_as_emptied(self):
|
||||||
diff = quality.compare(
|
diff = quality.compare(
|
||||||
snapshot(table={"account_invoice": 651}), snapshot(table={})
|
snapshot(table={"ma_table": 651}), snapshot(table={})
|
||||||
)
|
)
|
||||||
self.assertEqual(diff["rows_lost"], [("account_invoice", 651, 0)])
|
self.assertEqual(diff["rows_lost"], [("ma_table", 651, 0, None)])
|
||||||
|
|
||||||
def test_an_empty_table_that_disappears_is_not_a_loss(self):
|
def test_an_empty_table_that_disappears_is_not_a_loss(self):
|
||||||
# Rien à perdre : le signaler noierait les vraies pertes.
|
# Rien à perdre : le signaler noierait les vraies pertes.
|
||||||
|
|
@ -207,7 +209,7 @@ class TestNotCryingWolfOnRenames(Base):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
diff["renamed"], [("muk_dms_directory", "dms_directory", 7)]
|
diff["renamed"], [("muk_dms_directory", "dms_directory", 7)]
|
||||||
)
|
)
|
||||||
self.assertIn(("muk_dms_directory", 7, 0), diff["rows_lost"])
|
self.assertIn(("muk_dms_directory", 7, 0, None), diff["rows_lost"])
|
||||||
|
|
||||||
def test_the_report_says_it_is_only_probable(self):
|
def test_the_report_says_it_is_only_probable(self):
|
||||||
diff = quality.compare(
|
diff = quality.compare(
|
||||||
|
|
@ -219,6 +221,142 @@ class TestNotCryingWolfOnRenames(Base):
|
||||||
self.assertIn("probably renamed", texte)
|
self.assertIn("probably renamed", texte)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheSemanticMap(Base):
|
||||||
|
"""« 81 tables ont perdu des lignes » noyait les vraies questions.
|
||||||
|
|
||||||
|
La plus grosse d'entre elles — `ir_translation`, 32 984 lignes — est
|
||||||
|
une refonte voulue par Odoo en 16. Mettre les refontes et les pertes
|
||||||
|
réelles sur le même plan est la façon la plus sûre de ne pas voir les
|
||||||
|
secondes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def perte(
|
||||||
|
self,
|
||||||
|
table,
|
||||||
|
avant_n,
|
||||||
|
apres_n,
|
||||||
|
version="18.0",
|
||||||
|
cible=None,
|
||||||
|
cible_avant=0,
|
||||||
|
cible_apres=0,
|
||||||
|
):
|
||||||
|
tbl_avant = {table: avant_n}
|
||||||
|
tbl_apres = {} if apres_n == 0 else {table: apres_n}
|
||||||
|
if cible:
|
||||||
|
tbl_avant[cible] = cible_avant
|
||||||
|
tbl_apres[cible] = cible_apres
|
||||||
|
return quality.compare(
|
||||||
|
snapshot(odoo="12.0", table=tbl_avant),
|
||||||
|
snapshot(odoo=version, table=tbl_apres),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_known_merge_is_explained(self):
|
||||||
|
diff = self.perte(
|
||||||
|
"account_invoice",
|
||||||
|
651,
|
||||||
|
0,
|
||||||
|
cible="account_move",
|
||||||
|
cible_avant=1371,
|
||||||
|
cible_apres=1812,
|
||||||
|
)
|
||||||
|
connu = [x for x in diff["rows_lost"] if x[0] == "account_invoice"][0]
|
||||||
|
self.assertIsNotNone(connu[3])
|
||||||
|
self.assertEqual(connu[3]["into"], "account_move")
|
||||||
|
self.assertEqual(connu[3]["gained"], 441)
|
||||||
|
|
||||||
|
def test_a_retired_table_is_explained_without_a_target(self):
|
||||||
|
diff = self.perte("ir_translation", 32984, 0)
|
||||||
|
connu = [x for x in diff["rows_lost"] if x[0] == "ir_translation"][0]
|
||||||
|
self.assertIsNotNone(connu[3])
|
||||||
|
self.assertIsNone(connu[3]["into"])
|
||||||
|
|
||||||
|
def test_the_map_does_not_apply_BEFORE_its_version(self):
|
||||||
|
"""Une refonte de la 16 n'explique rien d'un palier 12 → 13.
|
||||||
|
|
||||||
|
L'accepter ferait taire une vraie perte sous prétexte que la table
|
||||||
|
porte le nom d'une autre, refondue trois versions plus tard.
|
||||||
|
"""
|
||||||
|
diff = self.perte("ir_translation", 32984, 0, version="13.0")
|
||||||
|
connu = [x for x in diff["rows_lost"] if x[0] == "ir_translation"][0]
|
||||||
|
self.assertIsNone(connu[3])
|
||||||
|
|
||||||
|
def test_it_applies_AT_its_version(self):
|
||||||
|
diff = self.perte("ir_translation", 32984, 0, version="16.0")
|
||||||
|
connu = [x for x in diff["rows_lost"] if x[0] == "ir_translation"][0]
|
||||||
|
self.assertIsNotNone(connu[3])
|
||||||
|
|
||||||
|
def test_an_explained_loss_is_STILL_in_the_list(self):
|
||||||
|
# La règle qui vaut plus que tout : expliquer n'est pas cacher.
|
||||||
|
diff = self.perte("ir_translation", 32984, 0)
|
||||||
|
self.assertIn("ir_translation", [x[0] for x in diff["rows_lost"]])
|
||||||
|
|
||||||
|
def test_the_partition_loses_nothing(self):
|
||||||
|
diff = quality.compare(
|
||||||
|
snapshot(
|
||||||
|
odoo="12.0", table={"ir_translation": 100, "ma_table": 50}
|
||||||
|
),
|
||||||
|
snapshot(odoo="18.0", table={}),
|
||||||
|
)
|
||||||
|
perdues = diff["rows_lost"]
|
||||||
|
ouvertes = [x for x in perdues if not x[3]]
|
||||||
|
connues = [x for x in perdues if x[3]]
|
||||||
|
self.assertEqual(len(perdues), len(ouvertes) + len(connues))
|
||||||
|
self.assertEqual(len(perdues), 2)
|
||||||
|
|
||||||
|
def test_a_merge_whose_target_gained_NOTHING_is_flagged(self):
|
||||||
|
"""Le cas qui compte : la carte dit où les données sont allées.
|
||||||
|
|
||||||
|
Si elles n'y sont pas, l'explication ne tient pas — et la classer
|
||||||
|
« attendue » puis passer à autre chose serait exactement l'erreur
|
||||||
|
que la carte devait empêcher.
|
||||||
|
"""
|
||||||
|
diff = self.perte(
|
||||||
|
"account_invoice",
|
||||||
|
651,
|
||||||
|
0,
|
||||||
|
cible="account_move",
|
||||||
|
cible_avant=1371,
|
||||||
|
cible_apres=1371,
|
||||||
|
)
|
||||||
|
connu = [x for x in diff["rows_lost"] if x[0] == "account_invoice"][0]
|
||||||
|
self.assertEqual(connu[3]["gained"], 0)
|
||||||
|
texte = "\n".join(quality.render_compare(diff, colour=False))
|
||||||
|
self.assertIn("gained nothing", texte)
|
||||||
|
|
||||||
|
def test_the_report_puts_the_unexplained_FIRST(self):
|
||||||
|
diff = quality.compare(
|
||||||
|
snapshot(
|
||||||
|
odoo="12.0", table={"ir_translation": 100, "ma_table": 50}
|
||||||
|
),
|
||||||
|
snapshot(odoo="18.0", table={}),
|
||||||
|
)
|
||||||
|
texte = "\n".join(quality.render_compare(diff, colour=False))
|
||||||
|
self.assertLess(texte.index("unexplained"), texte.index("moved or"))
|
||||||
|
|
||||||
|
def test_an_unknown_table_stays_unexplained(self):
|
||||||
|
diff = self.perte("ma_table_a_moi", 50, 0)
|
||||||
|
self.assertIsNone(diff["rows_lost"][0][3])
|
||||||
|
|
||||||
|
def test_every_entry_of_the_map_is_complete(self):
|
||||||
|
# Une entrée sans « why » expliquerait sans dire pourquoi.
|
||||||
|
for entree in quality.SEMANTIC_MAP:
|
||||||
|
for cle in ("since", "table", "into", "kind", "why"):
|
||||||
|
self.assertIn(cle, entree, entree)
|
||||||
|
self.assertTrue(entree["why"], entree)
|
||||||
|
self.assertIn(entree["kind"], ("merged", "renamed", "retired"))
|
||||||
|
|
||||||
|
def test_the_column_counts_only_what_needs_an_answer(self):
|
||||||
|
# Afficher 81 quand 14 sont des refontes voulues ferait fuir le
|
||||||
|
# lecteur du seul chiffre qui demande une réponse.
|
||||||
|
lst = [
|
||||||
|
snapshot(
|
||||||
|
odoo="12.0", table={"ir_translation": 100, "ma_table": 50}
|
||||||
|
),
|
||||||
|
snapshot(odoo="18.0", table={}),
|
||||||
|
]
|
||||||
|
self.assertEqual(qtui.rows(lst)[1]["detail"], "1")
|
||||||
|
|
||||||
|
|
||||||
class TestTheOverallReport(Base):
|
class TestTheOverallReport(Base):
|
||||||
def test_it_compares_the_ENDS_not_the_sum_of_steps(self):
|
def test_it_compares_the_ENDS_not_the_sum_of_steps(self):
|
||||||
"""Un module retiré en 15 puis remis en 17 n'a rien perdu.
|
"""Un module retiré en 15 puis remis en 17 n'a rien perdu.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue