From 17a8ec31dc56d58e8d6f4e05bf7214a3daaa9759 Mon Sep 17 00:00:00 2001 From: Zac Medico Date: Sat, 6 Sep 2014 16:25:24 -0700 Subject: [PATCH] depgraph._add_dep: fix bug #520950 This handles a case which occurs when _solve_non_slot_operator_slot_conflicts calls _create_graph. In this case, ignore unsatisfied deps for installed packages only if their depth is beyond the depth requested by the user and the dep was initially unsatisfied (not broken by a slot conflict in the current graph). Since depth is meaningless for packages that are not reachable as deep dependencies of arguments, the _UNREACHABLE_DEPTH constant is used as the depth value for any packages added via _complete_graph. Also, any sets added via _complete_graph have their reset_depth attribute set to False. --- pym/_emerge/depgraph.py | 97 +++++++++++++++++++--- pym/portage/tests/resolver/ResolverPlayground.py | 11 ++- .../test_slot_conflict_unsatisfied_deep_deps.py | 55 ++++++++++++ 3 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 pym/portage/tests/resolver/test_slot_conflict_unsatisfied_deep_deps.py diff --git a/pym/_emerge/depgraph.py b/pym/_emerge/depgraph.py index d6cd24d..386c9a2 100644 --- a/pym/_emerge/depgraph.py +++ b/pym/_emerge/depgraph.py @@ -107,7 +107,7 @@ def _wildcard_set(atoms): class _frozen_depgraph_config(object): - def __init__(self, settings, trees, myopts, spinner): + def __init__(self, settings, trees, myopts, params, spinner): self.settings = settings self.target_root = settings["EROOT"] self.myopts = myopts @@ -115,6 +115,7 @@ class _frozen_depgraph_config(object): if settings.get("PORTAGE_DEBUG", "") == "1": self.edebug = 1 self.spinner = spinner + self.requested_depth = params.get("deep", 0) self._running_root = trees[trees._running_eroot]["root_config"] self.pkgsettings = {} self.trees = {} @@ -502,13 +503,18 @@ class _dynamic_depgraph_config(object): class depgraph(object): + # Represents the depth of a node that is unreachable from explicit + # user arguments (or their deep dependencies). Such nodes are pulled + # in by the _complete_graph method. + _UNREACHABLE_DEPTH = object() + pkg_tree_map = RootConfig.pkg_tree_map def __init__(self, settings, trees, myopts, myparams, spinner, frozen_config=None, backtrack_parameters=BacktrackParameter(), allow_backtracking=False): if frozen_config is None: frozen_config = _frozen_depgraph_config(settings, trees, - myopts, spinner) + myopts, myparams, spinner) self._frozen_config = frozen_config self._dynamic_config = _dynamic_depgraph_config(self, myparams, allow_backtracking, backtrack_parameters) @@ -2095,6 +2101,13 @@ class depgraph(object): arg = arg_stack.pop() if arg in traversed_set_args: continue + + # If a node with the same hash already exists in + # the digraph, preserve the existing instance which + # may have a different reset_depth attribute + # (distiguishes user arguments from sets added for + # another reason such as complete mode). + arg = self._dynamic_config.digraph.get(arg, arg) traversed_set_args.add(arg) if add_to_digraph: @@ -2114,8 +2127,16 @@ class depgraph(object): if nested_set is None: nested_set = root_config.sets.get(s) if nested_set is not None: + # Propagate the reset_depth attribute from + # parent set to nested set. nested_arg = SetArg(arg=token, pset=nested_set, + reset_depth=arg.reset_depth, root_config=root_config) + + # Preserve instances already in the graph (same + # reason as for the "arg" variable above). + nested_arg = self._dynamic_config.digraph.get( + nested_arg, nested_arg) arg_stack.append(nested_arg) if add_to_digraph: self._dynamic_config.digraph.add(nested_arg, arg, @@ -2164,9 +2185,42 @@ class depgraph(object): dep.collapsed_priority.ignored): # This is an unnecessary build-time dep. return 1 + + # NOTE: For removal actions, allow_unsatisfied is always + # True since all existing removal actions traverse all + # installed deps deeply via the _complete_graph method, + # which calls _create_graph with allow_unsatisfied = True. if allow_unsatisfied: self._dynamic_config._unsatisfied_deps.append(dep) return 1 + + # The following case occurs when + # _solve_non_slot_operator_slot_conflicts calls + # _create_graph. In this case, ignore unsatisfied deps for + # installed packages only if their depth is beyond the depth + # requested by the user and the dep was initially + # unsatisfied (not broken by a slot conflict in the current + # graph). See bug #520950. + # NOTE: The value of dep.parent.depth is guaranteed to be + # either an integer or _UNREACHABLE_DEPTH, where + # _UNREACHABLE_DEPTH indicates that the parent has been + # pulled in by the _complete_graph method (rather than by + # explicit arguments or their deep dependencies). These + # cases must be distinguished because depth is meaningless + # for packages that are not reachable as deep dependencies + # of arguments. + if (self._dynamic_config._complete_mode and + isinstance(dep.parent, Package) and + dep.parent.installed and + (dep.parent.depth is self._UNREACHABLE_DEPTH or + (self._frozen_config.requested_depth is not True and + dep.parent.depth >= self._frozen_config.requested_depth))): + inst_pkg, in_graph = \ + self._select_pkg_from_installed(dep.root, dep.atom) + if inst_pkg is None: + self._dynamic_config._initially_unsatisfied_deps.append(dep) + return 1 + self._dynamic_config._unsatisfied_deps_for_display.append( ((dep.root, dep.atom), {"myparent":dep.parent})) @@ -2411,14 +2465,23 @@ class depgraph(object): # Installing package A, we need to make sure package A's deps are met. # emerge --deep ; we need to recursively check dependencies of pkgspec # If we are in --nodeps (no recursion) mode, we obviously only check 1 level of dependencies. - if arg_atoms and depth > 0: + if arg_atoms and depth != 0: for parent, atom in arg_atoms: if parent.reset_depth: depth = 0 break - if previously_added and pkg.depth is not None: - depth = min(pkg.depth, depth) + if previously_added and depth != 0 and \ + isinstance(pkg.depth, int): + # Use pkg.depth if it is less than depth. + if isinstance(depth, int): + depth = min(pkg.depth, depth) + else: + # depth is _UNREACHABLE_DEPTH and pkg.depth is + # an int, so use the int because it's considered + # to be less than _UNREACHABLE_DEPTH. + depth = pkg.depth + pkg.depth = depth deep = self._dynamic_config.myparams.get("deep", 0) update = "--update" in self._frozen_config.myopts @@ -2716,7 +2779,11 @@ class depgraph(object): def _wrapped_add_pkg_dep_string(self, pkg, dep_root, dep_priority, dep_string, allow_unsatisfied): - depth = pkg.depth + 1 + if isinstance(pkg.depth, int): + depth = pkg.depth + 1 + else: + depth = pkg.depth + deep = self._dynamic_config.myparams.get("deep", 0) recurse_satisfied = deep is True or depth <= deep debug = "--debug" in self._frozen_config.myopts @@ -3947,10 +4014,14 @@ class depgraph(object): # Recursively traversed virtual dependencies, and their # direct dependencies, are considered to have the same # depth as direct dependencies. - if parent.depth is None: - virt_depth = None - else: + if isinstance(parent.depth, int): virt_depth = parent.depth + 1 + else: + # The depth may be None when called via + # _select_atoms_probe, or it may be + # _UNREACHABLE_DEPTH for complete mode. + virt_depth = parent.depth + chosen_atom_ids = frozenset(id(atom) for atom in mycheck[1]) selected_atoms = OrderedDict() node_stack = [(parent, None, None)] @@ -5833,14 +5904,14 @@ class depgraph(object): pset = root_config.sets[s] atom = SETPREFIX + s args.append(SetArg(arg=atom, pset=pset, - root_config=root_config)) + reset_depth=False, root_config=root_config)) self._set_args(args) for arg in self._expand_set_args(args, add_to_digraph=True): for atom in arg.pset.getAtoms(): self._dynamic_config._dep_stack.append( Dependency(atom=atom, root=arg.root_config.root, - parent=arg)) + parent=arg, depth=self._UNREACHABLE_DEPTH)) if True: if self._dynamic_config._ignored_deps: @@ -8487,7 +8558,7 @@ def _backtrack_depgraph(settings, trees, myopts, myparams, myaction, myfiles, sp backtracked = 0 frozen_config = _frozen_depgraph_config(settings, trees, - myopts, spinner) + myopts, myparams, spinner) while backtracker: @@ -8569,7 +8640,7 @@ def _resume_depgraph(settings, trees, mtimedb, myopts, myparams, spinner): mergelist = mtimedb["resume"]["mergelist"] dropped_tasks = {} frozen_config = _frozen_depgraph_config(settings, trees, - myopts, spinner) + myopts, myparams, spinner) while True: mydepgraph = depgraph(settings, trees, myopts, myparams, spinner, frozen_config=frozen_config) diff --git a/pym/portage/tests/resolver/ResolverPlayground.py b/pym/portage/tests/resolver/ResolverPlayground.py index 077e271..e4a3f24 100644 --- a/pym/portage/tests/resolver/ResolverPlayground.py +++ b/pym/portage/tests/resolver/ResolverPlayground.py @@ -659,7 +659,8 @@ class ResolverPlaygroundTestCase(object): str((node1, node2))) + \ ", got: " + str(got)) - elif key in ("unstable_keywords", "needed_p_mask_changes") and expected is not None: + elif key in ("unstable_keywords", "needed_p_mask_changes", + "unsatisfied_deps") and expected is not None: expected = set(expected) if got != expected: @@ -675,9 +676,10 @@ class ResolverPlaygroundResult(object): checks = ( "success", "mergelist", "use_changes", "license_changes", "unstable_keywords", "slot_collision_solutions", - "circular_dependency_solutions", "needed_p_mask_changes", + "circular_dependency_solutions", "needed_p_mask_changes", "unsatisfied_deps", ) optional_checks = ( + "unsatisfied_deps" ) def __init__(self, atoms, success, mydepgraph, favorites): @@ -692,6 +694,7 @@ class ResolverPlaygroundResult(object): self.needed_p_mask_changes = None self.slot_collision_solutions = None self.circular_dependency_solutions = None + self.unsatisfied_deps = frozenset() if self.depgraph._dynamic_config._serialized_tasks_cache is not None: self.mergelist = [] @@ -751,6 +754,10 @@ class ResolverPlaygroundResult(object): sol = handler.solutions self.circular_dependency_solutions = dict(zip([x.cpv for x in sol.keys()], sol.values())) + if self.depgraph._dynamic_config._unsatisfied_deps_for_display: + self.unsatisfied_deps = set(dep_info[0][1] + for dep_info in self.depgraph._dynamic_config._unsatisfied_deps_for_display) + class ResolverPlaygroundDepcleanResult(object): checks = ( diff --git a/pym/portage/tests/resolver/test_slot_conflict_unsatisfied_deep_deps.py b/pym/portage/tests/resolver/test_slot_conflict_unsatisfied_deep_deps.py new file mode 100644 index 0000000..3ecbf53 --- /dev/null +++ b/pym/portage/tests/resolver/test_slot_conflict_unsatisfied_deep_deps.py @@ -0,0 +1,55 @@ +# Copyright 2014 Gentoo Foundation +# 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 SlotConflictUnsatisfiedDeepDepsTestCase(TestCase): + + def testSlotConflictUnsatisfiedDeepDeps(self): + + ebuilds = { + "dev-libs/A-1": { }, + "dev-libs/A-2": { "KEYWORDS": "~x86" }, + "dev-libs/B-1": { "DEPEND": "dev-libs/A" }, + "dev-libs/C-1": { "DEPEND": ">=dev-libs/A-2" }, + "dev-libs/D-1": { "DEPEND": "dev-libs/A" }, + } + + installed = { + "dev-libs/broken-1": { + "RDEPEND": "dev-libs/A dev-libs/initially-unsatisfied" + }, + } + + world = ( + "dev-libs/A", + "dev-libs/B", + "dev-libs/C", + "dev-libs/D", + "dev-libs/broken" + ) + + test_cases = ( + # Test bug #520950, where unsatisfied deps of installed + # packages are supposed to be ignored when they are beyond + # the depth requested by the user. + ResolverPlaygroundTestCase( + ["dev-libs/B", "dev-libs/C", "dev-libs/D"], + all_permutations=True, + options={"--autounmask": "y", "--complete-graph": True}, + mergelist=["dev-libs/A-2", "dev-libs/B-1", "dev-libs/C-1", "dev-libs/D-1"], + ignore_mergelist_order=True, + unstable_keywords=["dev-libs/A-2"], + unsatisfied_deps=[], + success=False), + ) + + playground = ResolverPlayground(ebuilds=ebuilds, installed=installed, + world=world, debug=False) + 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() -- 1.8.5.5