357 lines
13 KiB
Python
357 lines
13 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))
|
|
|
|
class DummyObject:
|
|
def __init__(self, name="TestObj", obj_type='MESH', parent=None, children=None):
|
|
self.name = name
|
|
self.type = obj_type
|
|
self.parent = parent
|
|
self.children = children or []
|
|
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
class DummyCollection:
|
|
def __init__(self, name="Col", objects=None):
|
|
self.name = name
|
|
self.objects = objects or []
|
|
self.all_objects = self.objects
|
|
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
if 'bpy' not in sys.modules:
|
|
class DummyBase: pass
|
|
mock_types = MagicMock()
|
|
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
|
|
mock_types.Node = type("Node", (DummyBase,), {})
|
|
mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {})
|
|
mock_types.NodeTree = type("NodeTree", (DummyBase,), {})
|
|
mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {})
|
|
mock_types.Operator = type("Operator", (DummyBase,), {})
|
|
mock_types.Panel = type("Panel", (DummyBase,), {})
|
|
mock_types.Menu = type("Menu", (DummyBase,), {})
|
|
mock_types.Object = DummyObject
|
|
mock_types.Collection = DummyCollection
|
|
|
|
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:
|
|
if 'bpy.props' not in sys.modules:
|
|
sys.modules['bpy.props'] = MagicMock()
|
|
|
|
class DummyNodeSocket:
|
|
def __init__(self, name="", bl_idname="NodeSocketObject", node=None, value=None):
|
|
self.name = name
|
|
self.identifier = name
|
|
self.bl_idname = bl_idname
|
|
self.node = node
|
|
self.is_linked = False
|
|
self.links = []
|
|
self.value = value
|
|
self.default_value = value
|
|
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
class DummyLink:
|
|
def __init__(self, from_socket, to_socket):
|
|
self.from_socket = from_socket
|
|
self.from_node = getattr(from_socket, "node", None)
|
|
self.to_socket = to_socket
|
|
self.to_node = getattr(to_socket, "node", None)
|
|
|
|
class DummyNode:
|
|
def __init__(self, name="Node", bl_idname="DummyNode", id_data=None):
|
|
self.name = name
|
|
self.bl_idname = bl_idname
|
|
self.id_data = id_data
|
|
self.inputs = []
|
|
self.outputs = []
|
|
self.bl_rna = MagicMock()
|
|
self.bl_rna.properties = []
|
|
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
class DummyGroupNode(DummyNode):
|
|
def __init__(self, name="GroupNode", slicer_tree=None, node_tree=None, id_data=None):
|
|
super().__init__(name, "SLICENODE_Group", id_data)
|
|
self.slicer_tree = slicer_tree
|
|
self.node_tree = node_tree
|
|
|
|
class DummyExecutorNode(DummyNode):
|
|
def __init__(self, name="Executor", auto_refresh=True, id_data=None):
|
|
super().__init__(name, "SLICENODE_Executor", id_data)
|
|
self.auto_refresh = auto_refresh
|
|
self.evaluated = False
|
|
self.evaluate_count = 0
|
|
|
|
def evaluate(self, operator=None):
|
|
self.evaluated = True
|
|
self.evaluate_count += 1
|
|
return "ok"
|
|
|
|
class DummyNodeTree:
|
|
def __init__(self, name="Tree"):
|
|
self.name = name
|
|
self.bl_idname = "SLICETREE_NodeTree"
|
|
self.nodes = []
|
|
|
|
def as_pointer(self):
|
|
return id(self)
|
|
|
|
|
|
# Import tree functions
|
|
from NodeSlicer.core.nodeslicer.tree import (
|
|
get_referenced_data,
|
|
get_referenced_objects,
|
|
get_referenced_trees,
|
|
run_auto_refresh,
|
|
request_auto_refresh,
|
|
SLICETREE_NodeTree
|
|
)
|
|
import NodeSlicer.core.nodeslicer.tree as tree_mod
|
|
import bpy
|
|
|
|
|
|
class TestAutoRefresh(unittest.TestCase):
|
|
def setUp(self):
|
|
tree_mod._is_updating = False
|
|
tree_mod._tree_changed = False
|
|
tree_mod._changed_trees.clear()
|
|
tree_mod._timer_scheduled = False
|
|
|
|
def tearDown(self):
|
|
tree_mod._is_updating = False
|
|
tree_mod._tree_changed = False
|
|
tree_mod._changed_trees.clear()
|
|
tree_mod._timer_scheduled = False
|
|
|
|
def test_referenced_objects_direct_and_upstream(self):
|
|
obj1 = DummyObject("Obj1")
|
|
obj2 = DummyObject("Obj2")
|
|
|
|
node_a = DummyNode("NodeA")
|
|
out_sock_a = DummyNodeSocket("Out", "NodeSocketObject", node=node_a, value=obj1)
|
|
node_a.outputs.append(out_sock_a)
|
|
|
|
node_b = DummyNode("NodeB")
|
|
in_sock_b = DummyNodeSocket("In", "NodeSocketObject", node=node_b)
|
|
node_b.inputs.append(in_sock_b)
|
|
|
|
# Link A to B
|
|
link = DummyLink(out_sock_a, in_sock_b)
|
|
in_sock_b.is_linked = True
|
|
in_sock_b.links.append(link)
|
|
|
|
# Direct unlinked socket on B with obj2
|
|
in_sock_b2 = DummyNodeSocket("In2", "SLICESOCK_Object", node=node_b, value=obj2)
|
|
node_b.inputs.append(in_sock_b2)
|
|
|
|
ref_objs = get_referenced_objects(node_b)
|
|
self.assertIn(obj1, ref_objs)
|
|
self.assertIn(obj2, ref_objs)
|
|
|
|
def test_referenced_objects_with_hierarchy(self):
|
|
parent_empty = DummyObject("ParentRig")
|
|
child_mesh = DummyObject("ChildDetail")
|
|
main_obj = DummyObject("MainMesh", parent=parent_empty, children=[child_mesh])
|
|
parent_empty.children = [main_obj]
|
|
child_mesh.parent = main_obj
|
|
|
|
node = DummyNode("MeshNode")
|
|
sock = DummyNodeSocket("Obj", "NodeSocketObject", node=node, value=main_obj)
|
|
node.inputs.append(sock)
|
|
|
|
ref_objs = get_referenced_objects(node)
|
|
self.assertIn(main_obj, ref_objs)
|
|
self.assertIn(parent_empty, ref_objs)
|
|
self.assertIn(child_mesh, ref_objs)
|
|
|
|
def test_referenced_objects_in_node_groups(self):
|
|
obj_inner = DummyObject("InnerSuzanne")
|
|
obj_outer = DummyObject("OuterCube")
|
|
|
|
# Group tree
|
|
group_tree = DummyNodeTree("GroupTree")
|
|
inner_node = DummyNode("InnerNode")
|
|
inner_sock = DummyNodeSocket("Obj", "NodeSocketObject", node=inner_node, value=obj_inner)
|
|
inner_node.inputs.append(inner_sock)
|
|
group_tree.nodes.append(inner_node)
|
|
|
|
# Group Node instance
|
|
group_node = DummyGroupNode("GroupInstance", slicer_tree=group_tree)
|
|
outer_sock = DummyNodeSocket("OuterIn", "NodeSocketObject", node=group_node, value=obj_outer)
|
|
group_node.inputs.append(outer_sock)
|
|
|
|
# Main node connected to group node
|
|
main_node = DummyNode("MainNode")
|
|
main_in = DummyNodeSocket("In", "SLICESOCK_Mesh", node=main_node)
|
|
group_out = DummyNodeSocket("Out", "SLICESOCK_Mesh", node=group_node)
|
|
group_node.outputs.append(group_out)
|
|
|
|
link = DummyLink(group_out, main_in)
|
|
main_in.is_linked = True
|
|
main_in.links.append(link)
|
|
main_node.inputs.append(main_in)
|
|
|
|
ref_objs = get_referenced_objects(main_node)
|
|
self.assertIn(obj_inner, ref_objs)
|
|
self.assertIn(obj_outer, ref_objs)
|
|
|
|
def test_referenced_objects_nested_groups(self):
|
|
obj_deep = DummyObject("DeepObj")
|
|
|
|
# Inner-most group
|
|
deep_tree = DummyNodeTree("DeepTree")
|
|
deep_node = DummyNode("DeepNode")
|
|
deep_node.inputs.append(DummyNodeSocket("DeepSock", value=obj_deep))
|
|
deep_tree.nodes.append(deep_node)
|
|
|
|
# Middle group containing inner group
|
|
mid_tree = DummyNodeTree("MidTree")
|
|
mid_group = DummyGroupNode("MidGroupNode", node_tree=deep_tree)
|
|
mid_tree.nodes.append(mid_group)
|
|
|
|
# Top group node
|
|
top_group = DummyGroupNode("TopGroupNode", slicer_tree=mid_tree)
|
|
|
|
ref_objs = get_referenced_objects(top_group)
|
|
self.assertIn(obj_deep, ref_objs)
|
|
|
|
def test_get_referenced_trees(self):
|
|
# Two group trees: GroupA and GroupB
|
|
tree_b = DummyNodeTree("TreeB")
|
|
node_b = DummyGroupNode("NodeB", slicer_tree=tree_b)
|
|
|
|
tree_a = DummyNodeTree("TreeA")
|
|
node_a = DummyGroupNode("NodeA", slicer_tree=tree_a)
|
|
sock_a = DummyNodeSocket("A_Out", node=node_a)
|
|
node_a.outputs.append(sock_a)
|
|
|
|
executor = DummyExecutorNode("Executor")
|
|
exec_in = DummyNodeSocket("In", node=executor)
|
|
exec_in.is_linked = True
|
|
exec_in.links.append(DummyLink(sock_a, exec_in))
|
|
executor.inputs.append(exec_in)
|
|
|
|
trees = get_referenced_trees(executor)
|
|
self.assertIn(tree_a, trees)
|
|
self.assertNotIn(tree_b, trees)
|
|
|
|
def test_collection_objects_referenced(self):
|
|
obj_c1 = DummyObject("C1")
|
|
obj_c2 = DummyObject("C2")
|
|
col = DummyCollection("TestCollection", [obj_c1, obj_c2])
|
|
|
|
col_node = DummyNode("ColNode", bl_idname="SLICENODE_CollectionObjects")
|
|
col_node.collection = col
|
|
|
|
# Output sockets named C1, C2
|
|
out_sock1 = DummyNodeSocket("C1", node=col_node)
|
|
out_sock2 = DummyNodeSocket("C2", node=col_node)
|
|
col_node.outputs.extend([out_sock1, out_sock2])
|
|
|
|
ref_objs, ref_cols = get_referenced_data(col_node)
|
|
self.assertIn(col, ref_cols)
|
|
self.assertIn(obj_c1, ref_objs)
|
|
self.assertIn(obj_c2, ref_objs)
|
|
|
|
def test_run_auto_refresh_on_object_change(self):
|
|
main_tree = DummyNodeTree("MainTree")
|
|
executor = DummyExecutorNode("Executor", auto_refresh=True, id_data=main_tree)
|
|
target_obj = DummyObject("TargetCube")
|
|
|
|
in_sock = DummyNodeSocket("MeshIn", node=executor, value=target_obj)
|
|
executor.inputs.append(in_sock)
|
|
main_tree.nodes.append(executor)
|
|
|
|
# Mock bpy.data.node_groups
|
|
old_groups = getattr(tree_mod.bpy.data, "node_groups", None)
|
|
tree_mod.bpy.data.node_groups = [main_tree]
|
|
bpy.data.node_groups = [main_tree]
|
|
try:
|
|
# 1. Unrelated object update -> should NOT evaluate
|
|
run_auto_refresh(updated_object_names={"UnrelatedObject"})
|
|
self.assertFalse(executor.evaluated)
|
|
|
|
# 2. Related object update -> SHOULD evaluate
|
|
run_auto_refresh(updated_object_names={"TargetCube"})
|
|
self.assertTrue(executor.evaluated)
|
|
self.assertEqual(executor.evaluate_count, 1)
|
|
|
|
# 3. auto_refresh = False -> should NOT evaluate
|
|
executor.evaluated = False
|
|
executor.auto_refresh = False
|
|
run_auto_refresh(updated_object_names={"TargetCube"})
|
|
self.assertFalse(executor.evaluated)
|
|
finally:
|
|
tree_mod.bpy.data.node_groups = old_groups
|
|
bpy.data.node_groups = old_groups
|
|
|
|
def test_run_auto_refresh_on_group_tree_change(self):
|
|
group_tree = DummyNodeTree("GroupTree")
|
|
main_tree = DummyNodeTree("MainTree")
|
|
unrelated_tree = DummyNodeTree("UnrelatedTree")
|
|
|
|
group_node = DummyGroupNode("GroupInstance", slicer_tree=group_tree, id_data=main_tree)
|
|
g_out = DummyNodeSocket("Out", node=group_node)
|
|
group_node.outputs.append(g_out)
|
|
|
|
executor = DummyExecutorNode("Executor", auto_refresh=True, id_data=main_tree)
|
|
e_in = DummyNodeSocket("In", node=executor)
|
|
e_in.is_linked = True
|
|
e_in.links.append(DummyLink(g_out, e_in))
|
|
executor.inputs.append(e_in)
|
|
main_tree.nodes.extend([group_node, executor])
|
|
|
|
old_groups = getattr(tree_mod.bpy.data, "node_groups", None)
|
|
tree_mod.bpy.data.node_groups = [main_tree, group_tree, unrelated_tree]
|
|
bpy.data.node_groups = [main_tree, group_tree, unrelated_tree]
|
|
try:
|
|
# 1. Change to unrelated tree -> should NOT evaluate
|
|
run_auto_refresh(changed_trees={unrelated_tree}, force_refresh=True)
|
|
self.assertFalse(executor.evaluated)
|
|
|
|
# 2. Change to group_tree -> SHOULD evaluate
|
|
run_auto_refresh(changed_trees={group_tree}, force_refresh=True)
|
|
self.assertTrue(executor.evaluated)
|
|
self.assertEqual(executor.evaluate_count, 1)
|
|
|
|
# 3. Change to main_tree -> SHOULD evaluate
|
|
executor.evaluated = False
|
|
run_auto_refresh(changed_trees={main_tree}, force_refresh=True)
|
|
self.assertTrue(executor.evaluated)
|
|
self.assertEqual(executor.evaluate_count, 2)
|
|
finally:
|
|
tree_mod.bpy.data.node_groups = old_groups
|
|
bpy.data.node_groups = old_groups
|
|
|
|
def test_request_auto_refresh_guard(self):
|
|
# When _is_updating is True, request_auto_refresh must do nothing
|
|
tree_mod._is_updating = True
|
|
tree_mod._tree_changed = False
|
|
tree_mod._timer_scheduled = True
|
|
request_auto_refresh()
|
|
self.assertFalse(tree_mod._tree_changed)
|
|
|
|
tree_mod._is_updating = False
|
|
request_auto_refresh()
|
|
self.assertTrue(tree_mod._tree_changed)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|