diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 7241c26dfec..b5af3e14237 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -22,6 +22,7 @@ set(USE_EXPERIMENTAL_TESTS FALSE) set(TEST_SRC_DIR ${CMAKE_SOURCE_DIR}/../lib/tests) +set(TEST_PYTHON_DIR ${CMAKE_SOURCE_DIR}/tests/python) set(TEST_OUT_DIR ${CMAKE_BINARY_DIR}/tests) # ugh, any better way to do this on testing only? @@ -126,13 +127,17 @@ add_blender_test( add_blender_test( bmesh_bevel ${TEST_SRC_DIR}/modeling/bevel_regression.blend - --python-text run_tests + --python ${TEST_PYTHON_DIR}/bevel_operator.py + -- + --run-all-tests ) add_blender_test( bmesh_boolean ${TEST_SRC_DIR}/modeling/bool_regression.blend - --python-text run_tests + --python ${TEST_PYTHON_DIR}/boolean_operator.py + -- + --run-all-tests ) add_blender_test( @@ -149,6 +154,24 @@ add_blender_test( --python-text run_tests.py ) +add_blender_test( + modifiers + ${TEST_SRC_DIR}/modeling/modifiers.blend + --python ${TEST_PYTHON_DIR}/modifiers.py + -- + --run-all-tests +) + +# ------------------------------------------------------------------------------ +# OPERATORS TESTS +add_blender_test( + operators + ${TEST_SRC_DIR}/modeling/operators.blend + --python ${TEST_PYTHON_DIR}/operators.py + -- + --run-all-tests +) + # ------------------------------------------------------------------------------ # IO TESTS diff --git a/tests/python/bevel_operator.py b/tests/python/bevel_operator.py new file mode 100644 index 00000000000..f91c208bae3 --- /dev/null +++ b/tests/python/bevel_operator.py @@ -0,0 +1,184 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# To run all tests, use +# BLENDER_VERBOSE=1 blender path/to/bevel_regression.blend --python path/to/bevel_operator.py -- --run_all_tests +# To run one test, use +# BLENDER_VERBOSE=1 blender path/to/bevel_regression.blend --python path/to/bevel_operator.py -- --run_test +# where is the index of the test specified in the list tests. + +import bpy +import os +import sys + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest + + +def main(): + tests = [ + # 0 + ['EDGE', {10}, 'Cube_test', 'Cube_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_2', 'bevel', {'offset': 0.2, 'offset_type': 'WIDTH'}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_3', 'bevel', {'offset': 0.2, 'offset_type': 'DEPTH'}], + ['EDGE', {10}, 'Cube_test', 'Cube_result_4', 'bevel', {'offset': 0.4, 'segments': 2}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_5', 'bevel', {'offset': 0.4, 'segments': 3}], + # 5 + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_6', 'bevel', {'offset': 0.4, 'segments': 4}], + ['EDGE', {0, 10, 4, 7}, 'Cube_test', 'Cube_result_7', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 0.2}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_8', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 0.25}], + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_9', 'bevel', {'offset': 0.4, 'segments': 6, 'profile': 0.9}], + ['EDGE', {10, 7}, 'Cube_test', 'Cube_result_10', 'bevel', {'offset': 0.4, 'segments': 4, 'profile': 1.0}], + # 10 + ['EDGE', {8, 10, 7}, 'Cube_test', 'Cube_result_11', 'bevel', {'offset': 0.4, 'segments': 5, 'profile': 1.0}], + ['EDGE', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 'Cube_test', 'Cube_result_12', 'bevel', + {'offset': 0.4, 'segments': 8}], + ['EDGE', {5}, 'Pyr4_test', 'Pyr4_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {2, 5}, 'Pyr4_test', 'Pyr4_result_2', 'bevel', {'offset': 0.2}], + ['EDGE', {2, 3, 5}, 'Pyr4_test', 'Pyr4_result_3', 'bevel', {'offset': 0.2}], + # 15 + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_5', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {2, 3}, 'Pyr4_test', 'Pyr4_result_6', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {1, 2, 3, 5}, 'Pyr4_test', 'Pyr4_result_7', 'bevel', {'offset': 0.2, 'segments': 4, 'profile': 0.15}], + ['VERT', {1}, 'Pyr4_test', 'Pyr4_result_8', 'bevel', {'offset': 0.75, 'segments': 4, 'vertex_only': True}], + # 20 + ['VERT', {1}, 'Pyr4_test', 'Pyr4_result_9', 'bevel', + {'offset': 0.75, 'segments': 3, 'vertex_only': True, 'profile': 0.25}], + ['EDGE', {2, 3}, 'Pyr6_test', 'Pyr6_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {8, 2, 3}, 'Pyr6_test', 'Pyr6_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {0, 2, 3, 4, 6, 7, 9, 10, 11}, 'Pyr6_test', 'Pyr6_result_3', 'bevel', + {'offset': 0.2, 'segments': 4, 'profile': 0.8}], + ['EDGE', {8, 9, 3, 11}, 'Sept_test', 'Sept_result_1', 'bevel', {'offset': 0.1}], + # 25 + ['EDGE', {8, 9, 11}, 'Sept_test', 'Sept_result_2', 'bevel', {'offset': 0.1, 'offset_type': 'WIDTH'}], + ['EDGE', {2, 8, 9, 12, 13, 14}, 'Saddle_test', 'Saddle_result_1', 'bevel', {'offset': 0.3, 'segments': 5}], + ['VERT', {4}, 'Saddle_test', 'Saddle_result_2', 'bevel', {'offset': 0.6, 'segments': 6, 'vertex_only': True}], + ['EDGE', {2, 5, 8, 11, 14, 18, 21, 24, 27, 30, 34, 37, 40, 43, 46, 50, 53, 56, 59, 62, 112, 113, 114, 115}, + 'Bent_test', 'Bent_result_1', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {1, 8, 9, 10, 11}, 'Bentlines_test', 'Bentlines_result_1', 'bevel', {'offset': 0.2, 'segments': 3}], + # 30 + ['EDGE', {26, 12, 20}, 'Flaretop_test', 'Flaretop_result_1', 'bevel', {'offset': 0.4, 'segments': 2}], + ['EDGE', {26, 12, 20}, 'Flaretop_test', 'Flaretop_result_2', 'bevel', + {'offset': 0.4, 'segments': 2, 'profile': 1.0}], + ['FACE', {1, 6, 7, 8, 9, 10, 11, 12}, 'Flaretop_test', 'Flaretop_result_3', 'bevel', + {'offset': 0.4, 'segments': 4}], + ['EDGE', {4, 8, 10, 18, 24}, 'BentL_test', 'BentL_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {0, 1, 2, 10}, 'Wires_test', 'Wires_test_result_1', 'bevel', {'offset': 0.3}], + # 35 + ['VERT', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 'Wires_test', 'Wires_test_result_2', 'bevel', + {'offset': 0.3, 'vertex_only': True}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri', 'tri_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri', 'tri_result_4', 'bevel', {'offset': 0.2}], + # 40 + ['EDGE', {3, 4}, 'tri', 'tri_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + ['VERT', {3}, 'tri', 'tri_result_6', 'bevel', {'offset': 0.2, 'vertex_only': True}], + ['VERT', {3}, 'tri', 'tri_result_7', 'bevel', {'offset': 0.2, 'segments': 2, 'vertex_only': True}], + ['VERT', {3}, 'tri', 'tri_result_8', 'bevel', {'offset': 0.2, 'segments': 3, 'vertex_only': True}], + ['VERT', {1}, 'tri', 'tri_result_9', 'bevel', {'offset': 0.2, 'vertex_only': True}], + # 45 + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri1gap', 'tri1gap_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + # 50 + ['EDGE', {3, 4}, 'tri1gap', 'tri1gap_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_7', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_8', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 5}, 'tri1gap', 'tri1gap_result_9', 'bevel', {'offset': 0.2, 'segments': 3}], + ['VERT', {3}, 'tri1gap', 'tri1gap_result_10', 'bevel', {'offset': 0.2, 'vertex_only': True}], + # 55 + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri2gaps', 'tri2gaps_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + # 60 + ['EDGE', {3, 4}, 'tri2gaps', 'tri2gaps_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_2', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {3, 4, 5}, 'tri3gaps', 'tri3gaps_result_3', 'bevel', {'offset': 0.2, 'segments': 3}], + ['EDGE', {32, 33, 34, 35, 24, 25, 26, 27, 28, 29, 30, 31}, 'cube3', 'cube3_result_1', 'bevel', {'offset': 0.2}], + # 65 + ['EDGE', {32, 33, 34, 35, 24, 25, 26, 27, 28, 29, 30, 31}, 'cube3', 'cube3_result_2', 'bevel', + {'offset': 0.2, 'segments': 2}], + ['EDGE', {32, 35}, 'cube3', 'cube3_result_3', 'bevel', {'offset': 0.2}], + ['EDGE', {24, 35}, 'cube3', 'cube3_result_4', 'bevel', {'offset': 0.2}], + ['EDGE', {24, 32, 35}, 'cube3', 'cube3_result_5', 'bevel', {'offset': 0.2, 'segments': 2}], + ['EDGE', {24, 32, 35}, 'cube3', 'cube3_result_6', 'bevel', {'offset': 0.2, 'segments': 3}], + # 70 + ['EDGE', {0, 1, 6, 7, 12, 14, 16, 17}, 'Tray', 'Tray_result_1', 'bevel', {'offset': 0.01, 'segments': 2}], + ['EDGE', {33, 4, 38, 8, 41, 10, 42, 12, 14, 17, 24, 31}, 'Bumptop', 'Bumptop_result_1', 'bevel', + {'offset': 0.1, 'segments': 4}], + ['EDGE', {16, 14, 15}, 'Multisegment_test', 'Multisegment_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {16, 14, 15}, 'Multisegment_test', 'Multisegment_result_1', 'bevel', {'offset': 0.2}], + ['EDGE', {19, 20, 23, 15}, 'Window_test', 'Window_result_1', 'bevel', {'offset': 0.05, 'segments': 2}], + # 75 + ['EDGE', {8}, 'Cube_hn_test', 'Cube_hn_result_1', 'bevel', {'offset': 0.2, 'harden_normals': True}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_1', 'bevel', + {'offset': 0.2, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_2', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_3', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'PATCH'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_4', 'bevel', + {'offset': 0.2, 'miter_outer': 'ARC'}], + # 80 + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_5', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_6', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_7', 'bevel', + {'offset': 0.2, 'miter_outer': 'PATCH', 'miter_inner': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps_test', 'Blocksteps_result_8', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'PATCH', 'miter_inner': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps2_test', 'Blocksteps2_result_9', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + # 85 + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps3_test', 'Blocksteps3_result_10', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps4_test', 'Blocksteps4_result_11', 'bevel', + {'offset': 0.2, 'segments': 2, 'miter_outer': 'ARC'}], + ['EDGE', {4, 7, 39, 27, 30, 31}, 'Blocksteps4_test', 'Blocksteps4_result_12', 'bevel', + {'offset': 0.2, 'segments': 3, 'miter_outer': 'ARC'}], + ['EDGE', {1, 7}, 'Spike_test', 'Spike_result_1', 'bevel', {'offset': 0.2, 'segments': 3}] + ] + + operator_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operator_test.run_all_tests() + break + elif cmd == "--run-test": + index = int(command[i + 1]) + operator_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/boolean_operator.py b/tests/python/boolean_operator.py new file mode 100644 index 00000000000..8183b527591 --- /dev/null +++ b/tests/python/boolean_operator.py @@ -0,0 +1,68 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# To run all tests, use +# BLENDER_VERBOSE=1 blender path/to/bool_regression.blend --python path/to/boolean_operator.py -- --run_all_tests +# To run one test, use +# BLENDER_VERBOSE=1 blender path/to/bool_regression.blend --python path/to/boolean_operator.py -- --run_test +# where is the index of the test specified in the list tests. + +import bpy +import os +import sys + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest + + +def main(): + tests = [ + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_2', 'intersect_boolean', {'operation': 'INTERSECT'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_3', 'intersect_boolean', {'operation': 'DIFFERENCE'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_4', 'intersect', {'separate_mode': 'CUT'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_5', 'intersect', {'separate_mode': 'ALL'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_6', 'intersect', {'separate_mode': 'NONE'}], + ['FACE', {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 'Cubecube', 'Cubecube_result_7', 'intersect', + {'mode': 'SELECT', 'separate_mode': 'NONE'}], + ['FACE', {6, 7, 8, 9, 10}, 'Cubecone', 'Cubecone_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecones', 'Cubecones_result_1', 'intersect_boolean', {'operation': 'UNION'}], + ] + + operator_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operator_test.run_all_tests() + break + elif cmd == "--run-test": + index = int(command[i + 1]) + operator_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/modifiers.py b/tests/python/modifiers.py new file mode 100644 index 00000000000..22ddfd163b1 --- /dev/null +++ b/tests/python/modifiers.py @@ -0,0 +1,156 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +import bpy +import os +import sys +from random import shuffle, seed +seed(0) + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import ModifierTest, ModifierSpec + + +def get_generate_modifiers_list(test_object_name, randomize=False): + """ + Construct a list of 'Generate' modifiers with default parameters. + :param test_object_name: str - name of test object. Some modifiers like boolean need an extra parameter beside + the default one. E.g. boolean needs object, mask needs vertex group etc... + The extra parameter name will be _ + :param randomize: bool - if True shuffle the list of modifiers. + :return: list of 'Generate' modifiers with default parameters. + """ + + boolean_test_object = bpy.data.objects[test_object_name + "_boolean"] + + generate_modifiers = [ + ModifierSpec('array', 'ARRAY', {}), + ModifierSpec('bevel', 'BEVEL', {'width': 0.1}), + ModifierSpec('boolean', 'BOOLEAN', {'object': boolean_test_object}), + ModifierSpec('build', 'BUILD', {'frame_start': 0, 'frame_duration': 1}), + ModifierSpec('decimate', 'DECIMATE', {}), + ModifierSpec('edge split', 'EDGE_SPLIT', {}), + + # mask can effectively delete the mesh since the vertex group need to be updated after each + # applied modifier. Needs to be tested separately. + # ModifierSpec('mask', 'MASK', {'vertex_group': mask_vertex_group}, False), + + ModifierSpec('mirror', 'MIRROR', {}), + ModifierSpec('multires', 'MULTIRES', {}), + + # remesh can also generate an empty mesh. Skip. + # ModifierSpec('remesh', 'REMESH', {}), + + # ModifierSpec('screw', 'SCREW', {}), # screw can make the test very slow. Skipping for now. + # ModifierSpec('skin', 'SKIN', {}), # skin is not reproducible . + + ModifierSpec('solidify', 'SOLIDIFY', {}), + ModifierSpec('subsurf', 'SUBSURF', {}), + ModifierSpec('triangulate', 'TRIANGULATE', {}), + ModifierSpec('wireframe', 'WIREFRAME', {}) + + ] + + if randomize: + shuffle(generate_modifiers) + + return generate_modifiers + + +def main(): + + mask_first_list = get_generate_modifiers_list("testCubeMaskFirst", randomize=True) + mask_vertex_group = "testCubeMaskFirst" + "_mask" + mask_first_list.insert(0, ModifierSpec('mask', 'MASK', {'vertex_group': mask_vertex_group})) + + tests = [ + ############################### + # List of 'Generate' modifiers on a cube + ############################### + # 0 + # ["testCube", "expectedCube", get_generate_modifiers_list("testCube")], + ["testCubeRandom", "expectedCubeRandom", get_generate_modifiers_list("testCubeRandom", randomize=True)], + ["testCubeMaskFirst", "expectedCubeMaskFirst", mask_first_list], + + ############################################ + # One 'Generate' modifier on primitive meshes + ############################################# + # 4 + ["testCubeArray", "expectedCubeArray", [ModifierSpec('array', 'ARRAY', {})]], + ["testCylinderBuild", "expectedCylinderBuild", [ModifierSpec('build', 'BUILD', {'frame_start': 0, 'frame_duration': 1})]], + + # 6 + ["testConeDecimate", "expectedConeDecimate", [ModifierSpec('decimate', 'DECIMATE', {'ratio': 0.5})]], + ["testCubeEdgeSplit", "expectedCubeEdgeSplit", [ModifierSpec('edge split', 'EDGE_SPLIT', {})]], + ["testSphereMirror", "expectedSphereMirror", [ModifierSpec('mirror', 'MIRROR', {})]], + ["testCylinderMask", "expectedCylinderMask", [ModifierSpec('mask', 'MASK', {'vertex_group': "mask_vertex_group"})]], + ["testConeMultiRes", "expectedConeMultiRes", [ModifierSpec('multires', 'MULTIRES', {})]], + + # 11 + ["testCubeScrew", "expectedCubeScrew", [ModifierSpec('screw', 'SCREW', {})]], + ["testCubeSolidify", "expectedCubeSolidify", [ModifierSpec('solidify', 'SOLIDIFY', {})]], + ["testMonkeySubsurf", "expectedMonkeySubsurf", [ModifierSpec('subsurf', 'SUBSURF', {})]], + ["testSphereTriangulate", "expectedSphereTriangulate", [ModifierSpec('triangulate', 'TRIANGULATE', {})]], + ["testMonkeyWireframe", "expectedMonkeyWireframe", [ModifierSpec('wireframe', 'WIREFRAME', {})]], + #ModifierSpec('skin', 'SKIN', {}), # skin is not reproducible . + + ############################################# + # One 'Deform' modifier on primitive meshes + ############################################# + # 16 + ["testMonkeyArmature", "expectedMonkeyArmature", + [ModifierSpec('armature', 'ARMATURE', {'object': bpy.data.objects['testArmature'], 'use_vertex_groups': True})]], + ["testTorusCast", "expectedTorusCast", [ModifierSpec('cast', 'CAST', {'factor': 2.64})]], + ["testCubeCurve", "expectedCubeCurve", + [ModifierSpec('curve', 'CURVE', {'object': bpy.data.objects['testBezierCurve']})]], + ["testMonkeyDisplace", "expectedMonkeyDisplace", [ModifierSpec('displace', "DISPLACE", {})]], + + # Hook modifier requires moving the hook object to get a mesh change, so can't test it with the current framework + # ["testMonkeyHook", "expectedMonkeyHook", + # [ModifierSpec('hook', 'HOOK', {'object': bpy.data.objects["EmptyHook"], 'vertex_group': "HookVertexGroup"})]], + + # 20 + #ModifierSpec('laplacian_deform', 'LAPLACIANDEFORM', {}) Laplacian requires a more complex mesh + ["testCubeLattice", "expectedCubeLattice", + [ModifierSpec('lattice', 'LATTICE', {'object': bpy.data.objects["testLattice"]})]], + ] + + modifiers_test = ModifierTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + modifiers_test.apply_modifiers = True + modifiers_test.run_all_tests() + break + elif cmd == "--run-test": + modifiers_test.apply_modifiers = False + index = int(command[i + 1]) + modifiers_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py new file mode 100644 index 00000000000..9fb487bcef9 --- /dev/null +++ b/tests/python/modules/mesh_test.py @@ -0,0 +1,495 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# A framework to run regression tests on mesh modifiers and operators based on howardt's mesh_ops_test.py +# +# General idea: +# A test is: +# Object mode +# Select +# Duplicate the object +# Select the object +# Apply operation for each operation in with given parameters +# (an operation is either a modifier or an operator) +# test_mesh = .data +# run test_mesh.unit_test_compare(.data) +# delete the duplicate object +# +# The words in angle brackets are parameters of the test, and are specified in +# the main class MeshTest. +# +# If the environment variable BLENDER_TEST_UPDATE is set to 1, the +# is updated with the new test result. +# Tests are verbose when the environment variable BLENDER_VERBOSE is set. + + +import bpy +import os +import inspect + + +class ModifierSpec: + """ + Holds one modifier and its parameters. + """ + + def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict): + """ + Constructs a modifier spec. + :param modifier_name: str - name of object modifier, e.g. "myFirstSubsurfModif" + :param modifier_type: str - type of object modifier, e.g. "SUBSURF" + :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} + """ + self.modifier_name = modifier_name + self.modifier_type = modifier_type + self.modifier_parameters = modifier_parameters + + def __str__(self): + return "Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ + " with parameters: " + str(self.modifier_parameters) + + +class OperatorSpec: + """ + Holds one operator and its parameters. + """ + + def __init__(self, operator_name: str, operator_parameters: dict, select_mode: str, selection: set): + """ + Constructs an operatorSpec. Raises ValueError if selec_mode is invalid. + :param operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" + :param operator_parameters: dict - {name : val} dictionary containing operator parameters. + :param select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' + :param selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. + """ + self.operator_name = operator_name + self.operator_parameters = operator_parameters + if select_mode not in ['VERT', 'EDGE', 'FACE']: + raise ValueError("select_mode must be either {}, {} or {}".format('VERT', 'EDGE', 'FACE')) + self.select_mode = select_mode + self.selection = selection + + def __str__(self): + return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_parameters) + \ + " in selection mode: " + self.select_mode + ", selecting " + str(self.selection) + + +class MeshTest: + """ + A mesh testing class targeted at testing modifiers and operators on a single object. + It holds a stack of mesh operations, i.e. modifiers or operators. The test is executed using + the public method run_test(). + """ + + def __init__(self, test_object_name: str, expected_object_name: str, operations_stack=None, apply_modifiers=False): + """ + Constructs a MeshTest object. Raises a KeyError if objects with names expected_object_name + or test_object_name don't exist. + :param test_object: str - Name of object of mesh type to run the operations on. + :param expected_object: str - Name of object of mesh type that has the expected + geometry after running the operations. + :param operations_stack: list - stack holding operations to perform on the test_object. + :param apply_modifier: bool - True if we want to apply the modifiers right after adding them to the object. + This affects operations of type ModifierSpec only. + """ + if operations_stack is None: + operations_stack = [] + for operation in operations_stack: + if not (isinstance(operation, ModifierSpec) or isinstance(operation, OperatorSpec)): + raise ValueError("Expected operation of type {} or {}. Got {}". + format(type(ModifierSpec), type(OperatorSpec), + type(operation))) + self.operations_stack = operations_stack + self.apply_modifier = apply_modifiers + + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self.update = os.getenv('BLENDER_TEST_UPDATE') is not None + + # Initialize test objects. + objects = bpy.data.objects + self.test_object = objects[test_object_name] + self.expected_object = objects[expected_object_name] + if self.verbose: + print("Found test object {}".format(test_object_name)) + print("Found test object {}".format(expected_object_name)) + + # Private flag to indicate whether the blend file was updated after the test. + self._test_updated = False + + def set_test_object(self, test_object_name): + """ + Set test object for the test. Raises a KeyError if object with given name does not exist. + :param test_object_name: name of test object to run operations on. + """ + objects = bpy.data.objects + self.test_object = objects[test_object_name] + + def set_expected_object(self, expected_object_name): + """ + Set expected object for the test. Raises a KeyError if object with given name does not exist + :param expected_object_name: Name of expected object. + """ + objects = bpy.data.objects + self.expected_object = objects[expected_object_name] + + def add_modifier(self, modifier_spec: ModifierSpec): + """ + Add a modifier to the operations stack. + :param modifier_spec: modifier to add to the operations stack + """ + self.operations_stack.append(modifier_spec) + if self.verbose: + print("Added modififier {}".format(modifier_spec)) + + def add_operator(self, operator_spec: OperatorSpec): + """ + Adds an operator to the operations stack. + :param operator_spec: OperatorSpec - operator to add to the operations stack. + """ + self.operations_stack.append(operator_spec) + + def _on_failed_test(self, compare, evaluated_test_object): + if self.update: + if self.verbose: + print("Test failed expectantly. Updating expected mesh...") + + # Replace expected object with object we ran operations on, i.e. evaluated_test_object. + evaluated_test_object.location = self.expected_object.location + expected_object_name = self.expected_object.name + + bpy.data.objects.remove(self.expected_object, do_unlink=True) + evaluated_test_object.name = expected_object_name + + # Save file + blend_file = bpy.data.filepath + bpy.ops.wm.save_as_mainfile(filepath=blend_file) + + self._test_updated = True + + # Set new expected object. + self.expected_object = evaluated_test_object + return True + + else: + blender_file = bpy.data.filepath + print("Test failed with error: {}. Resulting object mesh '{}' did not match expected object '{}' " + "from file blender file {}". + format(compare, evaluated_test_object.name, self.expected_object.name, blender_file)) + + return False + + def is_test_updated(self): + """ + Check whether running the test with BLENDER_TEST_UPDATE actually modified the .blend test file. + :return: Bool - True if blend file has been updated. False otherwise. + """ + return self._test_updated + + def _apply_modifier(self, test_object, modifier_spec: ModifierSpec): + """ + Add modifier to object and apply (if modifier_spec.apply_modifier is True) + :param test_object: bpy.types.Object - Blender object to apply modifier on. + :param modifier_spec: ModifierSpec - ModifierSpec object with parameters + """ + modifier = test_object.modifiers.new(modifier_spec.modifier_name, + modifier_spec.modifier_type) + if self.verbose: + print("Created modifier '{}' of type '{}'.". + format(modifier_spec.modifier_name, modifier_spec.modifier_type)) + + for param_name in modifier_spec.modifier_parameters: + try: + setattr(modifier, param_name, modifier_spec.modifier_parameters[param_name]) + if self.verbose: + print("\t set parameter '{}' with value '{}'". + format(param_name, modifier_spec.modifier_parameters[param_name])) + except AttributeError: + # Clean up first + bpy.ops.object.delete() + raise AttributeError("Modifier '{}' has no parameter named '{}'". + format(modifier_spec.modifier_type, param_name)) + + if self.apply_modifier: + bpy.ops.object.modifier_apply(modifier=modifier_spec.modifier_name) + + def _apply_operator(self, test_object, operator: OperatorSpec): + """ + Apply operator on test object. + :param test_object: bpy.types.Object - Blender object to apply operator on. + :param operator: OperatorSpec - OperatorSpec object with parameters. + """ + mesh = test_object.data + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + # Do selection. + bpy.context.tool_settings.mesh_select_mode = (operator.select_mode == 'VERT', + operator.select_mode == 'EDGE', + operator.select_mode == 'FACE') + for index in operator.selection: + if operator.select_mode == 'VERT': + mesh.vertices[index].select = True + elif operator.select_mode == 'EDGE': + mesh.edges[index].select = True + elif operator.select_mode == 'FACE': + mesh.polygons[index].select = True + else: + raise ValueError("Invalid selection mode") + + # Apply operator in edit mode. + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type=operator.select_mode) + mesh_operator = getattr(bpy.ops.mesh, operator.operator_name) + if not mesh_operator: + raise AttributeError("No mesh operator {}".format(operator.operator_name)) + retval = mesh_operator(**operator.operator_parameters) + if retval != {'FINISHED'}: + raise RuntimeError("Unexpected operator return value: {}".format(retval)) + if self.verbose: + print("Applied operator {}".format(operator)) + + bpy.ops.object.mode_set(mode='OBJECT') + + def run_test(self): + """ + Apply operations in self.operations_stack on self.test_object and compare the + resulting mesh with self.expected_object.data + :return: bool - True if the test passed, False otherwise. + """ + self._test_updated = False + bpy.context.view_layer.objects.active = self.test_object + + # Duplicate test object. + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = self.test_object + + self.test_object.select_set(True) + bpy.ops.object.duplicate() + evaluated_test_object = bpy.context.active_object + evaluated_test_object.name = "evaluated_object" + if self.verbose: + print(evaluated_test_object.name, "is set to active") + + # Add modifiers and operators. + for operation in self.operations_stack: + if isinstance(operation, ModifierSpec): + self._apply_modifier(evaluated_test_object, operation) + + elif isinstance(operation, OperatorSpec): + self._apply_operator(evaluated_test_object, operation) + else: + raise ValueError("Expected operation of type {} or {}. Got {}". + format(type(ModifierSpec), type(OperatorSpec), + type(operation))) + + # Compare resulting mesh with expected one. + if self.verbose: + print("Comparing expected mesh with resulting mesh...") + evaluated_test_mesh = evaluated_test_object.data + expected_mesh = self.expected_object.data + compare = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) + success = (compare == 'Same') + + if success: + if self.verbose: + print("Success!") + + # Clean up. + if self.verbose: + print("Cleaning up...") + # Delete evaluated_test_object. + bpy.ops.object.delete() + return True + + else: + return self._on_failed_test(compare, evaluated_test_object) + + +class OperatorTest: + """ + Helper class that stores and executes operator tests. + + Example usage: + + >>> tests = [ + >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_1', 'intersect_boolean', {'operation': 'UNION'}], + >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_2', 'intersect_boolean', {'operation': 'INTERSECT'}], + >>> ] + >>> operator_test = OperatorTest(tests) + >>> operator_test.run_all_tests() + """ + + def __init__(self, operator_tests): + """ + Constructs an operator test. + :param operator_tests: list - list of operator test cases. Each element in the list must contain the following + in the correct order: + 1) select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' + 2) selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. + 3) test_object_name: bpy.Types.Object - test object + 4) expected_object_name: bpy.Types.Object - expected object + 5) operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" + 6) operator_parameters: dict - {name : val} dictionary containing operator parameters. + """ + self.operator_tests = operator_tests + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self._failed_tests_list = [] + + def run_test(self, index: int): + """ + Run a single test from operator_tests list + :param index: int - index of test + :return: bool - True if test is successful. False otherwise. + """ + case = self.operator_tests[index] + if len(case) != 6: + raise ValueError("Expected exactly 6 parameters for each test case, got {}".format(len(case))) + select_mode = case[0] + selection = case[1] + test_object_name = case[2] + expected_object_name = case[3] + operator_name = case[4] + operator_parameters = case[5] + + operator_spec = OperatorSpec(operator_name, operator_parameters, select_mode, selection) + + test = MeshTest(test_object_name, expected_object_name) + test.add_operator(operator_spec) + + success = test.run_test() + if test.is_test_updated(): + # Run the test again if the blend file has been updated. + success = test.run_test() + return success + + def run_all_tests(self): + for index, _ in enumerate(self.operator_tests): + if self.verbose: + print() + print("Running test {}...".format(index)) + success = self.run_test(index) + + if not success: + self._failed_tests_list.append(index) + + if len(self._failed_tests_list) != 0: + print("Following tests failed: {}".format(self._failed_tests_list)) + + blender_path = bpy.app.binary_path + blend_path = bpy.data.filepath + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + python_path = module.__file__ + + print("Run following command to open Blender and run the failing test:") + print("{} {} --python {} -- {} {}" + .format(blender_path, blend_path, python_path, "--run-test", "")) + + raise Exception("Tests {} failed".format(self._failed_tests_list)) + + +class ModifierTest: + """ + Helper class that stores and executes modifier tests. + + Example usage: + + >>> modifier_list = [ + >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), + >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) + >>> ] + >>> tests = [ + >>> ["testCube", "expectedCube", modifier_list], + >>> ["testCube_2", "expectedCube_2", modifier_list] + >>> ] + >>> modifiers_test = ModifierTest(tests) + >>> modifiers_test.run_all_tests() + """ + + def __init__(self, modifier_tests: list, apply_modifiers=False): + """ + Construct a modifier test. + :param modifier_tests: list - list of modifier test cases. Each element in the list must contain the following + in the correct order: + 1) test_object_name: bpy.Types.Object - test object + 2) expected_object_name: bpy.Types.Object - expected object + 3) modifiers: list - list of mesh_test.ModifierSpec objects. + """ + self.modifier_tests = modifier_tests + self.apply_modifiers = apply_modifiers + self.verbose = os.environ.get("BLENDER_VERBOSE") is not None + self._failed_tests_list = [] + + def run_test(self, index: int): + """ + Run a single test from self.modifier_tests list + :param index: int - index of test + :return: bool - True if test passed, False otherwise. + """ + case = self.modifier_tests[index] + if len(case) != 3: + raise ValueError("Expected exactly 3 parameters for each test case, got {}".format(len(case))) + test_object_name = case[0] + expected_object_name = case[1] + spec_list = case[2] + + test = MeshTest(test_object_name, expected_object_name) + if self.apply_modifiers: + test.apply_modifier = True + + for modifier_spec in spec_list: + test.add_modifier(modifier_spec) + + success = test.run_test() + if test.is_test_updated(): + # Run the test again if the blend file has been updated. + success = test.run_test() + + return success + + def run_all_tests(self): + """ + Run all tests in self.modifiers_tests list. Raises an exception if one the tests fails. + """ + for index, _ in enumerate(self.modifier_tests): + if self.verbose: + print() + print("Running test {}...\n".format(index)) + success = self.run_test(index) + + if not success: + self._failed_tests_list.append(index) + + if len(self._failed_tests_list) != 0: + print("Following tests failed: {}".format(self._failed_tests_list)) + + blender_path = bpy.app.binary_path + blend_path = bpy.data.filepath + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + python_path = module.__file__ + + print("Run following command to open Blender and run the failing test:") + print("{} {} --python {} -- {} {}" + .format(blender_path, blend_path, python_path, "--run-test", "")) + + raise Exception("Tests {} failed".format(self._failed_tests_list)) diff --git a/tests/python/operators.py b/tests/python/operators.py new file mode 100644 index 00000000000..c5b3ac745c6 --- /dev/null +++ b/tests/python/operators.py @@ -0,0 +1,172 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +import bpy +import os +import sys +from random import shuffle, seed + +seed(0) + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import OperatorTest, OperatorSpec + +# Central vertical loop of Suzanne +MONKEY_LOOP_VERT = {68, 69, 71, 73, 74, 75, 76, 77, 90, 129, 136, 175, 188, 189, 198, 207, + 216, 223, 230, 301, 302, 303, 304, 305, 306, 307, 308} +MONKEY_LOOP_EDGE = {131, 278, 299, 305, 307, 334, 337, 359, 384, 396, 399, 412, 415, 560, + 567, 572, 577, 615, 622, 627, 632, 643, 648, 655, 660, 707} + + +def main(): + tests = [ + #### 0 + # bisect + ['FACE', {0, 1, 2, 3, 4, 5}, "testCubeBisect", "expectedCubeBisect", "bisect", + {"plane_co": (0, 0, 0), "plane_no": (0, 1, 1), "clear_inner": True, "use_fill": True}], + + # blend from shape + ['FACE', {0, 1, 2, 3, 4, 5}, "testCubeBlendFromShape", "expectedCubeBlendFromShape", "blend_from_shape", + {"shape": "Key 1"}], + + # bridge edge loops + ["FACE", {0, 1}, "testCubeBrigeEdgeLoop", "expectedCubeBridgeEdgeLoop", "bridge_edge_loops", {}], + + # decimate + ["FACE", {i for i in range(500)}, "testMonkeyDecimate", "expectedMonkeyDecimate", "decimate", {"ratio": 0.1}], + + ### 4 + # delete + ["VERT", {3}, "testCubeDeleteVertices", "expectedCubeDeleteVertices", "delete", {}], + ["FACE", {0}, "testCubeDeleteFaces", "expectedCubeDeleteFaces", "delete", {}], + ["EDGE", {0, 1, 2, 3}, "testCubeDeleteEdges", "expectedCubeDeleteEdges", "delete", {}], + + # delete edge loop + ["VERT", MONKEY_LOOP_VERT, "testMokneyDeleteEdgeLoopVertices", "expectedMonkeyDeleteEdgeLoopVertices", + "delete_edgeloop", {}], + ["EDGE", MONKEY_LOOP_EDGE, "testMokneyDeleteEdgeLoopEdges", "expectedMonkeyDeleteEdgeLoopEdges", + "delete_edgeloop", {}], + + ### 9 + # delete loose + ["VERT", {i for i in range(12)}, "testCubeDeleteLooseVertices", "expectedCubeDeleteLooseVertices", + "delete_loose", {"use_verts": True, "use_edges": False, "use_faces": False}], + ["EDGE", {i for i in range(14)}, "testCubeDeleteLooseEdges", "expectedCubeDeleteLooseEdges", + "delete_loose", {"use_verts": False, "use_edges": True, "use_faces": False}], + ["FACE", {i for i in range(7)}, "testCubeDeleteLooseFaces", "expectedCubeDeleteLooseFaces", + "delete_loose", {"use_verts": False, "use_edges": False, "use_faces": True}], + + # dissolve degenerate + ["VERT", {i for i in range(8)}, "testCubeDissolveDegenerate", "expectedCubeDissolveDegenerate", + "dissolve_degenerate", {}], + + ### 13 + # dissolve edges + ["EDGE", {0, 5, 6, 9}, "testCylinderDissolveEdges", "expectedCylinderDissolveEdges", + "dissolve_edges", {}], + + # dissolve faces + ["VERT", {5, 34, 47, 49, 83, 91, 95}, "testCubeDissolveFaces", "expectedCubeDissolveFaces", "dissolve_faces", + {}], + + ### 15 + # dissolve verts + ["VERT", {16, 20, 22, 23, 25}, "testCubeDissolveVerts", "expectedCubeDissolveVerts", "dissolve_verts", {}], + + # duplicate + ["VERT", {i for i in range(33)} - {23}, "testConeDuplicateVertices", "expectedConeDuplicateVertices", + "duplicate", {}], + ["VERT", {23}, "testConeDuplicateOneVertex", "expectedConeDuplicateOneVertex", "duplicate", {}], + ["FACE", {6, 9}, "testConeDuplicateFaces", "expectedConeDuplicateFaces", "duplicate", {}], + ["EDGE", {i for i in range(64)}, "testConeDuplicateEdges", "expectedConeDuplicateEdges", "duplicate", {}], + + ### 20 + # edge collapse + ["EDGE", {1, 9, 4}, "testCylinderEdgeCollapse", "expectedCylinderEdgeCollapse", "edge_collapse", {}], + + # edge face add + ["VERT", {1, 3, 4, 5, 7}, "testCubeEdgeFaceAddFace", "expectedCubeEdgeFaceAddFace", "edge_face_add", {}], + ["VERT", {4, 5}, "testCubeEdgeFaceAddEdge", "expectedCubeEdgeFaceAddEdge", "edge_face_add", {}], + + # edge rotate + ["EDGE", {1}, "testCubeEdgeRotate", "expectedCubeEdgeRotate", "edge_rotate", {}], + + # edge split + ["EDGE", {2, 5, 8, 11, 14, 17, 20, 23}, "testCubeEdgeSplit", "expectedCubeEdgeSplit", "edge_split", {}], + + ### 25 + # face make planar + ["FACE", {i for i in range(500)}, "testMonkeyFaceMakePlanar", "expectedMonkeyFaceMakePlanar", + "face_make_planar", {}], + + # face split by edges + ["VERT", {i for i in range(6)}, "testPlaneFaceSplitByEdges", "expectedPlaneFaceSplitByEdges", + "face_split_by_edges", {}], + + # fill + ["EDGE", {20, 21, 22, 23, 24, 45, 46, 47, 48, 49}, "testIcosphereFill", "expectedIcosphereFill", + "fill", {}], + ["EDGE", {20, 21, 22, 23, 24, 45, 46, 47, 48, 49}, "testIcosphereFillUseBeautyFalse", + "expectedIcosphereFillUseBeautyFalse", "fill", {"use_beauty": False}], + + # fill grid + ["EDGE", {1, 2, 3, 4, 5, 7, 9, 10, 11, 12, 13, 15}, "testPlaneFillGrid", "expectedPlaneFillGrid", + "fill_grid", {}], + ["EDGE", {1, 2, 3, 4, 5, 7, 9, 10, 11, 12, 13, 15}, "testPlaneFillGridSimpleBlending", + "expectedPlaneFillGridSimpleBlending", "fill_grid", {"use_interp_simple": True}], + + ### 31 + # fill holes + ["VERT", {i for i in range(481)}, "testSphereFillHoles", "expectedSphereFillHoles", "fill_holes", {"sides": 9}], + + # inset faces + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, "testCubeInset", + "expectedCubeInset", "inset", {"thickness": 0.2}], + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, + "testCubeInsetEvenOffsetFalse", "expectedCubeInsetEvenOffsetFalse", + "inset", {"thickness": 0.2, "use_even_offset": False}], + ["VERT", {5, 16, 17, 19, 20, 22, 23, 34, 47, 49, 50, 52, 59, 61, 62, 65, 83, 91, 95}, "testCubeInsetDepth", + "expectedCubeInsetDepth", "inset", {"thickness": 0.2, "depth": 0.2}], + ["FACE", {35, 36, 37, 45, 46, 47, 55, 56, 57}, "testGridInsetRelativeOffset", "expectedGridInsetRelativeOffset", + "inset", {"thickness": 0.4, "use_relative_offset": True}], + ] + + operators_test = OperatorTest(tests) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + operators_test.run_all_tests() + break + elif cmd == "--run-test": + operators_test.apply_modifiers = False + index = int(command[i + 1]) + operators_test.run_test(index) + break + + +if __name__ == "__main__": + try: + main() + except: + import traceback + + traceback.print_exc() + sys.exit(1)