commit:     4b885b9ca063c990b1e218c73a786e9d434717e8
Author:     Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Mon Jan  8 06:04:37 2024 +0000
Commit:     Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Mon Jan  8 08:08:51 2024 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=4b885b9c

_calc_depclean: add dep_check action

Add a dep_check action which can be used to check the
dependencies of all installed packages. The plan is for depgraph
to use this action to check for broken dependencies prior to the
merge order calculation. The new frozen_config parameter will
allow depgraph to pass a shared frozen config to _calc_depclean.

The result of the dep_check action becomes stale as soon as there
is any change to the installed packages. So, in order to account
for dependencies that may become broken or satisfied during the
process of updating installed packages, the merge order
calculation will need to refresh the dep_check calculation for
every merge order choice that it makes. This refresh will need
to be optimized to identify the portion of the graph that would
become stale due to a given change, so that it can avoid
unnecessary repetition of work.

Bug: https://bugs.gentoo.org/921333
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>

 lib/_emerge/actions.py                           | 21 ++++++-
 lib/_emerge/depgraph.py                          |  7 ++-
 lib/portage/tests/resolver/ResolverPlayground.py | 43 ++++++++++++--
 lib/portage/tests/resolver/meson.build           |  1 +
 lib/portage/tests/resolver/test_broken_deps.py   | 76 ++++++++++++++++++++++++
 5 files changed, 138 insertions(+), 10 deletions(-)

diff --git a/lib/_emerge/actions.py b/lib/_emerge/actions.py
index 20f3978f77..2710c4856c 100644
--- a/lib/_emerge/actions.py
+++ b/lib/_emerge/actions.py
@@ -909,7 +909,16 @@ _depclean_result = collections.namedtuple(
 )
 
 
-def _calc_depclean(settings, trees, ldpath_mtimes, myopts, action, args_set, 
spinner):
+def _calc_depclean(
+    settings,
+    trees,
+    ldpath_mtimes,
+    myopts,
+    action,
+    args_set,
+    spinner,
+    frozen_config=None,
+):
     allow_missing_deps = bool(args_set)
 
     debug = "--debug" in myopts
