backtracking: Take all branches in case of slot collisions v2.2_rc88
authorSebastian Luther <SebastianLuther@gmx.de>
Sun, 26 Sep 2010 21:08:03 +0000 (23:08 +0200)
committerZac Medico <zmedico@gentoo.org>
Mon, 27 Sep 2010 06:31:04 +0000 (23:31 -0700)
pym/_emerge/depgraph.py
pym/_emerge/resolver/backtracking.py [new file with mode: 0644]
pym/portage/tests/resolver/test_autounmask.py
pym/portage/tests/resolver/test_backtracking.py

index 50aefda28c537004897915bb8de89e126178753e..1131d060072df9b89964c68077453db9828eb3b7 100644 (file)
@@ -52,6 +52,7 @@ from _emerge.SetArg import SetArg
 from _emerge.show_invalid_depstring_notice import show_invalid_depstring_notice
 from _emerge.UnmergeDepPriority import UnmergeDepPriority
 
+from _emerge.resolver.backtracking import Backtracker, BacktrackParameter
 from _emerge.resolver.slot_collision import slot_conflict_handler
 from _emerge.resolver.circular_dependency import circular_dependency_handler
 from _emerge.resolver.output import display, filter_iuse_defaults
@@ -127,8 +128,7 @@ class _depgraph_sets(object):
 
 class _dynamic_depgraph_config(object):
 
-       def __init__(self, depgraph, myparams, allow_backtracking,
-               runtime_pkg_mask, needed_unstable_keywords, needed_use_config_changes, needed_license_changes):
+       def __init__(self, depgraph, myparams, allow_backtracking, backtrack_parameters):
                self.myparams = myparams.copy()
                self._vdb_loaded = False
                self._allow_backtracking = allow_backtracking
@@ -195,33 +195,16 @@ class _dynamic_depgraph_config(object):
                self._initially_unsatisfied_deps = []
                self._ignored_deps = []
                self._highest_pkg_cache = {}
-               if runtime_pkg_mask is None:
-                       runtime_pkg_mask = {}
-               else:
-                       runtime_pkg_mask = dict((k, v.copy()) for (k, v) in \
-                               runtime_pkg_mask.items())
-
-               if needed_unstable_keywords is None:
-                       self._needed_unstable_keywords = set()
-               else:
-                       self._needed_unstable_keywords = needed_unstable_keywords.copy()
-
-               if needed_license_changes is None:
-                       self._needed_license_changes = {}
-               else:
-                       self._needed_license_changes = needed_license_changes.copy()
 
-               if needed_use_config_changes is None:
-                       self._needed_use_config_changes = {}
-               else:
-                       self._needed_use_config_changes = \
-                               dict((k.copy(), (v[0].copy(), v[1].copy())) for (k, v) in \
-                                       needed_use_config_changes.items())
+               self._needed_unstable_keywords = backtrack_parameters.needed_unstable_keywords
+               self._needed_license_changes = backtrack_parameters.needed_license_changes
+               self._needed_use_config_changes = backtrack_parameters.needed_use_config_changes
+               self._runtime_pkg_mask = backtrack_parameters.runtime_pkg_mask
+               self._need_restart = False
+               self._backtrack_infos = {}
 
                self._autounmask = depgraph._frozen_config.myopts.get('--autounmask', 'n') == True
-
-               self._runtime_pkg_mask = runtime_pkg_mask
-               self._need_restart = False
+               self._success_without_autounmask = False
 
                for myroot in depgraph._frozen_config.trees:
                        self.sets[myroot] = _depgraph_sets()
@@ -300,15 +283,13 @@ class depgraph(object):
        _dep_keys = ["DEPEND", "RDEPEND", "PDEPEND"]
        
        def __init__(self, settings, trees, myopts, myparams, spinner,
-               frozen_config=None, runtime_pkg_mask=None, needed_unstable_keywords=None, \
-                       needed_use_config_changes=None, needed_license_changes=None, allow_backtracking=False):
+               frozen_config=None, backtrack_parameters=BacktrackParameter(), allow_backtracking=False):
                if frozen_config is None:
                        frozen_config = _frozen_depgraph_config(settings, trees,
                        myopts, spinner)
                self._frozen_config = frozen_config
                self._dynamic_config = _dynamic_depgraph_config(self, myparams,
-                       allow_backtracking, runtime_pkg_mask, needed_unstable_keywords, \
-                       needed_use_config_changes, needed_license_changes)
+                       allow_backtracking, backtrack_parameters)
 
                self._select_atoms = self._select_atoms_highest_available
                self._select_package = self._select_pkg_highest_available
