Skip to content

Commit 19d6690

Browse files
committed
bugs: prefer the version due for stabilization under --filter-stablereqs
A dependency pulled into the graph also goes thought the stablereq check, and if several versions of a package would do, the one the check considers due for stabilization is preferred, so a dependency pulled into the graph lands on the version actually waiting rather than an older one. It stays a preference: a version which is masked, live, or dropped from the candidates falls through to the previous choice, so the run is never left with nothing to file. Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
1 parent e839d0c commit 19d6690

3 files changed

Lines changed: 149 additions & 1 deletion

File tree

‎NEWS.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ pkgdev 0.2.18 (unreleased)
1919
- bugs: fix ``--filter-stablereqs`` collapsing a target which matches several
2020
packages down to a single one (Arthur Zamarin)
2121

22+
- bugs: under ``--filter-stablereqs``, when pulling dependency, prefer the
23+
version the stablereq check considers due for stabilization if possible
24+
(Arthur Zamarin)
25+
2226
pkgdev 0.2.17 (2026-08-14)
2327
--------------------------
2428

‎src/pkgdev/scripts/pkgdev_bugs.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,11 +476,25 @@ def __init__(self, out: Formatter, err: Formatter, options):
476476
self.added_repo = git_addon.cached_repo(GitAddedRepo)
477477
self.modified_repo = git_addon.cached_repo(GitModifiedRepo)
478478
self.stablereq_check = stablereq.StableRequestCheck(self.options, git_addon=git_addon)
479+
self._stablereq_due: dict[str, tuple[str, ...]] = {}
479480

480481
def mk_fake_pkg(self, pkg: package, keywords: set[str], stable: bool = True):
481482
kws = tuple(keywords) if stable else tuple(f"~{kw}" for kw in keywords)
482483
return MutatedPkg(pkg, {"keywords": kws})
483484

