import sys import unittest from unittest.mock import MagicMock from pathlib import Path import numpy as np PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Ensure bpy mocks class DummyObject: def __init__(self, name="TestObj"): self.name = name class DummyMesh: pass 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.Object = DummyObject mock_types.Mesh = DummyMesh mock_bpy = MagicMock() mock_bpy.context.scene = MagicMock() mock_bpy.context.scene.unit_settings.scale_length = 0.001 mock_bpy.types = mock_types mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/") 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.list_utils import ( is_list_data, is_input_a_list, is_vector_value, prepare_indexed_inputs, eval_element_wise_multivalue, flatten_list, eval_element_wise, set_socket_shape, is_socket_linked_to_list, is_output_socket_a_list, sync_node_list_sockets, group_input_can_accept_list, is_group_output_a_list, sync_group_node_socket_shapes, ) from NodeSlicer.core.slice import SlicePaths from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection, SlicingGroup class TestListUtils(unittest.TestCase): def test_is_list_data(self): self.assertFalse(is_list_data(None)) self.assertFalse(is_list_data("a string")) self.assertFalse(is_list_data(b"bytes")) self.assertFalse(is_list_data(123)) self.assertFalse(is_list_data(12.34)) # SlicePaths subclasses list, but represents an atomic toolpath set, NOT a list of toolpaths sp = SlicePaths() self.assertFalse(is_list_data(sp)) # Normal lists and tuples self.assertTrue(is_list_data([])) self.assertTrue(is_list_data([1, 2, 3])) self.assertTrue(is_list_data((1, 2))) self.assertTrue(is_list_data([sp, sp])) def test_flatten_list(self): sp1 = SlicePaths() sp2 = SlicePaths() nested = [sp1, [sp2, None], [["gcode1"], "gcode2"]] flattened = flatten_list(nested) self.assertEqual(flattened, [sp1, sp2, "gcode1", "gcode2"]) def test_eval_element_wise(self): def double(x): return x * 2 self.assertEqual(eval_element_wise(double, 5), 10) self.assertEqual(eval_element_wise(double, [1, 2, 3]), [2, 4, 6]) class DummySocket: def __init__(self, name="Socket", bl_idname="NodeSocketFloat", display_shape="CIRCLE"): self.name = name self.identifier = name self.bl_idname = bl_idname self.display_shape = display_shape self.is_linked = False self.links = [] self.node = None self.value = None class DummyLink: def __init__(self, from_socket, to_socket): self.from_socket = from_socket self.to_socket = to_socket class DummySocketCollection(list): def get(self, name, default=None): for s in self: if getattr(s, "name", "") == name: return s return default from NodeSlicer.infra.blender.blender_slice_nodes_generic import SliceNode class DummyNode(SliceNode): def __init__(self, bl_idname="DummyNode", name="DummyNode"): self.bl_idname = bl_idname self.name = name self.inputs = DummySocketCollection() self.outputs = DummySocketCollection() self.is_list_output = False def as_pointer(self): return id(self) def add_in(self, sock): sock.node = self self.inputs.append(sock) def add_out(self, sock): sock.node = self self.outputs.append(sock) class TestSocketShapesAndLinks(unittest.TestCase): def test_socket_linked_to_list(self): upstream_node = DummyNode("SLICENODE_MeshesToList") upstream_node.is_list_output = True out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST") upstream_node.add_out(out_sock) downstream_node = DummyNode("SLICENODE_SliceShells") in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE") downstream_node.add_in(in_sock) # Before linking self.assertFalse(is_socket_linked_to_list(in_sock)) self.assertTrue(is_output_socket_a_list(out_sock)) # Link them link = DummyLink(out_sock, in_sock) in_sock.is_linked = True in_sock.links = [link] self.assertTrue(is_socket_linked_to_list(in_sock)) def test_sync_node_list_sockets(self): upstream_node = DummyNode("SLICENODE_MeshesToList") out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST") upstream_node.add_out(out_sock) node = DummyNode("SLICENODE_SliceShells") in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE") out_path = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE") node.add_in(in_sock) node.add_out(out_path) link = DummyLink(out_sock, in_sock) in_sock.is_linked = True in_sock.links = [link] mapping = [("Mesh", "Paths")] sync_node_list_sockets(node, mapping) self.assertEqual(in_sock.display_shape, "LIST") self.assertEqual(out_path.display_shape, "LIST") class TestNodeEvaluationsWithLists(unittest.TestCase): def test_path_operations_with_first_input_list(self): from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations node = SLICENODE_PathOperations() node.operation = 'EXTEND' p1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)]) p2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)]) q = SlicePaths([np.array([[10, 10, 0], [11, 11, 0]], dtype=np.float64)]) # Mock inputs inp0 = DummySocket("Paths", "SLICESOCK_Paths") inp0.is_linked = True inp1 = DummySocket("Paths", "SLICESOCK_Paths") inp1.is_linked = True node.inputs = [inp0, inp1] # resolve input mocking def mock_resolve_input(sock, **kwargs): if sock is inp0: return [p1, p2] # List input elif sock is inp1: return q return None import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen original_resolve = b_gen.resolve_input b_gen.resolve_input = mock_resolve_input try: res = node.evaluate_output("Paths") self.assertIsInstance(res, list) self.assertEqual(len(res), 2) # res[0] is EXTEND of p1 and q self.assertEqual(len(res[0]), 2) # res[1] is EXTEND of p2 and q self.assertEqual(len(res[1]), 2) finally: b_gen.resolve_input = original_resolve def test_join_gcode_no_lists(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode node = SLICENODE_JoinGcode() node.resolve_multi_input = lambda label: ["G28", "G90", "M104 S200"] res = node.evaluate_output("Gcode") self.assertEqual(res, "G28\nG90\nM104 S200") def test_join_gcode_one_list(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode node = SLICENODE_JoinGcode() node.resolve_multi_input = lambda label: ["START", ["LAYER1", "LAYER2", "LAYER3"], "END"] res = node.evaluate_output("Gcode") self.assertEqual(res, [ "START\nLAYER1\nEND", "START\nLAYER2\nEND", "START\nLAYER3\nEND", ]) def test_join_gcode_multiple_lists_zipped(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode node = SLICENODE_JoinGcode() node.resolve_multi_input = lambda label: [ "INIT", ["A1", "A2", "A3"], "MID", ["B1", "B2", "B3"], "FINAL", ] res = node.evaluate_output("Gcode") self.assertEqual(res, [ "INIT\nA1\nMID\nB1\nFINAL", "INIT\nA2\nMID\nB2\nFINAL", "INIT\nA3\nMID\nB3\nFINAL", ]) def test_join_gcode_mismatched_lengths_raises_error(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode node = SLICENODE_JoinGcode() node.resolve_multi_input = lambda label: [["A1", "A2"], ["B1", "B2", "B3"]] with self.assertRaises(ValueError): node.evaluate_output("Gcode") def test_join_gcode_socket_shape_updates(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode node = SLICENODE_JoinGcode() in0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE") out0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE") node.inputs = DummySocketCollection([in0]) node.outputs = DummySocketCollection([out0]) # Initially without list link node.update() self.assertEqual(out0.display_shape, "CIRCLE") # Linked to list upstream = DummyNode("SLICENODE_GcodesToList") upstream.is_list_output = True up_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST") upstream.add_out(up_out) link = DummyLink(up_out, in0) in0.is_linked = True in0.links = [link] node.update() self.assertEqual(out0.display_shape, "LIST") def test_gcode_regex_replace_list(self): from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodeRegexReplace node = SLICENODE_GcodeRegexReplace() node.resolve = lambda field: { "Gcode": ["G1 X10 Y10", "G1 X20 Y20"], "Find": "G1", "Replace": "G0" }.get(field) res = node.evaluate_output("Gcode") self.assertEqual(res, ["G0 X10 Y10", "G0 X20 Y20"]) def test_list_joiners_evaluation(self): from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_ObjectsToList from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_MeshesToList from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathsToList from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodesToList # ObjectsToList obj1 = DummyObject("Obj1") obj2 = DummyObject("Obj2") o_node = SLICENODE_ObjectsToList() o_node.resolve_multi_input = lambda label: [[obj1], obj2] self.assertEqual(o_node.evaluate_output("Objects"), [obj1, obj2]) # MeshesToList sc1 = SlicingCollection([], "M1") sc2 = SlicingCollection([], "M2") m_node = SLICENODE_MeshesToList() m_node.resolve_multi_input = lambda label: [sc1, [sc2]] self.assertEqual(m_node.evaluate_output("Meshes"), [sc1, sc2]) # PathsToList sp1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)]) sp2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)]) p_node = SLICENODE_PathsToList() p_node.resolve_multi_input = lambda label: [sp1, [sp2]] self.assertEqual(p_node.evaluate_output("Paths"), [sp1, sp2]) # GcodesToList g_node = SLICENODE_GcodesToList() g_node.resolve_multi_input = lambda label: [["G1 X1"], "G1 X2"] self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2"]) def test_x_to_list_flattens_nested_and_incoming_lists(self): from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_ObjectsToList from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_MeshesToList from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathsToList from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodesToList # ObjectsToList with nested lists and single elements obj1, obj2, obj3, obj4 = DummyObject("O1"), DummyObject("O2"), DummyObject("O3"), DummyObject("O4") o_node = SLICENODE_ObjectsToList() o_node.resolve_multi_input = lambda label: [[obj1, obj2], obj3, [obj4]] self.assertEqual(o_node.evaluate_output("Objects"), [obj1, obj2, obj3, obj4]) # MeshesToList with nested lists sc1 = SlicingCollection([], "M1") sc2 = SlicingCollection([], "M2") sc3 = SlicingCollection([], "M3") m_node = SLICENODE_MeshesToList() m_node.resolve_multi_input = lambda label: [[sc1, sc2], [sc3]] self.assertEqual(m_node.evaluate_output("Meshes"), [sc1, sc2, sc3]) # PathsToList with list input and single input sp1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)]) sp2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)]) sp3 = SlicePaths([np.array([[4, 4, 0], [5, 5, 0]], dtype=np.float64)]) p_node = SLICENODE_PathsToList() p_node.resolve_multi_input = lambda label: [[sp1, sp2], sp3] self.assertEqual(p_node.evaluate_output("Paths"), [sp1, sp2, sp3]) # GcodesToList with list input and single input g_node = SLICENODE_GcodesToList() g_node.resolve_multi_input = lambda label: [["G1 X1", "G1 X2"], "G1 X3", ["G1 X4", "G1 X5"]] self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2", "G1 X3", "G1 X4", "G1 X5"]) def test_join_mesh_flattens_lists(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh node = SLICENODE_JoinMesh() so1 = SlicingObject() so1.name = "part1" so1.mesh = np.ones((1, 4, 3), dtype=np.float64) sc1 = SlicingCollection([], "Coll1") sc1.objects = [so1] so2 = SlicingObject() so2.name = "part2" so2.mesh = np.ones((2, 4, 3), dtype=np.float64) sc2 = SlicingCollection([], "Coll2") sc2.objects = [so2] so3 = SlicingObject() so3.name = "part3" so3.mesh = np.ones((3, 4, 3), dtype=np.float64) sc3 = SlicingCollection([], "Coll3") sc3.objects = [so3] # Mock inputs delivering [ [sc1, sc2], sc3 ] mock_in1 = DummySocket("Mesh", "SLICESOCK_Mesh") mock_in1.is_linked = True mock_in2 = DummySocket("Mesh", "SLICESOCK_Mesh") mock_in2.is_linked = True node.inputs = [mock_in1, mock_in2] import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen original_resolve = b_gen.resolve_input b_gen.resolve_input = lambda sock, **kw: [sc1, sc2] if sock is mock_in1 else sc3 try: res = node.evaluate_output("Mesh") self.assertIsInstance(res, SlicingCollection) self.assertEqual(len(res.objects), 3) self.assertEqual({o.name for o in res.objects}, {"part1", "part2", "part3"}) finally: b_gen.resolve_input = original_resolve def test_drop_geometry_list_evaluation(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry node = SLICENODE_DropGeometry() so1 = SlicingObject() so1.name = "part1" so1.mesh = np.array([[[0, 0, 5], [1, 0, 5], [0, 1, 5], [0, 0, 1]]], dtype=np.float64) sc1 = SlicingCollection([], "M1") sc1.objects = [so1] so2 = SlicingObject() so2.name = "part2" so2.mesh = np.array([[[0, 0, 2], [1, 0, 2], [0, 1, 2], [0, 0, 1]]], dtype=np.float64) sc2 = SlicingCollection([], "M2") sc2.objects = [so2] node.resolve = lambda field: [sc1, sc2] if field == "Mesh" else 0.0 res_mesh = node.evaluate_output("Mesh") res_trans = node.evaluate_output("Translation") self.assertIsInstance(res_mesh, list) self.assertEqual(len(res_mesh), 2) self.assertIsInstance(res_trans, list) self.assertEqual(res_trans, [(0.0, 0.0, -5.0), (0.0, 0.0, -2.0)]) def test_start_print_rejects_list_in_evaluate(self): from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_StartPrint node = SLICENODE_StartPrint() node.resolve = lambda field: ["G1 X0", "G1 X10"] if field == "Gcode" else None op = MagicMock() res = node.evaluate(operator=op) self.assertEqual(res, 'error') op.report.assert_called_with({'ERROR'}, "Start Print does not accept a list of Gcode") class DummyInterfaceItem: def __init__(self, name, identifier, in_out="INPUT", socket_type="SLICESOCK_Paths"): self.name = name self.identifier = identifier self.in_out = in_out self.socket_type = socket_type self.item_type = 'SOCKET' class DummyInterface: def __init__(self, items): self.items_tree = items class DummyGroupTree: def __init__(self, items=None, nodes=None): self.interface = DummyInterface(items or []) self.nodes = nodes or [] class DummyGroupNode(DummyNode): def __init__(self, name="Group", tree=None): super().__init__("SLICENODE_Group") self.name = name self.slicer_tree = tree class TestGroupNodeListShapes(unittest.TestCase): def test_group_input_can_accept_list_detection(self): item_paths = DummyInterfaceItem("Paths", "sock_paths", in_out="INPUT", socket_type="SLICESOCK_Paths") item_float = DummyInterfaceItem("FloatVal", "sock_float", in_out="INPUT", socket_type="NodeSocketFloat") gin = DummyNode("NodeGroupInput") gin_out_paths = DummySocket("Paths", "SLICESOCK_Paths") gin_out_paths.identifier = "sock_paths" gin_out_float = DummySocket("FloatVal", "NodeSocketFloat") gin_out_float.identifier = "sock_float" gin.add_out(gin_out_paths) gin.add_out(gin_out_float) target_node = DummyNode("SLICENODE_BufferShells") target_node.list_socket_mapping = [("Paths", "Paths")] target_in_paths = DummySocket("Paths", "SLICESOCK_Paths") target_node.add_in(target_in_paths) link = DummyLink(gin_out_paths, target_in_paths) gin_out_paths.is_linked = True gin_out_paths.links = [link] tree = DummyGroupTree([item_paths, item_float], [gin, target_node]) # Paths input can accept list because target is BufferShells self.assertTrue(group_input_can_accept_list(tree, item_paths)) # FloatVal cannot accept list because socket type is Float self.assertFalse(group_input_can_accept_list(tree, item_float)) def test_group_input_connected_to_forbidden_node_rejects_list(self): item_gcode = DummyInterfaceItem("Gcode", "sock_gcode", in_out="INPUT", socket_type="NodeSocketString") gin = DummyNode("NodeGroupInput") gin_out_gcode = DummySocket("Gcode", "NodeSocketString") gin_out_gcode.identifier = "sock_gcode" gin.add_out(gin_out_gcode) sp_node = DummyNode("SLICENODE_StartPrint") sp_in = DummySocket("Gcode", "NodeSocketString") sp_node.add_in(sp_in) link = DummyLink(gin_out_gcode, sp_in) gin_out_gcode.is_linked = True gin_out_gcode.links = [link] tree = DummyGroupTree([item_gcode], [gin, sp_node]) # StartPrint forbids lists, so group input must NOT accept list self.assertFalse(group_input_can_accept_list(tree, item_gcode)) def test_group_output_inherent_list(self): item_out = DummyInterfaceItem("Paths", "sock_out_paths", in_out="OUTPUT", socket_type="SLICESOCK_Paths") gout = DummyNode("NodeGroupOutput") gout_in = DummySocket("Paths", "SLICESOCK_Paths") gout_in.identifier = "sock_out_paths" gout.add_in(gout_in) list_joiner = DummyNode("SLICENODE_PathsToList") list_joiner.is_list_output = True lj_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST") list_joiner.add_out(lj_out) link = DummyLink(lj_out, gout_in) gout_in.is_linked = True gout_in.links = [link] tree = DummyGroupTree([item_out], [gout, list_joiner]) # Output fed from PathsToList is always a list self.assertTrue(is_group_output_a_list(None, tree, item_out)) def test_group_output_element_wise_propagation_and_sync(self): item_in = DummyInterfaceItem("Paths", "sock_in", in_out="INPUT", socket_type="SLICESOCK_Paths") item_out = DummyInterfaceItem("Paths", "sock_out", in_out="OUTPUT", socket_type="SLICESOCK_Paths") gin = DummyNode("NodeGroupInput") gin_out = DummySocket("Paths", "SLICESOCK_Paths") gin_out.identifier = "sock_in" gin.add_out(gin_out) buf_node = DummyNode("SLICENODE_BufferShells") buf_node.list_socket_mapping = [("Paths", "Paths")] buf_in = DummySocket("Paths", "SLICESOCK_Paths") buf_out = DummySocket("Paths", "SLICESOCK_Paths") buf_node.add_in(buf_in) buf_node.add_out(buf_out) gout = DummyNode("NodeGroupOutput") gout_in = DummySocket("Paths", "SLICESOCK_Paths") gout_in.identifier = "sock_out" gout.add_in(gout_in) link1 = DummyLink(gin_out, buf_in) gin_out.is_linked = True gin_out.links = [link1] buf_in.is_linked = True buf_in.links = [link1] link2 = DummyLink(buf_out, gout_in) buf_out.is_linked = True buf_out.links = [link2] gout_in.is_linked = True gout_in.links = [link2] tree = DummyGroupTree([item_in, item_out], [gin, buf_node, gout]) grp_node = DummyGroupNode("MyGroup", tree) grp_in = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE") grp_in.identifier = "sock_in" grp_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE") grp_out.identifier = "sock_out" grp_node.add_in(grp_in) grp_node.add_out(grp_out) # Before external link: # Input should be LIST (because list input is possible) # Output should be CIRCLE (because no list is fed) sync_group_node_socket_shapes(grp_node) self.assertEqual(grp_in.display_shape, "LIST") self.assertEqual(grp_out.display_shape, "CIRCLE") # Now link a list to the group's input externally: up_node = DummyNode("SLICENODE_PathsToList") up_node.is_list_output = True up_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST") up_node.add_out(up_out) ext_link = DummyLink(up_out, grp_in) grp_in.is_linked = True grp_in.links = [ext_link] # After external list link: # Output should now become LIST! sync_group_node_socket_shapes(grp_node) self.assertEqual(grp_in.display_shape, "LIST") self.assertEqual(grp_out.display_shape, "LIST") class TestImplicitGetMesh(unittest.TestCase): def test_resolve_input_implicit_get_mesh_single_object(self): from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache parent_obj = DummyObject("ParentCube") child_obj1 = DummyObject("Child1") child_obj2 = DummyObject("Child2") parent_obj.children = [child_obj1, child_obj2] # Source node outputting parent_obj src_node = DummyNode("SLICENODE_InputObject") src_node.name = "InputObject" src_out = DummySocket("Object", "NodeSocketObject") src_out.node = src_node src_node.outputs = DummySocketCollection([src_out]) src_node.evaluate_output = lambda socket_name, **kw: parent_obj # Target node with SLICESOCK_Mesh input dst_node = DummyNode("SLICENODE_SliceShells") dst_in = DummySocket("Mesh", "SLICESOCK_Mesh") dst_in.node = dst_node dst_node.inputs = DummySocketCollection([dst_in]) # Link them dst_in.is_linked = True link = DummyLink(src_out, dst_in) dst_in.links = [link] with execution_cache() as cache: result = resolve_input(dst_in, cache=cache) self.assertIsInstance(result, SlicingCollection) self.assertEqual(result.name, "ParentCube") def test_resolve_input_implicit_get_mesh_object_list(self): from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache obj1 = DummyObject("Obj1") obj2 = DummyObject("Obj2") src_node = DummyNode("SLICENODE_ObjectsToList") src_node.name = "ObjectsToList" src_out = DummySocket("Objects", "NodeSocketObject", display_shape="LIST") src_out.node = src_node src_node.outputs = DummySocketCollection([src_out]) src_node.evaluate_output = lambda socket_name, **kw: [obj1, obj2] dst_node = DummyNode("SLICENODE_SliceShells") dst_in = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="LIST") dst_in.node = dst_node dst_node.inputs = DummySocketCollection([dst_in]) dst_in.is_linked = True dst_in.links = [DummyLink(src_out, dst_in)] with execution_cache() as cache: result = resolve_input(dst_in, cache=cache) self.assertIsInstance(result, list) self.assertEqual(len(result), 2) self.assertIsInstance(result[0], SlicingCollection) self.assertIsInstance(result[1], SlicingCollection) self.assertEqual(result[0].name, "Obj1") self.assertEqual(result[1].name, "Obj2") def test_resolve_input_already_mesh_is_not_reconverted(self): from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache sc = SlicingCollection([], "ExistingMesh") src_node = DummyNode("SLICENODE_GetMesh") src_node.name = "GetMesh" src_out = DummySocket("Mesh", "SLICESOCK_Mesh") src_out.node = src_node src_node.outputs = DummySocketCollection([src_out]) src_node.evaluate_output = lambda socket_name, **kw: sc dst_node = DummyNode("SLICENODE_SliceShells") dst_in = DummySocket("Mesh", "SLICESOCK_Mesh") dst_in.node = dst_node dst_node.inputs = DummySocketCollection([dst_in]) dst_in.is_linked = True dst_in.links = [DummyLink(src_out, dst_in)] with execution_cache() as cache: result = resolve_input(dst_in, cache=cache) self.assertIs(result, sc) def test_resolve_input_object_to_object_socket_not_converted(self): from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache obj = DummyObject("MyObj") src_node = DummyNode("SLICENODE_InputObject") src_node.name = "InputObject" src_out = DummySocket("Object", "NodeSocketObject") src_out.node = src_node src_node.outputs = DummySocketCollection([src_out]) src_node.evaluate_output = lambda socket_name, **kw: obj dst_node = DummyNode("SLICENODE_ObjectsToList") dst_in = DummySocket("Object", "NodeSocketObject") dst_in.node = dst_node dst_node.inputs = DummySocketCollection([dst_in]) dst_in.is_linked = True dst_in.links = [DummyLink(src_out, dst_in)] with execution_cache() as cache: result = resolve_input(dst_in, cache=cache) self.assertIs(result, obj) def test_implicit_get_mesh_caching(self): from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache obj = DummyObject("SharedObj") src_node = DummyNode("SLICENODE_InputObject") src_node.name = "InputObject" src_out = DummySocket("Object", "NodeSocketObject") src_out.node = src_node src_node.outputs = DummySocketCollection([src_out]) src_node.evaluate_output = lambda socket_name, **kw: obj dst_node1 = DummyNode("SLICENODE_SliceShells") dst_in1 = DummySocket("Mesh", "SLICESOCK_Mesh") dst_in1.node = dst_node1 dst_node1.inputs = DummySocketCollection([dst_in1]) dst_in1.is_linked = True dst_in1.links = [DummyLink(src_out, dst_in1)] dst_node2 = DummyNode("SLICENODE_JoinMesh") dst_in2 = DummySocket("Mesh", "SLICESOCK_Mesh") dst_in2.node = dst_node2 dst_node2.inputs = DummySocketCollection([dst_in2]) dst_in2.is_linked = True dst_in2.links = [DummyLink(src_out, dst_in2)] with execution_cache() as cache: res1 = resolve_input(dst_in1, cache=cache) res2 = resolve_input(dst_in2, cache=cache) self.assertIs(res1, res2) def test_explicit_get_mesh_include_children(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GetMesh parent_obj = DummyObject("Parent") child_obj = DummyObject("Child") parent_obj.children = [child_obj] node_with = SLICENODE_GetMesh() node_with.include_children = True node_with.resolve = lambda label: parent_obj res_with = node_with.evaluate_output("Mesh") self.assertIsInstance(res_with, SlicingCollection) node_without = SLICENODE_GetMesh() node_without.include_children = False node_without.resolve = lambda label: parent_obj res_without = node_without.evaluate_output("Mesh") self.assertIsInstance(res_without, SlicingCollection) def test_implicit_get_mesh_passes_all_children_to_slicing_collection(self): from unittest.mock import patch from NodeSlicer.infra.blender.blender_slice_nodes_generic import object_to_mesh parent_obj = DummyObject("Parent") c1 = DummyObject("C1") c2 = DummyObject("C2") gc1 = DummyObject("GC1") c2.children = [gc1] parent_obj.children = [c1, c2] with patch.object(SlicingCollection, "__init__", return_value=None) as mock_init: object_to_mesh(parent_obj, include_children=True) self.assertTrue(mock_init.called) family, name = mock_init.call_args[0] self.assertEqual(name, "Parent") self.assertEqual(set(family), {parent_obj, c1, c2, gc1}) class TestGeneralizedListArchitecture(unittest.TestCase): def test_prepare_indexed_inputs_matching_length(self): main = ["A", "B", "C"] sec1 = [1, 2, 3] sec2 = ["x", "y", "z"] is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"]) self.assertTrue(is_list) self.assertEqual(items, ["A", "B", "C"]) self.assertEqual(sec_lists[0], [1, 2, 3]) self.assertEqual(sec_lists[1], ["x", "y", "z"]) def test_prepare_indexed_inputs_broadcast_single_value(self): main = ["A", "B", "C"] sec1 = 42 sec2 = ["single"] is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"]) self.assertTrue(is_list) self.assertEqual(items, ["A", "B", "C"]) self.assertEqual(sec_lists[0], [42, 42, 42]) self.assertEqual(sec_lists[1], ["single", "single", "single"]) def test_prepare_indexed_inputs_mismatched_length_raises(self): main = ["A", "B", "C"] sec = [1, 2] with self.assertRaises(ValueError) as ctx: prepare_indexed_inputs(main, sec, input_names=["my_sec"]) self.assertIn("my_sec", str(ctx.exception)) self.assertIn("3 items", str(ctx.exception)) self.assertIn("2 items", str(ctx.exception)) def test_prepare_indexed_inputs_single_main_with_single_sec(self): main = "A" sec1 = 42 sec2 = [99] is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"]) self.assertFalse(is_list) self.assertEqual(items, ["A"]) self.assertEqual(sec_lists[0], [42]) self.assertEqual(sec_lists[1], [99]) def test_prepare_indexed_inputs_single_main_with_multi_sec_raises(self): main = "A" sec = [1, 2] with self.assertRaises(ValueError) as ctx: prepare_indexed_inputs(main, sec, input_names=["my_sec"]) self.assertIn("single item", str(ctx.exception)) self.assertIn("2 items", str(ctx.exception)) def test_vector_input_distinction(self): class MockBpyPropArray: def __init__(self, items): self._items = list(items) def __iter__(self): return iter(self._items) def __len__(self): return len(self._items) def __getitem__(self, idx): return self._items[idx] MockBpyPropArray.__name__ = "bpy_prop_array" # Single vector variants (tuple, list, np.ndarray, MockBpyPropArray) vec_tuple = (1.0, 2.0, 3.0) vec_list = [1.0, 2.0, 3.0] vec_np = np.array([1.0, 2.0, 3.0]) vec_bpy = MockBpyPropArray([1.0, 2.0, 3.0]) for v in (vec_tuple, vec_list, vec_np, vec_bpy): self.assertTrue(is_vector_value(v), f"Failed for {type(v).__name__}") self.assertFalse(is_input_a_list(v, is_vector=True), f"Failed for {type(v).__name__}") # List of vectors IS a list list_of_vecs = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] self.assertFalse(is_vector_value(list_of_vecs)) self.assertTrue(is_input_a_list(list_of_vecs, is_vector=True)) # Broadcasting single vector across main list main = ["geom1", "geom2"] for v in (vec_tuple, vec_list, vec_np, vec_bpy): is_list, items, (t_list,) = prepare_indexed_inputs(main, v, is_vector=[True], input_names=["Translation"]) self.assertTrue(is_list) self.assertEqual(len(t_list), 2) self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0]) self.assertEqual(list(t_list[1]), [1.0, 2.0, 3.0]) # Single main geometry with single atomic vector must NOT be treated as a list of 3 items single_main = "geom1" for v in (vec_tuple, vec_list, vec_np, vec_bpy): is_list, items, (t_list,) = prepare_indexed_inputs(single_main, v, is_vector=[True], input_names=["Translation"]) self.assertFalse(is_list) self.assertEqual(len(t_list), 1) self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0]) # Matching list of vectors index-per-index is_list, items, (t_list,) = prepare_indexed_inputs(main, list_of_vecs, is_vector=[True], input_names=["Translation"]) self.assertTrue(is_list) self.assertEqual(t_list, [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]) def test_transform_geometry_atomic_vector_inputs(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry class MockBpyPropArray: def __init__(self, items): self._items = list(items) def __iter__(self): return iter(self._items) def __len__(self): return len(self._items) def __getitem__(self, idx): return self._items[idx] MockBpyPropArray.__name__ = "bpy_prop_array" so1 = SlicingObject() so1.name = "Part1" so1.mesh = np.zeros((1, 4, 3), dtype=np.float64) c1 = SlicingCollection([so1], "Col1") so2 = SlicingObject() so2.name = "Part2" so2.mesh = np.zeros((1, 4, 3), dtype=np.float64) c2 = SlicingCollection([so2], "Col2") tf_node = SLICENODE_TransformGeometry() # 1. Single geometry + atomic tuple translation tf_node.resolve = lambda label: { "Geometry": c1, "Translation": (10.0, 20.0, 30.0), "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) res = tf_node.evaluate_output("Geometry") self.assertNotIsInstance(res, list) self.assertIsInstance(res, SlicingCollection) self.assertEqual(res.transforms[-1]["translation"], (10.0, 20.0, 30.0)) # 2. Single geometry + atomic list translation [10, 20, 30] (NOT a list of 3 items!) tf_node.resolve = lambda label: { "Geometry": c1, "Translation": [10.0, 20.0, 30.0], "Rotation": [0.0, 0.0, 0.0], "Scale": [1.0, 1.0, 1.0], }.get(label) res = tf_node.evaluate_output("Geometry") self.assertNotIsInstance(res, list) self.assertIsInstance(res, SlicingCollection) self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0)) # 3. Single geometry + mock bpy_prop_array (unlinked socket default value in Blender) tf_node.resolve = lambda label: { "Geometry": c1, "Translation": MockBpyPropArray([10.0, 20.0, 30.0]), "Rotation": MockBpyPropArray([0.0, 0.0, 0.0]), "Scale": MockBpyPropArray([1.0, 1.0, 1.0]), }.get(label) res = tf_node.evaluate_output("Geometry") self.assertNotIsInstance(res, list) self.assertIsInstance(res, SlicingCollection) self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0)) # 4. Multiple geometries + atomic translation (broadcasted) tf_node.resolve = lambda label: { "Geometry": [c1, c2], "Translation": (10.0, 20.0, 30.0), "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) res_multi = tf_node.evaluate_output("Geometry") self.assertIsInstance(res_multi, list) self.assertEqual(len(res_multi), 2) self.assertEqual(res_multi[0].transforms[-1]["translation"], (10.0, 20.0, 30.0)) self.assertEqual(res_multi[1].transforms[-1]["translation"], (10.0, 20.0, 30.0)) # 5. Multiple geometries + list of 2 translations tf_node.resolve = lambda label: { "Geometry": [c1, c2], "Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)], "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) res_matched = tf_node.evaluate_output("Geometry") self.assertIsInstance(res_matched, list) self.assertEqual(len(res_matched), 2) self.assertEqual(res_matched[0].transforms[-1]["translation"], (10.0, 0.0, 0.0)) self.assertEqual(res_matched[1].transforms[-1]["translation"], (20.0, 0.0, 0.0)) # 6. Single geometry + list of 2 translations -> raises ValueError tf_node.resolve = lambda label: { "Geometry": c1, "Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)], "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) with self.assertRaises(ValueError): tf_node.evaluate_output("Geometry") def test_path_nodes_atomic_vector_inputs(self): from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOffset, SLICENODE_RotatePath class MockBpyPropArray: def __init__(self, items): self._items = list(items) def __iter__(self): return iter(self._items) def __len__(self): return len(self._items) def __getitem__(self, idx): return self._items[idx] MockBpyPropArray.__name__ = "bpy_prop_array" p1 = SlicePaths([np.array([[0, 0, 0], [10, 0, 0]], dtype=np.int32)]) # Path Offset with atomic (10, 0, 0) as tuple, list, and bpy_prop_array node_offset = SLICENODE_PathOffset() for off in ((10.0, 0.0, 0.0), [10.0, 0.0, 0.0], MockBpyPropArray([10.0, 0.0, 0.0])): node_offset.resolve = lambda label, o=off: { "Paths": p1, "Offset": o, }.get(label) res = node_offset.evaluate_output("Paths") self.assertIsInstance(res, SlicePaths) self.assertEqual(len(res), 1) # Rotate Path with atomic center node_rot = SLICENODE_RotatePath() for cent in ((5.0, 5.0, 0.0), [5.0, 5.0, 0.0], MockBpyPropArray([5.0, 5.0, 0.0])): node_rot.resolve = lambda label, c=cent: { "Paths": p1, "Angle": 45.0, "Center": c, }.get(label) res = node_rot.evaluate_output("Paths") self.assertIsInstance(res, SlicePaths) self.assertEqual(len(res), 1) def test_eval_element_wise_multivalue(self): op = lambda item, a, b: f"{item}_{a}_{b}" res = eval_element_wise_multivalue(op, ["x", "y"], [1, 2], "const") self.assertEqual(res, ["x_1_const", "y_2_const"]) single_res = eval_element_wise_multivalue(op, "x", [1], "const") self.assertEqual(single_res, "x_1_const") def test_math_node_list_broadcasting_and_matching(self): from NodeSlicer.core.nodeslicer.nodes.math import SLICENODE_FloatMath node = SLICENODE_FloatMath() in0 = DummySocket("Value", "NodeSocketFloat") in1 = DummySocket("Value", "NodeSocketFloat") node.inputs = DummySocketCollection([in0, in1]) node.operation = 'ADD' # Case 1: A is list, B is scalar node.resolve = lambda label: [1.0, 2.0, 3.0] if label == node.inputs[0] else 10.0 self.assertEqual(node.evaluate_output("Value"), [11.0, 12.0, 13.0]) # Case 2: A is scalar, B is list node.resolve = lambda label: 10.0 if label == node.inputs[0] else [1.0, 2.0, 3.0] self.assertEqual(node.evaluate_output("Value"), [11.0, 12.0, 13.0]) # Case 3: A and B are lists of equal length node.resolve = lambda label: [1.0, 2.0, 3.0] if label == node.inputs[0] else [10.0, 20.0, 30.0] self.assertEqual(node.evaluate_output("Value"), [11.0, 22.0, 33.0]) # Case 4: Mismatched list lengths raise ValueError node.resolve = lambda label: [1.0, 2.0] if label == node.inputs[0] else [10.0, 20.0, 30.0] with self.assertRaises(ValueError): node.evaluate_output("Value") def test_transform_geometry_with_get_transforms_list(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GetTransforms, SLICENODE_TransformGeometry # Create two mock slicing collections with transforms c1 = SlicingCollection([], "Part1") c1.transforms = [{"type": "INIT", "translation": (10.0, 0.0, 0.0), "rotation": (0.0, 0.0, 0.0), "scale": (1.0, 1.0, 1.0)}] c2 = SlicingCollection([], "Part2") c2.transforms = [{"type": "INIT", "translation": (20.0, 0.0, 0.0), "rotation": (0.0, 0.0, 0.0), "scale": (2.0, 2.0, 2.0)}] # Get Transforms on list of geometries get_t_node = SLICENODE_GetTransforms() get_t_node.resolve = lambda label: [c1, c2] t_list = get_t_node.evaluate_output("Translation") r_list = get_t_node.evaluate_output("Rotation") s_list = get_t_node.evaluate_output("Scale") self.assertEqual(len(t_list), 2) self.assertEqual(len(r_list), 2) self.assertEqual(len(s_list), 2) self.assertEqual(t_list[0], (10.0, 0.0, 0.0)) self.assertEqual(t_list[1], (20.0, 0.0, 0.0)) # Pass those into Transform Geometry tf_node = SLICENODE_TransformGeometry() tf_node.resolve = lambda label: { "Geometry": [c1, c2], "Translation": t_list, "Rotation": r_list, "Scale": s_list, }.get(label) res = tf_node.evaluate_output("Geometry") self.assertIsInstance(res, list) self.assertEqual(len(res), 2) # Mismatched translation list length raises ValueError tf_node.resolve = lambda label: { "Geometry": [c1, c2], "Translation": [(1.0, 0.0, 0.0)], # length 1 broadcasts "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) res_broadcast = tf_node.evaluate_output("Geometry") self.assertEqual(len(res_broadcast), 2) tf_node.resolve = lambda label: { "Geometry": [c1, c2], "Translation": [(1.0, 0.0, 0.0), (2.0, 0.0, 0.0), (3.0, 0.0, 0.0)], # length 3 != 2 "Rotation": (0.0, 0.0, 0.0), "Scale": (1.0, 1.0, 1.0), }.get(label) with self.assertRaises(ValueError): tf_node.evaluate_output("Geometry") def test_drop_geometry_list_matching(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry so1 = SlicingObject() so1.name = "Part1" so1.mesh = np.zeros((1, 4, 3), dtype=np.float64) so1.mesh[:, :3, 2] = 5.0 so2 = SlicingObject() so2.name = "Part2" so2.mesh = np.zeros((1, 4, 3), dtype=np.float64) so2.mesh[:, :3, 2] = 10.0 c1 = SlicingCollection([], "Part1") c1.objects = [so1] c2 = SlicingCollection([], "Part2") c2.objects = [so2] node = SLICENODE_DropGeometry() # Case 1: list of meshes + list of Z matching 1-to-1 node.resolve = lambda label: [c1, c2] if label == "Mesh" else [0.0, 2.0] res = node.evaluate_output("Mesh") self.assertIsInstance(res, list) self.assertEqual(len(res), 2) trans = node.evaluate_output("Translation") self.assertEqual(trans, [(0.0, 0.0, -5.0), (0.0, 0.0, -8.0)]) # Case 2: list of meshes + single Z (broadcast) node.resolve = lambda label: [c1, c2] if label == "Mesh" else 1.0 trans_broadcast = node.evaluate_output("Translation") self.assertEqual(trans_broadcast, [(0.0, 0.0, -4.0), (0.0, 0.0, -9.0)]) # Case 3: list of meshes + list of 1 Z (broadcast) node.resolve = lambda label: [c1, c2] if label == "Mesh" else [1.0] trans_broadcast_list1 = node.evaluate_output("Translation") self.assertEqual(trans_broadcast_list1, [(0.0, 0.0, -4.0), (0.0, 0.0, -9.0)]) # Case 4: length mismatch raises ValueError node.resolve = lambda label: [c1, c2] if label == "Mesh" else [1.0, 2.0, 3.0] with self.assertRaises(ValueError): node.evaluate_output("Mesh") # Case 5: single mesh + list of multiple Z raises ValueError node.resolve = lambda label: c1 if label == "Mesh" else [1.0, 2.0] with self.assertRaises(ValueError): node.evaluate_output("Mesh") def test_rename_mesh_list_matching(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_RenameMesh c1 = SlicingCollection([], "Old1") c2 = SlicingCollection([], "Old2") node = SLICENODE_RenameMesh() # Case 1: list of meshes + list of names matching 1-to-1 node.resolve = lambda label: [c1, c2] if label == "Geometry" else ["New1", "New2"] res = node.evaluate_output("Geometry") self.assertEqual([r.name for r in res], ["New1", "New2"]) # Case 2: list of meshes + single name (broadcast) node.resolve = lambda label: [c1, c2] if label == "Geometry" else "SameName" res = node.evaluate_output("Geometry") self.assertEqual([r.name for r in res], ["SameName", "SameName"]) # Case 3: length mismatch raises ValueError node.resolve = lambda label: [c1, c2] if label == "Geometry" else ["A", "B", "C"] with self.assertRaises(ValueError): node.evaluate_output("Geometry") # Case 4: single mesh + multiple names raises ValueError node.resolve = lambda label: c1 if label == "Geometry" else ["A", "B"] with self.assertRaises(ValueError): node.evaluate_output("Geometry") def test_path_operations_list_matching(self): from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations p1 = SlicePaths([np.array([[0, 0, 0], [10, 0, 0]], dtype=np.int32)]) p2 = SlicePaths([np.array([[0, 10, 0], [10, 10, 0]], dtype=np.int32)]) p3 = SlicePaths([np.array([[0, 20, 0], [10, 20, 0]], dtype=np.int32)]) node = SLICENODE_PathOperations() in0 = DummySocket("Paths", "SLICESOCK_Paths") in1 = DummySocket("Paths", "SLICESOCK_Paths") node.inputs = DummySocketCollection([in0, in1]) in0.is_linked = True in1.is_linked = True from unittest.mock import patch # Case 1: list of Paths A + list of Paths B matching 1-to-1 with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], [p3, p3]]): res = node.evaluate_output("Paths") self.assertIsInstance(res, list) self.assertEqual(len(res), 2) # Case 2: list of Paths A + single Path B (broadcast) with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], p3]): res = node.evaluate_output("Paths") self.assertIsInstance(res, list) self.assertEqual(len(res), 2) # Case 3: length mismatch raises ValueError with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], [p3, p3, p3]]): with self.assertRaises(ValueError): node.evaluate_output("Paths") # Case 4: single Path A + multiple Paths B raises ValueError with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[p1, [p2, p3]]): with self.assertRaises(ValueError): node.evaluate_output("Paths") if __name__ == '__main__': unittest.main()