[FIX] test long cache : ne pas compter le refus d'un miroir en faute

The verdict counted every upstream line of an already seen package as a file
the cache failed to serve. A mirror that publishes the distribution under
another path answers 404 to each package; the client moves to the next
mirror, which the cache serves from disk. The 404 goes upstream on both VMs
without anything missing, and an openSUSE campaign failed on six such lines.
An upstream answer of 400 or more is now reported apart and not counted.
Checked: a test for the refusal, and one where a delivered 200 still fails.

--- FR ---

Le verdict comptait toute ligne d'amont d'un paquet déjà vu comme un fichier
que le cache n'a pas servi. Un miroir qui publie la distribution sous un
autre chemin répond 404 à chaque paquet ; le client passe au miroir suivant,
que le cache sert du disque. Le 404 repart à l'amont aux deux VM sans que
rien ne manque, et une campagne openSUSE échouait sur six de ces lignes. Une
réponse d'amont de 400 ou plus est désormais signalée à part, sans compter.
Vérifié : un test du refus, et un où un 200 livré échoue toujours.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-16 03:00:57 -04:00
parent 433fe753c3
commit 156956a1f0
2 changed files with 57 additions and 4 deletions

View file

@ -828,7 +828,24 @@ def verdict(premier, second, journal):
vues1 = {l["url"] for l in p1}
# Le critère : ce que les DEUX ont demandé ne doit pas être ressorti.
fautes = [l for l in p2 if l["url"] in vues1 and l.get("upstream")]
#
# Un REFUS de l'amont n'en est pas : un miroir qui range sa distribution
# sous un autre chemin répond 404 à chaque paquet, le client passe au
# miroir suivant, et le cache sert celui-là du disque. Il n'a rien livré
# que le cache aurait dû garder — un 404 figé masquerait le fichier publié
# ensuite. Le compter en faute fait échouer une mesure où tout a été servi.
refus = [
l
for l in p2
if l.get("upstream")
and isinstance(l.get("status"), int)
and l["status"] >= 400
]
fautes = [
l
for l in p2
if l["url"] in vues1 and l.get("upstream") and l not in refus
]
# Ce que la seconde a découvert seule : légitime sur une publication
# continue, montré pour que personne ne prenne un miroir qui bouge pour
# une panne de cache.
@ -901,6 +918,12 @@ def verdict(premier, second, journal):
)
for l in neufs[:5]:
dire(f" + {l['url'].rsplit('/', 1)[-1]}", journal)
if refus:
dire(
f" ({len(refus)} refus de l'amont, non comptés : un miroir a"
" répondu par une erreur, le fichier est venu d'ailleurs)",
journal,
)
if fautes:
dire("", journal)
dire(

View file

@ -42,20 +42,25 @@ def charger():
QC = charger()
def ligne(url, upstream, octets=1000, issue="stored", classe="immutable"):
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.
se faire passer pour un paquet. Le STATUT n'est écrit que s'il est donné.
"""
return {
l = {
"url": url,
"upstream": upstream,
"bytes": octets,
"outcome": issue,
"class": classe,
}
if statut is not None:
l["status"] = statut
return l
PAQUET_A = (
@ -155,6 +160,31 @@ class TestVerdict(unittest.TestCase):
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 = [