154 lines
4.8 KiB
Python
154 lines
4.8 KiB
Python
import unittest
|
|
from unittest.mock import MagicMock
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.append(str(PROJECT_ROOT.parent))
|
|
|
|
# Dummy classes for bpy mocks
|
|
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="Node"):
|
|
self.name = name
|
|
self.inputs = []
|
|
self.outputs = []
|
|
self.bl_idname = "DummyNode"
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
class DummyNodeGroupInput(DummyNode):
|
|
def __init__(self):
|
|
super().__init__("NodeGroupInput")
|
|
self.bl_idname = "NodeGroupInput"
|
|
|
|
class DummyNodeGroupOutput(DummyNode):
|
|
def __init__(self):
|
|
super().__init__("NodeGroupOutput")
|
|
self.bl_idname = "NodeGroupOutput"
|
|
|
|
class DummyGroupNode(DummyNode):
|
|
def __init__(self, slicer_tree=None):
|
|
super().__init__("GroupNode")
|
|
self.bl_idname = "SLICENODE_Group"
|
|
self.slicer_tree = slicer_tree
|
|
|
|
class DummyLink:
|
|
def __init__(self, from_socket, to_socket):
|
|
self.from_socket = from_socket
|
|
self.to_socket = to_socket
|
|
|
|
class DummyNodeGroup(DummyNode): pass
|
|
|
|
mock_types = MagicMock()
|
|
mock_types.NodeSocket = DummyNodeSocket
|
|
mock_types.Node = DummyNode
|
|
mock_types.NodeGroup = DummyNodeGroup
|
|
mock_types.NodeGroupInput = DummyNodeGroupInput
|
|
mock_types.NodeGroupOutput = DummyNodeGroupOutput
|
|
mock_types.NodeCustomGroup = DummyGroupNode
|
|
|
|
if 'bpy' not in sys.modules:
|
|
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()
|
|
else:
|
|
mock_bpy = sys.modules['bpy']
|
|
mock_bpy.types.NodeSocket = DummyNodeSocket
|
|
mock_bpy.types.Node = DummyNode
|
|
mock_bpy.types.NodeGroup = DummyNodeGroup
|
|
mock_bpy.types.NodeGroupInput = DummyNodeGroupInput
|
|
mock_bpy.types.NodeGroupOutput = DummyNodeGroupOutput
|
|
mock_bpy.types.NodeCustomGroup = DummyGroupNode
|
|
|
|
from NodeSlicer.infra.blender.blender_slice_nodes_generic import (
|
|
_resolve_group_output,
|
|
_resolve_group_input,
|
|
resolve_input,
|
|
_resolve_output,
|
|
execution_cache,
|
|
SliceNode
|
|
)
|
|
|
|
class DummySliceNode(SliceNode, DummyNode):
|
|
def __init__(self, name="SliceNode"):
|
|
DummyNode.__init__(self, name)
|
|
self.bl_idname = "DummySliceNode"
|
|
self.inputs = []
|
|
self.outputs = []
|
|
|
|
def evaluate_output(self, socket_name: str, **kwargs):
|
|
inp = self.inputs[0] if self.inputs else None
|
|
if inp and inp.is_linked:
|
|
return resolve_input(inp, cache=kwargs.get("cache"), visited=kwargs.get("visited"))
|
|
return "val"
|
|
|
|
|
|
class TestNodeGroupsRecursion(unittest.TestCase):
|
|
def test_nested_group_cycle_prevention(self):
|
|
# Build a node group containing a SliceNode and NodeGroupInput/Output
|
|
tree = MagicMock()
|
|
group_input = DummyNodeGroupInput()
|
|
in_sock = DummyNodeSocket("In")
|
|
in_sock.node = group_input
|
|
group_input.outputs = [in_sock]
|
|
|
|
slice_node = DummySliceNode("InnerNode")
|
|
s_in = DummyNodeSocket("Mesh")
|
|
s_in.node = slice_node
|
|
s_out = DummyNodeSocket("Mesh")
|
|
s_out.node = slice_node
|
|
slice_node.inputs = [s_in]
|
|
slice_node.outputs = [s_out]
|
|
|
|
# Link NodeGroupInput to slice_node input
|
|
s_in.is_linked = True
|
|
s_in.links = [DummyLink(in_sock, s_in)]
|
|
|
|
group_output = DummyNodeGroupOutput()
|
|
out_sock = DummyNodeSocket("Out")
|
|
out_sock.node = group_output
|
|
group_output.inputs = [out_sock]
|
|
|
|
# Link slice_node output to NodeGroupOutput input
|
|
out_sock.is_linked = True
|
|
out_sock.links = [DummyLink(s_out, out_sock)]
|
|
|
|
tree.nodes = [group_input, slice_node, group_output]
|
|
|
|
# Group Node instance outside
|
|
group_node = DummyGroupNode(slicer_tree=tree)
|
|
g_in = DummyNodeSocket("In")
|
|
g_in.node = group_node
|
|
g_out = DummyNodeSocket("Out")
|
|
g_out.node = group_node
|
|
group_node.inputs = [g_in]
|
|
group_node.outputs = [g_out]
|
|
|
|
# Outer cycle: Group node's input is linked to Group node's output!
|
|
g_in.is_linked = True
|
|
g_in.links = [DummyLink(g_out, g_in)]
|
|
|
|
# Evaluating group output should NOT raise RecursionError due to cycle guard
|
|
with execution_cache():
|
|
result = _resolve_output(g_out)
|
|
|
|
# Result should be None (default) because cycle was broken cleanly
|
|
self.assertIsNone(result)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|