verbose errors
This commit is contained in:
parent
2f09772928
commit
4392eb393f
6 changed files with 221 additions and 15 deletions
|
|
@ -215,15 +215,19 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
||||||
|
|
||||||
if operator:
|
if operator:
|
||||||
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
|
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with execution_cache():
|
with execution_cache():
|
||||||
with profile_node_execution(upstream_node):
|
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 exec_res != 'ok' and exec_res is not None:
|
||||||
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
|
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
|
||||||
except Exception as e:
|
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 'error'
|
||||||
|
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
|
||||||
|
|
@ -750,11 +750,15 @@ class SLICENODE_OT_Execute(Operator):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with execution_cache():
|
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':
|
if res == 'ok':
|
||||||
self.report({"INFO"}, f"Execution Successful")
|
self.report({"INFO"}, "Execution Successful")
|
||||||
except Exception as e:
|
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
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -646,6 +646,91 @@ def _format_node_description(node: Any) -> str:
|
||||||
return str(node)
|
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]:
|
def _extract_caller_context() -> tuple[Any, str | None]:
|
||||||
try:
|
try:
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -888,6 +973,7 @@ def _resolve_output(
|
||||||
eval_exc = e
|
eval_exc = e
|
||||||
import traceback
|
import traceback
|
||||||
eval_tb = traceback.format_exc()
|
eval_tb = traceback.format_exc()
|
||||||
|
attach_node_failure_info(socket.node, e)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -232,14 +232,20 @@ def eval_multivalue(
|
||||||
else:
|
else:
|
||||||
vec_flags = is_vector
|
vec_flags = is_vector
|
||||||
|
|
||||||
return eval_element_wise_multivalue(
|
try:
|
||||||
op_func,
|
return eval_element_wise_multivalue(
|
||||||
main_data,
|
op_func,
|
||||||
*sec_vals,
|
main_data,
|
||||||
is_vector=vec_flags,
|
*sec_vals,
|
||||||
input_names=secondary_sockets,
|
is_vector=vec_flags,
|
||||||
output_index=output_index,
|
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:
|
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.
|
"""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("")
|
||||||
lines.append("!" * 80)
|
lines.append("!" * 80)
|
||||||
lines.append(f" EXCEPTION: {dump.get('exception')}")
|
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)
|
lines.append("!" * 80)
|
||||||
if dump.get("traceback"):
|
if dump.get("traceback"):
|
||||||
lines.append(dump["traceback"])
|
lines.append(dump["traceback"])
|
||||||
|
|
@ -518,7 +521,9 @@ def dump_node_execution(
|
||||||
|
|
||||||
if log_console:
|
if log_console:
|
||||||
if exception:
|
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}")
|
print(f"[NodeSlicer DEBUG] Review dump at: {last_txt_file}")
|
||||||
else:
|
else:
|
||||||
print(f"[NodeSlicer DEBUG] Dumped execution of '{node_name}' ({node_type}) -> {last_txt_file}")
|
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)
|
err_msg = str(ctx.exception)
|
||||||
self.assertIn("non-existent", err_msg)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue