Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve context resolve failure info #1083

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/rez/resolved_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
from rez.shells import create_shell
from rez.exceptions import ResolvedContextError, PackageCommandError, \
RezError, _NeverError, PackageCacheError, PackageNotFoundError
from rez.utils.graph_utils import write_dot, write_compacted, read_graph_from_string
from rez.utils.graph_utils import write_dot, write_compacted, \
read_graph_from_string
from rez.utils.resolve_graph import failure_detail_from_graph
from rez.vendor.six import six
from rez.vendor.version.version import VersionRange
from rez.vendor.version.requirement import Requirement
Expand Down Expand Up @@ -807,13 +809,13 @@ def _rt(t):
return time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(t))

if self.status_ in (ResolverStatus.failed, ResolverStatus.aborted):
_pr("The context failed to resolve:\n%s"
% self.failure_description, critical)
return
res_status = "resolve failed,"
else:
res_status = "resolved"

t_str = _rt(self.created)
_pr("resolved by %s@%s, on %s, using Rez v%s"
% (self.user, self.host, t_str, self.rez_version))
_pr("%s by %s@%s, on %s, using Rez v%s"
% (res_status, self.user, self.host, t_str, self.rez_version))
if self.requested_timestamp:
t_str = _rt(self.requested_timestamp)
_pr("packages released after %s were ignored" % t_str)
Expand Down Expand Up @@ -863,6 +865,21 @@ def _rt(t):
_pr(line, col)
_pr()

# show resolved, or not
#
if self.status_ in (ResolverStatus.failed, ResolverStatus.aborted):
_pr("The context failed to resolve:\n%s"
% self.failure_description, critical)

_pr()
_pr(failure_detail_from_graph(self.graph(as_dot=False)))
_pr()
_pr("To see a graph of the failed resolution, add --fail-graph "
"in your rez-env or rez-build command.")
_pr()

return

_pr("resolved packages:", heading)
rows = []
colors = []
Expand Down
129 changes: 129 additions & 0 deletions src/rez/utils/resolve_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

from rez.utils.graph_utils import _request_from_label


def failure_detail_from_graph(graph):
"""Generate detailed resolve failure messages from graph

Args:
graph (rez.vendor.pygraph.classes.digraph.digraph): context graph object

"""
# Base on `rez.solver._ResolvePhase.get_graph()`
# the failure reason has three types:
#
# * DependencyConflicts
# which have "conflict" edge in graph
#
# * TotalReduction
# which have "conflict" edge and may also have "reduct" edge in graph
#
# * Cycle
# which have "cycle" edge in graph
#
# so we find cycle edge first, and try other if not found.
#

# find cycle
cycled_edge = next((k for k, v in graph.edge_properties.items()
if v["label"] == "CYCLE"), None)
if cycled_edge:
return _cycled_detail_from_graph(graph, cycled_edge)

# find conflict
conflicted_edge = next((k for k, v in graph.edge_properties.items()
if v["label"] == "CONFLICT"), None)
if conflicted_edge:
return _conflicted_detail_from_graph(graph, conflicted_edge)

# should be a healthy graph
return ""


def _cycled_detail_from_graph(graph, cycled_edge):
"""Find all initial requests, and walk down till circle back"""

messages = ["Resolve paths starting from initial requests to cycle:"]

for init_request in _iter_init_request_nodes(graph):
node = init_request
visited = list()
while True:
visited.append(node)
down = next((ne for ne in graph.node_neighbors[node]), None)
if down in cycled_edge:
visited.append(down)
break
if down is None:
break

node = down

line = " %s" % _get_node_label(graph, visited[0]) # init request
for node in visited[1:]:
# should be more readable if opt-out requests
if not _is_request_node(graph, node):
line += " --> %s" % _get_node_label(graph, node)

messages.append(line)

return "\n".join(messages)


def _conflicted_detail_from_graph(graph, conflicted_edge):
"""Find all initial requests, and walk down till in conflicted edge"""

messages = ["Resolve paths starting from initial requests to conflict:"]

for init_request in _iter_init_request_nodes(graph):
node = init_request
visited = list()
while True:
visited.append(node)
down = next((ne for ne in graph.node_neighbors[node]), None)
if down is None:
break

node = down

line = " %s" % _get_node_label(graph, visited[0]) # init request
for node in visited[1:]:
# should be more readable if opt-out requests
if not _is_request_node(graph, node):
line += " --> %s" % _get_node_label(graph, node)

# show conflicted request at the end
if _is_request_node(graph, node) and node in conflicted_edge:
line += " --> %s" % _get_node_label(graph, node)
break

messages.append(line)

return "\n".join(messages)


def _iter_init_request_nodes(graph):
request_color = "#FFFFAA" # NOTE: the color code is hardcoded in `solver`
for node, attrs in graph.node_attr.items():
for at in attrs:
if at[0] == "fillcolor" and at[1] == request_color:
yield node


def _get_node_label(graph, node):
label_ = next(at[1] for at in graph.node_attr[node] if at[0] == "label")
return _request_from_label(label_)


def _is_request_node(graph, node):
style = next(at[1] for at in graph.node_attr[node] if at[0] == "style")
return "dashed" in style


def _print_each_graph_edges(graph):
"""for debug"""
for (from_, to_), properties in graph.edge_properties.items():
edge_status = properties["label"]
from_name = _get_node_label(graph, from_)
to_name = _get_node_label(graph, to_)
print("%s -> %s %s" % (from_name, to_name, edge_status))