diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 1fb9098e..62fa1241 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1107,6 +1107,7 @@ def select_views( """ views: list[RepoView] = [] seen: set[tuple[str, str]] = set() + gone = False # A port living on a branch of a fork is absent from the default tip, so # the branch it was read from is followed when the profile names one. declared = str(profile.get("source_branch") or "") or None @@ -1121,21 +1122,29 @@ def select_views( pin_field = "source" if field == "source_mirror" else field branch = declared if pin_field == "source" else None head = None - for candidate in branch_candidates(url): - head = upstream.resolve_head(repo, cache_dir, offline, candidate) - if head: - branch = candidate - break - pin, origin = resolve_pin( - profile, repo, cache_dir, offline, pin_field, mode, branch - ) - if not pin: + try: + for candidate in branch_candidates(url): + head = upstream.resolve_head(repo, cache_dir, offline, candidate) + if head: + branch = candidate + break + pin, origin = resolve_pin( + profile, repo, cache_dir, offline, pin_field, mode, branch + ) + if not pin: + continue + head = head or upstream.resolve_head(repo, cache_dir, offline, branch) + except upstream.GoneError: + # yuzu's source answers 451: the mirror declared after it is what + # keeps the profile checkable, so a gone repository is passed over. + gone = True continue - head = head or upstream.resolve_head(repo, cache_dir, offline, branch) if not head: continue seen.add((repo.host, repo.slug)) views.append(RepoView(repo, pin, head, origin, field)) + if gone and not views: + raise upstream.GoneError(f"{profile.get('emulator', '?')}: every declared repository is gone") views = _follow_fork_base(_follow_submodule(views, cache_dir, offline), cache_dir, offline) return _hold_upstream(profile, views) @@ -1583,7 +1592,7 @@ def build_report( report.skipped = f"upstream gone (declared): {stated_gone}" return report raise - if stated_gone and views: + if stated_gone and any(view.field != "source_mirror" for view in views): # Declared dead, yet it answered. A profile that keeps saying so stops # being read, so the contradiction is the finding. report.skipped = ( diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 6abc5471..d5b7548c 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -4912,6 +4912,32 @@ class UpstreamVendoredAsASubmodule(unittest.TestCase): self.assertEqual(views[1].head, "nesbox-tip") +class AMirrorOutlivesAGoneSource(unittest.TestCase): + """yuzu's source answers 451. select_views stopped on it and never read + the source_mirror declared after it, so the profile stayed unchecked.""" + + def test_the_mirror_is_read_when_the_source_is_gone(self): + from unittest import mock # noqa: PLC0415 + + gone_repo = profile_sync.upstream.make_repo("github.com", "yuzu-emu-mirror", "yuzu") + mirror = profile_sync.upstream.make_repo("github.com", "fork", "yuzu") + + def head(repo, cache_dir, offline=False, branch=None): + if repo == gone_repo: + raise profile_sync.upstream.GoneError("HTTP 451") + return "mirror-head" + + profile = { + "source": "https://github.com/yuzu-emu-mirror/yuzu", + "source_mirror": "https://github.com/fork/yuzu", + "source_commit": "pin", + } + with mock.patch.object(profile_sync.upstream, "resolve_head", side_effect=head), \ + mock.patch.object(profile_sync, "resolve_pin", return_value=("pin", "source_commit")): + views = profile_sync.select_views(profile, "cache", True) + self.assertEqual([(v.repo, v.field) for v in views], [(mirror, "source_mirror")]) + + class AHeldUpstreamStaysOnItsPin(unittest.TestCase): """aps3e vendors RPCS3 1f080485 and yaps2 PCSX2 342db515: a bump moved upstream_commit to the upstream HEAD neither port carries."""