diff options
author | Sybren A. Stüvel <sybren@stuvel.eu> | 2017-04-14 13:54:20 +0300 |
---|---|---|
committer | Sybren A. Stüvel <sybren@stuvel.eu> | 2017-04-14 13:54:20 +0300 |
commit | 6af131fa5cd90a6290cb44a683d2f6da01ad8392 (patch) | |
tree | ffdc2647aff1c934eac135e3358f9429b3aaf4a2 /tests | |
parent | 0b55b8cc6a37fa4e74c55c9ccb54950c5f546bd6 (diff) |
Added simple unittests for Alembic exporter
This test checks that a set of cubes are exported with the correct
transform, both with flatten=True and flatten=False.
This commit also adds an easy to use superclass for upcoming Alembic
unit tests.
Diffstat (limited to 'tests')
-rw-r--r-- | tests/python/CMakeLists.txt | 20 | ||||
-rwxr-xr-x | tests/python/alembic_tests.py | 225 |
2 files changed, 245 insertions, 0 deletions
diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 393aa512f0c..8ff2f77c38e 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -442,3 +442,23 @@ if(WITH_CYCLES) MESSAGE(STATUS "Disabling Cycles tests because tests folder does not exist") endif() endif() + +if(WITH_ALEMBIC) + if(MSVC) + add_test(NAME cycles_${subject}_test + COMMAND + "$<TARGET_FILE_DIR:blender>/${BLENDER_VERSION_MAJOR}.${BLENDER_VERSION_MINOR}/python/bin/python$<$<CONFIG:Debug>:_d>" + ${CMAKE_CURRENT_LIST_DIR}/alembic_tests.py + --blender "${TEST_BLENDER_EXE_BARE}" + --testdir "${TEST_SRC_DIR}/alembic" + --alembic-root "${ALEMBIC_ROOT_DIR}" + ) + else() + add_test(alembic_tests + ${CMAKE_CURRENT_LIST_DIR}/alembic_tests.py + --blender "${TEST_BLENDER_EXE_BARE}" + --testdir "${TEST_SRC_DIR}/alembic" + --alembic-root "${ALEMBIC_ROOT_DIR}" + ) + endif() +endif() diff --git a/tests/python/alembic_tests.py b/tests/python/alembic_tests.py new file mode 100755 index 00000000000..1af2a157b64 --- /dev/null +++ b/tests/python/alembic_tests.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# ##### 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 argparse +import functools +import shutil +import pathlib +import subprocess +import sys +import tempfile +import unittest + + +def with_tempdir(wrapped): + """Creates a temporary directory for the function, cleaning up after it returns normally. + + When the wrapped function raises an exception, the contents of the temporary directory + remain available for manual inspection. + + The wrapped function is called with an extra positional argument containing + the pathlib.Path() of the temporary directory. + """ + + @functools.wraps(wrapped) + def decorator(*args, **kwargs): + dirname = tempfile.mkdtemp(prefix='blender-alembic-test') + try: + retval = wrapped(*args, pathlib.Path(dirname), **kwargs) + except: + print('Exception in %s, not cleaning up temporary directory %s' % (wrapped, dirname)) + raise + else: + shutil.rmtree(dirname) + return retval + + return decorator + + +class AbstractAlembicTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + import re + + parser = argparse.ArgumentParser() + parser.add_argument('--blender', required=True) + parser.add_argument('--testdir', required=True) + parser.add_argument('--alembic-root', required=True) + args, _ = parser.parse_known_args() + + cls.blender = args.blender + cls.testdir = pathlib.Path(args.testdir) + cls.alembic_root = pathlib.Path(args.alembic_root) + + # 'abcls' outputs ANSI colour codes, even when stdout is not a terminal. + # See https://github.com/alembic/alembic/issues/120 + cls.ansi_remove_re = re.compile(rb'\x1b[^m]*m') + + # 'abcls' array notation, like "name[16]" + cls.abcls_array = re.compile(r'^(?P<name>[^\[]+)(\[(?P<arraysize>\d+)\])?$') + + def run_blender(self, filepath: str, python_script: str, timeout: int=300) -> str: + """Runs Blender by opening a blendfile and executing a script. + + Returns Blender's stdout + stderr combined into one string. + + :param filepath: taken relative to self.testdir. + :param timeout: in seconds + """ + + blendfile = self.testdir / filepath + + command = ( + self.blender, + '--background', + '-noaudio', + '--factory-startup', + '--enable-autoexec', + str(blendfile), + '-E', 'CYCLES', + '--python-exit-code', '47', + '--python-expr', python_script, + ) + + proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + timeout=timeout) + output = proc.stdout.decode('utf8') + if proc.returncode: + self.fail('Error %d running Blender:\n%s' % (proc.returncode, output)) + + return output + + def abcprop(self, filepath: pathlib.Path, proppath: str) -> dict: + """Uses abcls to obtain compound property values from an Alembic object. + + A dict of subproperties is returned, where the values are just strings + as returned by abcls. + + The Python bindings for Alembic are old, and only compatible with Python 2.x, + so that's why we can't use them here, and have to rely on other tooling. + """ + + abcls = self.alembic_root / 'bin' / 'abcls' + + command = (str(abcls), '-vl', '%s%s' % (filepath, proppath)) + proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + timeout=30) + + coloured_output = proc.stdout + output = self.ansi_remove_re.sub(b'', coloured_output).decode('utf8') + + if proc.returncode: + self.fail('Error %d running abcls:\n%s' % (proc.returncode, output)) + + # Mapping from value type to callable that can convert a string to Python values. + converters = { + 'bool_t': int, + 'uint8_t': int, + 'float64_t': float, + } + + result = {} + + # Ideally we'd get abcls to output JSON, see https://github.com/alembic/alembic/issues/121 + lines = output.split('\n') + for info, value in zip(lines[0::2], lines[1::2]): + proptype, valtype_and_arrsize, name_and_extent = info.split() + + # Parse name and extent + m = self.abcls_array.match(name_and_extent) + if not m: + self.fail('Unparsable name/extent from abcls: %s' % name_and_extent) + name, extent = m.group('name'), m.group('arraysize') + + if extent != '1': + self.fail('Unsupported extent %s for property %s/%s' % (extent, proppath, name)) + + # Parse type and convert values + m = self.abcls_array.match(valtype_and_arrsize) + if not m: + self.fail('Unparsable value type from abcls: %s' % valtype_and_arrsize) + valtype, arraysize = m.group('name'), m.group('arraysize') + + try: + conv = converters[valtype] + except KeyError: + self.fail('Unsupported type %s for property %s/%s' % (valtype, proppath, name)) + + if arraysize is None: + result[name] = conv(value) + else: + values = [conv(v.strip()) for v in value.split(',')] + result[name] = values + + return result + + def assertAlmostEqualFloatArray(self, actual, expect, places=6, delta=None): + """Asserts that the arrays of floats are almost equal.""" + + self.assertEqual(len(actual), len(expect), + 'Actual array has %d items, expected %d' % (len(actual), len(expect))) + + for idx, (act, exp) in enumerate(zip(actual, expect)): + self.assertAlmostEqual(act, exp, places=places, delta=delta, + msg='%f != %f at index %d' % (act, exp, idx)) + + +class HierarchicalAndFlatExportTest(AbstractAlembicTest): + @with_tempdir + def test_hierarchical_export(self, tempdir: pathlib.Path): + abc = tempdir / 'cubes_hierarchical.abc' + script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \ + "renderable_only=True, visible_layers_only=True, flatten=False)" % abc + self.run_blender('cubes-hierarchy.blend', script) + + # Now check the resulting Alembic file. + xform = self.abcprop(abc, '/Cube/Cube_002/Cube_012/.xform') + self.assertEqual(1, xform['.inherits']) + self.assertAlmostEqualFloatArray( + xform['.vals'], + [1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 3.07484, -2.92265, 0.0586434, 1.0] + ) + + @with_tempdir + def test_flat_export(self, tempdir: pathlib.Path): + abc = tempdir / 'cubes_flat.abc' + script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \ + "renderable_only=True, visible_layers_only=True, flatten=True)" % abc + self.run_blender('cubes-hierarchy.blend', script) + + # Now check the resulting Alembic file. + xform = self.abcprop(abc, '/Cube_012/.xform') + self.assertEqual(0, xform['.inherits']) + + self.assertAlmostEqualFloatArray( + xform['.vals'], + [0.343134, 0.485243, 0.804238, 0, + 0.0, 0.856222, -0.516608, 0, + -0.939287, 0.177266, 0.293799, 0, + 1, 3, 4, 1], + ) + + +if __name__ == '__main__': + unittest.main(argv=sys.argv[0:1]) |