verbose errors
This commit is contained in:
parent
2f09772928
commit
4392eb393f
6 changed files with 221 additions and 15 deletions
|
|
@ -219,11 +219,15 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
|||
try:
|
||||
with execution_cache():
|
||||
with profile_node_execution(upstream_node):
|
||||
exec_res = upstream_node.evaluate(operator=operator)
|
||||
from ....infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
||||
wrapped_op = NodeErrorReportingProxy(operator, upstream_node)
|
||||
exec_res = upstream_node.evaluate(operator=wrapped_op)
|
||||
if exec_res != 'ok' and exec_res is not None:
|
||||
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
|
||||
except Exception as e:
|
||||
if operator: operator.report({'ERROR'}, f"Failed executing {upstream_node.bl_label}: {str(e)}")
|
||||
from ....infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
||||
attach_node_failure_info(upstream_node, e)
|
||||
if operator: operator.report({'ERROR'}, str(e))
|
||||
return 'error'
|
||||
|
||||
return 'ok'
|
||||
|
|
|
|||
|
|
@ -750,11 +750,15 @@ class SLICENODE_OT_Execute(Operator):
|
|||
|
||||
try:
|
||||
with execution_cache():
|
||||
res = node.evaluate(operator=self)
|
||||
from ...infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
||||
wrapped_op = NodeErrorReportingProxy(self, node)
|
||||
res = node.evaluate(operator=wrapped_op)
|
||||
if res == 'ok':
|
||||
self.report({"INFO"}, f"Execution Successful")
|
||||
self.report({"INFO"}, "Execution Successful")
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Execution failed: {str(e)}")
|
||||
from ...infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
||||
attach_node_failure_info(node, e)
|
||||
self.report({"ERROR"}, str(e))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
|
|
|||
|
|
@ -646,6 +646,91 @@ def _format_node_description(node: Any) -> str:
|
|||
return str(node)
|
||||
|
||||
|
||||
def get_node_input_names(node: Any, fallback_inputs: list[str] | None = None) -> list[str]:
|
||||
"""Retrieves all input socket names for a node, preserving order without duplicates."""
|
||||
names: list[str] = []
|
||||
inputs = getattr(node, "inputs", None)
|
||||
if inputs:
|
||||
for s in inputs:
|
||||
s_name = getattr(s, "name", "")
|
||||
if s_name and s_name not in names:
|
||||
names.append(s_name)
|
||||
elif not s_name:
|
||||
ident = getattr(s, "identifier", "")
|
||||
if ident and ident not in names:
|
||||
names.append(ident)
|
||||
if not names and fallback_inputs:
|
||||
for s_name in fallback_inputs:
|
||||
if s_name and s_name not in names:
|
||||
names.append(s_name)
|
||||
if not names:
|
||||
mapping = getattr(node, "list_socket_mapping", None)
|
||||
if mapping:
|
||||
for item in mapping:
|
||||
m_in = item[0] if isinstance(item, (list, tuple)) and item else None
|
||||
if m_in and m_in not in names:
|
||||
names.append(m_in)
|
||||
return names
|
||||
|
||||
|
||||
def format_node_failure_message(node: Any, exc_or_msg: Any, fallback_inputs: list[str] | None = None) -> str:
|
||||
"""Formats a concise, informative execution failure message specifying the node and input names."""
|
||||
node_str = _format_node_description(node) or "Unknown Node"
|
||||
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
|
||||
inputs_str = ", ".join(input_names) if input_names else "(None)"
|
||||
msg = str(exc_or_msg)
|
||||
if msg.startswith("Execution failed at node "):
|
||||
return msg
|
||||
return f"Execution failed at node {node_str} [Inputs: {inputs_str}]: {msg}"
|
||||
|
||||
|
||||
def attach_node_failure_info(node: Any, exc: Exception, fallback_inputs: list[str] | None = None) -> Exception:
|
||||
"""Enriches an exception in-place with the failing node and input socket names, preventing duplicate wrapping."""
|
||||
if getattr(exc, "_nodeslicer_node", None) is not None:
|
||||
return exc
|
||||
try:
|
||||
setattr(exc, "_nodeslicer_node", node)
|
||||
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
|
||||
setattr(exc, "_nodeslicer_inputs", input_names)
|
||||
|
||||
node_str = _format_node_description(node) or "Unknown Node"
|
||||
inputs_str = ", ".join(input_names) if input_names else "(None)"
|
||||
prefix = f"Execution failed at node {node_str} [Inputs: {inputs_str}]: "
|
||||
|
||||
if exc.args:
|
||||
orig_msg = str(exc.args[0])
|
||||
if not orig_msg.startswith("Execution failed at node "):
|
||||
exc.args = (prefix + orig_msg, *exc.args[1:])
|
||||
else:
|
||||
exc.args = (prefix + type(exc).__name__,)
|
||||
|
||||
print(f"[NodeSlicer ERROR] {str(exc)}")
|
||||
except Exception:
|
||||
pass
|
||||
return exc
|
||||
|
||||
|
||||
class NodeErrorReportingProxy:
|
||||
"""Wraps Blender operator to ensure any operator.report({'ERROR'}, ...) calls
|
||||
always tell which node failed and the names of its inputs."""
|
||||
def __init__(self, operator: Any, node: Any):
|
||||
self._operator = operator
|
||||
self._node = node
|
||||
|
||||
def report(self, type_set: set[str], message: str) -> None:
|
||||
if self._operator and hasattr(self._operator, "report"):
|
||||
if "ERROR" in type_set:
|
||||
formatted_msg = format_node_failure_message(self._node, message)
|
||||
print(f"[NodeSlicer ERROR] {formatted_msg}")
|
||||
self._operator.report(type_set, formatted_msg)
|
||||
else:
|
||||
self._operator.report(type_set, message)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._operator, name)
|
||||
|
||||
|
||||
|
||||
def _extract_caller_context() -> tuple[Any, str | None]:
|
||||
try:
|
||||
import sys
|
||||
|
|
@ -888,6 +973,7 @@ def _resolve_output(
|
|||
eval_exc = e
|
||||
import traceback
|
||||
eval_tb = traceback.format_exc()
|
||||
attach_node_failure_info(socket.node, e)
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -232,14 +232,20 @@ def eval_multivalue(
|
|||
else:
|
||||
vec_flags = is_vector
|
||||
|
||||
return eval_element_wise_multivalue(
|
||||
op_func,
|
||||
main_data,
|
||||
*sec_vals,
|
||||
is_vector=vec_flags,
|
||||
input_names=secondary_sockets,
|
||||
output_index=output_index,
|
||||
)
|
||||
try:
|
||||
return eval_element_wise_multivalue(
|
||||
op_func,
|
||||
main_data,
|
||||
*sec_vals,
|
||||
is_vector=vec_flags,
|
||||
input_names=secondary_sockets,
|
||||
output_index=output_index,
|
||||
)
|
||||
except Exception as e:
|
||||
from .blender_slice_nodes_generic import attach_node_failure_info
|
||||
known_inputs = ([main_socket] if main_socket else []) + (secondary_sockets or [])
|
||||
attach_node_failure_info(node, e, fallback_inputs=known_inputs)
|
||||
raise
|
||||
|
||||
def eval_element_wise(op_func: Callable[[Any], Any], data: Any) -> Any:
|
||||
"""If data is a list, applies op_func to each element and returns a list.
|
||||
|
|
|
|||
|
|
@ -348,6 +348,9 @@ def format_debug_report(dump: dict[str, Any]) -> str:
|
|||
lines.append("")
|
||||
lines.append("!" * 80)
|
||||
lines.append(f" EXCEPTION: {dump.get('exception')}")
|
||||
lines.append(f" FAILED NODE: {dump.get('node_name', 'Unknown Node')} ({dump.get('node_type', '')})")
|
||||
input_keys = list(dump.get("inputs", {}).keys())
|
||||
lines.append(f" INPUTS: {', '.join(input_keys) if input_keys else '(None)'}")
|
||||
lines.append("!" * 80)
|
||||
if dump.get("traceback"):
|
||||
lines.append(dump["traceback"])
|
||||
|
|
@ -518,7 +521,9 @@ def dump_node_execution(
|
|||
|
||||
if log_console:
|
||||
if exception:
|
||||
print(f"[NodeSlicer DEBUG ERROR] Node '{node_name}' ({node_type}) failed: {exception}")
|
||||
input_names_list = list(raw_inputs_dict.keys())
|
||||
inputs_str = ", ".join(input_names_list) if input_names_list else "(None)"
|
||||
print(f"[NodeSlicer DEBUG ERROR] Node '{node_name}' ({node_type}) failed: {exception} (Inputs: {inputs_str})")
|
||||
print(f"[NodeSlicer DEBUG] Review dump at: {last_txt_file}")
|
||||
else:
|
||||
print(f"[NodeSlicer DEBUG] Dumped execution of '{node_name}' ({node_type}) -> {last_txt_file}")
|
||||
|
|
|
|||
|
|
@ -353,6 +353,107 @@ class TestNodeDebugSystem(unittest.TestCase):
|
|||
err_msg = str(ctx.exception)
|
||||
self.assertIn("non-existent", err_msg)
|
||||
|
||||
def test_node_failure_includes_node_and_input_names(self):
|
||||
from NodeSlicer.infra.blender.blender_slice_nodes_generic import attach_node_failure_info, get_node_input_names
|
||||
|
||||
class FailingNode(SliceNode):
|
||||
bl_idname = "SLICENODE_FailingOp"
|
||||
bl_label = "Failing Operation"
|
||||
|
||||
def __init__(self):
|
||||
self.name = "Custom Failing Node"
|
||||
self.inputs = DummySocketCollection([
|
||||
DummyNodeSocket("Primary", "Primary", "SLICESOCK_Mesh"),
|
||||
DummyNodeSocket("Offset Val", "Offset Val", "NodeSocketFloat"),
|
||||
DummyNodeSocket("Rotation Angle", "Rotation Angle", "NodeSocketRotation"),
|
||||
], node=self)
|
||||
|
||||
node = FailingNode()
|
||||
inputs = get_node_input_names(node)
|
||||
self.assertEqual(inputs, ["Primary", "Offset Val", "Rotation Angle"])
|
||||
|
||||
exc = ValueError("Invalid operation arguments")
|
||||
attach_node_failure_info(node, exc)
|
||||
|
||||
err_msg = str(exc)
|
||||
self.assertIn("Custom Failing Node", err_msg)
|
||||
self.assertIn("Failing Operation", err_msg)
|
||||
self.assertIn("Primary, Offset Val, Rotation Angle", err_msg)
|
||||
self.assertIn("Invalid operation arguments", err_msg)
|
||||
|
||||
def test_node_failure_preserves_innermost_node_on_bubble_up(self):
|
||||
from NodeSlicer.infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
||||
|
||||
class InnerNode(SliceNode):
|
||||
bl_idname = "SLICENODE_Inner"
|
||||
bl_label = "Inner Worker"
|
||||
|
||||
def __init__(self):
|
||||
self.name = "My Inner Worker"
|
||||
self.inputs = DummySocketCollection([
|
||||
DummyNodeSocket("InnerInput1", "InnerInput1", "NodeSocketFloat"),
|
||||
DummyNodeSocket("InnerInput2", "InnerInput2", "NodeSocketString"),
|
||||
], node=self)
|
||||
|
||||
class OuterNode(SliceNode):
|
||||
bl_idname = "SLICENODE_Outer"
|
||||
bl_label = "Outer Caller"
|
||||
|
||||
def __init__(self):
|
||||
self.name = "My Outer Caller"
|
||||
self.inputs = DummySocketCollection([
|
||||
DummyNodeSocket("OuterInput", "OuterInput", "NodeSocketFloat")
|
||||
], node=self)
|
||||
|
||||
inner = InnerNode()
|
||||
outer = OuterNode()
|
||||
|
||||
# Inner node fails first
|
||||
inner_exc = RuntimeError("Hardware buffer failure")
|
||||
attach_node_failure_info(inner, inner_exc)
|
||||
|
||||
# Bubbles up to outer node
|
||||
attach_node_failure_info(outer, inner_exc)
|
||||
|
||||
# Must still point to inner node and inner inputs, NOT overwritten by outer node
|
||||
msg = str(inner_exc)
|
||||
self.assertIn("My Inner Worker", msg)
|
||||
self.assertIn("Inner Worker", msg)
|
||||
self.assertIn("InnerInput1, InnerInput2", msg)
|
||||
self.assertNotIn("My Outer Caller", msg)
|
||||
self.assertNotIn("OuterInput", msg)
|
||||
|
||||
def test_node_error_reporting_proxy_formats_operator_errors(self):
|
||||
from NodeSlicer.infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
||||
|
||||
class ActionNode(SliceNode):
|
||||
bl_idname = "SLICENODE_Export"
|
||||
bl_label = "Export Geometry"
|
||||
|
||||
def __init__(self):
|
||||
self.name = "Export Step"
|
||||
self.inputs = DummySocketCollection([
|
||||
DummyNodeSocket("Geometry", "Geometry", "SLICESOCK_Mesh"),
|
||||
DummyNodeSocket("Folder", "Folder", "NodeSocketString"),
|
||||
DummyNodeSocket("Filename", "Filename", "NodeSocketString"),
|
||||
], node=self)
|
||||
|
||||
node = ActionNode()
|
||||
mock_operator = MagicMock()
|
||||
proxy = NodeErrorReportingProxy(mock_operator, node)
|
||||
|
||||
# Call error report
|
||||
proxy.report({"ERROR"}, "Directory does not exist")
|
||||
|
||||
mock_operator.report.assert_called_once()
|
||||
args = mock_operator.report.call_args[0]
|
||||
self.assertEqual(args[0], {"ERROR"})
|
||||
reported_msg = args[1]
|
||||
self.assertIn("Export Step", reported_msg)
|
||||
self.assertIn("Export Geometry", reported_msg)
|
||||
self.assertIn("Geometry, Folder, Filename", reported_msg)
|
||||
self.assertIn("Directory does not exist", reported_msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue