import unittest from unittest.mock import MagicMock from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Define dummy classes to avoid metaclass conflicts class DummyNodeSocket: pass class DummyNodeBase: pass class DummyObject: pass class DummyMesh: pass class DummyNodeSocketShader: pass class DummyNodeCustomGroup: pass class DummyNodeTree: pass class DummyOperator: pass class DummyMenu: pass class DummyPanel: pass class DummyPropertyGroup: pass mock_types = MagicMock() mock_types.NodeSocket = DummyNodeSocket mock_types.Node = DummyNodeBase mock_types.Object = DummyObject mock_types.Mesh = DummyMesh mock_types.NodeSocketShader = DummyNodeSocketShader mock_types.NodeCustomGroup = DummyNodeCustomGroup mock_types.NodeTree = DummyNodeTree mock_types.Operator = DummyOperator mock_types.Menu = DummyMenu mock_types.Panel = DummyPanel mock_types.PropertyGroup = DummyPropertyGroup mock_bpy = MagicMock() mock_bpy.context.scene = MagicMock() mock_bpy.context.scene.unit_settings.scale_length = 0.001 mock_bpy.types = mock_types sys.modules['bpy'] = mock_bpy sys.modules['bpy.types'] = mock_types sys.modules['bpy.props'] = MagicMock() sys.modules['bpy.utils'] = MagicMock() sys.modules['bpy.utils.previews'] = MagicMock() sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock() import time from NodeSlicer.infra.error_reporting import ( attach_node_failure_info, format_node_failure_message, get_node_input_names, NodeErrorReportingProxy, NodeErrorHandler, handle_node_errors, node_execution_scope, ) from NodeSlicer.infra.profiling import ( NodeProfiler, get_node_type, profiler_context, profile_node_execution, NodeProfileExecution, ) class DummyNode: def __init__(self, name="TestNode", label="Test Label", idname="SLICENODE_Test"): self.name = name self.bl_label = label self.bl_idname = idname self.inputs = [ MagicMock(name="Geometry"), MagicMock(name="Distance"), ] self.inputs[0].name = "Geometry" self.inputs[1].name = "Distance" class TestModularErrorReporting(unittest.TestCase): def test_handle_node_errors_as_context_manager(self): node = DummyNode(name="WorkerNode", label="Worker") with self.assertRaises(ValueError) as ctx: with handle_node_errors(node): raise ValueError("Operation failed") err_msg = str(ctx.exception) self.assertIn("WorkerNode", err_msg) self.assertIn("Geometry, Distance", err_msg) self.assertIn("Operation failed", err_msg) def test_handle_node_errors_as_decorator(self): node = DummyNode(name="DecoratedNode", label="Decorated") @handle_node_errors(node) def failing_func(): raise RuntimeError("Decorator crash") with self.assertRaises(RuntimeError) as ctx: failing_func() err_msg = str(ctx.exception) self.assertIn("DecoratedNode", err_msg) self.assertIn("Decorator crash", err_msg) def test_node_execution_scope_success(self): node = DummyNode(name="ScopedNode") op = MagicMock() with node_execution_scope(node, operator=op) as scope: self.assertIsInstance(scope.operator, NodeErrorReportingProxy) # Simulated operator report scope.operator.report({"ERROR"}, "Directory missing") op.report.assert_called_once() args = op.report.call_args[0] self.assertEqual(args[0], {"ERROR"}) self.assertIn("ScopedNode", args[1]) self.assertIn("Directory missing", args[1]) def test_node_execution_scope_catches_errors(self): node = DummyNode(name="FailingScopeNode") with self.assertRaises(TypeError) as ctx: with node_execution_scope(node): raise TypeError("Bad operand") err_msg = str(ctx.exception) self.assertIn("FailingScopeNode", err_msg) self.assertIn("Bad operand", err_msg) class TestModularProfiling(unittest.TestCase): def test_profile_node_execution_as_context_manager(self): node = DummyNode(label="ProfNode") with profiler_context() as prof: with profile_node_execution(node): time.sleep(0.005) self.assertIn("ProfNode", prof.stats) count, tot = prof.stats["ProfNode"] self.assertEqual(count, 1) self.assertGreaterEqual(tot, 0.004) def test_profile_node_execution_as_decorator(self): node = DummyNode(label="DecoratedProfNode") @profile_node_execution(node) def run_step(): time.sleep(0.005) with profiler_context() as prof: run_step() self.assertIn("DecoratedProfNode", prof.stats) count, tot = prof.stats["DecoratedProfNode"] self.assertEqual(count, 1) self.assertGreaterEqual(tot, 0.004) def test_profiling_inactive_is_noop(self): node = DummyNode(label="InactiveProfNode") # No profiler active; should be a zero-cost pass-through with profile_node_execution(node): pass if __name__ == '__main__': unittest.main()