diff --git a/conda/models/prefix_graph.py b/conda/models/prefix_graph.py index 53eacdfd419..569a84fe76e 100644 --- a/conda/models/prefix_graph.py +++ b/conda/models/prefix_graph.py @@ -409,7 +409,23 @@ def __init__(self, records, specs=()): consolidated_graph[node.name] = cg self.graph_by_name = consolidated_graph - def depth_first_search_by_name(self, root_spec, spec_name, allowed_specs): + def breadth_first_search_by_name(self, root_spec, spec_name): + """Return shorted path from root_spec to spec_name""" + queue = [] + queue.append([root_spec.name]) + while queue: + path = queue.pop(0) + node = path[-1] + if node == spec_name: + return path + children = sorted(self.graph_by_name.get(node, set()), + key=lambda x: list(self.graph_by_name.keys()).index(x)) + for adj in children: + new_path = list(path) + new_path.append(adj) + queue.append(new_path) + + def depth_first_search_by_name(self, root_spec, spec_name): """Return paths from root_spec to spec_name""" if root_spec.name == spec_name: return [[root_spec]] @@ -434,8 +450,11 @@ def build_dependency_chain(node, spc, chains=None): return chains chains = build_dependency_chain(root_spec.name, spec_name) + return chains + def get_dependency_chains(self, root_spec, spec_name, allowed_specs): final_chains = [] + chains = [self.breadth_first_search_by_name(root_spec, spec_name)] for chain in sorted(chains, key=len): if chain[0] == root_spec.name and chain[-1] == spec_name: # remap to matchspecs diff --git a/conda/resolve.py b/conda/resolve.py index b2e4a607204..87667c3bfa3 100644 --- a/conda/resolve.py +++ b/conda/resolve.py @@ -415,7 +415,7 @@ def build_conflict_map(self, specs, specs_to_add=None, history_specs=None): key=lambda x: list(g.graph_by_name.keys()).index(x.name)) for spec in spec_order: # the DFS approach works well when things are actually in the graph - bad_deps.extend(g.depth_first_search_by_name(spec, dep, sdeps[spec])) + bad_deps.extend(g.get_dependency_chains(spec, dep, sdeps[spec])) if not bad_deps: # no conflicting nor missing packages found, return the bad specs diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 3efa7fcfb87..973a2f54465 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -355,6 +355,56 @@ def test_unsat_simple(): assert "b -> c[version='>=2,<3']" in str(excinfo.value) +def test_unsat_shortest_chain(): + index = ( + simple_rec(name='a', depends=['d', 'c <1.3.0']), + simple_rec(name='b', depends=['c']), + simple_rec(name='c', version='1.3.6',), + simple_rec(name='c', version='1.2.8',), + simple_rec(name='d', depends=['c >=0.8.0']), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + with pytest.raises(UnsatisfiableError) as excinfo: + r.install(['c=1.3.6', 'a', 'b']) + assert "a -> c[version='<1.3.0']" in str(excinfo.value) + assert "b -> c" in str(excinfo.value) + assert "c=1.3.6" in str(excinfo.value) + + +def test_unsat_shortest_reverse_chain(): + index = ( + simple_rec(name='a', depends=['d', 'c >=0.8.0']), + simple_rec(name='b', depends=['c']), + simple_rec(name='c', version='1.3.6',), + simple_rec(name='c', version='1.2.8',), + simple_rec(name='d', depends=['c <1.3.0']), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + with pytest.raises(UnsatisfiableError) as excinfo: + r.install(['c=1.3.6', 'a', 'b']) + assert "a -> d -> c[version='<1.3.0']" in str(excinfo.value) + assert "b -> c" in str(excinfo.value) + assert "c=1.3.6" in str(excinfo.value) + + +def test_unsat_shortest_long_chain(): + index = ( + simple_rec(name='a', depends=['f', 'e']), + simple_rec(name='b', depends=['c']), + simple_rec(name='c', version='1.3.6',), + simple_rec(name='c', version='1.2.8',), + simple_rec(name='d', depends=['c >=0.8.0']), + simple_rec(name='e', depends=['c <1.3.0']), + simple_rec(name='f', depends=['d']), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + with pytest.raises(UnsatisfiableError) as excinfo: + r.install(['c=1.3.6', 'a', 'b']) + assert "a -> e -> c[version='<1.3.0']" in str(excinfo.value) + assert "b -> c" in str(excinfo.value) + assert "c=1.3.6" in str(excinfo.value) + + def test_unsat_chain(): # a -> b -> c=1.x -> d=1.x # e -> c=2.x -> d=2.x