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 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() 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()