Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHoward Trickey <howard.trickey@gmail.com>2020-01-13 15:11:45 +0300
committerHoward Trickey <howard.trickey@gmail.com>2020-01-13 15:11:45 +0300
commit3fdc04d3ee0ae459eebb33dede58abab20a2c1d4 (patch)
treecc055816c783eee60077eaa61968600303edc756 /tests/python
parenta60606e4678a3291f89ebac8ec5f94f5a6a40aff (diff)
Accepting patch D5357: Modifiers and operators automated testing.
Patch from Habib Gahbiche (zazizizou) moves the "run operator and compare mesh to a golden" paradigm used in bevel and boolean tests into a general framework that separates the test specs from the blend files. Then adds some other operator and modifier tests using the new framework. Diff D5357.id20724.diff was applied. New .blend files, modifiers.blend and operators.blend are needed in the tests/modeling svn directory; those were separately committed.
Diffstat (limited to 'tests/python')
-rw-r--r--tests/python/CMakeLists.txt27
-rw-r--r--tests/python/bevel_operator.py184
-rw-r--r--tests/python/boolean_operator.py68
-rw-r--r--tests/python/modifiers.py156
-rw-r--r--tests/python/modules/mesh_test.py495
-rw-r--r--tests/python/operators.py172
6 files changed, 1100 insertions, 2 deletions
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 <index>
+# where <index> 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 #####
+
+# <pep8 compliant>
+
+# 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 <index>
+# where <index> 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 #####
+
+# <pep8 compliant>
+
+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 <test_object_name>_<modifier_type>
+ :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 #####
+
+# <pep8 compliant>
+
+# 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 <test_object>
+# Duplicate the object
+# Select the object
+# Apply operation for each operation in <operations_stack> with given parameters
+# (an operation is either a modifier or an operator)
+# test_mesh = <test_object>.data
+# run test_mesh.unit_test_compare(<expected object>.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 <expected_object>
+# 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", "<test_index>"))
+
+ 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", "<test_index>"))
+
+ 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 #####
+
+# <pep8 compliant>
+
+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)