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