@@ -722,16 +703,14 @@ class depgraph(object):
                                                        (dep.parent,
                                                        self._dynamic_config._runtime_pkg_mask[
                                                        dep.parent]), noiselevel=-1)
-                               else:
+                               elif not self.need_restart():
                                        # Do not backtrack if only USE have to be changed in
                                        # order to satisfy the dependency.
                                        dep_pkg, existing_node = \
                                                self._select_package(dep.root, dep.atom.without_use,
                                                        onlydeps=dep.onlydeps)
                                        if dep_pkg is None:
-                                               self._dynamic_config._runtime_pkg_mask.setdefault(
-                                                       dep.parent, {})["missing dependency"] = \
-                                                               set([(dep.parent, dep.root, dep.atom)])
+                                               self._dynamic_config._backtrack_infos["missing dependency"] = dep
                                                self._dynamic_config._need_restart = True
                                                if "--debug" in self._frozen_config.myopts:
                                                        msg = []
@@ -882,47 +861,46 @@ class depgraph(object):
                                                                self._dynamic_config._runtime_pkg_mask[
                                                                existing_node]), noiselevel=-1)
                                        elif self._dynamic_config._allow_backtracking and \
-                                               not self._accept_blocker_conflicts():
+                                               not self._accept_blocker_conflicts() and \
+                                               not self.need_restart():
+
                                                self._add_slot_conflict(pkg)
                                                if dep.atom is not None and dep.parent is not None:
                                                        self._add_parent_atom(pkg, (dep.parent, dep.atom))
+
                                                if arg_atoms:
                                                        for parent_atom in arg_atoms:
                                                                parent, atom = parent_atom
                                                                self._add_parent_atom(pkg, parent_atom)
                                                self._process_slot_conflicts()
 
-                                               # NOTE: We always mask existing_node since
-                                               # _select_package tries to avoid slot conflicts when
-                                               # possible and therefore a conflict typically means
-                                               # that existing_node was a poor choice.
-                                               to_be_masked = existing_node
-
-                                               parent_atoms = \
-                                                       self._dynamic_config._parent_atoms.get(pkg, set())
-                                               if parent_atoms:
-                                                       conflict_atoms = self._dynamic_config._slot_conflict_parent_atoms.intersection(parent_atoms)
-                                                       if conflict_atoms:
-                                                               parent_atoms = conflict_atoms
-
-                                               if pkg >= existing_node:
-                                                       # We only care about the parent atoms
-                                                       # when they trigger a downgrade.
-                                                       parent_atoms = set()
-
-                                               self._dynamic_config._runtime_pkg_mask.setdefault(
-                                                       to_be_masked, {})["slot conflict"] = parent_atoms
+                                               backtrack_data = []
+                                               all_parents = set()
+                                               for node, other_node in (existing_node, pkg), (pkg, existing_node):
+                                                       parent_atoms = \
+                                                               self._dynamic_config._parent_atoms.get(node, set())
+                                                       if parent_atoms:
+                                                               conflict_atoms = self._dynamic_config._slot_conflict_parent_atoms.intersection(parent_atoms)
+                                                               if conflict_atoms:
+                                                                       parent_atoms = conflict_atoms
+                                                       all_parents.update(parent_atoms)
+                                                       if node < other_node:
+                                                               parent_atoms = set()
+                                                       backtrack_data.append((node, parent_atoms))
+
+                                               self._dynamic_config._backtrack_infos["slot conflict"] = backtrack_data
                                                self._dynamic_config._need_restart = True
                                                if "--debug" in self._frozen_config.myopts:
                                                        msg = []
                                                        msg.append("")
                                                        msg.append("")
                                                        msg.append("backtracking due to slot conflict:")
-                                                       msg.append("   package: %s" % to_be_masked)
-                                                       msg.append("      slot: %s" % to_be_masked.slot_atom)
+                                                       msg.append("   first package:  %s" % existing_node)
+                                                       msg.append("   second package: %s" % pkg)
+                                                       msg.append("      slot: %s" % pkg.slot_atom)
                                                        msg.append("   parents: %s" % \
                                                                [(str(parent), atom) \
-                                                               for parent, atom in parent_atoms])
+                                                               for parent, atom in all_parents])
                                                        msg.append("")
                                                        writemsg_level("".join("%s\n" % l for l in msg),
                                                                noiselevel=-1, level=logging.DEBUG)
