Ctrl K
ComunidadGuías…

test_runtime_ecriture_atomique.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Contrats des petits fichiers runtime, sur des destinations temporaires."""

import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock

from nico579_commons import atomique

import runtime


class TestsEcrituresAtomiques(unittest.TestCase):
    def setUp(self):
        temporaire = tempfile.TemporaryDirectory(prefix="blink-ecritures-")
        self.addCleanup(temporaire.cleanup)
        self.racine = Path(temporaire.name)
        self.donnees = self.racine / "donnees"
        self.controle = self.racine / "controle"
        self.donnees.mkdir()
        self.controle.mkdir()
        self.patch = mock.patch.object(runtime, "app_dir", return_value=self.donnees)
        self.patch.start()
        self.addCleanup(self.patch.stop)
        controle = mock.patch.object(runtime, "_dossier_controle", return_value=self.controle)
        controle.start()
        self.addCleanup(controle.stop)

    def ecritures(self):
        fiche = {"name": "Entrée", "enfants": [12, 34]}
        return (
            (self.donnees / runtime.REGLAGES,
             lambda: runtime.ecrire_reglages(**runtime.REGLAGES_DEFAUT),
             json.dumps(runtime.REGLAGES_DEFAUT)),
            (self.donnees / runtime.LANGUE, lambda: runtime.ecrire_langue("en"), "en"),
            (self.donnees / runtime.SUPPRESSION_AUTO,
             lambda: runtime.ecrire_suppression_auto({"Jardin", "Entrée"}),
             json.dumps(["Entrée", "Jardin"], ensure_ascii=False)),
            (self.controle / "instance.json",
             lambda: runtime._ecrire_fiche(self.controle / "instance.json", fiche),
             json.dumps(fiche, ensure_ascii=False)),
            (self.controle / runtime.MARQUEUR_CONFIGURATION_EN_ATTENTE,
             lambda: runtime._ecrire_marqueur_configuration(
                 runtime.MARQUEUR_CONFIGURATION_EN_ATTENTE), runtime.VERSION),
        )

    def test_contenu_utf8_et_destination_conserves(self):
        for cible, ecrire, attendu in self.ecritures():
            with self.subTest(cible=cible.name):
                ecrire()
                self.assertEqual(cible.read_text(encoding="utf-8"), attendu)
                self.assertEqual(list(self.racine.rglob("*.part")), [])

    def test_ancienne_version_reste_lisible_jusqu_au_remplacement(self):
        remplacer = os.replace
        for cible, ecrire, attendu in self.ecritures():
            with self.subTest(cible=cible.name):
                cible.write_text("ancienne version", encoding="utf-8")
                remplacements = []

                def verifier(source, destination):
                    self.assertEqual(destination, cible)
                    self.assertEqual(source.parent, cible.parent)
                    self.assertNotEqual(source, cible)
                    self.assertEqual(cible.read_text(encoding="utf-8"), "ancienne version")
                    self.assertEqual(source.read_text(encoding="utf-8"), attendu)
                    remplacements.append(source)
                    return remplacer(source, destination)

                with mock.patch.object(atomique.os, "replace", verifier):
                    ecrire()
                self.assertEqual(len(remplacements), 1)
                self.assertFalse(remplacements[0].exists())

    def test_ecriture_partielle_refusee_preserve_la_cible_et_nettoie(self):
        ecrire_texte = Path.write_text
        for cible, ecrire, _ in self.ecritures():
            with self.subTest(cible=cible.name):
                cible.write_text("ancienne version", encoding="utf-8")
                erreur = PermissionError("écriture interrompue")

                def interrompre(source, contenu, *args, **kwargs):
                    self.assertNotEqual(source, cible)
                    ecrire_texte(source, contenu[:2], *args, **kwargs)
                    raise erreur

                with mock.patch.object(Path, "write_text", interrompre):
                    with self.assertRaises(PermissionError) as recue:
                        ecrire()
                self.assertIs(recue.exception, erreur)
                self.assertEqual(cible.read_text(encoding="utf-8"), "ancienne version")
                self.assertEqual(list(self.racine.rglob("*.part")), [])

    def test_remplacement_refuse_preserve_la_cible_et_nettoie(self):
        for cible, ecrire, _ in self.ecritures():
            with self.subTest(cible=cible.name):
                cible.write_text("ancienne version", encoding="utf-8")
                erreur = PermissionError("destination verrouillée")
                with mock.patch.object(atomique.os, "replace", side_effect=erreur),                         mock.patch.object(atomique.time, "sleep"):
                    with self.assertRaises(PermissionError) as recue:
                        ecrire()
                self.assertIs(recue.exception, erreur)
                self.assertEqual(cible.read_text(encoding="utf-8"), "ancienne version")
                self.assertEqual(list(self.racine.rglob("*.part")), [])

    def test_deux_ecritures_imbriquees_ont_des_temporaires_distincts(self):
        remplacer = os.replace
        for cible, ecrire, attendu in self.ecritures():
            with self.subTest(cible=cible.name):
                sources = []

                def imbriquer(source, destination):
                    sources.append(source)
                    if len(sources) == 1:
                        ecrire()
                        self.assertTrue(source.exists())
                    return remplacer(source, destination)

                with mock.patch.object(atomique.os, "replace", imbriquer):
                    ecrire()
                self.assertEqual(len(set(sources)), 2)
                self.assertEqual(cible.read_text(encoding="utf-8"), attendu)
                self.assertEqual(list(self.racine.rglob("*.part")), [])

    def test_reglages_dans_destination_explicite_sans_changer_la_racine_active(self):
        runtime.ecrire_reglages(**runtime.REGLAGES_DEFAUT, dossier=self.controle)
        self.assertTrue((self.controle / runtime.REGLAGES).is_file())
        self.assertFalse((self.donnees / runtime.REGLAGES).exists())
        self.assertEqual(runtime.app_dir(), self.donnees)

    def test_fiche_ne_cree_pas_silencieusement_un_dossier_absent(self):
        cible = self.racine / "absent" / "instance.json"
        with self.assertRaises(FileNotFoundError):
            runtime._ecrire_fiche(cible, {})
        self.assertFalse(cible.parent.exists())

    def test_marqueur_conserve_la_creation_de_la_racine_de_controle(self):
        controle = self.racine / "nouveau-controle"
        with mock.patch.object(runtime, "_dossier_controle", return_value=controle):
            runtime._ecrire_marqueur_configuration(runtime.MARQUEUR_CONFIGURATION_EN_ATTENTE)
        self.assertEqual(
            (controle / runtime.MARQUEUR_CONFIGURATION_EN_ATTENTE).read_text(encoding="utf-8"),
            runtime.VERSION)
        self.assertFalse((self.donnees / runtime.MARQUEUR_CONFIGURATION_EN_ATTENTE).exists())


if __name__ == "__main__":
    unittest.main()