nodeslicer/tests/test_node_groups.py

146 lines
4.4 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
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 (
_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()