@@ -1917,6 +1895,8 @@ class depgraph(object):
                        set(self._dynamic_config.digraph).intersection( \
                        self._dynamic_config._needed_license_changes) :
                        #We failed if the user needs to change the configuration
+                       if not missing:
+                               self._dynamic_config._success_without_autounmask = True
                        return False, myfavorites
 
                # We're true here unless we are missing binaries.
@@ -2631,9 +2611,18 @@ class depgraph(object):
 
                if masked_by_unstable_keywords:
                        self._dynamic_config._needed_unstable_keywords.add(pkg)
+                       backtrack_infos = self._dynamic_config._backtrack_infos
+                       backtrack_infos.setdefault("config", {})
+                       backtrack_infos["config"].setdefault("needed_unstable_keywords", set())
+                       backtrack_infos["config"]["needed_unstable_keywords"].add(pkg)
+                       
 
                if missing_licenses:
                        self._dynamic_config._needed_license_changes.setdefault(pkg, set()).update(missing_licenses)
+                       backtrack_infos = self._dynamic_config._backtrack_infos
+                       backtrack_infos.setdefault("config", {})
+                       backtrack_infos["config"].setdefault("needed_license_changes", set())
+                       backtrack_infos["config"]["needed_license_changes"].add((pkg, frozenset(missing_licenses)))
 
                return True
 
@@ -2709,8 +2698,12 @@ class depgraph(object):
                        if required_use and check_required_use(required_use, old_use, pkg.iuse.is_valid_flag) and \
                                not check_required_use(required_use, new_use, pkg.iuse.is_valid_flag):
                                return old_use
-                       
+
                        self._dynamic_config._needed_use_config_changes[pkg] = (new_use, new_changes)
+                       backtrack_infos = self._dynamic_config._backtrack_infos
+                       backtrack_infos.setdefault("config", {})
+                       backtrack_infos["config"].setdefault("needed_use_config_changes", [])
+                       backtrack_infos["config"]["needed_use_config_changes"].append((pkg, (new_use, new_changes)))
                        if want_restart_for_use_change(pkg, new_use):
                                self._dynamic_config._need_restart = True
                return new_use
@@ -5136,18 +5129,12 @@ class depgraph(object):
 
        def need_restart(self):
                return self._dynamic_config._need_restart
+       
+       def success_without_autounmask(self):
+               return self._dynamic_config._success_without_autounmask
 
-       def get_backtrack_parameters(self):
-               return {
-                       "needed_unstable_keywords":
-                               self._dynamic_config._needed_unstable_keywords.copy(), \
-                       "runtime_pkg_mask":
-                               self._dynamic_config._runtime_pkg_mask.copy(),
-                       "needed_use_config_changes":
-                               self._dynamic_config._needed_use_config_changes.copy(),
-                       "needed_license_changes":
-                               self._dynamic_config._needed_license_changes.copy(),
-                       }
+       def get_backtrack_infos(self):
+               return self._dynamic_config._backtrack_infos
                        
 
 class _dep_check_composite_db(dbapi):
@@ -5375,44 +5362,42 @@ def backtrack_depgraph(settings, trees, myopts, myparams,
        finally:
                _spinner_stop(spinner)
 
-def _backtrack_depgraph(settings, trees, myopts, myparams, 
-       myaction, myfiles, spinner):
 
-       backtrack_max = myopts.get('--backtrack', 5)
-       backtrack_parameters = {}
-       needed_unstable_keywords = None
-       allow_backtracking = backtrack_max > 0
-       backtracked = 0
+def _backtrack_depgraph(settings, trees, myopts, myparams, myaction, myfiles, spinner):
+
+       max_depth = myopts.get('--backtrack', 5)
+       allow_backtracking = max_depth > 0
+       backtracker = Backtracker(max_depth)
+
        frozen_config = _frozen_depgraph_config(settings, trees,
                myopts, spinner)
-       while True:
+
+       while backtracker:
+               backtrack_parameters = backtracker.get()
+
                mydepgraph = depgraph(settings, trees, myopts, myparams, spinner,
                        frozen_config=frozen_config,
                        allow_backtracking=allow_backtracking,
-                       **backtrack_parameters)
+                       backtrack_parameters=backtrack_parameters)
                success, favorites = mydepgraph.select_files(myfiles)
