223 lines
7.5 KiB
Python
223 lines
7.5 KiB
Python
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()
|