167 lines
5.2 KiB
Python
167 lines
5.2 KiB
Python
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()
|