-               if not success:
-                       if mydepgraph.need_restart() and backtracked < backtrack_max:
-                               backtrack_parameters = mydepgraph.get_backtrack_parameters()
-                               backtracked += 1
-                       elif backtracked and allow_backtracking:
-                               if "--debug" in myopts:
-                                       writemsg_level(
-                                               "\n\nbacktracking aborted after %s tries\n\n" % \
-                                               backtracked, noiselevel=-1, level=logging.DEBUG)
-                               # Backtracking failed, so disable it and do
-                               # a plain dep calculation + error message.
-                               allow_backtracking = False
-                               #Don't reset needed_unstable_keywords here, since we don't want to
-                               #send the user through a "one step at a time" unmasking session for
-                               #no good reason.
-                               backtrack_parameters.pop('runtime_pkg_mask', None)
-                       else:
-                               break
-               else:
+
+               if success or mydepgraph.success_without_autounmask():
                        break
+               elif mydepgraph.need_restart():
+                       backtracker.feedback(mydepgraph.get_backtrack_infos())
+
+       if not (success or mydepgraph.success_without_autounmask()) and backtracker.backtracked(): 
+               backtrack_parameters = backtracker.get_best_run()
+
+               mydepgraph = depgraph(settings, trees, myopts, myparams, spinner,
+                       frozen_config=frozen_config,
+                       allow_backtracking=False,
+                       backtrack_parameters=backtrack_parameters)
+               success, favorites = mydepgraph.select_files(myfiles)
+
        return (success, mydepgraph, favorites)
 
