erplibre/test/test_qemu_cache_mesure.py
Mathieu Benoit cfbdd9406e [REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.

--- EN ---

This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.

Assisted-by: Claude Opus 5
2026-09-24 14:40:38 -04:00

670 lines
26 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Le verdict du test long dit-il la vérité sur ce qu'il a mesuré ?
Le test long crée deux VM et dure des dizaines de minutes ; son CRITÈRE, lui,
est une fonction pure sur des lignes de journal. La tester ici coûte des
millisecondes et évite la seule chose qu'un test de quarante minutes ne
pardonne pas : un verdict faux au bout de la course.
Le piège que ces cas verrouillent : Arch est une publication continue. Entre
les deux déploiements, un miroir publie des versions neuves que la seconde VM
tirera légitimement — le cache ne sert jamais un index tant que l'amont
répond, donc elle les VERRA. Un critère fondé sur « zéro octet d'amont »
déclarerait le cache en panne alors qu'il fonctionne.
"""
import argparse
import importlib.util
import json
import re
import sys
import tempfile
import unittest
import unittest.mock
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(RACINE / "long_test"))
def charger():
"""Le script long chargé comme module : il n'est pas un paquet."""
chemin = RACINE / "long_test" / "qemu_cache.py"
spec = importlib.util.spec_from_file_location("qemu_cache_long", chemin)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
QC = charger()
def ligne(
url, upstream, octets=1000, issue="stored", classe="immutable", statut=None
):
"""Une ligne du journal d'accès du cache.
La CLASSE est ce que le filtre lit désormais : c'est le cache qui décide
si un fichier est figé, et la tenir ici en dur revenait à laisser un index
se faire passer pour un paquet. Le STATUT n'est écrit que s'il est donné.
"""
l = {
"url": url,
"upstream": upstream,
"bytes": octets,
"outcome": issue,
"class": classe,
}
if statut is not None:
l["status"] = statut
return l
PAQUET_A = (
"https://miroir.example/arch/core/os/x86_64/bash-5.2-1-x86_64.pkg.tar.zst"
)
PAQUET_B = (
"https://miroir.example/arch/core/os/x86_64/git-2.51-1-x86_64.pkg.tar.zst"
)
PAQUET_NEUF = (
"https://miroir.example/arch/core/os/x86_64/rust-1.91-1-x86_64.pkg.tar.zst"
)
INDEX = "https://miroir.example/arch/core/os/x86_64/core.db"
class TestFiltre(unittest.TestCase):
def test_les_index_sont_ecartes(self):
"""Un index n'est JAMAIS servi du cache quand l'amont répond : le
compter ferait échouer un test qui mesure autre chose."""
lignes = [ligne(INDEX, True, classe="volatile"), ligne(PAQUET_A, True)]
gardees = QC.paquets_seulement(lignes)
self.assertEqual(len(gardees), 1)
self.assertEqual(gardees[0]["url"], PAQUET_A)
def test_toutes_les_familles_de_paquets(self):
"""Le filtre ne connaissait que les extensions d'Arch : sur Ubuntu ou
Fedora, la mesure ne trouvait AUCUN fichier et déclarait que rien
n'avait traversé le cache, alors qu'il venait de servir une
installation entière. Il lit maintenant la classe que le cache pose."""
for url in (
PAQUET_A.replace(".pkg.tar.zst", ".pkg.tar.xz"),
"http://miroir.example/pool/main/b/bash/bash_5.2-1_amd64.deb",
"http://miroir.example/Packages/b/bash-5.2-1.fc43.x86_64.rpm",
"https://pypi.example/ab/cd/requests-2.33.0-py3-none-any.whl",
):
self.assertEqual(
len(QC.paquets_seulement([ligne(url, True)])),
1,
f"« {url} » n'est pas compté comme fichier figé",
)
def test_le_filtre_ne_lit_pas_lextension(self):
"""Un fichier que le cache dit volatile est écarté quel que soit son
nom : la décision lui appartient, et une seconde table dériverait."""
self.assertEqual(
QC.paquets_seulement([ligne(PAQUET_A, True, classe="volatile")]),
[],
)
def test_une_url_absente_ne_casse_pas(self):
self.assertEqual(QC.paquets_seulement([{"upstream": True}]), [])
class TestVerdict(unittest.TestCase):
def test_le_cache_a_servi(self):
premier = [ligne(PAQUET_A, True), ligne(PAQUET_B, True)]
second = [
ligne(PAQUET_A, False, issue="hit"),
ligne(PAQUET_B, False, issue="hit"),
]
self.assertTrue(QC.verdict(premier, second, None))
def test_un_paquet_deja_vu_ressorti_est_un_echec(self):
premier = [ligne(PAQUET_A, True)]
second = [ligne(PAQUET_A, True)]
self.assertFalse(
QC.verdict(premier, second, None),
"un fichier déjà tiré est ressorti sur le réseau sans que le "
"verdict s'en plaigne",
)
def test_un_paquet_neuf_ne_fait_pas_echouer(self):
"""LE cas qui justifie le critère : le miroir a publié entre les deux
déploiements. La seconde VM tire légitimement du neuf, et le cache a
pourtant parfaitement servi ce qu'il avait."""
premier = [ligne(PAQUET_A, True)]
second = [
ligne(PAQUET_A, False, issue="hit"),
ligne(PAQUET_NEUF, True, octets=90_000_000),
]
self.assertTrue(
QC.verdict(premier, second, None),
"un paquet publié entre les deux VM a été pris pour une panne",
)
def test_une_mesure_vide_est_un_echec(self):
"""Le cas qui a coûté vingt minutes : un détournement posé sur le
mauvais sous-réseau laisse les VM sortir en direct. Le cache ne voit
rien, le verdict n'a « aucune faute » à signaler — et rendre vrai
déclarerait un succès qu'il n'a jamais mesuré."""
self.assertFalse(
QC.verdict([], [], None),
"un cache que personne ne traverse est déclaré bon",
)
def test_une_seconde_vm_muette_est_un_echec(self):
"""La première a rempli, la seconde n'a rien demandé : il n'y a pas
de mesure, donc pas de succès."""
self.assertFalse(QC.verdict([ligne(PAQUET_A, True)], [], None))
def test_un_miroir_qui_refuse_ne_fait_pas_echouer(self):
"""Un miroir rangé sous un autre chemin répond 404 à chaque paquet ; le
client prend le fichier ailleurs, et le cache le sert du disque. Le
404 repart à l'amont aux deux VM sans que rien n'ait manqué."""
mauvais = PAQUET_A.replace("miroir.example", "autre.example/x")
premier = [
ligne(mauvais, True, octets=153, issue="fetched", statut=404),
ligne(PAQUET_A, True, statut=200),
]
second = [
ligne(mauvais, True, octets=153, issue="fetched", statut=404),
ligne(PAQUET_A, False, issue="hit", statut=200),
]
self.assertTrue(
QC.verdict(premier, second, None),
"le 404 d'un miroir a été compté comme un paquet ressorti",
)
def test_un_paquet_livre_deux_fois_reste_une_faute(self):
"""Le refus est écarté, pas la réponse livrée : un 200 déjà vu qui
repart à l'amont fait toujours échouer."""
premier = [ligne(PAQUET_A, True, statut=200)]
second = [ligne(PAQUET_A, True, statut=200)]
self.assertFalse(QC.verdict(premier, second, None))
def test_le_melange(self):
premier = [ligne(PAQUET_A, True), ligne(PAQUET_B, True)]
second = [
ligne(PAQUET_A, False, issue="hit"),
ligne(PAQUET_B, True),
ligne(PAQUET_NEUF, True),
]
self.assertFalse(
QC.verdict(premier, second, None),
"un seul fichier déjà vu ressorti suffit à faire échouer",
)
class TestLectureDuJournal(unittest.TestCase):
def test_ne_lit_que_ce_qui_suit_le_decalage(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "acces.jsonl"
p.write_text(
json.dumps(ligne(PAQUET_A, True)) + "\n", encoding="utf-8"
)
decalage = p.stat().st_size
with p.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(ligne(PAQUET_B, False)) + "\n")
lues, neuf = QC.lignes_depuis(str(p), decalage)
self.assertEqual([x["url"] for x in lues], [PAQUET_B])
self.assertEqual(neuf, p.stat().st_size)
def test_journal_tourne_repart_du_debut(self):
"""Un journal plus court que le décalage a été remplacé : lire à
l'ancienne position rendrait n'importe quoi, ou rien."""
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "acces.jsonl"
p.write_text(
json.dumps(ligne(PAQUET_A, True)) + "\n", encoding="utf-8"
)
lues, _ = QC.lignes_depuis(str(p), 10_000_000)
self.assertEqual(len(lues), 1)
def test_derniere_ligne_incomplete_sautee(self):
"""Le service écrit pendant qu'on lit : la dernière ligne peut être
tronquée, et une exception ici perdrait toute la mesure."""
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "acces.jsonl"
p.write_text(
json.dumps(ligne(PAQUET_A, True)) + '\n{"url": "tron',
encoding="utf-8",
)
lues, _ = QC.lignes_depuis(str(p), 0)
self.assertEqual(len(lues), 1)
def test_journal_absent(self):
lues, decalage = QC.lignes_depuis("/inexistant/acces.jsonl", 42)
self.assertEqual(lues, [])
self.assertEqual(decalage, 42)
class TestConventionsDuTestLong(unittest.TestCase):
"""Ce que « long_test/ » exige de ses scripts."""
def setUp(self):
self.source = (RACINE / "long_test" / "qemu_cache.py").read_text(
encoding="utf-8"
)
def test_defaire_et_a_blanc(self):
for drapeau in ("--dry-run", "--detruire"):
self.assertIn(drapeau, self.source, f"{drapeau} manque")
def test_destruction_par_uuid(self):
"""Un nom se réutilise, et « --remove-all-storage » efface un disque
pour de bon : le dépôt identifie ses machines par UUID."""
self.assertIn("detruire_etage1", self.source)
self.assertIn("attendu=", self.source)
self.assertIn("uuid_libvirt", self.source)
def test_la_coupure_est_deleguee_et_non_recopiee(self):
"""Les règles vivent dans `script/qemu/cache_offline.py`, que le
formulaire de déploiement emploie aussi : la case « Sans connexion
internet » coupe donc exactement ce que cette mesure mesure.
Leur contenu — le compte visé plutôt que le port, sans quoi la
session ssh de l'opérateur tomberait — est éprouvé chez elles, dans
`test_qemu_cache_offline.py`.
"""
self.assertIn("cache_offline.cut_cmd()", self.source)
self.assertIn("cache_offline.restore_cmd()", self.source)
self.assertNotIn("meta skuid", self.source)
def test_la_coupure_est_toujours_retiree(self):
self.assertIn("finally:", self.source)
self.assertIn("rebrancher_lamont", self.source)
def test_hors_du_lanceur_unitaire(self):
"""Le lanceur balaie « test/test_*.py » : ce script ne doit pas y
être, ni porter un nom qu'il ramasserait."""
self.assertFalse(
(RACINE / "test" / "test_qemu_cache_long.py").exists(),
"un test qui crée de vraies machines est entré dans test/",
)
class TestCeQuIlFautDefaire(unittest.TestCase):
"""Un échec ne doit pas faire perdre le moyen de défaire ce qui existe.
Le nom d'une machine est noté AVANT sa création, pour qu'une création
interrompue à mi-chemin laisse une trace. Une exécution qui échoue tout de
suite écrit donc un rapport qui NOMME une machine sans la connaître, et
s'en tenir au dernier rapport ferait retomber la destruction sur le nom —
ce que ce dépôt a appris à ne plus faire, « --remove-all-storage »
effaçant un disque pour de bon.
"""
def rapports(self, *contenus):
"""Écrit des rapports datés dans un faux dépôt de rapports.
Le balayage des machines VIVANTES est neutralisé ici : sans cela ces
contrôles liraient le libvirt de la machine qui les exécute, et
passeraient ou tomberaient selon ce qui y tourne. Le balayage a ses
propres contrôles, où il est la chose mesurée.
"""
import contextlib
import json as _json
import tempfile
from unittest import mock
d = tempfile.mkdtemp()
for i, c in enumerate(contenus):
nom = f"qemu_cache-2026090{i + 1}-000000.json"
(Path(d) / nom).write_text(_json.dumps(c), encoding="utf-8")
@contextlib.contextmanager
def isole():
with (
mock.patch.object(
QC.os.path,
"expanduser",
lambda p: d if "longtest" in p else p,
),
mock.patch.object(QC, "machines_vivantes", return_value=[]),
):
yield
return d, isole()
def test_un_uuid_connu_survit_a_un_rapport_muet(self):
d, patch = self.rapports(
{"vms": ["vm-1"], "uuids": {"vm-1": "UUID-1"}},
{"vms": ["vm-1"]}, # l'échec qui suit, sans UUID
)
with patch:
machines, lus = QC.machines_a_defaire()
self.assertEqual(len(lus), 2)
self.assertEqual(
machines.get("vm-1"),
"UUID-1",
"l'échec le plus récent a fait perdre l'UUID qui permet de"
" détruire sans risque",
)
def test_les_machines_de_plusieurs_essais_sont_reunies(self):
d, patch = self.rapports(
{"vms": ["vm-1", "vm-2"], "uuids": {"vm-1": "U1", "vm-2": "U2"}},
{"vms": ["vm-3"], "uuids": {"vm-3": "U3"}},
)
with patch:
machines, _ = QC.machines_a_defaire()
self.assertEqual(
sorted(machines),
["vm-1", "vm-2", "vm-3"],
"s'en tenir au dernier rapport laisserait vivantes les machines"
" d'un essai antérieur",
)
def test_un_rapport_sans_machine_est_saute(self):
d, patch = self.rapports({"vms": []}, {"vms": ["vm-1"]})
with patch:
machines, lus = QC.machines_a_defaire()
self.assertEqual(len(lus), 1)
self.assertEqual(sorted(machines), ["vm-1"])
def test_aucun_rapport_et_aucune_machine(self):
"""Rien à lire, rien qui vit : il n'y a rien à défaire."""
d, patch = self.rapports()
with patch:
machines, lus = QC.machines_a_defaire()
self.assertEqual((machines, lus), ({}, []))
class TestUnPlanNestPasUneMesure(unittest.TestCase):
"""Un essai à blanc écrit un rapport, et le comparatif le comptait.
Le plan ne crée aucune VM et n'installe aucun paquet : ses durées valent
zéro. Comptées comme des mesures, elles montrent dans le tableau des
exécutions qui n'ont jamais eu lieu et tirent la moyenne vers le bas — un
témoin à blanc faisait ainsi croire à une comparaison qui n'existait pas.
"""
def test_un_rapport_marque_a_blanc_est_ecarte(self):
self.assertFalse(
QC.mesure_reelle({"dry_run": True, "durees": {"vm-1": 42.0}})
)
def test_un_rapport_ancien_se_trahit_par_ses_zeros(self):
"""Les rapports d'avant la marque : une installation de paquets qui
prend zéro seconde n'a pas eu lieu."""
self.assertFalse(
QC.mesure_reelle({"durees": {"vm-1": 0.0, "vm-2": 0.0}})
)
def test_une_vraie_mesure_passe(self):
self.assertTrue(
QC.mesure_reelle({"durees": {"vm-1": 19.6, "vm-2": 20.6}})
)
def test_un_rapport_sans_duree_ne_mesure_rien(self):
self.assertFalse(QC.mesure_reelle({}))
def test_le_comparatif_applique_bien_le_filtre(self):
"""Le contrôle précédent vérifie la RÈGLE ; celui-ci vérifie qu'elle
est branchée. Sans lui, retirer l'appel laisse les tests verts et
remet les lignes fantômes dans le tableau."""
with tempfile.TemporaryDirectory() as rep:
rapports = {
"qemu_cache-20260101-000001.json": {
"outil": "qemu_cache",
"debut": "2026-01-01T00:00:01",
"dry_run": True,
"durees": {"vm-1": 0.0},
},
"qemu_cache-20260101-000002.json": {
"outil": "qemu_cache",
"debut": "2026-01-01T00:00:02",
"durees": {"vm-1": 12.5},
},
}
for nom, contenu in rapports.items():
with open(Path(rep) / nom, "w", encoding="utf-8") as fh:
json.dump(contenu, fh)
with unittest.mock.patch.object(
QC.os.path, "expanduser", return_value=rep
):
vus = QC.rapports_recents()
self.assertEqual(
[r["durees"] for r in vus],
[{"vm-1": 12.5}],
"un plan à blanc revient dans le rapport de performance",
)
class TestUnPrefixeParMode(unittest.TestCase):
"""Les trois modes ne doivent plus se disputer les mêmes machines.
Ils créent tous une « première » et une « seconde » VM. Sous un préfixe
unique, lancer le témoin après la mesure butait sur « machine(s) d'un
essai précédent encore là » alors qu'il s'agissait d'une AUTRE expérience,
et il fallait tout défaire pour comparer — ce que la comparaison exige
justement de ne pas faire.
"""
def mode(self, **kw):
base = {
"sans_cache": False,
"hors_ligne": False,
"distro": "arch",
"version": "latest",
"charge": "minimum",
}
base.update(kw)
return argparse.Namespace(**base)
def test_les_trois_preflxes_different(self):
noms = {
QC.base_des_noms(self.mode()),
QC.base_des_noms(self.mode(sans_cache=True)),
QC.base_des_noms(self.mode(hors_ligne=True)),
}
self.assertEqual(
len(noms), 3, f"des modes partagent un préfixe : {noms}"
)
def test_le_temoin_se_nomme_sans_cache(self):
self.assertTrue(
QC.base_des_noms(self.mode(sans_cache=True)).startswith(
"el-no-cache-"
)
)
def test_le_nom_porte_le_systeme_et_la_charge(self):
"""Lire « virsh list » doit suffire à savoir d'où vient une machine,
et deux essais qui ne portent pas sur la même chose ne doivent plus se
disputer les mêmes noms."""
self.assertEqual(
QC.base_des_noms(
self.mode(distro="ubuntu", version="24.04", charge="erplibre")
),
"el-cache-ubuntu_2404-erplibre_odoo_18",
)
def test_latest_ne_figure_pas_dans_le_nom(self):
"""Une distribution en publication continue n'a qu'une version : le
segment ne distinguerait aucune machine d'une autre."""
self.assertEqual(
QC.base_des_noms(self.mode(distro="arch", version="latest")),
"el-cache-arch-minimum",
)
def test_un_essai_ne_bloque_pas_un_essai_different(self):
"""Le grief exact : mesurer Ubuntu butait sur des machines Arch."""
arch = QC.base_des_noms(self.mode())
ubuntu = QC.base_des_noms(
self.mode(distro="ubuntu", version="24.04", charge="erplibre")
)
self.assertFalse(ubuntu.startswith(arch))
self.assertFalse(arch.startswith(ubuntu))
def test_le_prealable_ne_regarde_que_son_propre_prefixe(self):
"""Une machine d'une autre expérience n'entre en conflit avec rien."""
vivantes = "el-cache-test-1\nel-no-cache-test-1\nautre-vm\n"
with unittest.mock.patch.object(
QC, "executer", return_value=(0, vivantes)
):
self.assertEqual(
QC.machines_vivantes("el-no-cache-test"),
["el-no-cache-test-1"],
)
def test_la_destruction_balaie_les_trois(self):
"""Les rapports sont bornés : une machine plus ancienne que la fenêtre
ne serait jamais défaite et bloquerait tous les essais suivants."""
vivantes = "el-cache-test-9\nel-offline-test-9\nel-no-cache-test-9\n"
with (
unittest.mock.patch.object(
QC, "executer", return_value=(0, vivantes)
),
unittest.mock.patch.object(
QC.os.path, "expanduser", return_value="/inexistant"
),
):
machines, _ = QC.machines_a_defaire()
self.assertEqual(
sorted(machines),
["el-cache-test-9", "el-no-cache-test-9", "el-offline-test-9"],
)
class TestLeMenuNeRefabriquePasLesNoms(unittest.TestCase):
"""Le menu annonce les machines qui vont naître : il les DEMANDE.
Deux fabriques de noms dériveraient en silence, et le menu annoncerait
alors des VM qui ne sont pas celles qui apparaissent dans « virsh list » —
pire que de ne rien annoncer. Ce contrôle interdit donc au menu d'écrire un
préfixe en dur.
"""
def menu_source(self):
return (
Path(__file__).resolve().parent.parent
/ "script"
/ "todo"
/ "qemu_cache_menu.py"
).read_text(encoding="utf-8")
def test_aucun_prefixe_ecrit_en_dur(self):
trouves = set(re.findall(r'"(el-[a-z-]+)"', self.menu_source()))
self.assertEqual(
trouves,
set(),
f"le menu écrit des noms de machine en dur : {trouves}",
)
def test_le_menu_appelle_la_fabrique_du_script(self):
self.assertIn("module.nom_de_base(", self.menu_source())
class TestLeGainSeCalculeParCondition(unittest.TestCase):
"""Comparer une installation d'ERPLibre à un lot de paquets ne mesure que
le mélange.
Le rapport annonçait 46 % de gain en moyennant une seconde VM d'Ubuntu qui
installe ERPLibre en six minutes avec une seconde VM d'Arch qui pose cinq
paquets en vingt secondes. À condition égale, le même essai en donne 21 %.
"""
def rapport(self, distro, charge, cache, duree):
return {
"debut": "2026-01-01T00:00:00",
"distro": distro,
"charge": charge,
"cache": cache,
"durees": {f"vm-1": duree, f"vm-2": duree},
"octets": {},
}
def sortie(self, rapports):
import contextlib
import io as _io
tampon = _io.StringIO()
with (
unittest.mock.patch.object(
QC, "rapports_recents", return_value=rapports
),
contextlib.redirect_stdout(tampon),
):
QC.rapport_comparatif()
return tampon.getvalue()
def test_deux_conditions_ne_se_melangent_pas(self):
texte = self.sortie(
[
self.rapport("ubuntu", "erplibre", True, 360),
self.rapport("ubuntu", "erplibre", False, 450),
self.rapport("arch", "minimum", True, 20),
self.rapport("arch", "minimum", False, 30),
]
)
self.assertIn("ubuntu · erplibre", texte)
self.assertIn("arch · minimum", texte)
# 450 → 360 : vingt pour cent. 30 → 20 : trente-trois.
self.assertIn("20 %", texte)
self.assertIn("33 %", texte)
def test_une_condition_sans_temoin_ne_produit_pas_de_gain(self):
texte = self.sortie(
[
self.rapport("ubuntu", "erplibre", True, 360),
self.rapport("arch", "minimum", False, 30),
]
)
self.assertNotIn("gain", texte)
self.assertIn("MÊME système", texte)
def test_le_temoin_dune_autre_condition_ne_sert_pas(self):
"""Le piège exact : un témoin Arch faisait croire à une comparaison
pour une exécution Ubuntu."""
texte = self.sortie(
[
self.rapport("ubuntu", "erplibre", True, 360),
self.rapport("arch", "minimum", False, 450),
]
)
self.assertNotIn("gain", texte)
class UneUrlQueLaPremiereNaJamaisObtenue(unittest.TestCase):
"""Deux miroirs rangent le même paquet sous deux chemins, et la clé porte
le chemin. La première VM reçoit « 504 » sur l'un — amont jugé muet — puis
est servie du disque par l'autre ; rien n'entre au magasin sous le premier
chemin. Le téléchargement de la seconde n'y est donc pas une faute.
"""
AUTRE = PAQUET_A.replace("miroir.example", "autre-miroir.example")
def test_un_504_chez_la_premiere_ne_rend_pas_la_seconde_fautive(self):
premier = [
ligne(
self.AUTRE, True, issue="offline-miss", octets=0, statut=504
),
ligne(PAQUET_A, False, issue="hit", statut=200),
]
second = [ligne(self.AUTRE, True, issue="stored", statut=200)]
self.assertTrue(
QC.verdict(premier, second, None),
"un 504 de la première VM fait accuser le cache d'un fichier"
" qu'il n'a jamais eu à garder",
)
def test_un_fichier_livre_deux_fois_reste_une_faute(self):
premier = [ligne(PAQUET_A, True, statut=200)]
second = [ligne(PAQUET_A, True, statut=200)]
self.assertFalse(QC.verdict(premier, second, None))
def test_un_statut_absent_vaut_livre(self):
"""Les journaux d'avant ne l'écrivaient pas toujours : le prendre pour
un échec effacerait des mesures entières."""
self.assertTrue(QC.livre({"url": PAQUET_A}))
self.assertTrue(QC.livre({"url": PAQUET_A, "status": 206}))
self.assertFalse(QC.livre({"url": PAQUET_A, "status": 504}))
self.assertFalse(QC.livre({"url": PAQUET_A, "status": 404}))
if __name__ == "__main__":
unittest.main()