nodeslicer/tests/test_auto_refresh.py
2026-10-14 12:02:15 +03:00

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