+
 def resume_depgraph(settings, trees, mtimedb, myopts, myparams, spinner):
        """
        Raises PackageSetNotFound if myfiles contains a missing package set.
diff --git a/pym/_emerge/resolver/backtracking.py b/pym/_emerge/resolver/backtracking.py
new file mode 100644 (file)
index 0000000..ded97e1
--- /dev/null
@@ -0,0 +1,184 @@
+# Copyright 2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+import copy
+
+class BacktrackParameter(object):
+
+       __slots__ = (
+               "needed_unstable_keywords", "runtime_pkg_mask", "needed_use_config_changes", "needed_license_changes",
+       )
+
+       def __init__(self):
+               self.needed_unstable_keywords = set()
+               self.runtime_pkg_mask = {}
+               self.needed_use_config_changes = {}
+               self.needed_license_changes = {}
+
+       def __deepcopy__(self, memo=None):
+               if memo is None:
+                       memo = {}
+               result = BacktrackParameter()
+               memo[id(self)] = result
+
+               #Shallow copies are enough here, as we only need to ensure that nobody adds stuff
+               #to our sets and dicts. The existing content is immutable.
+               result.needed_unstable_keywords = copy.copy(self.needed_unstable_keywords)
+               result.runtime_pkg_mask = copy.copy(self.runtime_pkg_mask)
+               result.needed_use_config_changes = copy.copy(self.needed_use_config_changes)
+               result.needed_license_changes = copy.copy(self.needed_license_changes)
+
+               return result
+
+       def __eq__(self, other):
+               return self.needed_unstable_keywords == other.needed_unstable_keywords and \
+                       self.runtime_pkg_mask == other.runtime_pkg_mask and \
+                       self.needed_use_config_changes == other.needed_use_config_changes and \
+                       self.needed_license_changes == other.needed_license_changes
+
+
+class _BacktrackNode:
+
+       __slots__ = (
+               "parameter", "depth", "mask_steps", "terminal",
+       )
+
+       def __init__(self, parameter=BacktrackParameter(), depth=0, mask_steps=0, terminal=True):
+               self.parameter = parameter
+               self.depth = depth
+               self.mask_steps = mask_steps
+               self.terminal = terminal
+
+       def __eq__(self, other):
+               return self.parameter == other.parameter
+
+
+class Backtracker(object):
+
+       __slots__ = (
+               "_max_depth", "_unexplored_nodes", "_current_node", "_nodes", "_root",
+       )
+
+       def __init__(self, max_depth):
+               self._max_depth = max_depth
+               self._unexplored_nodes = []
+               self._current_node = None
+               self._nodes = []
+
+               self._root = _BacktrackNode()
+               self._add(self._root)
+
+
+       def _add(self, node, explore=True):
+               """
+               Adds a newly computed backtrack parameter. Makes sure that it doesn't already exist and
+               that we don't backtrack deeper than we are allowed by --backtrack.
+               """
+               if node.mask_steps <= self._max_depth and node not in self._nodes:
+                       if explore:
+                               self._unexplored_nodes.append(node)
+                       self._nodes.append(node)
+
+
+       def get(self):
+               """
+               Returns a backtrack paramater. The backtrack graph is explored with depth first.
+               """
+               if self._unexplored_nodes:
+                       node = self._unexplored_nodes.pop()
+                       self._current_node = node
+                       return copy.deepcopy(node.parameter)
+               else:
+                       return None
+
+
+       def __len__(self):
+               return len(self._unexplored_nodes)
+
+
+       def _feedback_slot_conflict(self, conflict_data):
+               for pkg, parent_atoms in conflict_data:
+                       new_node = copy.deepcopy(self._current_node)
+                       new_node.depth += 1
+                       new_node.mask_steps += 1
+                       new_node.terminal = False
+                       for other_pkg, other_parent_atoms in conflict_data:
+                               if other_pkg is pkg:
+                                       continue
+                               new_node.parameter.runtime_pkg_mask.setdefault(
+                                       other_pkg, {})["slot conflict"] = other_parent_atoms
+                       self._add(new_node)
+
+
+       def _feedback_missing_dep(self, dep):
+               new_node = copy.deepcopy(self._current_node)
+               new_node.depth += 1
+               new_node.mask_steps += 1
+               new_node.terminal = False
+
+               new_node.parameter.runtime_pkg_mask.setdefault(
+                       dep.parent, {})["missing dependency"] = \
+                               set([(dep.parent, dep.root, dep.atom)])
+
+               self._add(new_node)
+
+
+       def _feedback_config(self, changes, explore=True):
+               """
+               Handle config changes. Don't count config changes for the maximum backtrack depth.
+               """
+               new_node = copy.deepcopy(self._current_node)
+               new_node.depth += 1
+               para = new_node.parameter
+
+               for change, data in changes.items():
+                       if change == "needed_unstable_keywords":
+                               para.needed_unstable_keywords.update(data)
+                       elif change == "needed_license_changes":
+                               for pkg, missing_licenses in data:
+                                       para.needed_license_changes.setdefault(pkg, set()).update(missing_licenses)
+                       elif change == "needed_use_config_changes":
+                               for pkg, (new_use, new_changes) in data:
+                                       para.needed_use_config_changes[pkg] = (new_use, new_changes)
+
+               self._add(new_node, explore=explore)
+               self._current_node = new_node
+
+
+       def feedback(self, infos):
+               """
+               Takes infomration from the depgraph and computes new backtrack parameters to try.
+               """
+               assert self._current_node is not None, "call feedback() only after get() was called"
+
+               #Not all config changes require a restart, that's why they can appear together
+               #with other conflicts.
+               if "config" in infos:
+                       self._feedback_config(infos["config"], explore=(len(infos)==1))
+
+               #There is at most one of the following types of conflicts for a given restart.
+               if "slot conflict" in infos:
+                       self._feedback_slot_conflict(infos["slot conflict"])
+               elif "missing dependency" in infos:
+                       self._feedback_missing_dep(infos["missing dependency"])
+
+
+       def backtracked(self):
+               """
+               If we dind't backtrack, there is only the root.
+               """
+               return len(self._nodes) > 1
+
+
+       def get_best_run(self):
+               """
+               Like, get() but returns the backtrack parameter that has as many config changes as possible,
+               but has no masks. This maskes --autounmask effective, but prevents confusing error messages
+               with "masked by backtracking".
+               """
+               best_node = self._root
+               for node in self._nodes:
+                       if node.terminal and node.depth > best_node.depth:
+                               best_node = node
+
+               return copy.deepcopy(best_node.parameter)
index ce3ce38f0737f9b5dad9a5b3032971c2daad2812..760c764874d90de388852a545fc725343ade6550 100644 (file)
@@ -194,6 +194,11 @@ class AutounmaskTestCase(TestCase):
                        "dev-libs/A-1": { "LICENSE": "TEST" },
                        "dev-libs/B-1": { "LICENSE": "TEST", "IUSE": "foo", "KEYWORDS": "~x86"},
                        "dev-libs/C-1": { "DEPEND": "dev-libs/B[foo]", "EAPI": 2 },
+                       
+                       "dev-libs/D-1": { "DEPEND": "dev-libs/E dev-libs/F", "LICENSE": "TEST" },
+                       "dev-libs/E-1": { "LICENSE": "TEST" },
+                       "dev-libs/E-2": { "LICENSE": "TEST" },
+                       "dev-libs/F-1": { "DEPEND": "=dev-libs/E-1", "LICENSE": "TEST" },
                        }
 
                test_cases = (
@@ -217,6 +222,14 @@ class AutounmaskTestCase(TestCase):
                                        license_changes = { "dev-libs/B-1": set(["TEST"]) },
                                        unstable_keywords = ["dev-libs/B-1"],
                                        use_changes = { "dev-libs/B-1": { "foo": True } }),
+
+                               #Test license with backtracking.
+                               ResolverPlaygroundTestCase(
+                                       ["=dev-libs/D-1"],
+                                       options = {"--autounmask": True},
+                                       success = False,
+                                       mergelist = ["dev-libs/E-1", "dev-libs/F-1", "dev-libs/D-1"],
+                                       license_changes = { "dev-libs/D-1": set(["TEST"]), "dev-libs/E-1": set(["TEST"]), "dev-libs/E-2": set(["TEST"]), "dev-libs/F-1": set(["TEST"]) }),
                        )
 
                playground = ResolverPlayground(ebuilds=ebuilds)
index 91a739aafc3da89ad5a5023b592a9d9754daa40b..41b6d50b68905ccae1e2b6204b3b9524ed4e13d9 100644 (file)
@@ -30,6 +30,44 @@ class BacktrackingTestCase(TestCase):
                finally:
                        playground.cleanup()
 
+
+       def testHittingTheBacktrackLimit(self):
+               ebuilds = {
+                       "dev-libs/A-1": {},
+                       "dev-libs/A-2": {},
+                       "dev-libs/B-1": {},
+                       "dev-libs/B-2": {},
+                       "dev-libs/C-1": { "DEPEND": "dev-libs/A dev-libs/B" },
+                       "dev-libs/D-1": { "DEPEND": "=dev-libs/A-1 =dev-libs/B-1" },
+                       }
+
+               test_cases = (
+                               ResolverPlaygroundTestCase(
+                                       ["dev-libs/C", "dev-libs/D"],
+                                       all_permutations = True,
+                                       mergelist = ["dev-libs/A-1", "dev-libs/B-1", "dev-libs/C-1", "dev-libs/D-1"],
+                                       ignore_mergelist_order = True,
+                                       success = True),
+                               #This one hits the backtrack limit. Be aware that this depends on the argument order.
+                               ResolverPlaygroundTestCase(
+                                       ["dev-libs/D", "dev-libs/C"],
+                                       options = { "--backtrack": 1 },
+                                       mergelist = ["dev-libs/A-1", "dev-libs/B-1", "dev-libs/A-2", "dev-libs/B-2", "dev-libs/C-1", "dev-libs/D-1"],
+                                       ignore_mergelist_order = True,
+                                       slot_collision_solutions = [],
+                                       success = False),
+                       )
+
+               playground = ResolverPlayground(ebuilds=ebuilds)
+
+               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()
+
+
        def testBacktrackingGoodVersionFirst(self):
                """
                When backtracking due to slot conflicts, we masked the version that has been pulled
@@ -59,3 +97,37 @@ class BacktrackingTestCase(TestCase):
                                self.assertEqual(test_case.test_success, True, test_case.fail_msg)
                finally:
                        playground.cleanup()
+
+       def testBacktrackWithoutUpdates(self):
+               """
+               If --update is not given we might have to mask the old installed version later.
+               """
+
+               ebuilds = {
+                       "dev-libs/A-1": { "DEPEND": "dev-libs/Z" },
+                       "dev-libs/B-1": { "DEPEND": ">=dev-libs/Z-2" },
+                       "dev-libs/Z-1": { },
+                       "dev-libs/Z-2": { },
+                       }
+
+               installed = {
+                       "dev-libs/Z-1": { "USE": "" },
+                       }
+
+               test_cases = (
+                               ResolverPlaygroundTestCase(
+                                       ["dev-libs/B", "dev-libs/A"],
+                                       all_permutations = True,
+                                       mergelist = ["dev-libs/Z-2", "dev-libs/B-1", "dev-libs/A-1", ],
+                                       ignore_mergelist_order = True,
+                                       success = True),
+                       )
+
+               playground = ResolverPlayground(ebuilds=ebuilds, installed=installed)
+
+               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()