From 7bc893c8279457ef32eb4a0d4e98fe40de5afdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 25 Feb 2020 12:16:34 +0100 Subject: Start of unit test framework for constraints Currently this only tests the Child Of constraint. My aim is to cover constraints with tests before they are refactored/altered. No functional changes. --- tests/python/CMakeLists.txt | 7 ++ tests/python/bl_constraints.py | 179 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 tests/python/bl_constraints.py (limited to 'tests') diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 358200dece4..c47c7a5b4fc 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -183,6 +183,13 @@ add_blender_test( --run-all-tests ) +add_blender_test( + constraints + --python ${CMAKE_CURRENT_LIST_DIR}/bl_constraints.py + -- + --testdir "${TEST_SRC_DIR}/constraints" +) + # ------------------------------------------------------------------------------ # OPERATORS TESTS add_blender_test( diff --git a/tests/python/bl_constraints.py b/tests/python/bl_constraints.py new file mode 100644 index 00000000000..1b3ced9b4d7 --- /dev/null +++ b/tests/python/bl_constraints.py @@ -0,0 +1,179 @@ +# ##### 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 ##### + +""" +./blender.bin --background -noaudio --factory-startup --python tests/python/bl_constraints.py -- --testdir /path/to/lib/tests/constraints +""" + +import pathlib +import sys +import unittest + +import bpy +from mathutils import Matrix + + +class AbstractConstraintTests(unittest.TestCase): + """Useful functionality for constraint tests.""" + + def setUp(self): + bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "constraints.blend")) + + def assert_matrix(self, actual, expect, places=6, delta=None): + """Asserts that the matrices almost equal.""" + self.assertEqual(len(actual), 4, 'Expected a 4x4 matrix') + + # TODO(Sybren): decompose the matrices and compare loc, rot, and scale separately. + # That'll probably improve readability & understandability of test failures. + for row, (act_row, exp_row) in enumerate(zip(actual, expect)): + for col, (act, exp) in enumerate(zip(act_row, exp_row)): + self.assertAlmostEqual(act, exp, places=places, delta=delta, + msg=f'{act} != {exp} at element [{row}][{col}]') + + def matrix(self, object_name: str) -> Matrix: + """Return the evaluated world matrix.""" + depsgraph = bpy.context.view_layer.depsgraph + depsgraph.update() + ob_orig = bpy.context.scene.objects[object_name] + ob_eval = ob_orig.evaluated_get(depsgraph) + return ob_eval.matrix_world + + def matrix_test(self, object_name: str, expect: Matrix): + """Assert that the object's world matrix is as expected.""" + actual = self.matrix(object_name) + self.assert_matrix(actual, expect) + + def constraint_context(self, constraint_name: str) -> dict: + """Return a context suitable for calling constraint operators. + + Assumes the owner is called "{constraint_name}.owner". + """ + owner = bpy.context.scene.objects['Child Of.owner'] + constraint = owner.constraints[constraint_name] + context = { + **bpy.context.copy(), + 'constraint': constraint, + } + return context + +class ChildOfTest(AbstractConstraintTests): + def test_childof_simple_parent(self): + """Child Of: simple evaluation.""" + initial_matrix = Matrix(( + (0.5872668623924255, -0.3642929494380951, 0.29567837715148926, 1.0886117219924927), + (0.31689348816871643, 0.7095895409584045, 0.05480116978287697, 2.178966999053955), + (-0.21244174242019653, 0.06738340109586716, 0.8475662469863892, 3.2520291805267334), + (0.0, 0.0, 0.0, 1.0), + )) + self.matrix_test('Child Of.owner', initial_matrix) + + context = self.constraint_context('Child Of') + bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', Matrix(( + (0.9992385506629944, 0.019844001159071922, -0.03359175845980644, 0.10000011324882507), + (-0.01744179055094719, 0.997369647026062, 0.07035345584154129, 0.1999998837709427), + (0.034899525344371796, -0.06971397250890732, 0.9969563484191895, 0.3000001311302185), + (0.0, 0.0, 0.0, 1.0), + ))) + + bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', initial_matrix) + + def test_childof_rotation_only(self): + """Child Of: rotation only.""" + owner = bpy.context.scene.objects['Child Of.owner'] + constraint = owner.constraints['Child Of'] + constraint.use_location_x = constraint.use_location_y = constraint.use_location_z = False + constraint.use_scale_x = constraint.use_scale_y = constraint.use_scale_z = False + + initial_matrix = Matrix(( + (0.8340795636177063, -0.4500490725040436, 0.31900957226753235, 0.10000000149011612), + (0.4547243118286133, 0.8883093595504761, 0.06428192555904388, 0.20000000298023224), + (-0.31230923533439636, 0.09144517779350281, 0.9455690383911133, 0.30000001192092896), + (0.0, 0.0, 0.0, 1.0), + )) + self.matrix_test('Child Of.owner', initial_matrix) + + context = self.constraint_context('Child Of') + bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', Matrix(( + (0.9992386102676392, 0.019843975082039833, -0.033591702580451965, 0.10000000149011612), + (-0.017441781237721443, 0.9973695874214172, 0.0703534483909607, 0.20000000298023224), + (0.03489946573972702, -0.06971397250890732, 0.9969563484191895, 0.30000001192092896), + (0.0, 0.0, 0.0, 1.0), + ))) + + bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', initial_matrix) + + + def test_childof_no_x_axis(self): + """Child Of: loc/rot/scale on only Y and Z axes.""" + owner = bpy.context.scene.objects['Child Of.owner'] + constraint = owner.constraints['Child Of'] + constraint.use_location_x = False + constraint.use_rotation_x = False + constraint.use_scale_x = False + + initial_matrix = Matrix(( + (0.8294582366943359, -0.4013831615447998, 0.2102886438369751, 0.10000000149011612), + (0.46277597546577454, 0.6895919442176819, 0.18639995157718658, 2.2317214012145996), + (-0.31224438548088074, -0.06574578583240509, 0.8546382784843445, 3.219514846801758), + (0.0, 0.0, 0.0, 1.0), + )) + self.matrix_test('Child Of.owner', initial_matrix) + + context = self.constraint_context('Child Of') + bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', Matrix(( + (0.9228900671005249, 0.23250490427017212, -0.035540513694286346, 0.10000000149011612), + (-0.011224273592233658, 0.9838480949401855, 0.24731633067131042, 0.21246682107448578), + (0.0383986234664917, -0.3163823187351227, 0.9553266167640686, 0.27248233556747437), + (0.0, 0.0, 0.0, 1.0), + ))) + + bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of') + self.matrix_test('Child Of.owner', initial_matrix) + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +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) -- cgit v1.2.3