@@ -988,12 +997,14 @@ def _calc_depclean(settings, trees, ldpath_mtimes, 
myopts, action, args_set, spi
 
     writemsg_level("\nCalculating dependencies  ")
     resolver_params = create_depgraph_params(myopts, "remove")
-    resolver = depgraph(settings, trees, myopts, resolver_params, spinner)
+    resolver = depgraph(
+        settings, trees, myopts, resolver_params, spinner, 
frozen_config=frozen_config
+    )
     resolver._load_vdb()
     vardb = resolver._frozen_config.trees[eroot]["vartree"].dbapi
     real_vardb = trees[eroot]["vartree"].dbapi
 
-    if action == "depclean":
+    if action in ("dep_check", "depclean"):
         if args_set:
             if deselect:
                 # Start with an empty set.
@@ -1002,6 +1013,7 @@ def _calc_depclean(settings, trees, ldpath_mtimes, 
myopts, action, args_set, spi
                 # Pull in any sets nested within the selected set.
                 selected_set.update(psets["selected"].getNonAtoms())
 
+        if args_set or action == "dep_check":
             # Pull in everything that's installed but not matched
             # by an argument atom since we don't want to clean any
             # package if something depends on it.
@@ -1098,6 +1110,9 @@ def _calc_depclean(settings, trees, ldpath_mtimes, 
myopts, action, args_set, spi
     if not success:
         return _depclean_result(1, [], False, 0, resolver)
 
+    if action == "dep_check":
+        return _depclean_result(0, [], False, 0, resolver)
+
     def unresolved_deps():
         soname_deps = set()
         unresolvable = set()

diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py
index a2865cad23..b859e68224 100644
--- a/lib/_emerge/depgraph.py
+++ b/lib/_emerge/depgraph.py
@@ -11723,6 +11723,7 @@ def backtrack_depgraph(
     myaction: Optional[str],
     myfiles: list[str],
     spinner: "_emerge.stdout_spinner.stdout_spinner",
+    frozen_config: Optional[_frozen_depgraph_config] = None,
 ) -> tuple[Any, depgraph, list[str]]:
     """
 
@@ -11747,6 +11748,7 @@ def _backtrack_depgraph(
     myaction: Optional[str],
     myfiles: list[str],
     spinner: "_emerge.stdout_spinner.stdout_spinner",
+    frozen_config: Optional[_frozen_depgraph_config] = None,
 ) -> tuple[Any, depgraph, list[str], int, int]:
     debug = "--debug" in myopts
     mydepgraph = None
@@ -11756,7 +11758,10 @@ def _backtrack_depgraph(
     backtracker = Backtracker(max_depth)
     backtracked = 0
 
-    frozen_config = _frozen_depgraph_config(settings, trees, myopts, myparams, 
spinner)
+    if frozen_config is None:
+        frozen_config = _frozen_depgraph_config(
+            settings, trees, myopts, myparams, spinner
+        )
 
     while backtracker:
         if debug and mydepgraph is not None:

diff --git a/lib/portage/tests/resolver/ResolverPlayground.py 
b/lib/portage/tests/resolver/ResolverPlayground.py
index 592f5cc5dc..2d26012873 100644
--- a/lib/portage/tests/resolver/ResolverPlayground.py
+++ b/lib/portage/tests/resolver/ResolverPlayground.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2023 Gentoo Authors
+# Copyright 2010-2024 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
 import bz2
@@ -33,7 +33,11 @@ from _emerge.actions import _calc_depclean
 from _emerge.Blocker import Blocker
 from _emerge.create_depgraph_params import create_depgraph_params
 from _emerge.DependencyArg import DependencyArg
-from _emerge.depgraph import backtrack_depgraph
+from _emerge.depgraph import (
+    _frozen_depgraph_config,
+    backtrack_depgraph,
+)
+from _emerge.Package import Package
 from _emerge.RootConfig import RootConfig
 
 
@@ -732,7 +736,16 @@ class ResolverPlayground:
                 portage.util.noiselimit = -2
             _emerge.emergelog._disable = True
 
-            if action in ("depclean", "prune"):
+            # NOTE: frozen_config could be cached and reused if options and 
params were constant.
+            params_action = (
+                "remove" if action in ("dep_check", "depclean", "prune") else 
action
+            )
+            params = create_depgraph_params(options, params_action)
+            frozen_config = _frozen_depgraph_config(
+                self.settings, self.trees, options, params, None
+            )
+
+            if params_action == "remove":
                 depclean_result = _calc_depclean(
                     self.settings,
                     self.trees,
@@ -741,6 +754,7 @@ class ResolverPlayground:
                     action,
                     InternalPackageSet(initial_atoms=atoms, 
allow_wildcard=True),
                     None,
+                    frozen_config=frozen_config,
                 )
                 result = ResolverPlaygroundDepcleanResult(
                     atoms,
@@ -751,9 +765,15 @@ class ResolverPlayground:
                     depclean_result.depgraph,
                 )
             else:
-                params = create_depgraph_params(options, action)
                 success, depgraph, favorites = backtrack_depgraph(
-                    self.settings, self.trees, options, params, action, atoms, 
None
+                    self.settings,
+                    self.trees,
+                    options,
+                    params,
+                    action,
+                    atoms,
+                    None,
+                    frozen_config=frozen_config,
                 )
                 depgraph._show_merge_list()
                 depgraph.display_problems()
@@ -939,7 +959,8 @@ class ResolverPlaygroundTestCase:
                 )
                 and expected is not None
             ):
-                expected = set(expected)
+                # unsatisfied_deps can be a dict for depclean-like actions
+                expected = expected if isinstance(expected, dict) else 
set(expected)
 
             elif key == "forced_rebuilds" and expected is not None:
                 expected = {k: set(v) for k, v in expected.items()}
@@ -1109,11 +1130,14 @@ class ResolverPlaygroundDepcleanResult:
         "ordered",
         "req_pkg_count",
         "graph_order",
+        "unsatisfied_deps",
     )
     optional_checks = (
+        "cleanlist",
         "ordered",
         "req_pkg_count",
         "graph_order",
+        "unsatisfied_deps",
     )
 
     def __init__(self, atoms, rval, cleanlist, ordered, req_pkg_count, 
depgraph):
@@ -1125,3 +1149,10 @@ class ResolverPlaygroundDepcleanResult:
         self.graph_order = [
             _mergelist_str(node, depgraph) for node in 
depgraph._dynamic_config.digraph
         ]
+        self.unsatisfied_deps = {}
+        for dep in depgraph._dynamic_config._initially_unsatisfied_deps:
+            if isinstance(dep.parent, Package):
+                parent_repr = dep.parent.cpv
+            else:
+                parent_repr = dep.parent.arg
+            self.unsatisfied_deps.setdefault(parent_repr, set()).add(dep.atom)

diff --git a/lib/portage/tests/resolver/meson.build 
b/lib/portage/tests/resolver/meson.build
index 77c65a511e..8892c78131 100644
--- a/lib/portage/tests/resolver/meson.build
+++ b/lib/portage/tests/resolver/meson.build
@@ -15,6 +15,7 @@ py.install_sources(
         'test_bdeps.py',
         'test_binary_pkg_ebuild_visibility.py',
         'test_blocker.py',
+        'test_broken_deps.py',
         'test_changed_deps.py',
         'test_circular_choices.py',
         'test_circular_choices_rust.py',

diff --git a/lib/portage/tests/resolver/test_broken_deps.py 
b/lib/portage/tests/resolver/test_broken_deps.py
new file mode 100644
index 0000000000..8ca7809d34
--- /dev/null
+++ b/lib/portage/tests/resolver/test_broken_deps.py
@@ -0,0 +1,76 @@
+# Copyright 2024 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.tests import TestCase
+from portage.tests.resolver.ResolverPlayground import (
+    ResolverPlayground,
+    ResolverPlaygroundTestCase,
+)
+
+
+class BrokenDepsTestCase(TestCase):
+    def testBrokenDeps(self):
+        """
+        Test the _calc_depclean "dep_check" action which will eventually
+        be used to check for unsatisfied deps of installed packages
+        for bug 921333.
+        """
+        ebuilds = {
+            "dev-qt/qtcore-5.15.12": {
+                "EAPI": "8",
+            },
+            "dev-qt/qtcore-5.15.11-r1": {
+                "EAPI": "8",
+            },
+            "dev-qt/qtxmlpatterns-5.15.12": {
+                "EAPI": "8",
+                "DEPEND": "=dev-qt/qtcore-5.15.12*",
+                "RDEPEND": "=dev-qt/qtcore-5.15.12*",
+            },
+            "dev-qt/qtxmlpatterns-5.15.11": {
+                "EAPI": "8",
+                "DEPEND": "=dev-qt/qtcore-5.15.11*",
+                "RDEPEND": "=dev-qt/qtcore-5.15.11*",
+            },
+            "kde-frameworks/syntax-highlighting-5.113.0": {
+                "EAPI": "8",
+                "DEPEND": ">=dev-qt/qtxmlpatterns-5.15.9:5",
+            },
+        }
+        installed = {
+            "dev-qt/qtcore-5.15.12": {
+                "EAPI": "8",
+            },
+            "dev-qt/qtxmlpatterns-5.15.11": {
+                "EAPI": "8",
+                "DEPEND": "=dev-qt/qtcore-5.15.11*",
+                "RDEPEND": "=dev-qt/qtcore-5.15.11*",
+            },
+            "kde-frameworks/syntax-highlighting-5.113.0": {
+                "EAPI": "8",
+                "DEPEND": ">=dev-qt/qtxmlpatterns-5.15.9:5",
+            },
+        }
+
+        world = ("kde-frameworks/syntax-highlighting",)
+
+        test_cases = (
+            ResolverPlaygroundTestCase(
+                [],
+                action="dep_check",
+                success=True,
+                unsatisfied_deps={
+                    "dev-qt/qtxmlpatterns-5.15.11": {"=dev-qt/qtcore-5.15.11*"}
+                },
+            ),
+        )
+
+        playground = ResolverPlayground(
+            ebuilds=ebuilds, installed=installed, world=world
+        )
+        try:
+            for test_case in test_cases:
+                playground.run_TestCase(test_case)
+                self.assertEqual(test_case.test_success, True, 
test_case.fail_msg)
+        finally:
+            playground.cleanup()

Reply via email to