165 lines
6.2 KiB
Python
165 lines
6.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
"""Le journal de migration doit survivre à la migration suivante.
|
||
|
|
|
||
|
|
Il ne vit qu'à un seul endroit, et repartir de zéro écrit par-dessus. Ce qu'on
|
||
|
|
perdait — quels paliers étaient passés, quels modules manquaient — est
|
||
|
|
précisément ce qu'on cherche APRÈS avoir dû recommencer.
|
||
|
|
|
||
|
|
Ces tests portent sur deux choses distinctes : que la copie contienne bien ce
|
||
|
|
qu'elle doit contenir, et qu'elle parte au bon moment — ni sur une reprise
|
||
|
|
partielle, qui ne perd rien, ni sur un journal qui n'a rien à dire.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import glob
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
||
|
|
|
||
|
|
|
||
|
|
def progression(**override):
|
||
|
|
"""Un journal ayant réellement avancé."""
|
||
|
|
data = {
|
||
|
|
"date_create": "2026-08-11 12:05:08",
|
||
|
|
"date_update": "2026-08-12 01:13:23",
|
||
|
|
"migration_file": "./image_db/technolibre_2026-08-12_02h36m38s.zip",
|
||
|
|
"config_database_name": "technolibre_migration_01_neutralize",
|
||
|
|
"state_0_install_odoo": True,
|
||
|
|
"state_1_restore_database": True,
|
||
|
|
"state_4_switch_odoo_lst": [True, False, False],
|
||
|
|
}
|
||
|
|
data.update(override)
|
||
|
|
return data
|
||
|
|
|
||
|
|
|
||
|
|
class ArchiveCase(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
# Chaque test dans un arbre neuf : l'archive s'écrit sous un chemin
|
||
|
|
# relatif, donc le répertoire courant EST le contexte.
|
||
|
|
self.previous = os.getcwd()
|
||
|
|
self.addCleanup(os.chdir, self.previous)
|
||
|
|
os.chdir(tempfile.mkdtemp())
|
||
|
|
|
||
|
|
def archives(self):
|
||
|
|
return sorted(
|
||
|
|
glob.glob("private/odoo/migration/*/migration_log/*.json")
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestWhatIsKept(ArchiveCase):
|
||
|
|
def test_the_whole_log_is_copied_untouched(self):
|
||
|
|
old = progression()
|
||
|
|
path = TodoUpgrade.archive_progression(old, "restart_from_zero")
|
||
|
|
saved = json.load(open(path))
|
||
|
|
for key, value in old.items():
|
||
|
|
self.assertEqual(saved[key], value, key)
|
||
|
|
|
||
|
|
def test_the_copy_says_when_and_from_which_database(self):
|
||
|
|
# La demande, mot pour mot : une date de la copie et le nom de la base
|
||
|
|
# d'origine.
|
||
|
|
path = TodoUpgrade.archive_progression(
|
||
|
|
progression(), "restart_from_zero"
|
||
|
|
)
|
||
|
|
saved = json.load(open(path))
|
||
|
|
self.assertEqual(
|
||
|
|
saved["archived_database"], "technolibre_migration_01_neutralize"
|
||
|
|
)
|
||
|
|
self.assertTrue(saved["archived_at"].startswith("20"))
|
||
|
|
self.assertEqual(saved["archived_reason"], "restart_from_zero")
|
||
|
|
|
||
|
|
def test_the_file_name_carries_both_too(self):
|
||
|
|
# Pour qu'un fichier déplacé hors de son dossier se décrive encore.
|
||
|
|
path = TodoUpgrade.archive_progression(progression(), "x")
|
||
|
|
name = os.path.basename(path)
|
||
|
|
self.assertTrue(
|
||
|
|
name.startswith("technolibre_migration_01_neutralize_")
|
||
|
|
)
|
||
|
|
self.assertRegex(name, r"_\d{4}-\d{2}-\d{2}_\d{2}h\d{2}m\d{2}s\.json$")
|
||
|
|
|
||
|
|
def test_it_lands_under_private_not_in_the_venv(self):
|
||
|
|
# Une réinstallation efface .venv.erplibre/, donc y garder l'historique
|
||
|
|
# reviendrait à le perdre au pire moment.
|
||
|
|
path = TodoUpgrade.archive_progression(progression(), "x")
|
||
|
|
self.assertTrue(path.startswith(os.path.join("private", "odoo")))
|
||
|
|
self.assertNotIn(".venv", path)
|
||
|
|
|
||
|
|
def test_the_database_name_falls_back_to_the_backup_name(self):
|
||
|
|
old = progression()
|
||
|
|
del old["config_database_name"]
|
||
|
|
path = TodoUpgrade.archive_progression(old, "x")
|
||
|
|
self.assertIn("technolibre_2026-08-12_02h36m38s", path)
|
||
|
|
|
||
|
|
def test_two_restarts_in_the_same_second_do_not_overwrite_each_other(self):
|
||
|
|
# L'horodatage est à la seconde : deux copies rapprochées portent le
|
||
|
|
# même nom. Écraser la première annulerait la perte qu'on évite — et
|
||
|
|
# un test qui saute ce cas ne vérifie rien.
|
||
|
|
first = TodoUpgrade.archive_progression(progression(), "x")
|
||
|
|
second = TodoUpgrade.archive_progression(
|
||
|
|
progression(date_update="2026-08-12 02:00:00"), "y"
|
||
|
|
)
|
||
|
|
self.assertNotEqual(first, second)
|
||
|
|
self.assertEqual(len(self.archives()), 2)
|
||
|
|
self.assertEqual(
|
||
|
|
json.load(open(first))["date_update"], "2026-08-12 01:13:23"
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
json.load(open(second))["date_update"], "2026-08-12 02:00:00"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestWhenNothingIsKept(ArchiveCase):
|
||
|
|
def test_an_empty_log_is_not_archived(self):
|
||
|
|
self.assertIsNone(TodoUpgrade.archive_progression({}, "x"))
|
||
|
|
self.assertEqual(self.archives(), [])
|
||
|
|
|
||
|
|
def test_a_log_without_any_state_is_not_archived(self):
|
||
|
|
# Un journal qui n'a rien enregistré n'apprend rien à personne ;
|
||
|
|
# l'archiver ne ferait qu'accumuler des fichiers vides.
|
||
|
|
self.assertIsNone(
|
||
|
|
TodoUpgrade.archive_progression({"migration_file": "a.zip"}, "x")
|
||
|
|
)
|
||
|
|
self.assertEqual(self.archives(), [])
|
||
|
|
|
||
|
|
|
||
|
|
class TestWhenItTriggers(ArchiveCase):
|
||
|
|
"""Seules les réponses qui repartent de zéro archivent."""
|
||
|
|
|
||
|
|
def answer(self, letter):
|
||
|
|
upgrade = TodoUpgrade.__new__(TodoUpgrade)
|
||
|
|
result = TodoUpgrade.apply_resume_answer(
|
||
|
|
upgrade, progression(), letter, {"versions": []}
|
||
|
|
)
|
||
|
|
return result, self.archives()
|
||
|
|
|
||
|
|
def test_restart_from_zero_archives(self):
|
||
|
|
result, archives = self.answer("n")
|
||
|
|
self.assertEqual(result, ({}, True))
|
||
|
|
self.assertEqual(len(archives), 1)
|
||
|
|
|
||
|
|
def test_restart_with_the_same_backup_archives(self):
|
||
|
|
# « r » garde le zip mais jette tous les états : la perte est la même.
|
||
|
|
result, archives = self.answer("r")
|
||
|
|
self.assertTrue(result[1])
|
||
|
|
self.assertNotIn("state_0_install_odoo", result[0])
|
||
|
|
self.assertEqual(len(archives), 1)
|
||
|
|
|
||
|
|
def test_continuing_archives_nothing(self):
|
||
|
|
result, archives = self.answer("c")
|
||
|
|
self.assertEqual(archives, [])
|
||
|
|
self.assertIn("state_0_install_odoo", result[0])
|
||
|
|
|
||
|
|
def test_rewinding_to_a_step_archives_nothing(self):
|
||
|
|
# Une reprise partielle garde le journal : rien n'est perdu, donc rien
|
||
|
|
# n'est à sauver.
|
||
|
|
_, archives = self.answer("0")
|
||
|
|
self.assertEqual(archives, [])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|