mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-13 23:13:23 -05:00
feat: keep a profile checkable when its forge goes
Four Switch profiles were unverifiable and said so on stderr every pass. yuzu and suyu answer 451, citron's host stopped resolving, and git.eden-emu.dev returns 403 to anything that is not a browser. Each aborted its own report, so nothing could be said about any of them, and the noise repeated on every run over the whole corpus. A withdrawn forge is now a fact rather than a failure. GoneError covers 451, 410 and a host that does not resolve; none is retried, since three attempts with backoff end in the same place. Those profiles land in their own summary bucket, out of the review backlog where nobody could act on them anyway. A 403 stays what it was, a refusal, because small Forgejo instances behind anti-bot filters issue it routinely. A profile can now name a source_mirror, consulted after source and upstream so a live primary always decides attribution. Reaching it took two more changes: a repository that refuses is muted for the rest of the pass instead of ending it, keyed by host as well as slug because a mirror carries the same slug on another forge; and a refused miss is not cached, or the mute would answer for the mirror that was about to be asked. eden now reads from its Codeberg copy, which holds the same head and the pinned commit: 5 refs, all anchored, where the profile could not be checked at all. yuzu, suyu and citron have no mirror that serves content, and now say so once instead of failing loudly.
This commit is contained in:
1 parent
e6d325ed1b
commit
ca307a4ef1
6 files changed
+230
-14
No files matched your search
@@ -218,6 +218,74 @@ class TestCollectTokens(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestSourceMirror(unittest.TestCase):
|
||||
"""A profile whose forge is gone can name a mirror and stay checkable.
|
||||
|
||||
Nothing else recovers yuzu, suyu or citron: their trees exist, the host
|
||||
that served them does not. The mirror is consulted last so a live
|
||||
primary always decides attribution.
|
||||
"""
|
||||
|
||||
def test_mirror_is_declared_after_source_and_upstream(self):
|
||||
profile = {
|
||||
"source": "https://github.com/o/port",
|
||||
"upstream": "https://github.com/o/up",
|
||||
"source_mirror": "https://codeberg.org/o/mirror",
|
||||
}
|
||||
self.assertEqual(
|
||||
[url for _, _, url in profile_sync.declared_repositories(profile)],
|
||||
[
|
||||
"https://github.com/o/port",
|
||||
"https://github.com/o/up",
|
||||
"https://codeberg.org/o/mirror",
|
||||
],
|
||||
)
|
||||
|
||||
def test_mirror_alone_is_enough(self):
|
||||
profile = {"source_mirror": "https://codeberg.org/o/mirror"}
|
||||
self.assertEqual(
|
||||
profile_sync.declared_repositories(profile),
|
||||
[("source_mirror", "", "https://codeberg.org/o/mirror")],
|
||||
)
|
||||
|
||||
def test_a_profile_without_a_mirror_is_unchanged(self):
|
||||
profile = {"source": "https://github.com/o/port"}
|
||||
self.assertEqual(
|
||||
profile_sync.declared_repositories(profile),
|
||||
[("source", "", "https://github.com/o/port")],
|
||||
)
|
||||
|
||||
|
||||
class TestGoneUpstreamIsNotAnError(unittest.TestCase):
|
||||
"""A withdrawn forge is reported once, in its own bucket.
|
||||
|
||||
yuzu and suyu answer 451, citron's host no longer resolves. Printing
|
||||
that on stderr every pass is noise nobody can act on, and counting it
|
||||
with real upstream failures hides the forges that are merely having a
|
||||
bad minute.
|
||||
"""
|
||||
|
||||
def test_a_gone_forge_is_skipped_not_errored(self):
|
||||
report = ProfileReport(
|
||||
name="yuzu", entries=[], counts={},
|
||||
skipped="upstream gone: https://host/x: HTTP 451, withdrawn for legal reasons",
|
||||
)
|
||||
self.assertEqual(report.needs_review(), 0)
|
||||
self.assertTrue(report.skipped.startswith("upstream gone:"))
|
||||
|
||||
def test_gone_and_failing_forges_land_in_different_buckets(self):
|
||||
gone = ProfileReport(name="a", entries=[], counts={},
|
||||
skipped="upstream gone: h: HTTP 451, withdrawn for legal reasons")
|
||||
failing = ProfileReport(name="b", entries=[], counts={},
|
||||
skipped="upstream error: h: HTTP 403")
|
||||
buffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(buffer):
|
||||
profile_sync._print_elided([gone, failing], 0)
|
||||
out = buffer.getvalue()
|
||||
self.assertIn("upstream gone", out)
|
||||
self.assertIn("upstream error", out)
|
||||
|
||||
|
||||
class TestWorstStatus(unittest.TestCase):
|
||||
def test_gone_beats_everything(self):
|
||||
self.assertEqual(worst_status(["ANCHORED", "GONE", "SHIFTED"]), "GONE")
|
||||
|
||||
@@ -7,6 +7,7 @@ import http.client
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import socket
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -245,6 +246,73 @@ class TestFetchRetry(unittest.TestCase):
|
||||
self.assertEqual(self.calls, 1)
|
||||
|
||||
|
||||
class TestGoneUpstream(unittest.TestCase):
|
||||
"""A forge that will not come back is a fact, not a failure to retry.
|
||||
|
||||
yuzu and suyu answer 451, citron's host no longer resolves. Retrying
|
||||
those three times with backoff costs a minute per pass and still ends
|
||||
in the same place, and calling them errors buries the profiles that
|
||||
have something to say.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self._orig = (urllib.request.urlopen, upstream._sleep)
|
||||
self.slept: list[float] = []
|
||||
upstream._sleep = self.slept.append
|
||||
self.calls = 0
|
||||
|
||||
def tearDown(self):
|
||||
urllib.request.urlopen, upstream._sleep = self._orig
|
||||
|
||||
def _serve(self, outcome):
|
||||
def opener(req, timeout=None):
|
||||
self.calls += 1
|
||||
raise outcome
|
||||
|
||||
urllib.request.urlopen = opener
|
||||
|
||||
def test_legal_takedown_is_gone_and_not_retried(self):
|
||||
self._serve(_http_error(451))
|
||||
with self.assertRaises(upstream.GoneError):
|
||||
upstream._fetch("https://host/x")
|
||||
self.assertEqual(self.calls, 1)
|
||||
self.assertEqual(self.slept, [])
|
||||
|
||||
def test_http_gone_is_gone(self):
|
||||
self._serve(_http_error(410))
|
||||
with self.assertRaises(upstream.GoneError):
|
||||
upstream._fetch("https://host/x")
|
||||
self.assertEqual(self.calls, 1)
|
||||
|
||||
def test_unresolvable_host_is_gone_and_not_retried(self):
|
||||
self._serve(
|
||||
urllib.error.URLError(socket.gaierror(-2, "Name or service not known"))
|
||||
)
|
||||
with self.assertRaises(upstream.GoneError):
|
||||
upstream._fetch("https://host/x")
|
||||
self.assertEqual(self.calls, 1)
|
||||
self.assertEqual(self.slept, [])
|
||||
|
||||
def test_a_gone_error_is_still_an_upstream_error(self):
|
||||
self._serve(_http_error(451))
|
||||
with self.assertRaises(upstream.UpstreamError):
|
||||
upstream._fetch("https://host/x")
|
||||
|
||||
def test_refused_request_is_not_gone(self):
|
||||
"""403 is a forge refusing a request, which anti-bot filters do."""
|
||||
self._serve(_http_error(403))
|
||||
with self.assertRaises(upstream.UpstreamError) as caught:
|
||||
upstream._fetch("https://host/x")
|
||||
self.assertNotIsInstance(caught.exception, upstream.GoneError)
|
||||
|
||||
def test_a_dropped_connection_is_not_gone(self):
|
||||
self._serve(http.client.RemoteDisconnected("closed"))
|
||||
with self.assertRaises(upstream.UpstreamError) as caught:
|
||||
upstream._fetch("https://host/x")
|
||||
self.assertNotIsInstance(caught.exception, upstream.GoneError)
|
||||
self.assertEqual(self.calls, upstream.RETRIES)
|
||||
|
||||
|
||||
class TestCache(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
|
||||
Reference in new issue
Block a user