Changeset View
Changeset View
Standalone View
Standalone View
tests/python/modules/mesh_test.py
| Show First 20 Lines • Show All 92 Lines • ▼ Show 20 Lines | def __init__(self, operator_name: str, operator_parameters: dict, select_mode: str, selection: set): | ||||
| self.select_mode = select_mode | self.select_mode = select_mode | ||||
| self.selection = selection | self.selection = selection | ||||
| def __str__(self): | def __str__(self): | ||||
| return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_parameters) + \ | return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_parameters) + \ | ||||
| " in selection mode: " + self.select_mode + ", selecting " + str(self.selection) | " in selection mode: " + self.select_mode + ", selecting " + str(self.selection) | ||||
| class TransformOperatorSpec: | |||||
| """ | |||||
| Holds one transform operator and its parameters. | |||||
| """ | |||||
| def __init__(self, mode: str, operator_name: str, args: tuple, kw: dict): | |||||
| """ | |||||
| Constructs an operatorSpec. Raises ValueError if selec_mode is invalid. | |||||
| :param operator_name: str - name of transform operator from bpy.ops.transform, e.g. "translate" or "rotate" | |||||
| :param *args and **kw: operator arguments | |||||
| """ | |||||
| self.mode = mode | |||||
| self.operator_name = operator_name | |||||
| self.operator_args = args | |||||
| self.operator_kw = kw | |||||
| def __str__(self): | |||||
| return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_kw) + \ | |||||
| ") in mode " + self.mode | |||||
| class MeshTest: | class MeshTest: | ||||
| """ | """ | ||||
| A mesh testing class targeted at testing modifiers and operators on a single object. | 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 | It holds a stack of mesh operations, i.e. modifiers or operators. The test is executed using | ||||
| the public method run_test(). | 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): | ||||
| ▲ Show 20 Lines • Show All 58 Lines • ▼ Show 20 Lines | class MeshTest: | ||||
| def add_operator(self, operator_spec: OperatorSpec): | def add_operator(self, operator_spec: OperatorSpec): | ||||
| """ | """ | ||||
| Adds an operator to the operations stack. | Adds an operator to the operations stack. | ||||
| :param operator_spec: OperatorSpec - operator to add to the operations stack. | :param operator_spec: OperatorSpec - operator to add to the operations stack. | ||||
| """ | """ | ||||
| self.operations_stack.append(operator_spec) | self.operations_stack.append(operator_spec) | ||||
| def add_transform_operator(self, transform_operator_spec: TransformOperatorSpec): | |||||
| """ | |||||
| Adds a transform operator to the operations stack. | |||||
| :param transform_operator_spec: transform operator to add to the operations stack. | |||||
| """ | |||||
| self.operations_stack.append(transform_operator_spec) | |||||
| def _on_failed_test(self, compare_result, validation_success, evaluated_test_object): | def _on_failed_test(self, compare_result, validation_success, evaluated_test_object): | ||||
| if self.update and validation_success: | if self.update and validation_success: | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Test failed expectantly. Updating expected mesh...") | print("Test failed expectantly. Updating expected mesh...") | ||||
| # Replace expected object with object we ran operations on, i.e. evaluated_test_object. | # Replace expected object with object we ran operations on, i.e. evaluated_test_object. | ||||
| evaluated_test_object.location = self.expected_object.location | evaluated_test_object.matrix_world = self.expected_object.matrix_world | ||||
| expected_object_name = self.expected_object.name | expected_object_name = self.expected_object.name | ||||
| bpy.data.objects.remove(self.expected_object, do_unlink=True) | bpy.data.objects.remove(self.expected_object, do_unlink=True) | ||||
| evaluated_test_object.name = expected_object_name | evaluated_test_object.name = expected_object_name | ||||
| # Save file | # Save file | ||||
| bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) | bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) | ||||
| ▲ Show 20 Lines • Show All 79 Lines • ▼ Show 20 Lines | def _apply_operator(self, test_object, operator: OperatorSpec): | ||||
| retval = mesh_operator(**operator.operator_parameters) | retval = mesh_operator(**operator.operator_parameters) | ||||
| if retval != {'FINISHED'}: | if retval != {'FINISHED'}: | ||||
| raise RuntimeError("Unexpected operator return value: {}".format(retval)) | raise RuntimeError("Unexpected operator return value: {}".format(retval)) | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Applied operator {}".format(operator)) | print("Applied operator {}".format(operator)) | ||||
| bpy.ops.object.mode_set(mode='OBJECT') | bpy.ops.object.mode_set(mode='OBJECT') | ||||
| def _apply_transform_operator(self, test_object, operator: TransformOperatorSpec): | |||||
sergey: This is very weird to have test-specific code in a generic code.
Better design is to have all… | |||||
| """ | |||||
| Apply operator on test object. | |||||
| :param test_object: bpy.types.Object - Blender object to apply operator on. | |||||
| :param operator: OperatorSpec - OperatorSpec object with parameters. | |||||
| """ | |||||
| bpy.ops.object.mode_set(mode=operator.mode) | |||||
| if operator.mode == 'EDIT': | |||||
| # Do selection. | |||||
| bpy.context.tool_settings.mesh_select_mode = (True, False, False) | |||||
| bpy.ops.mesh.select_mode(type='VERT') | |||||
| bpy.ops.mesh.select_all(action='SELECT') | |||||
| transform_operator = getattr(bpy.ops.transform, operator.operator_name) | |||||
| if not transform_operator: | |||||
| raise AttributeError("No transform operator {}".format(operator.operator_name)) | |||||
| retval = transform_operator(*operator.operator_args, **operator.operator_kw) | |||||
| if retval != {'FINISHED'}: | |||||
| raise RuntimeError("Unexpected operator return value: {}".format(retval)) | |||||
| if self.verbose: | |||||
| print("Applied operator {}".format(operator)) | |||||
| if operator.mode != 'OBJECT': | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| def run_test(self): | def run_test(self): | ||||
| """ | """ | ||||
| Apply operations in self.operations_stack on self.test_object and compare the | Apply operations in self.operations_stack on self.test_object and compare the | ||||
| resulting mesh with self.expected_object.data | resulting mesh with self.expected_object.data | ||||
| :return: bool - True if the test passed, False otherwise. | :return: bool - True if the test passed, False otherwise. | ||||
| """ | """ | ||||
| self._test_updated = False | self._test_updated = False | ||||
| bpy.context.view_layer.objects.active = self.test_object | bpy.context.view_layer.objects.active = self.test_object | ||||
| # Duplicate test object. | # Duplicate test object. | ||||
| bpy.ops.object.mode_set(mode="OBJECT") | bpy.ops.object.mode_set(mode="OBJECT") | ||||
| bpy.ops.object.select_all(action="DESELECT") | bpy.ops.object.select_all(action="DESELECT") | ||||
| bpy.context.view_layer.objects.active = self.test_object | bpy.context.view_layer.objects.active = self.test_object | ||||
| self.test_object.select_set(True) | self.test_object.select_set(True) | ||||
| bpy.ops.object.duplicate() | bpy.ops.object.duplicate() | ||||
| evaluated_test_object = bpy.context.active_object | evaluated_test_object = bpy.context.active_object | ||||
| evaluated_test_object.name = "evaluated_object" | evaluated_test_object.name = "evaluated_object" | ||||
| if self.verbose: | if self.verbose: | ||||
| print(evaluated_test_object.name, "is set to active") | print(evaluated_test_object.name, "is set to active") | ||||
| # Add modifiers and operators. | # Add modifiers and operators. | ||||
| compare_matrix = False | |||||
| for operation in self.operations_stack: | for operation in self.operations_stack: | ||||
| if isinstance(operation, ModifierSpec): | if isinstance(operation, ModifierSpec): | ||||
| self._apply_modifier(evaluated_test_object, operation) | self._apply_modifier(evaluated_test_object, operation) | ||||
| elif isinstance(operation, OperatorSpec): | elif isinstance(operation, OperatorSpec): | ||||
| self._apply_operator(evaluated_test_object, operation) | self._apply_operator(evaluated_test_object, operation) | ||||
| elif isinstance(operation, TransformOperatorSpec): | |||||
| self._apply_transform_operator(evaluated_test_object, operation) | |||||
| compare_matrix = operation.mode == 'OBJECT' | |||||
| else: | else: | ||||
| raise ValueError("Expected operation of type {} or {}. Got {}". | raise ValueError("Expected operation of type {} or {}. Got {}". | ||||
| format(type(ModifierSpec), type(OperatorSpec), | format(type(ModifierSpec), type(OperatorSpec), | ||||
| type(operation))) | type(operation))) | ||||
| # Compare resulting mesh with expected one. | # Compare resulting with expected one. | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Comparing expected mesh with resulting mesh...") | print("Comparing expected object with resulting object...") | ||||
| if compare_matrix: | |||||
| compare_result = '' | |||||
| compare_success = evaluated_test_object.matrix_world == self.expected_object.matrix_world | |||||
| validation_success = True | |||||
| else: | |||||
| evaluated_test_mesh = evaluated_test_object.data | evaluated_test_mesh = evaluated_test_object.data | ||||
| expected_mesh = self.expected_object.data | expected_mesh = self.expected_object.data | ||||
| compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) | compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh) | ||||
| compare_success = (compare_result == 'Same') | compare_success = (compare_result == 'Same') | ||||
| # Also check if invalid geometry (which is never expected) had to be corrected... | # Also check if invalid geometry (which is never expected) had to be corrected... | ||||
| validation_success = evaluated_test_mesh.validate(verbose=True) == False | validation_success = evaluated_test_mesh.validate(verbose=True) == False | ||||
| if compare_success and validation_success: | if compare_success and validation_success: | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Success!") | print("Success!") | ||||
| # Clean up. | # Clean up. | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Cleaning up...") | print("Cleaning up...") | ||||
| ▲ Show 20 Lines • Show All 169 Lines • ▼ Show 20 Lines | def run_all_tests(self): | ||||
| module = inspect.getmodule(frame[0]) | module = inspect.getmodule(frame[0]) | ||||
| python_path = module.__file__ | python_path = module.__file__ | ||||
| print("Run following command to open Blender and run the failing test:") | print("Run following command to open Blender and run the failing test:") | ||||
| print("{} {} --python {} -- {} {}" | print("{} {} --python {} -- {} {}" | ||||
| .format(blender_path, blend_path, python_path, "--run-test", "<test_index>")) | .format(blender_path, blend_path, python_path, "--run-test", "<test_index>")) | ||||
| raise Exception("Tests {} failed".format(self._failed_tests_list)) | raise Exception("Tests {} failed".format(self._failed_tests_list)) | ||||
| class TransformOperatorTest: | |||||
| """ | |||||
| Helper class that stores and executes transform operator tests. | |||||
| Example usage: | |||||
| >>> tests = [ | |||||
| >>> ['MyObject', 'MyObject_trans_global_expected', 'EDIT', 'translate', (override_context,), {'value': (0,0,1), 'constraint_axis': (True,True,True)}], | |||||
| >>> ] | |||||
| >>> operator_test = TransformOperatorTest(tests) | |||||
| >>> operator_test.run_all_tests() | |||||
| """ | |||||
| def __init__(self, transform_operator_tests): | |||||
| """ | |||||
| Constructs a transform operator test. | |||||
| :param transform_operator_tests: list - list of operator 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) object_mode: str - e.g. 'OBJECT' or 'EDIT' | |||||
| 4) operator_name: str - name of transform operator from bpy.ops.transform, e.g. "translate" or "rotate" | |||||
| 5) operator_args: *args | |||||
| 6) operator_kwargs: **kwargs | |||||
| """ | |||||
| self.operator_tests = transform_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))) | |||||
| test_object_name = case[0] | |||||
| expected_object_name = case[1] | |||||
| operator_mode = case[2] | |||||
| operator_name = case[3] | |||||
| operator_args = case[4] | |||||
| operator_kwargs = case[5] | |||||
| operator_spec = TransformOperatorSpec(operator_mode, operator_name, operator_args, operator_kwargs) | |||||
| 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): | |||||
| OperatorTest.run_all_tests(self) | |||||
This is very weird to have test-specific code in a generic code.
Better design is to have all transform-related code in the transform test. Otherwise the generic code becomes even mode complicated that the actual code you're testing.