From 9802804a9aa52b285c4f15d0a44436c7ccc04267 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:46:57 +0200 Subject: [PATCH] fix: tell an offline cache miss from absence Offline, fetch_file answered None for a path the cache never stored, the same answer as a path known to be absent, and profile_sync reported existing files as gone. A miss now raises NotCachedError and the profile is reported unverified. --- scripts/mame_ref_audit.py | 10 +++++++--- scripts/profile_sync.py | 9 ++++++++- scripts/upstream.py | 16 ++++++++++++++-- tests/test_fileless_audit.py | 6 +++++- tests/test_profile_sync.py | 14 ++++++++++++++ tests/test_upstream.py | 13 +++++++++++-- 6 files changed, 59 insertions(+), 9 deletions(-) diff --git a/scripts/mame_ref_audit.py b/scripts/mame_ref_audit.py index 28d12002..af6b538f 100644 --- a/scripts/mame_ref_audit.py +++ b/scripts/mame_ref_audit.py @@ -124,9 +124,13 @@ def main() -> None: total = 0 for name in args.emulators: - findings, agreed, unjudged = audit( - name, args.emulators_dir, args.cache_dir, args.offline - ) + try: + findings, agreed, unjudged = audit( + name, args.emulators_dir, args.cache_dir, args.offline + ) + except upstream.UpstreamError as exc: + print(f"{name}: unreachable, {exc}", file=sys.stderr) + continue total += len(findings) print( f"{name}: {agreed} agree, {len(findings)} point elsewhere, " diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index bff81387..7de18484 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1803,7 +1803,7 @@ def build_report( return None try: return upstream.fetch_file(repo, sha, wanted, cache_dir, offline) - except upstream.RateLimitError: + except (upstream.RateLimitError, upstream.NotCachedError): raise except upstream.UpstreamError as exc: mute.add(key) @@ -3677,6 +3677,13 @@ def main() -> None: ) except upstream.RateLimitError: raise + except upstream.NotCachedError as exc: + # Offline with a cache that never stored this file: the profile + # is unverified, not wrong. Reporting GONE sent readers after + # files that exist. + report = ProfileReport( + name=name, entries=[], counts={}, skipped=f"offline: {exc}" + ) except upstream.GoneError as exc: # The forge is not coming back: a takedown, or a host that no # longer resolves. Saying so once in the summary beats repeating diff --git a/scripts/upstream.py b/scripts/upstream.py index 422d6cec..2a27fce6 100644 --- a/scripts/upstream.py +++ b/scripts/upstream.py @@ -80,6 +80,14 @@ class RateLimitError(UpstreamError): """The forge refused the request for quota reasons.""" +class NotCachedError(UpstreamError): + """An offline read of something the cache never stored. + + Distinct from absence: returning None made profile_sync report a file + that exists as GONE, "absent at HEAD, no rename found". + """ + + class GoneError(UpstreamError): """The upstream is not coming back. @@ -317,13 +325,17 @@ def fetch_file( cache_dir: str, offline: bool = False, ) -> list[str] | None: - """Lines of one path at one revision, or None when absent.""" + """Lines of one path at one revision, or None when absent. + + Offline, a path the cache never stored raises NotCachedError: nothing + is known about it, which is not the same as knowing it is absent. + """ target = cache_path(cache_dir, repo, sha, path) if target.is_file(): text = target.read_text(encoding="utf-8") return None if text == ABSENT else text.splitlines() if offline: - return None + raise NotCachedError(f"{repo.slug}@{sha[:8]}:{path} is not in the offline cache") text = _http_text(raw_url(repo, sha, path)) write_cache(target, ABSENT if text is None else text) return None if text is None else text.splitlines() diff --git a/tests/test_fileless_audit.py b/tests/test_fileless_audit.py index 5a565e12..4738b2b3 100644 --- a/tests/test_fileless_audit.py +++ b/tests/test_fileless_audit.py @@ -115,7 +115,11 @@ class TestCorpusIsAnswered(unittest.TestCase): read = [] def counted(*args, **kwargs): - lines = fetch(*args, **kwargs) + # A cold path is unread here, not absent. + try: + lines = fetch(*args, **kwargs) + except fileless_audit.upstream.NotCachedError: + return None if lines is not None: read.append(args[2]) return lines diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 3d5f62cd..345bae3e 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -3932,5 +3932,19 @@ class UpstreamVendoredAsASubmodule(unittest.TestCase): self.assertEqual(views[1].head, "nesbox-tip") +class OfflineCacheMissIsNotGone(TestBuildReport): + """Offline, a file the cache never stored came back as None and the + report read "GONE (absent at HEAD, no rename found)" for files that + exist. The miss now stops the profile as unverified.""" + + def test_an_uncached_file_is_not_reported_gone(self): + def cold(repo, sha, path, cache_dir, offline=False): + raise profile_sync.upstream.NotCachedError(f"{path} not cached") + + profile_sync.upstream.fetch_file = cold + with self.assertRaises(profile_sync.upstream.NotCachedError): + build_report("test", self._profile(["a.c:2"]), self.dir, offline=True) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_upstream.py b/tests/test_upstream.py index bebdcaa6..2eded270 100644 --- a/tests/test_upstream.py +++ b/tests/test_upstream.py @@ -379,9 +379,18 @@ class TestCache(unittest.TestCase): self.assertEqual(second, first) self.assertEqual(len(self.calls), 1) - def test_offline_miss_returns_none_without_request(self): - self.assertIsNone( + def test_offline_miss_is_not_absence(self): + # Returning None made profile_sync report an existing file as GONE. + with self.assertRaises(upstream.NotCachedError): upstream.fetch_file(self.repo, "abc", "a.c", self.dir, offline=True) + self.assertEqual(self.calls, []) + + def test_offline_cached_absence_stays_absence(self): + upstream.write_cache( + upstream.cache_path(self.dir, self.repo, "abc", "gone.c"), upstream.ABSENT + ) + self.assertIsNone( + upstream.fetch_file(self.repo, "abc", "gone.c", self.dir, offline=True) ) self.assertEqual(self.calls, [])