verbose errors

This commit is contained in:
Nicolas Predella 2026-10-14 13:54:04 +03:00
parent 2f09772928
commit 4392eb393f
6 changed files with 221 additions and 15 deletions

View file

@ -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'

View file

@ -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()

View file

@ -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:

View file

@ -232,6 +232,7 @@ def eval_multivalue(
else:
vec_flags = is_vector
try:
return eval_element_wise_multivalue(
op_func,
main_data,
@ -240,6 +241,11 @@ def eval_multivalue(
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.

View file

@ -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}")

View 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()