485+
def stablereq_versions(self, unversioned: atom) -> tuple[str, ...]:
486+
"""The versions of a package the stablereq check flags as due."""
487+
if (due := self._stablereq_due.get(unversioned.key)) is None:
488+
# the check reads every version, to tell which of them are stable
489+
pkgset = self.options.search_repo.match(unversioned)
490+
due = tuple(
491+
f"{res.category}/{res.package}-{res.version}"
492+
for res in self.stablereq_check.feed(sorted(pkgset))
493+
if isinstance(res, stablereq.StableRequest)
494+
)
495+
self._stablereq_due[unversioned.key] = due
496+
return due
497+
484498
def find_best_match(self, restrict, pkgset: list[package], prefer_semi_stable=True) -> package:
485499
restrict = boolean.AndRestriction(
486500
*restrict,
@@ -495,6 +509,12 @@ def find_best_match(self, restrict, pkgset: list[package], prefer_semi_stable=Tr
495509
if intersect := tuple(filter(restrict.match, all_pkgs)):
496510
return max(intersect)
497511
matches = sorted(filter(restrict.match, pkgset), reverse=True)
512+
# prefer the version the stablereq check considers due for stabilization
513+
if self.options.filter_stablereqs and len(matches) > 1:
514+
by_cpv = {match.versioned_atom.cpvstr: match for match in matches}
515+
for cpvstr in self.stablereq_versions(matches[0].unversioned_atom):
516+
if match := by_cpv.get(cpvstr):
517+
return match
498518
# prefer package with any stable keyword
499519
if prefer_semi_stable:
500520
for match in matches:

‎tests/scripts/test_pkgdev_bugs.py‎

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ def test_keywordreq(self, repo):
142142
def mk_graph(repo, category=BugCategory.STABLEREQ):
143143
# build a DependencyGraph without running its heavy __init__
144144
graph = bugs.DependencyGraph.__new__(bugs.DependencyGraph)
145-
graph.options = SimpleNamespace(repo=repo, search_repo=repo, category=category)
145+
graph.options = SimpleNamespace(
146+
repo=repo, search_repo=repo, category=category, filter_stablereqs=False
147+
)
146148
graph.out = SimpleNamespace(
147149
write=lambda *a, **k: None, warn=lambda *a, **k: None, flush=lambda: None
148150
)
@@ -151,6 +153,8 @@ def mk_graph(repo, category=BugCategory.STABLEREQ):
151153
graph.starting_nodes = set()
152154
graph.targets = ()
153155
graph.target_arches = {}
156+
graph.stablereq_check = SimpleNamespace(feed=lambda pkgs: iter(()))
157+
graph._stablereq_due = {}
154158
return graph
155159

156160

@@ -648,3 +652,123 @@ def test_single_package_target_is_unchanged(self, repo):
648652
graph, _ = self.mk_stablereq_graph(repo, {"cat/a-2"})
649653
graph.load_targets([(None, parserestrict.parse_match("cat/a"), frozenset())])
650654
assert [str(pkg.versioned_atom) for pkg in graph.targets] == ["=cat/a-2"]
655+
656+
657+
STABLE_THEN_UNSTABLE = {
658+
"1": {"KEYWORDS": ["amd64"]},
659+
"2": {"KEYWORDS": ["~amd64"]},
660+
"3": {"KEYWORDS": ["~amd64"]},
661+
}
662+
663+
664+
class TestFindBestMatch:
665+
"""Each step of the preference chain deciding which version gets filed."""
666+
667+
def mk(self, repo, versions=None, masks=(), due=()):
668+
for ver, kwargs in (versions or STABLE_THEN_UNSTABLE).items():
669+
repo.create_ebuild(f"cat/dep-{ver}", **kwargs)
670+
if masks:
671+
with open(pjoin(repo.location, "profiles", "package.mask"), "w") as f:
672+
f.write("\n".join(masks) + "\n")
673+
repo.sync()
674+
675+
graph = mk_graph(repo)
676+
if due:
677+
678+
def feed(pkgs):
679+
for pkg in pkgs:
680+
if pkg.versioned_atom.cpvstr in due:
681+
yield bugs.stablereq.StableRequest(
682+
slot=pkg.slot, keywords=pkg.keywords, age=40, pkg=pkg
683+
)
684+
685+
graph.stablereq_check = SimpleNamespace(feed=feed)
686+
graph.options.filter_stablereqs = True
687+
return graph, repo.match(atom("cat/dep"))
688+
689+
@staticmethod
690+
def pick(graph, pkgset, **kwargs):
691+
return str(graph.find_best_match([atom("cat/dep")], pkgset, **kwargs).versioned_atom)
692+
693+
@staticmethod
694+
def version(pkgset, fullver):
695+
return next(pkg for pkg in pkgset if pkg.fullver == fullver)
696+
697+
def test_a_user_selected_target_wins(self, repo):
698+
graph, pkgset = self.mk(repo)
699+
graph.targets = (self.version(pkgset, "2"),)
700+
assert self.pick(graph, pkgset) == "=cat/dep-2"
701+
702+
def test_the_newest_user_selected_target_wins(self, repo):
703+
graph, pkgset = self.mk(repo)
704+
graph.targets = (self.version(pkgset, "1"), self.version(pkgset, "2"))
705+
assert self.pick(graph, pkgset) == "=cat/dep-2"
706+
707+
def test_a_package_already_in_the_graph_wins(self, repo):
708+
graph, pkgset = self.mk(repo)
709+
graph.nodes = {bugs.GraphNode(((self.version(pkgset, "2"), {"*"}),))}
710+
assert self.pick(graph, pkgset) == "=cat/dep-2"
711+
712+
def test_a_user_target_beats_the_graph(self, repo):
713+
graph, pkgset = self.mk(repo)
714+
graph.targets = (self.version(pkgset, "1"),)
715+
graph.nodes = {bugs.GraphNode(((self.version(pkgset, "3"), {"*"}),))}
716+
assert self.pick(graph, pkgset) == "=cat/dep-1"
717+
718+
def test_the_stablereq_due_version_wins(self, repo):
719+
graph, pkgset = self.mk(repo, due={"cat/dep-2"})
720+
assert self.pick(graph, pkgset) == "=cat/dep-2"
721+
722+
def test_the_graph_beats_the_stablereq_due_version(self, repo):
723+
graph, pkgset = self.mk(repo, due={"cat/dep-2"})
724+
graph.nodes = {bugs.GraphNode(((self.version(pkgset, "3"), {"*"}),))}
725+
assert self.pick(graph, pkgset) == "=cat/dep-3"
726+
727+
def test_the_stablereq_step_needs_filter_stablereqs(self, repo):
728+
graph, pkgset = self.mk(repo, due={"cat/dep-2"})
729+
graph.options.filter_stablereqs = False
730+
assert self.pick(graph, pkgset) == "=cat/dep-1"
731+
732+
def test_a_due_version_outside_the_candidates_is_passed_over(self, repo):
733+
graph, pkgset = self.mk(repo, due={"cat/dep-3"})
734+
candidates = [pkg for pkg in pkgset if pkg.fullver != "3"]
735+
assert self.pick(graph, candidates) == "=cat/dep-1"
736+
737+
def test_the_stablereq_answer_is_cached_per_package(self, repo):
738+
graph, pkgset = self.mk(repo, due={"cat/dep-2"})
739+
calls = []
740+
inner = graph.stablereq_check.feed
741+
graph.stablereq_check = SimpleNamespace(feed=lambda pkgs: (calls.append(1), inner(pkgs))[1])
742+
self.pick(graph, pkgset)
743+
self.pick(graph, pkgset)
744+
assert len(calls) == 1
745+
746+
def test_a_semi_stable_version_beats_a_newer_unstable_one(self, repo):
747+
graph, pkgset = self.mk(repo)
748+
assert self.pick(graph, pkgset) == "=cat/dep-1"
749+
750+
def test_the_newest_wins_when_semi_stable_is_not_preferred(self, repo):
751+
graph, pkgset = self.mk(repo)
752+
assert self.pick(graph, pkgset, prefer_semi_stable=False) == "=cat/dep-3"
753+
754+
def test_versions_without_keywords_are_passed_over(self, repo):
755+
versions = dict(STABLE_THEN_UNSTABLE, **{"3": {"KEYWORDS": []}})
756+
graph, pkgset = self.mk(repo, versions)
757+
assert self.pick(graph, pkgset, prefer_semi_stable=False) == "=cat/dep-2"
758+
759+
def test_the_newest_is_the_last_resort(self, repo):
760+
versions = {ver: {"KEYWORDS": []} for ver in ("1", "2", "3")}
761+
graph, pkgset = self.mk(repo, versions)
762+
assert self.pick(graph, pkgset) == "=cat/dep-3"
763+
764+
def test_live_versions_are_excluded(self, repo):
765+
# keyworded, so only the live filter can keep it out of the running
766+
versions = dict(
767+
STABLE_THEN_UNSTABLE, **{"9999": {"PROPERTIES": "live", "KEYWORDS": ["~amd64"]}}
768+
)
769+
graph, pkgset = self.mk(repo, versions)
770+
assert self.pick(graph, pkgset, prefer_semi_stable=False) == "=cat/dep-3"
771+
772+
def test_masked_versions_are_excluded(self, repo):
773+
graph, pkgset = self.mk(repo, masks=("=cat/dep-3",))
774+
assert self.pick(graph, pkgset, prefer_semi_stable=False) == "=cat/dep-2"

0 commit comments

Comments
 (0)