import unittest from unittest.mock import MagicMock, patch import time from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) class DummyNodeSocket: def __init__(self, name=""): self.name = name self.identifier = name self.is_linked = False self.links = [] self.default = None self.node = None class DummyNode: def __init__(self, name: str = "Node", label: str = "", idname: str = ""): self.name = name if label: self.bl_label = label if idname: self.bl_idname = idname self.inputs = [] self.outputs = [] def as_pointer(self): return id(self) mock_types = MagicMock() mock_types.NodeSocket = DummyNodeSocket mock_types.Node = DummyNode mock_types.NodeCustomGroup = DummyNode mock_bpy = MagicMock() 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() from NodeSlicer.infra.blender.blender_slice_nodes_generic import ( NodeProfiler, get_node_type, profiler_context, profile_node_execution, show_in_text_editor, ) from NodeSlicer.core.nodeslicer.nodes.system import SLICENODE_Executor class TestNodeProfiler(unittest.TestCase): def test_get_node_type(self): n1 = DummyNode(label="Slice Shells", idname="SLICENODE_Slice") self.assertEqual(get_node_type(n1), "Slice Shells") n2 = DummyNode(idname="SLICENODE_Offset") self.assertEqual(get_node_type(n2), "SLICENODE_Offset") class GenericObject: pass self.assertEqual(get_node_type(GenericObject()), "GenericObject") def test_profile_single_node(self): profiler = NodeProfiler() node = DummyNode(label="TestNode") with profiler.profile_node(node): time.sleep(0.01) self.assertIn("TestNode", profiler.stats) count, tot_time = profiler.stats["TestNode"] self.assertEqual(count, 1) self.assertGreaterEqual(tot_time, 0.008) def test_profile_nested_nodes_self_time(self): profiler = NodeProfiler() parent = DummyNode(label="ParentNode") child = DummyNode(label="ChildNode") with profiler.profile_node(parent): time.sleep(0.01) with profiler.profile_node(child): time.sleep(0.02) time.sleep(0.01) self.assertIn("ParentNode", profiler.stats) self.assertIn("ChildNode", profiler.stats) parent_count, parent_tot_time = profiler.stats["ParentNode"] child_count, child_tot_time = profiler.stats["ChildNode"] self.assertEqual(parent_count, 1) self.assertEqual(child_count, 1) # Child should have ~0.02s self.assertGreaterEqual(child_tot_time, 0.018) # Parent self-time should be ~0.02s (0.01s before + 0.01s after), NOT 0.04s self.assertGreaterEqual(parent_tot_time, 0.018) self.assertLess(parent_tot_time, 0.035) def test_profile_multiple_executions(self): profiler = NodeProfiler() node1 = DummyNode(label="Offset") node2 = DummyNode(label="Offset") with profiler.profile_node(node1): time.sleep(0.005) with profiler.profile_node(node2): time.sleep(0.005) self.assertIn("Offset", profiler.stats) count, tot_time = profiler.stats["Offset"] self.assertEqual(count, 2) self.assertGreaterEqual(tot_time, 0.008) def test_format_table(self): profiler = NodeProfiler() profiler.stats["Slice Shells"] = [1.0, 0.045] profiler.stats["Buffer Shells"] = [2.0, 0.024] table = profiler.format_table() self.assertIn("node_type", table) self.assertIn("execution_count", table) self.assertIn("avg_execution_time", table) self.assertIn("tot_execution_time", table) self.assertIn("Slice Shells", table) self.assertIn("Buffer Shells", table) self.assertIn("Total", table) def test_format_empty_table(self): profiler = NodeProfiler() table = profiler.format_table() self.assertIn("node_type", table) self.assertIn("(No nodes executed)", table) def test_profiler_context_and_profile_node_execution(self): node = DummyNode(label="Slice") with profiler_context() as profiler: with profile_node_execution(node): time.sleep(0.005) self.assertIn("Slice", profiler.stats) # Outside context, profile_node_execution should be a no-op with profile_node_execution(node): pass class TestShowInTextEditor(unittest.TestCase): def test_show_in_text_editor(self): with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.bpy") as mock_bpy_module: mock_text_block = MagicMock() mock_bpy_module.data.texts.get.return_value = None mock_bpy_module.data.texts.new.return_value = mock_text_block mock_space = MagicMock() mock_space.type = 'TEXT_EDITOR' mock_area = MagicMock() mock_area.type = 'TEXT_EDITOR' mock_area.spaces = [mock_space] mock_window = MagicMock() mock_window.screen.areas = [mock_area] mock_bpy_module.context.window_manager.windows = [mock_window] result = show_in_text_editor("Performance", "Table content") self.assertEqual(result, mock_text_block) mock_bpy_module.data.texts.new.assert_called_with(name="Performance") mock_text_block.from_string.assert_called_with("Table content") self.assertEqual(mock_space.text, mock_text_block) class TestExecutorPerformance(unittest.TestCase): @patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.show_in_text_editor") @patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input") def test_executor_with_log_enabled(self, mock_resolve_input, mock_show): executor = SLICENODE_Executor() executor.log = True upstream = MagicMock() upstream.bl_label = "Export Gcode" upstream.evaluate.return_value = 'ok' mock_resolve_input.return_value = [upstream] dummy_socket = MagicMock() executor.inputs = [dummy_socket] res = executor.evaluate() self.assertEqual(res, 'ok') upstream.evaluate.assert_called_once() mock_show.assert_called_once() args, _ = mock_show.call_args self.assertEqual(args[0], "Performance") self.assertIn("Export Gcode", args[1]) self.assertIn("node_type", args[1]) @patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.show_in_text_editor") @patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input") def test_executor_with_log_disabled(self, mock_resolve_input, mock_show): executor = SLICENODE_Executor() executor.log = False upstream = MagicMock() upstream.bl_label = "Export Gcode" upstream.evaluate.return_value = 'ok' mock_resolve_input.return_value = [upstream] dummy_socket = MagicMock() executor.inputs = [dummy_socket] res = executor.evaluate() self.assertEqual(res, 'ok') upstream.evaluate.assert_called_once() mock_show.assert_not_called() if __name__ == "__main__": unittest.main()