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:
Diffstat (limited to 'tests/python/modules/mesh_test.py')
-rw-r--r--tests/python/modules/mesh_test.py524
1 files changed, 312 insertions, 212 deletions
diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py
index 6d921959e6f..4cf545d3292 100644
--- a/tests/python/modules/mesh_test.py
+++ b/tests/python/modules/mesh_test.py
@@ -33,13 +33,14 @@
# delete the duplicate object
#
# The words in angle brackets are parameters of the test, and are specified in
-# the main class MeshTest.
+# the abstract 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.
+from abc import ABC, abstractmethod
import bpy
import functools
import inspect
@@ -161,121 +162,321 @@ class DeformModifierSpec:
return "Modifier: " + str(self.modifier_list) + " with object operator " + str(self.object_operator_spec)
-class MeshTest:
+class MeshTest(ABC):
"""
- 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().
+ A mesh testing Abstract class that hold common functionalities for testting operations.
"""
- def __init__(
- self,
- test_name: str,
- test_object_name: str,
- expected_object_name: str,
- operations_stack=None,
- apply_modifiers=False,
- do_compare=False,
- threshold=None
- ):
- """
- Constructs a MeshTest object. Raises a KeyError if objects with names expected_object_name
- or test_object_name don't exist.
- :param test_name: str - unique test name identifier.
+ def __init__(self, test_object_name, exp_object_name, test_name=None, threshold=None, do_compare=True):
+ """
:param test_object_name: str - Name of object of mesh type to run the operations on.
- :param expected_object_name: str - Name of object of mesh type that has the expected
+ :param exp_object_name: 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_modifiers: bool - True if we want to apply the modifiers right after adding them to the object.
- - True if we want to apply the modifier to a list of modifiers, after some operation.
- This affects operations of type ModifierSpec and DeformModifierSpec.
+ :param test_name: str - Name of the test.
+ :param threshold: exponent: To allow variations and accept difference to a certain degree.
:param do_compare: bool - True if we want to compare the test and expected objects, False otherwise.
- :param threshold : exponent: To allow variations and accept difference to a certain degree.
-
"""
- if operations_stack is None:
- operations_stack = []
- for operation in operations_stack:
- if not (isinstance(operation, ModifierSpec) or isinstance(operation, OperatorSpecEditMode)
- or isinstance(operation, OperatorSpecObjectMode) or isinstance(operation, DeformModifierSpec)
- or isinstance(operation, ParticleSystemSpec)):
- raise ValueError("Expected operation of type {} or {} or {} or {}. Got {}".
- format(type(ModifierSpec), type(OperatorSpecEditMode),
- type(DeformModifierSpec), type(ParticleSystemSpec),
- type(operation)))
- self.operations_stack = operations_stack
- self.apply_modifier = apply_modifiers
- self.do_compare = do_compare
+ self.test_object_name = test_object_name
+ self.exp_object_name = exp_object_name
+ if test_name:
+ self.test_name = test_name
+ else:
+ filepath = bpy.data.filepath
+ self.test_name = bpy.path.display_name_from_filepath(filepath)
self.threshold = threshold
- self.test_name = test_name
+ self.do_compare = do_compare
+ self.update = os.getenv("BLENDER_TEST_UPDATE") is not None
+ self.verbose = os.getenv("BLENDER_VERBOSE") is not None
+ self.test_updated_counter = 0
+ objects = bpy.data.objects
+ self.evaluated_object = None
+ self.test_object = objects[self.test_object_name]
- self.verbose = os.environ.get("BLENDER_VERBOSE") is not None
- self.update = os.getenv('BLENDER_TEST_UPDATE') is not None
+ if self.update:
+ if exp_object_name in objects:
+ self.expected_object = objects[self.exp_object_name]
+ else:
+ self.create_expected_object()
+ else:
+ self.expected_object = objects[self.exp_object_name]
- # Initialize test objects.
- objects = bpy.data.objects
- self.test_object = objects[test_object_name]
- self.expected_object = objects[expected_object_name]
+ def create_expected_object(self):
+ """
+ Creates an expected object 10 units away
+ in Y direction from test object.
+ """
if self.verbose:
- print("Found test object {}".format(test_object_name))
- print("Found test object {}".format(expected_object_name))
+ print("Creating expected object...")
+ self.create_evaluated_object()
+ self.expected_object = self.evaluated_object
+ self.expected_object.name = self.exp_object_name
+ x, y, z = self.test_object.location
+ self.expected_object.location = (x, y+10, z)
+ bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
- # Private flag to indicate whether the blend file was updated after the test.
- self._test_updated = False
+ def create_evaluated_object(self):
+ """
+ Creates an evaluated object.
+ """
+ bpy.context.view_layer.objects.active = self.test_object
- def set_test_object(self, test_object_name):
+ # 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()
+ self.evaluated_object = bpy.context.active_object
+ self.evaluated_object.name = "evaluated_object"
+
+ @staticmethod
+ def _print_result(result):
"""
- 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.
+ Prints the comparison, selection and validation result.
"""
- objects = bpy.data.objects
- self.test_object = objects[test_object_name]
+ print("Results:")
+ for key in result:
+ print("{} : {}".format(key, result[key][1]))
+ print()
- def set_expected_object(self, expected_object_name):
+ def run_test(self):
"""
- 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.
+ Runs a single test, runs it again if test file is updated.
"""
- objects = bpy.data.objects
- self.expected_object = objects[expected_object_name]
- def _on_failed_test(self, compare_result, validation_success, evaluated_test_object):
- if self.update and validation_success:
- if self.verbose:
- print("Test failed expectantly. Updating expected mesh...")
+ self.create_evaluated_object()
+ self.apply_operations(self.evaluated_object.name)
- # 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
- evaluated_selection = {v.index for v in evaluated_test_object.data.vertices if v.select}
+ if not self.do_compare:
+ print("\nVisualization purpose only: Open Blender in GUI mode")
+ print("Compare evaluated and expected object in Blender.\n")
+ return False
+
+ result = self.compare_meshes(self.evaluated_object, self.expected_object, self.threshold)
- bpy.data.objects.remove(self.expected_object, do_unlink=True)
- evaluated_test_object.name = expected_object_name
- self._do_selection(evaluated_test_object.data, "VERT", evaluated_selection)
+ # Initializing with True to get correct resultant of result_code booleans.
+ success = True
+ inside_loop_flag = False
+ for key in result:
+ inside_loop_flag = True
+ success = success and result[key][0]
- # Save file.
- bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
+ # Check "success" is actually evaluated and is not the default True value.
+ if not inside_loop_flag:
+ success = False
- self._test_updated = True
- # Set new expected object.
- self.expected_object = evaluated_test_object
+ if success:
+ self.print_passed_test_result(result)
+ # Clean up.
+ if self.verbose:
+ print("Cleaning up...")
+ # Delete evaluated_test_object.
+ bpy.ops.object.delete()
return True
- else:
- print("Test comparison result: {}".format(compare_result))
- print("Test validation result: {}".format(validation_success))
- print("Resulting object mesh '{}' did not match expected object '{}' from file {}".
- format(evaluated_test_object.name, self.expected_object.name, bpy.data.filepath))
+ elif self.update:
+ self.print_failed_test_result(result)
+ self.update_failed_test()
+ # Check for testing the blend file is updated and re-running.
+ # Also safety check to avoid infinite recursion loop.
+ if self.test_updated_counter == 1:
+ print("Re-running test...")
+ self.run_test()
+ else:
+ print("The test fails consistently. Exiting...")
+ return False
+ else:
+ self.print_failed_test_result(result)
return False
- def is_test_updated(self):
+ def print_failed_test_result(self, result):
+ """
+ Print results for failed test.
+ """
+ print("\nFAILED {} test with the following: ".format(self.test_name))
+ self._print_result(result)
+
+ def print_passed_test_result(self, result):
+ """
+ Print results for passing test.
+ """
+ print("\nPASSED {} test successfully.".format(self.test_name))
+ self._print_result(result)
+
+ def do_selection(self, mesh: bpy.types.Mesh, select_mode: str, selection: set):
+ """
+ Do selection on a mesh.
+ :param mesh: bpy.types.Mesh - input mesh
+ :param: select_mode: str - selection mode. Must be 'VERT', 'EDGE' or 'FACE'
+ :param: selection: set - indices of selection.
+
+ Example: select_mode='VERT' and selection={1,2,3} selects veritces 1, 2 and 3 of input mesh
"""
- 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.
+ # Deselect all objects.
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ bpy.context.tool_settings.mesh_select_mode = (select_mode == 'VERT',
+ select_mode == 'EDGE',
+ select_mode == 'FACE')
+
+ items = (mesh.vertices if select_mode == 'VERT'
+ else mesh.edges if select_mode == 'EDGE'
+ else mesh.polygons if select_mode == 'FACE'
+ else None)
+ if items is None:
+ raise ValueError("Invalid selection mode")
+ for index in selection:
+ items[index].select = True
+
+ def update_failed_test(self):
+ """
+ Updates expected object.
"""
- return self._test_updated
+ self.evaluated_object.location = self.expected_object.location
+ expected_object_name = self.expected_object.name
+ evaluated_selection = {
+ v.index for v in self.evaluated_object.data.vertices if v.select}
+
+ bpy.data.objects.remove(self.expected_object, do_unlink=True)
+ self.evaluated_object.name = expected_object_name
+ self.do_selection(self.evaluated_object.data,
+ "VERT", evaluated_selection)
+
+ # Save file.
+ bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
+ self.test_updated_counter += 1
+ self.expected_object = self.evaluated_object
+
+ @staticmethod
+ def compare_meshes(evaluated_object, expected_object, threshold):
+ """
+ Compares evaluated object mesh with expected object mesh.
+ :param evaluated_object: first object for comparison.
+ :param expected_object: second object for comparison.
+ :param threshold: exponent: To allow variations and accept difference to a certain degree.
+ :return: dict: Contains results of different comparisons.
+ """
+ objects = bpy.data.objects
+ evaluated_test_mesh = objects[evaluated_object.name].data
+ expected_mesh = expected_object.data
+ result_codes = {}
+
+ # Mesh Comparison.
+ if threshold:
+ result_mesh = expected_mesh.unit_test_compare(
+ mesh=evaluated_test_mesh, threshold=threshold)
+ else:
+ result_mesh = expected_mesh.unit_test_compare(
+ mesh=evaluated_test_mesh)
+
+ if result_mesh == "Same":
+ result_codes['Mesh Comparison'] = (True, result_mesh)
+ else:
+ result_codes['Mesh Comparison'] = (False, result_mesh)
+
+ # Selection comparison.
+
+ selected_evaluated_verts = [
+ v.index for v in evaluated_test_mesh.vertices if v.select]
+ selected_expected_verts = [
+ v.index for v in expected_mesh.vertices if v.select]
+
+ if selected_evaluated_verts == selected_expected_verts:
+ result_selection = "Same"
+ result_codes['Selection Comparison'] = (True, result_selection)
+ else:
+ result_selection = "Selection doesn't match."
+ result_codes['Selection Comparison'] = (False, result_selection)
+
+ # Validation check.
+ result_validation = evaluated_test_mesh.validate(verbose=True)
+ if result_validation:
+ result_validation = "Invalid Mesh"
+ result_codes['Mesh Validation'] = (False, result_validation)
+ else:
+ result_validation = "Valid"
+ result_codes['Mesh Validation'] = (True, result_validation)
+
+ return result_codes
+
+ @abstractmethod
+ def apply_operations(self, object_name):
+ """
+ Apply operations on this object.
+
+ object_name (str): Name of the test object on which operations will be applied.
+ """
+ pass
+
+
+class SpecMeshTest(MeshTest):
+ """
+ A mesh testing class inherited from MeshTest 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 MeshTest's run_test.
+ """
+
+ def __init__(self, test_name,
+ test_object_name,
+ exp_object_name,
+ operations_stack=None,
+ apply_modifier=True,
+ threshold=None):
+ """
+ Constructor for SpecMeshTest.
+
+ :param test_name: str - Name of the test.
+ :param test_object_name: str - Name of object of mesh type to run the operations on.
+ :param exp_object_name: 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.
+ - True if we want to apply the modifier to list of modifiers, after some operation.
+ This affects operations of type ModifierSpec and DeformModifierSpec.
+ """
+
+ super().__init__(test_object_name, exp_object_name, test_name, threshold)
+ self.test_name = test_name
+ if operations_stack is None:
+ self.operations_stack = []
+ else:
+ self.operations_stack = operations_stack
+ self.apply_modifier = apply_modifier
+
+ def apply_operations(self, evaluated_test_object_name):
+ # Add modifiers and operators.
+ SpecMeshTest.apply_operations.__doc__ = MeshTest.apply_operations.__doc__
+ evaluated_test_object = bpy.data.objects[evaluated_test_object_name]
+ if self.verbose:
+ print("Applying operations...")
+ for operation in self.operations_stack:
+ if isinstance(operation, ModifierSpec):
+ self._add_modifier(evaluated_test_object, operation)
+ if self.apply_modifier:
+ self._apply_modifier(
+ evaluated_test_object, operation.modifier_name)
+
+ elif isinstance(operation, OperatorSpecEditMode):
+ self._apply_operator_edit_mode(
+ evaluated_test_object, operation)
+
+ elif isinstance(operation, OperatorSpecObjectMode):
+ self._apply_operator_object_mode(operation)
+
+ elif isinstance(operation, DeformModifierSpec):
+ self._apply_deform_modifier(evaluated_test_object, operation)
+
+ elif isinstance(operation, ParticleSystemSpec):
+ self._apply_particle_system(evaluated_test_object, operation)
+
+ else:
+ raise ValueError("Expected operation of type {} or {} or {} or {}. Got {}".
+ format(type(ModifierSpec), type(OperatorSpecEditMode),
+ type(OperatorSpecObjectMode), type(ParticleSystemSpec), type(operation)))
def _set_parameters_impl(self, modifier, modifier_parameters, nested_settings_path, modifier_name):
"""
@@ -312,14 +513,15 @@ class MeshTest:
for key in modifier_parameters:
nested_settings_path.append(key)
- self._set_parameters_impl(modifier, modifier_parameters[key], nested_settings_path, modifier_name)
+ self._set_parameters_impl(
+ modifier, modifier_parameters[key], nested_settings_path, modifier_name)
if nested_settings_path:
nested_settings_path.pop()
def set_parameters(self, modifier, modifier_parameters):
"""
- Wrapper for _set_parameters_util
+ Wrapper for _set_parameters_impl.
"""
settings = []
modifier_name = modifier.name
@@ -430,40 +632,14 @@ class MeshTest:
if self.apply_modifier:
self._apply_modifier(test_object, particle_sys_spec.modifier_name)
- def _do_selection(self, mesh: bpy.types.Mesh, select_mode: str, selection: set):
- """
- Do selection on a mesh
- :param mesh: bpy.types.Mesh - input mesh
- :param: select_mode: str - selection mode. Must be 'VERT', 'EDGE' or 'FACE'
- :param: selection: set - indices of selection.
-
- Example: select_mode='VERT' and selection={1,2,3} selects veritces 1, 2 and 3 of input mesh
- """
- # deselect all
- bpy.ops.object.mode_set(mode='EDIT')
- bpy.ops.mesh.select_all(action='DESELECT')
- bpy.ops.object.mode_set(mode='OBJECT')
-
- bpy.context.tool_settings.mesh_select_mode = (select_mode == 'VERT',
- select_mode == 'EDGE',
- select_mode == 'FACE')
-
- items = (mesh.vertices if select_mode == 'VERT'
- else mesh.edges if select_mode == 'EDGE'
- else mesh.polygons if select_mode == 'FACE'
- else None)
- if items is None:
- raise ValueError("Invalid selection mode")
- for index in selection:
- items[index].select = True
-
def _apply_operator_edit_mode(self, test_object, operator: OperatorSpecEditMode):
"""
Apply operator on test object.
:param test_object: bpy.types.Object - Blender object to apply operator on.
:param operator: OperatorSpecEditMode - OperatorSpecEditMode object with parameters.
"""
- self._do_selection(test_object.data, operator.select_mode, operator.selection)
+ self.do_selection(
+ test_object.data, operator.select_mode, operator.selection)
# Apply operator in edit mode.
bpy.ops.object.mode_set(mode='EDIT')
@@ -528,96 +704,27 @@ class MeshTest:
for mod_name in modifier_names:
self._apply_modifier(test_object, mod_name)
- 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()
- print(evaluated_test_object.name, "is set to active")
-
- # Add modifiers and operators.
- for operation in self.operations_stack:
- if isinstance(operation, ModifierSpec):
- self._add_modifier(evaluated_test_object, operation)
- if self.apply_modifier:
- self._apply_modifier(evaluated_test_object, operation.modifier_name)
-
- elif isinstance(operation, OperatorSpecEditMode):
- self._apply_operator_edit_mode(evaluated_test_object, operation)
-
- elif isinstance(operation, OperatorSpecObjectMode):
- self._apply_operator_object_mode(operation)
-
- elif isinstance(operation, DeformModifierSpec):
- self._apply_deform_modifier(evaluated_test_object, operation)
-
- elif isinstance(operation, ParticleSystemSpec):
- self._apply_particle_system(evaluated_test_object, operation)
-
- else:
- raise ValueError("Expected operation of type {} or {} or {} or {}. Got {}".
- format(type(ModifierSpec), type(OperatorSpecEditMode),
- type(OperatorSpecObjectMode), type(ParticleSystemSpec), type(operation)))
-
- # Compare resulting mesh with expected one.
- # Compare only when self.do_compare is set to True, it is set to False for run-test and returns.
- if not self.do_compare:
- print("Meshes/objects are not compared, compare evaluated and expected object in Blender for "
- "visualization only.")
- return False
-
- if self.verbose:
- print("Comparing expected mesh with resulting mesh...")
- evaluated_test_mesh = evaluated_test_object.data
- expected_mesh = self.expected_object.data
- 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')
-
- selected_evaluatated_verts = [v.index for v in evaluated_test_mesh.vertices if v.select]
- selected_expected_verts = [v.index for v in expected_mesh.vertices if v.select]
- if selected_evaluatated_verts != selected_expected_verts:
- compare_result = "Selection doesn't match"
- compare_success = False
- # Also check if invalid geometry (which is never expected) had to be corrected...
- validation_success = not evaluated_test_mesh.validate(verbose=True)
+class BlendFileTest(MeshTest):
+ """
+ A mesh testing class inherited from MeshTest aimed at testing operations like modifiers loaded directly from
+ blend file i.e. without adding them from scratch or without adding specifications.
+ """
- if compare_success and validation_success:
- if self.verbose:
- print("Success!")
+ def apply_operations(self, evaluated_test_object_name):
- # 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_result, validation_success, evaluated_test_object)
+ BlendFileTest.apply_operations.__doc__ = MeshTest.apply_operations.__doc__
+ evaluated_test_object = bpy.data.objects[evaluated_test_object_name]
+ modifiers_list = evaluated_test_object.modifiers
+ if not modifiers_list:
+ raise Exception("No modifiers are added to test object.")
+ for modifier in modifiers_list:
+ bpy.ops.object.modifier_apply(modifier=modifier.name)
class RunTest:
"""
- Helper class that stores and executes modifier tests.
+ Helper class that stores and executes SpecMeshTest tests.
Example usage:
@@ -629,9 +736,9 @@ class RunTest:
>>> OperatorSpecEditMode("delete_edgeloop", {}, "EDGE", MONKEY_LOOP_EDGE),
>>> ]
>>> tests = [
- >>> MeshTest("Test1", "testCube", "expectedCube", modifier_list),
- >>> MeshTest("Test2", "testCube_2", "expectedCube_2", modifier_list),
- >>> MeshTest("MonkeyDeleteEdge", "testMonkey","expectedMonkey", operator_list)
+ >>> SpecMeshTest("Test1", "testCube", "expectedCube", modifier_list),
+ >>> SpecMeshTest("Test2", "testCube_2", "expectedCube_2", modifier_list),
+ >>> SpecMeshTest("MonkeyDeleteEdge", "testMonkey","expectedMonkey", operator_list)
>>> ]
>>> modifiers_test = RunTest(tests)
>>> modifiers_test.run_all_tests()
@@ -639,7 +746,7 @@ class RunTest:
def __init__(self, tests, apply_modifiers=False, do_compare=False):
"""
- Construct a modifier test.
+ Construct a test suite.
:param tests: list - list of modifier or operator test cases. Each element in the list must contain the
following
in the correct order:
@@ -674,7 +781,7 @@ class RunTest:
def run_all_tests(self):
"""
- Run all tests in self.tests list. Raises an exception if one the tests fails.
+ Run all tests in self.tests list. Displays all failed tests at bottom.
"""
for test_number, each_test in enumerate(self.tests):
test_name = each_test.test_name
@@ -717,15 +824,8 @@ class RunTest:
raise Exception('No test called {} found!'.format(test_name))
test = case
- if self.apply_modifiers:
- test.apply_modifier = True
-
- if self.do_compare:
- test.do_compare = True
+ test.apply_modifier = self.apply_modifiers
+ test.do_compare = self.do_compare
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