[ADD] migration: convert the <tree> tags Odoo 18 renamed to <list>
OpenUpgrade converts the COLUMN and never the ARCH:
def _fix_list_view_type(cr):
cr.execute("UPDATE ir_ui_view SET type='list' WHERE type='tree'")
so the load dies on "the root node of a list view should be <list>".
A module view recovers at its next update, which rewrites the arch from
XML. A view with no xmlid -- a hand-made list, a website copy -- is
rewritten by nothing and stays broken forever. The report says which is
which, because the two call for different work.
Every occurrence, not just the root: a <tree> nested in a form is a
one2many list and is refused just the same. Odoo 18's own addons hold
none, so nothing legitimate is at risk. Gated on version 18: before it,
the tag is correct and "fixing" it would break healthy views.
Four tests run the SQL against a real PostgreSQL. A text assertion
cannot see what a query does -- it took a live run to prove every
language of the jsonb arch is converted, not just en_US.
--- FR ---
OpenUpgrade convertit la COLONNE et jamais l'ARCH, d'où l'échec sur « le
nœud racine d'une vue list devrait être <list> ». Une vue de module s'en
remet à sa prochaine mise à jour ; une vue sans xmlid — liste faite
main, copie de site — n'est réécrite par rien. Le rapport distingue les
deux, car le travail diffère.
Toutes les occurrences, pas seulement la racine : un <tree> imbriqué
dans un formulaire est une liste one2many, refusée pareillement. Les
addons d'Odoo 18 n'en portent aucun. Bridé à la 18 : avant, la balise
est juste.
Quatre tests exécutent le SQL contre un vrai PostgreSQL. Une assertion
sur du texte ne voit pas ce qu'une requête fait — il a fallu l'exécuter
pour prouver que toutes les langues du jsonb sont converties.
Assisted-by: Claude Opus 5
This commit is contained in:
parent
300238fe83
commit
7e30377e56
3 changed files with 469 additions and 21 deletions
|
|
@ -2,7 +2,10 @@
|
|||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Une vue dont le `type` stocké contredit son héritage.
|
||||
"""Deux données de vue qu'Odoo refuse, et qu'aucune mise à jour ne répare.
|
||||
|
||||
A — le `type` stocké contredit l'héritage
|
||||
=========================================
|
||||
|
||||
Le symptôme est brutal : Odoo refuse de charger la base.
|
||||
|
||||
|
|
@ -37,6 +40,29 @@ lui-même, citée plus haut. Vérifié : zéro écart sur une installation 18
|
|||
neuve et sur quatre bases migrées, un seul sur celle qui refusait de
|
||||
charger. Le détecteur ne crie pas au loup.
|
||||
|
||||
B — un `<tree>` survit dans une base Odoo 18
|
||||
============================================
|
||||
La 18 a renommé le type `tree` en `list`, balise comprise. OpenUpgrade
|
||||
convertit la COLONNE :
|
||||
|
||||
def _fix_list_view_type(cr):
|
||||
'''Former tree views have view type list now.'''
|
||||
cr.execute("UPDATE ir_ui_view SET type='list' WHERE type='tree'")
|
||||
|
||||
…et jamais l'ARCH. Le chargement meurt alors sur
|
||||
|
||||
Le nœud racine d'une vue list devrait être <list>, et non <tree>
|
||||
|
||||
Une vue de module s'en remet à la première mise à jour du module, qui
|
||||
réécrit son arch depuis le XML. Une vue SANS xmlid — une liste faite à
|
||||
la main, une copie de site — n'est réécrite par rien : elle reste
|
||||
cassée pour toujours.
|
||||
|
||||
Le remplacement porte sur TOUTES les occurrences, pas seulement la
|
||||
racine : un `<tree>` imbriqué dans un formulaire (une liste one2many)
|
||||
est tout aussi refusé. Vérifié : les addons d'Odoo 18 n'en contiennent
|
||||
plus une seule, donc tout `<tree>` d'une base 18 est un reste.
|
||||
|
||||
Codes de sortie : 0 rien à signaler, 1 des trouvailles, 2 l'outil a échoué.
|
||||
"""
|
||||
|
||||
|
|
@ -85,6 +111,10 @@ ORDER BY v.id
|
|||
"""
|
||||
|
||||
|
||||
# La 18 renomme `tree` en `list`. Avant, la balise est légitime.
|
||||
PREMIERE_VERSION_LIST = 18
|
||||
|
||||
|
||||
def run_psql(database, sql, read_only=True):
|
||||
"""Interroger la base. En lecture seule sauf demande explicite."""
|
||||
env = os.environ.copy()
|
||||
|
|
@ -121,6 +151,97 @@ def find(database):
|
|||
]
|
||||
|
||||
|
||||
def db_major(database):
|
||||
"""La version majeure d'Odoo de cette base, ou None.
|
||||
|
||||
Celle qu'Odoo inscrit, pas celle du checkout : on répare une base,
|
||||
pas un répertoire.
|
||||
"""
|
||||
lignes = run_psql(
|
||||
database,
|
||||
"SELECT latest_version FROM ir_module_module WHERE name = 'base'",
|
||||
)
|
||||
if not lignes or not lignes[0][0]:
|
||||
return None
|
||||
try:
|
||||
return int(lignes[0][0].split(".")[0])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def arch_is_jsonb(database):
|
||||
"""`arch_db` est un jsonb depuis la 16, un texte avant."""
|
||||
lignes = run_psql(
|
||||
database,
|
||||
"SELECT data_type FROM information_schema.columns"
|
||||
" WHERE table_name = 'ir_ui_view' AND column_name = 'arch_db'",
|
||||
)
|
||||
return bool(lignes) and lignes[0][0] == "jsonb"
|
||||
|
||||
|
||||
def arch_as_text(jsonb):
|
||||
"""L'expression SQL qui rend l'arch en texte, quelle que soit sa forme."""
|
||||
return "arch_db::text" if jsonb else "coalesce(arch_db, '')"
|
||||
|
||||
|
||||
def find_tree(database, jsonb):
|
||||
"""Les vues qui portent encore un `<tree>`. [] hors des bases 18+."""
|
||||
texte = arch_as_text(jsonb)
|
||||
lignes = run_psql(
|
||||
database,
|
||||
f"SELECT v.id::text, v.type,"
|
||||
f" coalesce(d.module || '.' || d.name, '-'),"
|
||||
f" coalesce(v.model, '-'),"
|
||||
f" CASE WHEN d.id IS NULL THEN 'custom' ELSE 'module' END"
|
||||
f" FROM ir_ui_view v"
|
||||
f" LEFT JOIN ir_model_data d"
|
||||
f" ON d.model = 'ir.ui.view' AND d.res_id = v.id"
|
||||
f" WHERE {texte} LIKE '%<tree%'"
|
||||
f" ORDER BY v.id",
|
||||
)
|
||||
if lignes is None:
|
||||
return None
|
||||
return [
|
||||
{
|
||||
"id": int(ligne[0]),
|
||||
"type": ligne[1],
|
||||
"xmlid": ligne[2],
|
||||
"model": ligne[3],
|
||||
"origin": ligne[4],
|
||||
}
|
||||
for ligne in lignes
|
||||
if len(ligne) >= 5
|
||||
]
|
||||
|
||||
|
||||
def repair_tree_sql(vues, jsonb):
|
||||
"""Le SQL qui remplace `<tree>` par `<list>`, ou "".
|
||||
|
||||
TOUTES les occurrences : un `<tree>` imbriqué dans un formulaire est
|
||||
refusé autant que celui de la racine. Les addons d'Odoo 18 n'en
|
||||
contiennent plus une seule, donc il n'y a rien de légitime à épargner.
|
||||
"""
|
||||
if not vues:
|
||||
return ""
|
||||
liste = ", ".join(str(vue["id"]) for vue in vues)
|
||||
if jsonb:
|
||||
# jsonb : reconstruire l'objet, langue par langue. Une traduction
|
||||
# oubliée laisserait la vue cassée dans cette langue seulement —
|
||||
# une panne qui ne se montre qu'à certains.
|
||||
remplace = (
|
||||
"(SELECT jsonb_object_agg(cle,"
|
||||
" replace(replace(valeur, '<tree', '<list'),"
|
||||
" '</tree>', '</list>'))"
|
||||
" FROM jsonb_each_text(arch_db) AS paires(cle, valeur))"
|
||||
)
|
||||
else:
|
||||
remplace = (
|
||||
"replace(replace(arch_db, '<tree', '<list'),"
|
||||
" '</tree>', '</list>')"
|
||||
)
|
||||
return f"UPDATE ir_ui_view SET arch_db = {remplace} WHERE id IN ({liste})"
|
||||
|
||||
|
||||
def repair_sql(vues):
|
||||
"""Le SQL de correction, ou "" s'il n'y a rien à corriger.
|
||||
|
||||
|
|
@ -142,7 +263,7 @@ def repair_sql(vues):
|
|||
|
||||
def render(vues, applique=False):
|
||||
if not vues:
|
||||
return [f"✅ {t('Every view type agrees with its inheritance.')}"]
|
||||
return []
|
||||
lignes = [
|
||||
f"🩹 {len(vues)} {t('view(s) whose stored type contradicts')}"
|
||||
f" {t('their inheritance')} :"
|
||||
|
|
@ -162,6 +283,31 @@ def render(vues, applique=False):
|
|||
return lignes
|
||||
|
||||
|
||||
def render_tree(vues, applique=False):
|
||||
if not vues:
|
||||
return []
|
||||
lignes = [
|
||||
f"🩹 {len(vues)} {t('view(s) still carry a <tree> tag Odoo 18')}"
|
||||
f" {t('renamed to <list>')} :"
|
||||
]
|
||||
for vue in vues:
|
||||
# L'ORIGINE est le renseignement utile : une vue de module se
|
||||
# répare d'une mise à jour, une vue sans xmlid n'est réécrite
|
||||
# par rien et reste cassée pour toujours.
|
||||
marque = (
|
||||
t("custom — nothing will ever rewrite it")
|
||||
if vue["origin"] == "custom"
|
||||
else t("from a module — a module update also fixes it")
|
||||
)
|
||||
lignes.append(
|
||||
f" {vue['id']:<7} {vue['xmlid'][:40]:<42}"
|
||||
f" {vue['type']:<8} {vue['model'][:22]:<24} {marque}"
|
||||
)
|
||||
if applique:
|
||||
lignes.append(f" ✅ {t('Corrected.')}")
|
||||
return lignes
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
|
|
@ -181,21 +327,36 @@ def main(argv=None):
|
|||
if vues is None:
|
||||
print(f"❌ {t('Cannot read the database: ')}{config.database}")
|
||||
return 2
|
||||
if not vues:
|
||||
print("\n".join(render(vues)))
|
||||
# Le renommage `tree` → `list` n'a de sens qu'à partir de la 18 :
|
||||
# avant, la balise est parfaitement légitime et la « corriger »
|
||||
# casserait des vues saines.
|
||||
jsonb = arch_is_jsonb(config.database)
|
||||
majeure = db_major(config.database)
|
||||
arbres = []
|
||||
if majeure and majeure >= PREMIERE_VERSION_LIST:
|
||||
arbres = find_tree(config.database, jsonb) or []
|
||||
|
||||
if not vues and not arbres:
|
||||
print(f"✅ {t('Every view agrees with what Odoo expects.')}")
|
||||
return 0
|
||||
if not config.apply:
|
||||
print("\n".join(render(vues)))
|
||||
print("\n".join(render(vues) + render_tree(arbres)))
|
||||
return 1
|
||||
sql = repair_sql(vues)
|
||||
if run_psql(config.database, sql, read_only=False) is None:
|
||||
|
||||
for sql in (repair_sql(vues), repair_tree_sql(arbres, jsonb)):
|
||||
if sql and run_psql(config.database, sql, read_only=False) is None:
|
||||
print(f"❌ {t('The correction failed.')}")
|
||||
return 2
|
||||
# Relire APRÈS : annoncer « corrigé » sans vérifier ferait relancer
|
||||
# la migration sur le même mur.
|
||||
if find(config.database) or (arbres and find_tree(config.database, jsonb)):
|
||||
print(f"❌ {t('The correction failed.')}")
|
||||
return 2
|
||||
reste = find(config.database)
|
||||
if reste:
|
||||
print(f"❌ {t('The correction failed.')}")
|
||||
return 2
|
||||
print("\n".join(render(vues, applique=True)))
|
||||
print(
|
||||
"\n".join(
|
||||
render(vues, applique=True) + render_tree(arbres, applique=True)
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6135,10 +6135,6 @@ TRANSLATIONS = {
|
|||
"fr": "Corrigé.",
|
||||
"en": "Corrected.",
|
||||
},
|
||||
"Every view type agrees with its inheritance.": {
|
||||
"fr": "Tous les types de vue s'accordent avec leur héritage.",
|
||||
"en": "Every view type agrees with its inheritance.",
|
||||
},
|
||||
"No module update will fix this: the type is set once,": {
|
||||
"fr": "Aucune mise à jour de module ne corrigera cela : le type est posé une fois,",
|
||||
"en": "No module update will fix this: the type is set once,",
|
||||
|
|
@ -6175,6 +6171,26 @@ TRANSLATIONS = {
|
|||
"fr": "Désinstaller le(s) thème(s) que l'erreur nomme",
|
||||
"en": "Uninstall the theme(s) the error names",
|
||||
},
|
||||
"Every view agrees with what Odoo expects.": {
|
||||
"fr": "Toutes les vues s'accordent avec ce qu'Odoo attend.",
|
||||
"en": "Every view agrees with what Odoo expects.",
|
||||
},
|
||||
"custom — nothing will ever rewrite it": {
|
||||
"fr": "faite main — rien ne la réécrira jamais",
|
||||
"en": "custom — nothing will ever rewrite it",
|
||||
},
|
||||
"from a module — a module update also fixes it": {
|
||||
"fr": "d'un module — une mise à jour la répare aussi",
|
||||
"en": "from a module — a module update also fixes it",
|
||||
},
|
||||
"renamed to <list>": {
|
||||
"fr": "a renommée en <list>",
|
||||
"en": "renamed to <list>",
|
||||
},
|
||||
"view(s) still carry a <tree> tag Odoo 18": {
|
||||
"fr": "vue(s) portent encore une balise <tree> qu'Odoo 18",
|
||||
"en": "view(s) still carry a <tree> tag Odoo 18",
|
||||
},
|
||||
"Census": {
|
||||
"fr": "Recensement",
|
||||
"en": "Census",
|
||||
|
|
|
|||
|
|
@ -88,11 +88,8 @@ class TestTheRepairSql(unittest.TestCase):
|
|||
|
||||
|
||||
class TestTheReport(unittest.TestCase):
|
||||
def test_a_clean_database_says_so(self):
|
||||
texte = "\n".join(fvt.render([]))
|
||||
self.assertIn(
|
||||
todo_i18n.t("Every view type agrees with its inheritance."), texte
|
||||
)
|
||||
def test_a_clean_check_renders_nothing(self):
|
||||
self.assertEqual(fvt.render([]), [])
|
||||
|
||||
def test_a_finding_names_the_view_and_both_types(self):
|
||||
texte = "\n".join(fvt.render([vue(637, "tree", "search")]))
|
||||
|
|
@ -193,6 +190,280 @@ class TestTheExitCodes(unittest.TestCase):
|
|||
self.assertEqual(code, 2)
|
||||
|
||||
|
||||
def arbre(vid=3931, origine="custom", type_="list", xmlid="-"):
|
||||
return {
|
||||
"id": vid,
|
||||
"type": type_,
|
||||
"xmlid": xmlid,
|
||||
"model": "res.partner",
|
||||
"origin": origine,
|
||||
}
|
||||
|
||||
|
||||
class TestTheTreeTag(unittest.TestCase):
|
||||
"""Odoo 18 a renommé `tree` en `list`, balise comprise.
|
||||
|
||||
OpenUpgrade convertit la COLONNE `type` et jamais l'ARCH. Une vue de
|
||||
module s'en remet à la première mise à jour ; une vue sans xmlid
|
||||
n'est réécrite par rien.
|
||||
"""
|
||||
|
||||
def test_the_replacement_covers_open_AND_close(self):
|
||||
sql = fvt.repair_tree_sql([arbre()], jsonb=True)
|
||||
self.assertIn("'<tree', '<list'", sql)
|
||||
self.assertIn("'</tree>', '</list>'", sql)
|
||||
|
||||
def test_it_replaces_EVERY_occurrence_not_just_the_root(self):
|
||||
# Un `<tree>` imbriqué dans un formulaire — une liste one2many —
|
||||
# est refusé autant que celui de la racine. `replace` en SQL
|
||||
# remplace tout ; un ancrage `^` n'aurait pris que le premier.
|
||||
sql = fvt.repair_tree_sql([arbre()], jsonb=True)
|
||||
self.assertNotIn("^", sql)
|
||||
self.assertNotIn("regexp", sql.lower())
|
||||
|
||||
def test_jsonb_is_rebuilt_language_by_language(self):
|
||||
# Une traduction oubliée laisserait la vue cassée dans cette
|
||||
# langue seulement — une panne qui ne se montre qu'à certains.
|
||||
sql = fvt.repair_tree_sql([arbre()], jsonb=True)
|
||||
self.assertIn("jsonb_each_text", sql)
|
||||
self.assertIn("jsonb_object_agg", sql)
|
||||
|
||||
def test_a_text_column_is_handled_too(self):
|
||||
sql = fvt.repair_tree_sql([arbre()], jsonb=False)
|
||||
self.assertNotIn("jsonb", sql)
|
||||
self.assertIn("replace(replace(arch_db", sql)
|
||||
|
||||
def test_nothing_to_fix_gives_no_sql(self):
|
||||
self.assertEqual(fvt.repair_tree_sql([], jsonb=True), "")
|
||||
|
||||
def test_it_targets_the_listed_ids_only(self):
|
||||
sql = fvt.repair_tree_sql([arbre(11), arbre(22)], jsonb=True)
|
||||
self.assertIn("WHERE id IN (11, 22)", sql)
|
||||
|
||||
def test_the_report_tells_custom_from_module(self):
|
||||
# C'est LE renseignement utile : l'une se répare d'une mise à
|
||||
# jour, l'autre ne se répare jamais toute seule.
|
||||
texte = "\n".join(
|
||||
fvt.render_tree([arbre(1, "custom"), arbre(2, "module")])
|
||||
)
|
||||
self.assertIn(
|
||||
todo_i18n.t("custom — nothing will ever rewrite it"), texte
|
||||
)
|
||||
self.assertIn(
|
||||
todo_i18n.t("from a module — a module update also fixes it"), texte
|
||||
)
|
||||
|
||||
def test_no_tree_renders_nothing(self):
|
||||
self.assertEqual(fvt.render_tree([]), [])
|
||||
|
||||
|
||||
class TestTheSqlAgainstARealPostgres(unittest.TestCase):
|
||||
"""Le SQL, exécuté pour de vrai.
|
||||
|
||||
Une assertion sur le TEXTE d'une requête ne voit pas ce qu'elle
|
||||
fait : ajouter un `LIMIT 1` la laisserait passer alors qu'une seule
|
||||
langue serait convertie — une vue cassée dans les autres, une panne
|
||||
qui ne se montre qu'à certains.
|
||||
"""
|
||||
|
||||
BASE = "tmp_fix_view_type_test"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import shutil as _shutil
|
||||
import subprocess as _sub
|
||||
|
||||
if not _shutil.which("psql") or not _shutil.which("createdb"):
|
||||
raise unittest.SkipTest("PostgreSQL absent")
|
||||
_sub.run(["dropdb", "--if-exists", cls.BASE], capture_output=True)
|
||||
fait = _sub.run(["createdb", cls.BASE], capture_output=True)
|
||||
if fait.returncode:
|
||||
raise unittest.SkipTest("createdb impossible")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
import shutil as _shutil
|
||||
import subprocess as _sub
|
||||
|
||||
if _shutil.which("dropdb"):
|
||||
_sub.run(["dropdb", "--if-exists", cls.BASE], capture_output=True)
|
||||
|
||||
def prepare(self, colonne, valeur):
|
||||
fvt.run_psql(
|
||||
self.BASE, "DROP TABLE IF EXISTS ir_ui_view", read_only=False
|
||||
)
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
f"CREATE TABLE ir_ui_view (id serial PRIMARY KEY,"
|
||||
f" arch_db {colonne})",
|
||||
read_only=False,
|
||||
)
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
f"INSERT INTO ir_ui_view (arch_db) VALUES ({valeur})",
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
def arch(self):
|
||||
lignes = fvt.run_psql(
|
||||
self.BASE, "SELECT arch_db::text FROM ir_ui_view"
|
||||
)
|
||||
return lignes[0][0] if lignes else ""
|
||||
|
||||
def test_every_language_is_converted(self):
|
||||
self.prepare(
|
||||
"jsonb",
|
||||
'\'{"en_US": "<tree><field/></tree>",'
|
||||
' "fr_CA": "<tree><field/></tree>"}\'::jsonb',
|
||||
)
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
fvt.repair_tree_sql([arbre(1)], jsonb=True),
|
||||
read_only=False,
|
||||
)
|
||||
obtenu = self.arch()
|
||||
self.assertNotIn("<tree", obtenu)
|
||||
self.assertEqual(obtenu.count("<list>"), 2)
|
||||
self.assertEqual(obtenu.count("</list>"), 2)
|
||||
|
||||
def test_a_nested_tree_is_converted_too(self):
|
||||
# Une liste one2many dans un formulaire : refusée autant que la
|
||||
# racine, et invisible pour un contrôle qui ne verrait que celle-ci.
|
||||
self.prepare(
|
||||
"jsonb",
|
||||
'\'{"en_US": "<form><field><tree/></field></form>"}\'::jsonb',
|
||||
)
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
fvt.repair_tree_sql([arbre(1)], jsonb=True),
|
||||
read_only=False,
|
||||
)
|
||||
self.assertNotIn("<tree", self.arch())
|
||||
self.assertIn("<list/>", self.arch())
|
||||
|
||||
def test_a_text_column_works_too(self):
|
||||
self.prepare("text", "'<tree><field/></tree>'")
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
fvt.repair_tree_sql([arbre(1)], jsonb=False),
|
||||
read_only=False,
|
||||
)
|
||||
self.assertEqual(self.arch(), "<list><field/></list>")
|
||||
|
||||
def test_the_detection_query_runs(self):
|
||||
# Une requête qui ne compile pas rendrait None, et l'outil
|
||||
# dirait « base illisible » au lieu de « rien à corriger ».
|
||||
self.prepare("jsonb", '\'{"en_US": "<form/>"}\'::jsonb')
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
"CREATE TABLE IF NOT EXISTS ir_model_data"
|
||||
" (id serial, model text, res_id integer, module text, name text)",
|
||||
read_only=False,
|
||||
)
|
||||
fvt.run_psql(
|
||||
self.BASE,
|
||||
"ALTER TABLE ir_ui_view ADD COLUMN IF NOT EXISTS type text,"
|
||||
" ADD COLUMN IF NOT EXISTS model text",
|
||||
read_only=False,
|
||||
)
|
||||
self.assertEqual(fvt.find_tree(self.BASE, jsonb=True), [])
|
||||
|
||||
|
||||
class TestTheVersionGate(unittest.TestCase):
|
||||
"""Avant la 18, `<tree>` est parfaitement légitime."""
|
||||
|
||||
def setUp(self):
|
||||
self.vrai = fvt.run_psql
|
||||
self.ecritures = []
|
||||
|
||||
def tearDown(self):
|
||||
fvt.run_psql = self.vrai
|
||||
|
||||
def branche(self, majeure, arbres=True):
|
||||
def faux(database, sql, read_only=True):
|
||||
if not read_only:
|
||||
self.ecritures.append(sql)
|
||||
return []
|
||||
if "latest_version" in sql:
|
||||
return [[f"{majeure}.0.1.3"]]
|
||||
if "information_schema" in sql:
|
||||
return [["jsonb"]]
|
||||
if "WITH RECURSIVE" in sql:
|
||||
return []
|
||||
if "<tree" in sql:
|
||||
return (
|
||||
[["3931", "list", "-", "res.partner", "custom"]]
|
||||
if arbres
|
||||
else []
|
||||
)
|
||||
return []
|
||||
|
||||
fvt.run_psql = faux
|
||||
|
||||
def lance(self, argv):
|
||||
tampon = io.StringIO()
|
||||
with redirect_stdout(tampon):
|
||||
code = fvt.main(argv)
|
||||
return code, tampon.getvalue()
|
||||
|
||||
def test_a_17_database_is_left_alone(self):
|
||||
# « Corriger » un <tree> légitime casserait une vue saine.
|
||||
self.branche(17)
|
||||
code, sortie = self.lance(["-d", "db"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(self.ecritures, [])
|
||||
self.assertIn(
|
||||
todo_i18n.t("Every view agrees with what Odoo expects."), sortie
|
||||
)
|
||||
|
||||
def test_an_18_database_reports_it(self):
|
||||
self.branche(18)
|
||||
code, sortie = self.lance(["-d", "db"])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("3931", sortie)
|
||||
self.assertEqual(self.ecritures, [])
|
||||
|
||||
def test_an_unknown_version_stays_on_the_safe_side(self):
|
||||
# Sans version on ne SAIT pas : ne rien toucher.
|
||||
self.branche(None)
|
||||
code, _ = self.lance(["-d", "db"])
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
def test_apply_writes_once_per_repair(self):
|
||||
appels = {"n": 0}
|
||||
|
||||
def faux(database, sql, read_only=True):
|
||||
if not read_only:
|
||||
self.ecritures.append(sql)
|
||||
return []
|
||||
if "latest_version" in sql:
|
||||
return [["18.0.1.3"]]
|
||||
if "information_schema" in sql:
|
||||
return [["jsonb"]]
|
||||
if "WITH RECURSIVE" in sql:
|
||||
return []
|
||||
if "<tree" in sql:
|
||||
appels["n"] += 1
|
||||
# Trouvé au premier passage, plus rien au second.
|
||||
return (
|
||||
[["3931", "list", "-", "res.partner", "custom"]]
|
||||
if appels["n"] == 1
|
||||
else []
|
||||
)
|
||||
return []
|
||||
|
||||
fvt.run_psql = faux
|
||||
code, sortie = self.lance(["-d", "db", "--apply"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(len(self.ecritures), 1)
|
||||
self.assertIn(todo_i18n.t("Corrected."), sortie)
|
||||
|
||||
def test_a_correction_that_did_not_take_exits_two(self):
|
||||
self.branche(18)
|
||||
code, _ = self.lance(["-d", "db", "--apply"])
|
||||
self.assertEqual(code, 2)
|
||||
|
||||
|
||||
class TestTheWiring(unittest.TestCase):
|
||||
RACINE = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue