diff --git a/release/scripts/startup/bl_operators/wm.py b/release/scripts/startup/bl_operators/wm.py index f900b0ee3df..9e2ce8b83ad 100644 --- a/release/scripts/startup/bl_operators/wm.py +++ b/release/scripts/startup/bl_operators/wm.py @@ -87,8 +87,10 @@ def context_path_validate(context, data_path): # One of the items in the rna path is None, just ignore this value = Ellipsis else: - # We have a real error in the rna path, don't ignore that - raise + # Print invalid path, but don't show error to the users and fully + # break the UI if the operator is bound to an event like left click. + print("context_path_validate error: context.%s not found (invalid keymap entry?)" % data_path) + value = Ellipsis return value diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a3d4b2a501e..80d749c7040 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,7 +39,8 @@ unset(_default_test_python_exe) # set(TEST_BLENDER_EXE valgrind --track-origins=yes --error-limit=no ${TEST_BLENDER_EXE}) # Standard Blender arguments for running tests. -set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup) +# Specify exit code so that if a Python script error happens, the test fails. +set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --python-exit-code 1) # Python CTests if(WITH_BLENDER AND WITH_PYTHON) diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 753bc7c5b91..a3df01fdbe2 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -184,6 +184,22 @@ add_blender_test( --run-all-tests ) +add_blender_test( + physics_cloth + ${TEST_SRC_DIR}/physics/cloth_test.blend + --python ${TEST_PYTHON_DIR}/physics_cloth.py + -- + --run-all-tests +) + +add_blender_test( + physics_softbody + ${TEST_SRC_DIR}/physics/softbody_test.blend + --python ${TEST_PYTHON_DIR}/physics_softbody.py + -- + --run-all-tests +) + add_blender_test( constraints --python ${CMAKE_CURRENT_LIST_DIR}/bl_constraints.py diff --git a/tests/python/bevel_operator.py b/tests/python/bevel_operator.py index 3cdbeb9300b..884bd356b96 100644 --- a/tests/python/bevel_operator.py +++ b/tests/python/bevel_operator.py @@ -176,9 +176,4 @@ def main(): if __name__ == "__main__": - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_alembic_io_test.py b/tests/python/bl_alembic_io_test.py index 2786a2db4d7..b9eca3057e7 100644 --- a/tests/python/bl_alembic_io_test.py +++ b/tests/python/bl_alembic_io_test.py @@ -375,12 +375,4 @@ def main(): if __name__ == "__main__": - import traceback - # So a python error exits Blender itself too - try: - main() - except SystemExit: - raise - except: - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_blendfile_io.py b/tests/python/bl_blendfile_io.py index 0b055b9d46a..ab06e313566 100644 --- a/tests/python/bl_blendfile_io.py +++ b/tests/python/bl_blendfile_io.py @@ -77,9 +77,4 @@ def main(): if __name__ == '__main__': import sys sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_blendfile_liblink.py b/tests/python/bl_blendfile_liblink.py index 7d93d7c8455..d1cc7efc7fd 100644 --- a/tests/python/bl_blendfile_liblink.py +++ b/tests/python/bl_blendfile_liblink.py @@ -75,9 +75,4 @@ def main(): if __name__ == '__main__': import sys sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_bundled_modules.py b/tests/python/bl_bundled_modules.py index 3ef5040af01..d3fe2861d9e 100644 --- a/tests/python/bl_bundled_modules.py +++ b/tests/python/bl_bundled_modules.py @@ -21,7 +21,7 @@ # Test that modules we ship with our Python installation are available import bz2 -import cffi +import ctypes import lzma import numpy import sqlite3 diff --git a/tests/python/bl_constraints.py b/tests/python/bl_constraints.py index 13a431541bc..9fce8acc84e 100644 --- a/tests/python/bl_constraints.py +++ b/tests/python/bl_constraints.py @@ -255,12 +255,4 @@ def main(): if __name__ == "__main__": - import traceback - # So a python error exits Blender itself too - try: - main() - except SystemExit: - raise - except: - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_load_addons.py b/tests/python/bl_load_addons.py index f0c2f3f7fdf..01f0b4d72d8 100644 --- a/tests/python/bl_load_addons.py +++ b/tests/python/bl_load_addons.py @@ -144,11 +144,4 @@ def main(): if __name__ == "__main__": - - # So a python error exits(1) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_load_py_modules.py b/tests/python/bl_load_py_modules.py index 5d1a5dd8ee0..c6ad53b1f74 100644 --- a/tests/python/bl_load_py_modules.py +++ b/tests/python/bl_load_py_modules.py @@ -234,10 +234,4 @@ def main(): if __name__ == "__main__": - # So a python error exits(1) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_mesh_modifiers.py b/tests/python/bl_mesh_modifiers.py index 80f810ffad8..746e7a183a3 100644 --- a/tests/python/bl_mesh_modifiers.py +++ b/tests/python/bl_mesh_modifiers.py @@ -842,17 +842,7 @@ if __name__ == "__main__": print("Load Handler:", bpy.data.filepath) if load_handler.first is False: bpy.app.handlers.scene_update_post.remove(load_handler) - try: - main() - import sys - sys.exit(0) - except: - import traceback - traceback.print_exc() - - # import sys - # sys.exit(1) # comment to debug - + main() else: load_handler.first = False diff --git a/tests/python/bl_mesh_validate.py b/tests/python/bl_mesh_validate.py index 47a5e5efe47..8c5d914f92a 100644 --- a/tests/python/bl_mesh_validate.py +++ b/tests/python/bl_mesh_validate.py @@ -152,10 +152,4 @@ def main(): if __name__ == "__main__": - # So a python error exits(1) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/bl_pyapi_idprop_datablock.py b/tests/python/bl_pyapi_idprop_datablock.py index 648b63d1637..44fec6a9043 100644 --- a/tests/python/bl_pyapi_idprop_datablock.py +++ b/tests/python/bl_pyapi_idprop_datablock.py @@ -20,7 +20,6 @@ import bpy import sys import os import tempfile -import traceback import inspect from bpy.types import UIList @@ -331,11 +330,4 @@ def main(): if __name__ == "__main__": - try: - main() - except: - import traceback - - traceback.print_exc() - sys.stderr.flush() - os._exit(1) + main() diff --git a/tests/python/bl_test.py b/tests/python/bl_test.py index 173d575a912..6315ffbfa9d 100644 --- a/tests/python/bl_test.py +++ b/tests/python/bl_test.py @@ -136,12 +136,7 @@ def main(): print(" Running: '%s'" % run) print(" MD5: '%s'!" % md5) - try: - result = eval(run) - except: - import traceback - traceback.print_exc() - sys.exit(1) + result = eval(run) if write_blend is not None: print(" Writing Blend: %s" % write_blend) @@ -188,10 +183,4 @@ def main(): if __name__ == "__main__": - # So a python error exits(1) - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/boolean_operator.py b/tests/python/boolean_operator.py index b05e60eea6c..5a674c35a47 100644 --- a/tests/python/boolean_operator.py +++ b/tests/python/boolean_operator.py @@ -60,9 +60,4 @@ def main(): if __name__ == "__main__": - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/modifiers.py b/tests/python/modifiers.py index 697cddc9ba2..5e032f658af 100644 --- a/tests/python/modifiers.py +++ b/tests/python/modifiers.py @@ -255,9 +255,4 @@ def main(): if __name__ == "__main__": - try: - main() - except: - import traceback - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py index f188d998884..af0e78257d5 100644 --- a/tests/python/modules/mesh_test.py +++ b/tests/python/modules/mesh_test.py @@ -73,6 +73,28 @@ class ModifierSpec: " with parameters: " + str(self.modifier_parameters) +class PhysicsSpec: + """ + Holds one Physics modifier and its parameters. + """ + + def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict, frame_end: int): + """ + Constructs a physics spec. + :param modifier_name: str - name of object modifier, e.g. "Cloth" + :param modifier_type: str - type of object modifier, e.g. "CLOTH" + :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} + :param frame_end:int - the last frame of the simulation at which it is baked + """ + self.modifier_name = modifier_name + self.modifier_type = modifier_type + self.modifier_parameters = modifier_parameters + self.frame_end = frame_end + + def __str__(self): + return "Physics Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ + " with parameters: " + str(self.modifier_parameters) + " with frame end: " + str(self.frame_end) + class OperatorSpec: """ Holds one operator and its parameters. @@ -105,7 +127,7 @@ class MeshTest: the public method run_test(). """ - def __init__(self, test_object_name: str, expected_object_name: str, operations_stack=None, apply_modifiers=False): + def __init__(self, test_object_name: str, expected_object_name: str, operations_stack=None, apply_modifiers=False, threshold=None): """ Constructs a MeshTest object. Raises a KeyError if objects with names expected_object_name or test_object_name don't exist. @@ -125,6 +147,7 @@ class MeshTest: type(operation))) self.operations_stack = operations_stack self.apply_modifier = apply_modifiers + self.threshold = threshold self.verbose = os.environ.get("BLENDER_VERBOSE") is not None self.update = os.getenv('BLENDER_TEST_UPDATE') is not None @@ -235,6 +258,49 @@ class MeshTest: if self.apply_modifier: bpy.ops.object.modifier_apply(modifier=modifier_spec.modifier_name) + + def _bake_current_simulation(self, obj, test_mod_type, test_mod_name, frame_end): + for scene in bpy.data.scenes: + for modifier in obj.modifiers: + if modifier.type == test_mod_type: + obj.modifiers[test_mod_name].point_cache.frame_end = frame_end + override = {'scene': scene, 'active_object': obj, 'point_cache': modifier.point_cache} + bpy.ops.ptcache.bake(override, bake=True) + break + + def _apply_physics_settings(self, test_object, physics_spec: PhysicsSpec): + """ + Apply Physics settings to test objects. + """ + scene = bpy.context.scene + scene.frame_set(1) + modifier = test_object.modifiers.new(physics_spec.modifier_name, + physics_spec.modifier_type) + physics_setting = modifier.settings + if self.verbose: + print("Created modifier '{}' of type '{}'.". + format(physics_spec.modifier_name, physics_spec.modifier_type)) + + + for param_name in physics_spec.modifier_parameters: + try: + setattr(physics_setting, param_name, physics_spec.modifier_parameters[param_name]) + if self.verbose: + print("\t set parameter '{}' with value '{}'". + format(param_name, physics_spec.modifier_parameters[param_name])) + except AttributeError: + # Clean up first + bpy.ops.object.delete() + raise AttributeError("Modifier '{}' has no parameter named '{}'". + format(physics_spec.modifier_type, param_name)) + + scene.frame_set(physics_spec.frame_end + 1) + + self._bake_current_simulation(test_object, physics_spec.modifier_type, physics_spec.modifier_name, physics_spec.frame_end) + if self.apply_modifier: + bpy.ops.object.modifier_apply(modifier=physics_spec.modifier_name) + + def _apply_operator(self, test_object, operator: OperatorSpec): """ Apply operator on test object. @@ -302,9 +368,12 @@ class MeshTest: elif isinstance(operation, OperatorSpec): self._apply_operator(evaluated_test_object, operation) + + elif isinstance(operation, PhysicsSpec): + self._apply_physics_settings(evaluated_test_object, operation) else: - raise ValueError("Expected operation of type {} or {}. Got {}". - format(type(ModifierSpec), type(OperatorSpec), + raise ValueError("Expected operation of type {} or {} or {}. Got {}". + format(type(ModifierSpec), type(OperatorSpec), type(PhysicsSpec), type(operation))) # Compare resulting mesh with expected one. @@ -312,7 +381,10 @@ class MeshTest: print("Comparing expected mesh with resulting mesh...") evaluated_test_mesh = evaluated_test_object.data expected_mesh = self.expected_object.data - compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) + if self.threshold: + compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh, threshold=self.threshold) + else: + compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) compare_success = (compare_result == 'Same') # Also check if invalid geometry (which is never expected) had to be corrected... @@ -434,7 +506,7 @@ class ModifierTest: >>> modifiers_test.run_all_tests() """ - def __init__(self, modifier_tests: list, apply_modifiers=False): + def __init__(self, modifier_tests: list, apply_modifiers=False, threshold=None): """ Construct a modifier test. :param modifier_tests: list - list of modifier test cases. Each element in the list must contain the following @@ -445,6 +517,7 @@ class ModifierTest: """ self.modifier_tests = modifier_tests self.apply_modifiers = apply_modifiers + self.threshold = threshold self.verbose = os.environ.get("BLENDER_VERBOSE") is not None self._failed_tests_list = [] @@ -461,7 +534,7 @@ class ModifierTest: expected_object_name = case[1] spec_list = case[2] - test = MeshTest(test_object_name, expected_object_name) + test = MeshTest(test_object_name, expected_object_name, threshold=self.threshold) if self.apply_modifiers: test.apply_modifier = True diff --git a/tests/python/operators.py b/tests/python/operators.py index c5b3ac745c6..626aaedc724 100644 --- a/tests/python/operators.py +++ b/tests/python/operators.py @@ -163,10 +163,4 @@ def main(): if __name__ == "__main__": - try: - main() - except: - import traceback - - traceback.print_exc() - sys.exit(1) + main() diff --git a/tests/python/physics_cloth.py b/tests/python/physics_cloth.py new file mode 100644 index 00000000000..5b9151ea089 --- /dev/null +++ b/tests/python/physics_cloth.py @@ -0,0 +1,51 @@ +# ##### 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 os +import sys + +import bpy + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import ModifierTest, PhysicsSpec + + +def main(): + test = [ + ["testCloth", "expectedCloth", + [PhysicsSpec('Cloth', 'CLOTH', {'quality': 5}, 35)]], + ] + cloth_test = ModifierTest(test, threshold=1e-3) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + cloth_test.apply_modifiers = True + cloth_test.run_all_tests() + break + elif cmd == "--run-test": + cloth_test.apply_modifiers = False + index = int(command[i + 1]) + cloth_test.run_test(index) + break + + +if __name__ == "__main__": + main() diff --git a/tests/python/physics_softbody.py b/tests/python/physics_softbody.py new file mode 100644 index 00000000000..8d431be742c --- /dev/null +++ b/tests/python/physics_softbody.py @@ -0,0 +1,51 @@ +# ##### 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 os +import sys + +import bpy + +sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from modules.mesh_test import ModifierTest, PhysicsSpec + + +def main(): + test = [ + ["testSoftBody", "expectedSoftBody", + [PhysicsSpec('Softbody', 'SOFT_BODY', {'use_goal': False, 'bend': 8, 'pull': 0.8, 'push': 0.8}, 45)]], + ] + softBody_test = ModifierTest(test) + + command = list(sys.argv) + for i, cmd in enumerate(command): + if cmd == "--run-all-tests": + softBody_test.apply_modifiers = True + softBody_test.run_all_tests() + break + elif cmd == "--run-test": + softBody_test.apply_modifiers = False + index = int(command[i + 1]) + softBody_test.run_test(index) + break + + +if __name__ == "__main__": + main()