Changeset View
Changeset View
Standalone View
Standalone View
tests/python/modules/mesh_test.py
| Show First 20 Lines • Show All 48 Lines • ▼ Show 20 Lines | |||||
| # Output from this module and from blender itself will occur during tests. | # Output from this module and from blender itself will occur during tests. | ||||
| # We need to flush python so that the output is properly interleaved, otherwise | # We need to flush python so that the output is properly interleaved, otherwise | ||||
| # blender's output for one test will end up showing in the middle of another test... | # blender's output for one test will end up showing in the middle of another test... | ||||
| print = functools.partial(print, flush=True) | print = functools.partial(print, flush=True) | ||||
| class ModifierSpec: | class ModifierSpec: | ||||
| """ | """ | ||||
| Holds one modifier and its parameters. | Holds a Generate or Deform or Physics modifier type and its parameters. | ||||
| """ | """ | ||||
| def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict): | def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict, frame_end=0): | ||||
| """ | """ | ||||
| Constructs a modifier spec. | Constructs a modifier spec. | ||||
| :param modifier_name: str - name of object modifier, e.g. "myFirstSubsurfModif" | :param modifier_name: str - name of object modifier, e.g. "myFirstSubsurfModif" | ||||
| :param modifier_type: str - type of object modifier, e.g. "SUBSURF" | :param modifier_type: str - type of object modifier, e.g. "SUBSURF" | ||||
| :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} | :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} | ||||
| :param frame_end:int - frame at which simulation needs to be baked or modifier needs to be applied. | |||||
| """ | """ | ||||
| self.modifier_name = modifier_name | self.modifier_name = modifier_name | ||||
| self.modifier_type = modifier_type | self.modifier_type = modifier_type | ||||
| self.modifier_parameters = modifier_parameters | self.modifier_parameters = modifier_parameters | ||||
| self.frame_end = frame_end | |||||
| def __str__(self): | def __str__(self): | ||||
| return "Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ | return "Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ | ||||
| " with parameters: " + str(self.modifier_parameters) | " with parameters: " + str(self.modifier_parameters) | ||||
| class PhysicsSpec: | class ParticleSystemSpec: | ||||
| """ | """ | ||||
| Holds one Physics modifier and its parameters. | Holds a Particle System modifier and its parameters. | ||||
| """ | """ | ||||
| def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict, frame_end: int): | def __init__(self, modifier_name: str, modifier_type: str, modifier_parameters: dict, frame_end: int): | ||||
| """ | """ | ||||
| Constructs a physics spec. | Constructs a particle system spec. | ||||
| :param modifier_name: str - name of object modifier, e.g. "Cloth" | :param modifier_name: str - name of object modifier, e.g. "Particles" | ||||
| :param modifier_type: str - type of object modifier, e.g. "CLOTH" | :param modifier_type: str - type of object modifier, e.g. "PARTICLE_SYSTEM" | ||||
| :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"quality" : 4} | :param modifier_parameters: dict - {name : val} dictionary giving modifier parameters, e.g. {"seed" : 1} | ||||
| :param frame_end:int - the last frame of the simulation at which it is baked | :param frame_end:int - the last frame of the simulation at which the modifier is applied | ||||
| """ | """ | ||||
| self.modifier_name = modifier_name | self.modifier_name = modifier_name | ||||
| self.modifier_type = modifier_type | self.modifier_type = modifier_type | ||||
| self.modifier_parameters = modifier_parameters | self.modifier_parameters = modifier_parameters | ||||
| self.frame_end = frame_end | self.frame_end = frame_end | ||||
| def __str__(self): | def __str__(self): | ||||
| return "Physics Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ | return "Physics Modifier: " + self.modifier_name + " of type " + self.modifier_type + \ | ||||
| " with parameters: " + str(self.modifier_parameters) + " with frame end: " + str(self.frame_end) | " with parameters: " + str(self.modifier_parameters) + " with frame end: " + str(self.frame_end) | ||||
| class OperatorSpec: | class OperatorSpec: | ||||
| """ | """ | ||||
| Holds one operator and its parameters. | Holds one operator and its parameters. | ||||
| """ | """ | ||||
| def __init__(self, operator_name: str, operator_parameters: dict, select_mode: str, selection: set): | def __init__(self, operator_name: str, operator_parameters: dict, select_mode: str, selection: set): | ||||
| """ | """ | ||||
| Constructs an operatorSpec. Raises ValueError if selec_mode is invalid. | Constructs an operatorSpec. Raises ValueError if selec_mode is invalid. | ||||
| Show All 9 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 ObjectOperatorSpec: | |||||
| """ | |||||
| Holds an object operator and its parameters. Helper class for DeformModifierSpec. | |||||
| Needed to support operations in Object Mode and not Edit Mode which is supported by OperatorSpec. | |||||
| """ | |||||
| def __init__(self, operator_name: str, operator_parameters: dict): | |||||
| """ | |||||
| Constructs an Object Operator spec | |||||
| :param operator_name: str - name of the object operator from bpy.ops.object, e.g. "shade_smooth" or "shape_keys" | |||||
| :param operator_parameters: dict - contains operator parameters. | |||||
| """ | |||||
| self.operator_name = operator_name | |||||
| self.operator_parameters = operator_parameters | |||||
| def __str__(self): | |||||
| return "Operator: " + self.operator_name + " with parameters: " + str(self.operator_parameters) | |||||
| class DeformModifierSpec: | |||||
| """ | |||||
| Holds a list of deform modifier and ObjectOperatorSpec. | |||||
| For deform modifiers which have an object operator | |||||
| """ | |||||
| def __init__(self, frame_number: int, modifier_list: list, object_operator_spec: ObjectOperatorSpec = None): | |||||
| """ | |||||
| Constructs a Deform Modifier spec (for user input) | |||||
| :param frame_number: int - the frame at which animated keyframe is inserted | |||||
| :param modifier_list: ModifierSpec - contains modifiers | |||||
| :param object_operator_spec: ObjectOperatorSpec - contains object operators | |||||
| """ | |||||
| self.frame_number = frame_number | |||||
| self.modifier_list = modifier_list | |||||
| self.object_operator_spec = object_operator_spec | |||||
| def __str__(self): | |||||
| return "Modifier: " + str(self.modifier_list) + " with object operator " + str(self.object_operator_spec) | |||||
| 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, threshold=None): | def __init__(self, test_name: str, 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 | Constructs a MeshTest object. Raises a KeyError if objects with names expected_object_name | ||||
| or test_object_name don't exist. | or test_object_name don't exist. | ||||
| :param test_name: str - unique test name identifier. | |||||
| :param test_object: str - Name of object of mesh type to run the operations on. | :param test_object: str - Name of object of mesh type to run the operations on. | ||||
| :param expected_object: str - Name of object of mesh type that has the expected | :param expected_object: str - Name of object of mesh type that has the expected | ||||
| geometry after running the operations. | geometry after running the operations. | ||||
| :param operations_stack: list - stack holding operations to perform on the test_object. | :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. | :param apply_modifier: bool - True if we want to apply the modifiers right after adding them to the object. | ||||
| This affects operations of type ModifierSpec only. | - 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 threshold : exponent: To allow variations and accept difference to a certain degree. | |||||
| """ | """ | ||||
| if operations_stack is None: | if operations_stack is None: | ||||
| operations_stack = [] | operations_stack = [] | ||||
| for operation in operations_stack: | for operation in operations_stack: | ||||
| if not (isinstance(operation, ModifierSpec) or isinstance(operation, OperatorSpec)): | if not (isinstance(operation, ModifierSpec) or isinstance(operation, OperatorSpec) | ||||
| raise ValueError("Expected operation of type {} or {}. Got {}". | or isinstance(operation, ObjectOperatorSpec) or isinstance(operation, DeformModifierSpec) | ||||
| or isinstance(operation, ParticleSystemSpec)): | |||||
| raise ValueError("Expected operation of type {} or {} or {} or {}. Got {}". | |||||
| format(type(ModifierSpec), type(OperatorSpec), | format(type(ModifierSpec), type(OperatorSpec), | ||||
| type(DeformModifierSpec), type(ParticleSystemSpec), | |||||
| type(operation))) | type(operation))) | ||||
| self.operations_stack = operations_stack | self.operations_stack = operations_stack | ||||
| self.apply_modifier = apply_modifiers | self.apply_modifier = apply_modifiers | ||||
| self.threshold = threshold | self.threshold = threshold | ||||
| self.test_name = test_name | |||||
| self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | ||||
| self.update = os.getenv('BLENDER_TEST_UPDATE') is not None | self.update = os.getenv('BLENDER_TEST_UPDATE') is not None | ||||
| # Initialize test objects. | # Initialize test objects. | ||||
| objects = bpy.data.objects | objects = bpy.data.objects | ||||
| self.test_object = objects[test_object_name] | self.test_object = objects[test_object_name] | ||||
| self.expected_object = objects[expected_object_name] | self.expected_object = objects[expected_object_name] | ||||
| Show All 15 Lines | class MeshTest: | ||||
| def set_expected_object(self, expected_object_name): | def set_expected_object(self, expected_object_name): | ||||
| """ | """ | ||||
| Set expected object for the test. Raises a KeyError if object with given name does not exist | 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. | :param expected_object_name: Name of expected object. | ||||
| """ | """ | ||||
| objects = bpy.data.objects | objects = bpy.data.objects | ||||
| self.expected_object = objects[expected_object_name] | self.expected_object = objects[expected_object_name] | ||||
| def add_modifier(self, modifier_spec: ModifierSpec): | def add_modifier_to_stack(self, modifier_spec: ModifierSpec): | ||||
| """ | """ | ||||
| Add a modifier to the operations stack. | Add a modifier to the operations stack. | ||||
| :param modifier_spec: modifier to add to the operations stack | :param modifier_spec: modifier to add to the operations stack | ||||
| """ | """ | ||||
| self.operations_stack.append(modifier_spec) | self.operations_stack.append(modifier_spec) | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Added modififier {}".format(modifier_spec)) | print("Added modifier {}".format(modifier_spec)) | ||||
| def add_operator(self, operator_spec: OperatorSpec): | def add_operator_to_stack(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 _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: | ||||
| Show All 26 Lines | class MeshTest: | ||||
| def is_test_updated(self): | def is_test_updated(self): | ||||
| """ | """ | ||||
| Check whether running the test with BLENDER_TEST_UPDATE actually modified the .blend test file. | 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. | :return: Bool - True if blend file has been updated. False otherwise. | ||||
| """ | """ | ||||
| return self._test_updated | return self._test_updated | ||||
| def _apply_modifier(self, test_object, modifier_spec: ModifierSpec): | def _set_parameters_util(self, modifier, modifier_parameters, nested_settings_path, modifier_name): | ||||
| """ | |||||
| Doing a depth first traversal of the modifier parameters and setting their values. | |||||
| :param: modifier: Of type modifier, its altered to become a setting in recursion | |||||
| :param: modifier_parameters : dict, a simple/nested dictionary of modifier parameters. | |||||
| :param: nested_settings_path : list(stack): helps in tracing path to each node. | |||||
| """ | |||||
| if not isinstance(modifier_parameters, dict): | |||||
| param_setting = None | |||||
| for i, setting in enumerate(nested_settings_path): | |||||
| try: | |||||
| # We want to set the attribute only when we have reached the last setting | |||||
| # Applying of intermediate settings is meaningless. | |||||
| if i == len(nested_settings_path)-1: | |||||
| setattr(modifier, setting, modifier_parameters) | |||||
| else: | |||||
| param_setting = getattr(modifier, setting) | |||||
| # getattr doesn't accept canvas_surfaces["Surface"], but we need to pass it to setattr | |||||
| if setting == "canvas_surfaces": | |||||
| modifier = param_setting.active | |||||
| else: | |||||
| modifier = param_setting | |||||
| except AttributeError: | |||||
| # Clean up first | |||||
| bpy.ops.object.delete() | |||||
| raise AttributeError("Modifier '{}' has no parameter named '{}'". | |||||
| format(modifier_name, setting)) | |||||
| # It pops the current node before moving on to its sibling. | |||||
| nested_settings_path.pop() | |||||
| return | |||||
| for key in modifier_parameters: | |||||
| nested_settings_path.append(key) | |||||
| self._set_parameters_util(modifier, modifier_parameters[key], nested_settings_path, modifier_name) | |||||
| if len(nested_settings_path) != 0: | |||||
| nested_settings_path.pop() | |||||
| def set_parameters(self, modifier, modifier_parameters): | |||||
| """ | |||||
| Wrapper for _set_parameters_util | |||||
| """ | |||||
| settings = [] | |||||
| modifier_copy = modifier | |||||
| modifier_name = modifier.name | |||||
| self._set_parameters_util(modifier_copy, modifier_parameters, settings, modifier_name) | |||||
| def _add_modifier(self, test_object, modifier_spec: ModifierSpec): | |||||
| """ | """ | ||||
| Add modifier to object and apply (if modifier_spec.apply_modifier is True) | Add modifier to object and apply (if modifier_spec.apply_modifier is True) | ||||
| :param test_object: bpy.types.Object - Blender object to apply modifier on. | :param test_object: bpy.types.Object - Blender object to apply modifier on. | ||||
| :param modifier_spec: ModifierSpec - ModifierSpec object with parameters | :param modifier_spec: ModifierSpec - ModifierSpec object with parameters | ||||
| """ | """ | ||||
| bakers_list = ['CLOTH', 'SOFT_BODY', 'DYNAMIC_PAINT', 'FLUID'] | |||||
| scene = bpy.context.scene | |||||
| scene.frame_set(0) | |||||
| modifier = test_object.modifiers.new(modifier_spec.modifier_name, | modifier = test_object.modifiers.new(modifier_spec.modifier_name, | ||||
| modifier_spec.modifier_type) | modifier_spec.modifier_type) | ||||
| if modifier is None: | |||||
| raise Exception("This modifier type is already added on the Test Object, please remove it and try again.") | |||||
| if self.verbose: | if self.verbose: | ||||
| print("Created modifier '{}' of type '{}'.". | print("Created modifier '{}' of type '{}'.". | ||||
| format(modifier_spec.modifier_name, modifier_spec.modifier_type)) | format(modifier_spec.modifier_name, modifier_spec.modifier_type)) | ||||
| for param_name in modifier_spec.modifier_parameters: | # Special case for Dynamic Paint, need to toggle Canvas on. | ||||
| try: | if modifier.type == "DYNAMIC_PAINT": | ||||
| setattr(modifier, param_name, modifier_spec.modifier_parameters[param_name]) | bpy.ops.dpaint.type_toggle(type='CANVAS') | ||||
| if self.verbose: | |||||
| print("\t set parameter '{}' with value '{}'". | self.set_parameters(modifier, modifier_spec.modifier_parameters) | ||||
| format(param_name, modifier_spec.modifier_parameters[param_name])) | |||||
| except AttributeError: | if modifier.type in bakers_list: | ||||
| # Clean up first | self._bake_current_simulation(test_object, modifier.name, modifier_spec.frame_end) | ||||
| bpy.ops.object.delete() | |||||
| raise AttributeError("Modifier '{}' has no parameter named '{}'". | scene.frame_set(modifier_spec.frame_end) | ||||
| format(modifier_spec.modifier_type, param_name)) | |||||
| def _apply_modifier(self, test_object, modifier_name): | |||||
| if self.apply_modifier: | # Modifier automatically gets applied when converting from Curve to Mesh. | ||||
| bpy.ops.object.modifier_apply(modifier=modifier_spec.modifier_name) | if test_object.type == 'CURVE': | ||||
| bpy.ops.object.convert(target='MESH') | |||||
| elif test_object.type == 'MESH': | |||||
| bpy.ops.object.modifier_apply(modifier=modifier_name) | |||||
| else: | |||||
| raise Exception("This object type is not yet supported!") | |||||
| def _bake_current_simulation(self, test_object, test_modifier_name, frame_end): | |||||
| """ | |||||
| FLUID: Bakes the simulation | |||||
| SOFT BODY, CLOTH, DYNAMIC PAINT: Overrides the point_cache context and then bakes. | |||||
| """ | |||||
| def _bake_current_simulation(self, obj, test_mod_type, test_mod_name, frame_end): | |||||
| for scene in bpy.data.scenes: | for scene in bpy.data.scenes: | ||||
| for modifier in obj.modifiers: | for modifier in test_object.modifiers: | ||||
| if modifier.type == test_mod_type: | if modifier.type == 'FLUID': | ||||
| obj.modifiers[test_mod_name].point_cache.frame_end = frame_end | bpy.ops.fluid.bake_all() | ||||
| override = {'scene': scene, 'active_object': obj, 'point_cache': modifier.point_cache} | break | ||||
| elif modifier.type == 'CLOTH' or modifier.type == 'SOFT_BODY': | |||||
| test_object.modifiers[test_modifier_name].point_cache.frame_end = frame_end | |||||
| override_setting = modifier.point_cache | |||||
| override = {'scene': scene, 'active_object': test_object, 'point_cache': override_setting} | |||||
| bpy.ops.ptcache.bake(override, bake=True) | bpy.ops.ptcache.bake(override, bake=True) | ||||
| break | break | ||||
| def _apply_physics_settings(self, test_object, physics_spec: PhysicsSpec): | elif modifier.type == 'DYNAMIC_PAINT': | ||||
| dynamic_paint_setting = modifier.canvas_settings.canvas_surfaces.active | |||||
| override_setting = dynamic_paint_setting.point_cache | |||||
| override = {'scene': scene, 'active_object': test_object, 'point_cache': override_setting} | |||||
| bpy.ops.ptcache.bake(override, bake=True) | |||||
| break | |||||
| def _apply_particle_system(self, test_object, particle_sys_spec: ParticleSystemSpec): | |||||
| """ | """ | ||||
| Apply Physics settings to test objects. | Applies Particle System settings to test objects | ||||
| """ | """ | ||||
| scene = bpy.context.scene | bpy.context.scene.frame_set(0) | ||||
| scene.frame_set(1) | bpy.ops.object.select_all(action='DESELECT') | ||||
| modifier = test_object.modifiers.new(physics_spec.modifier_name, | |||||
| physics_spec.modifier_type) | test_object.modifiers.new(particle_sys_spec.modifier_name, particle_sys_spec.modifier_type) | ||||
| physics_setting = modifier.settings | |||||
| settings_name = test_object.particle_systems.active.settings.name | |||||
| particle_setting = bpy.data.particles[settings_name] | |||||
| if self.verbose: | if self.verbose: | ||||
| print("Created modifier '{}' of type '{}'.". | print("Created modifier '{}' of type '{}'.". | ||||
| format(physics_spec.modifier_name, physics_spec.modifier_type)) | format(particle_sys_spec.modifier_name, particle_sys_spec.modifier_type)) | ||||
| for param_name in particle_sys_spec.modifier_parameters: | |||||
| for param_name in physics_spec.modifier_parameters: | |||||
| try: | try: | ||||
| setattr(physics_setting, param_name, physics_spec.modifier_parameters[param_name]) | if param_name == "seed": | ||||
| system_setting = test_object.particle_systems[particle_sys_spec.modifier_name] | |||||
| setattr(system_setting, param_name, particle_sys_spec.modifier_parameters[param_name]) | |||||
| else: | |||||
| setattr(particle_setting, param_name, particle_sys_spec.modifier_parameters[param_name]) | |||||
| if self.verbose: | if self.verbose: | ||||
| print("\t set parameter '{}' with value '{}'". | print("\t set parameter '{}' with value '{}'". | ||||
| format(param_name, physics_spec.modifier_parameters[param_name])) | format(param_name, particle_sys_spec.modifier_parameters[param_name])) | ||||
| except AttributeError: | except AttributeError: | ||||
| # Clean up first | # Clean up first | ||||
| bpy.ops.object.delete() | bpy.ops.object.delete() | ||||
| raise AttributeError("Modifier '{}' has no parameter named '{}'". | raise AttributeError("Modifier '{}' has no parameter named '{}'". | ||||
| format(physics_spec.modifier_type, param_name)) | format(particle_sys_spec.modifier_type, param_name)) | ||||
| scene.frame_set(physics_spec.frame_end + 1) | bpy.context.scene.frame_set(particle_sys_spec.frame_end) | ||||
| test_object.select_set(True) | |||||
| self._bake_current_simulation(test_object, physics_spec.modifier_type, physics_spec.modifier_name, physics_spec.frame_end) | bpy.ops.object.duplicates_make_real() | ||||
| test_object.select_set(True) | |||||
| bpy.ops.object.join() | |||||
| if self.apply_modifier: | if self.apply_modifier: | ||||
| bpy.ops.object.modifier_apply(modifier=physics_spec.modifier_name) | self._apply_modifier(test_object, particle_sys_spec.modifier_name) | ||||
| def _apply_operator(self, test_object, operator: OperatorSpec): | def _apply_operator(self, test_object, operator: OperatorSpec): | ||||
| """ | """ | ||||
| Apply operator on test object. | Apply operator on test object. | ||||
| :param test_object: bpy.types.Object - Blender object to apply operator on. | :param test_object: bpy.types.Object - Blender object to apply operator on. | ||||
| :param operator: OperatorSpec - OperatorSpec object with parameters. | :param operator: OperatorSpec - OperatorSpec object with parameters. | ||||
| """ | """ | ||||
| mesh = test_object.data | mesh = test_object.data | ||||
| Show All 14 Lines | def _apply_operator(self, test_object, operator: OperatorSpec): | ||||
| mesh.polygons[index].select = True | mesh.polygons[index].select = True | ||||
| else: | else: | ||||
| raise ValueError("Invalid selection mode") | raise ValueError("Invalid selection mode") | ||||
| # Apply operator in edit mode. | # Apply operator in edit mode. | ||||
| bpy.ops.object.mode_set(mode='EDIT') | bpy.ops.object.mode_set(mode='EDIT') | ||||
| bpy.ops.mesh.select_mode(type=operator.select_mode) | bpy.ops.mesh.select_mode(type=operator.select_mode) | ||||
| mesh_operator = getattr(bpy.ops.mesh, operator.operator_name) | mesh_operator = getattr(bpy.ops.mesh, operator.operator_name) | ||||
| if not mesh_operator: | |||||
| raise AttributeError("No mesh operator {}".format(operator.operator_name)) | try: | ||||
| mesh_operator(**operator.operator_parameters) | |||||
| retval = mesh_operator(**operator.operator_parameters) | retval = mesh_operator(**operator.operator_parameters) | ||||
| except AttributeError: | |||||
| raise AttributeError("bpy.ops.mesh has no attribute {}".format(operator.operator_name)) | |||||
| except TypeError: | |||||
| raise TypeError("Incorrect operator parameters {}".format(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_object_operator(self, operator: ObjectOperatorSpec): | |||||
| """ | |||||
| Applies the object operator. | |||||
| """ | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| object_operator = getattr(bpy.ops.object, operator.operator_name) | |||||
| try: | |||||
| object_operator(**operator.operator_parameters) | |||||
| retval = object_operator(**operator.operator_parameters) | |||||
| except AttributeError: | |||||
| raise AttributeError("bpy.ops.mesh has no attribute {}".format(operator.operator_name)) | |||||
| except TypeError: | |||||
| raise TypeError("Incorrect operator parameters {}".format(operator.operator_parameters)) | |||||
| if retval != {'FINISHED'}: | |||||
| raise RuntimeError("Unexpected operator return value: {}".format(retval)) | |||||
| if self.verbose: | |||||
| print("Applied operator {}".format(operator)) | |||||
| def _apply_deform_modifier(self, test_object, operation: list): | |||||
| """ | |||||
| param: operation: list: List of modifiers or combination of modifier and object operator. | |||||
| """ | |||||
| scene = bpy.context.scene | |||||
| scene.frame_set(1) | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| modifier_operations_list = operation.modifier_list | |||||
| modifier_names = [] | |||||
| object_operations = operation.object_operator_spec | |||||
| for modifier_operations in modifier_operations_list: | |||||
| if isinstance(modifier_operations, ModifierSpec): | |||||
| self._add_modifier(test_object, modifier_operations) | |||||
| modifier_names.append(modifier_operations.modifier_name) | |||||
| if isinstance(object_operations, ObjectOperatorSpec): | |||||
| self._apply_object_operator(object_operations) | |||||
| print("NAME", list(test_object.modifiers)) | |||||
| scene.frame_set(operation.frame_number) | |||||
| if self.apply_modifier: | |||||
| for mod_name in modifier_names: | |||||
| self._apply_modifier(test_object, mod_name) | |||||
| 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() | |||||
| 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. | ||||
| 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._add_modifier(evaluated_test_object, operation) | ||||
| if self.apply_modifier: | |||||
| self._apply_modifier(evaluated_test_object,operation.modifier_name) | |||||
| 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, PhysicsSpec): | elif isinstance(operation, ObjectOperatorSpec): | ||||
| self._apply_physics_settings(evaluated_test_object, operation) | self._apply_object_operator(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: | else: | ||||
| raise ValueError("Expected operation of type {} or {} or {}. Got {}". | raise ValueError("Expected operation of type {} or {} or {} or {}. Got {}". | ||||
| format(type(ModifierSpec), type(OperatorSpec), type(PhysicsSpec), | format(type(ModifierSpec), type(OperatorSpec), | ||||
| type(operation))) | type(ObjectOperatorSpec), type(ParticleSystemSpec), type(operation))) | ||||
| # Compare resulting mesh with expected one. | # Compare resulting mesh with expected one. | ||||
| if self.verbose: | if self.verbose: | ||||
| print("Comparing expected mesh with resulting mesh...") | print("Comparing expected mesh with resulting mesh...") | ||||
| 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 | ||||
| if self.threshold: | if self.threshold: | ||||
| compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh, threshold=self.threshold) | compare_result = evaluated_test_mesh.unit_test_compare(mesh=expected_mesh, threshold=self.threshold) | ||||
| Show All 35 Lines | class OperatorTest: | ||||
| def __init__(self, operator_tests): | def __init__(self, operator_tests): | ||||
| """ | """ | ||||
| Constructs an operator test. | Constructs an operator test. | ||||
| :param operator_tests: list - list of operator test cases. Each element in the list must contain the following | :param operator_tests: list - list of operator test cases. Each element in the list must contain the following | ||||
| in the correct order: | in the correct order: | ||||
| 1) select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' | 1) select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' | ||||
| 2) selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. | 2) selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. | ||||
| 3) test_object_name: bpy.Types.Object - test object | 3) test_name: str - unique name for each test | ||||
| 4) expected_object_name: bpy.Types.Object - expected object | 4) test_object_name: bpy.Types.Object - test object | ||||
| 5) operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" | 5) expected_object_name: bpy.Types.Object - expected object | ||||
| 6) operator_parameters: dict - {name : val} dictionary containing operator parameters. | 6) operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" | ||||
| 7) operator_parameters: dict - {name : val} dictionary containing operator parameters. | |||||
| """ | """ | ||||
| self.operator_tests = operator_tests | self.operator_tests = operator_tests | ||||
| self._check_for_unique_test_name() | |||||
| self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | ||||
| self._failed_tests_list = [] | self._failed_tests_list = [] | ||||
| def run_test(self, index: int): | def _check_for_unique_test_name(self): | ||||
| """ | |||||
| Check if the test name is unique in existing test names within the file. | |||||
| """ | |||||
| all_test_names = [] | |||||
| for index, _ in enumerate(self.operator_tests): | |||||
| test_name = self.operator_tests[index][2] | |||||
| all_test_names.append(test_name) | |||||
| seen_name = set() | |||||
| for ele in all_test_names: | |||||
| if ele in seen_name: | |||||
| raise ValueError("{} is a duplicate, write a new unique name.".format(ele)) | |||||
| else: | |||||
| seen_name.add(ele) | |||||
| def run_test(self, test_name: str): | |||||
| """ | """ | ||||
| Run a single test from operator_tests list | Run a single test from operator_tests list | ||||
| :param index: int - index of test | :param test_name: str - name of test | ||||
| :return: bool - True if test is successful. False otherwise. | :return: bool - True if test is successful. False otherwise. | ||||
| """ | """ | ||||
| case = None | |||||
| len_test = len(self.operator_tests) | |||||
| count = 0 | |||||
| # Finding the index of the test to match the "test name" | |||||
| for index,_ in enumerate(self.operator_tests): | |||||
| if test_name == self.operator_tests[index][2]: | |||||
| case = self.operator_tests[index] | case = self.operator_tests[index] | ||||
| if len(case) != 6: | break | ||||
| raise ValueError("Expected exactly 6 parameters for each test case, got {}".format(len(case))) | count = count + 1 | ||||
| if count == len_test: | |||||
| raise Exception("No test {} found!".format(test_name)) | |||||
| if len(case) != 7: | |||||
| raise ValueError("Expected exactly 7 parameters for each test case, got {}".format(len(case))) | |||||
| select_mode = case[0] | select_mode = case[0] | ||||
| selection = case[1] | selection = case[1] | ||||
| test_object_name = case[2] | test_name = case[2] | ||||
| expected_object_name = case[3] | test_object_name = case[3] | ||||
| operator_name = case[4] | expected_object_name = case[4] | ||||
| operator_parameters = case[5] | operator_name = case[5] | ||||
| operator_parameters = case[6] | |||||
| operator_spec = OperatorSpec(operator_name, operator_parameters, select_mode, selection) | operator_spec = OperatorSpec(operator_name, operator_parameters, select_mode, selection) | ||||
| test = MeshTest(test_object_name, expected_object_name) | test = MeshTest(test_name, test_object_name, expected_object_name) | ||||
| test.add_operator(operator_spec) | test.add_operator_to_stack(operator_spec) | ||||
| success = test.run_test() | success = test.run_test() | ||||
| if test.is_test_updated(): | if test.is_test_updated(): | ||||
| # Run the test again if the blend file has been updated. | # Run the test again if the blend file has been updated. | ||||
| success = test.run_test() | success = test.run_test() | ||||
| return success | return success | ||||
| def run_all_tests(self): | def run_all_tests(self): | ||||
| for index, _ in enumerate(self.operator_tests): | for index, _ in enumerate(self.operator_tests): | ||||
| test_name = self.operator_tests[index][2] | |||||
| if self.verbose: | if self.verbose: | ||||
| print() | print() | ||||
| print("Running test {}...".format(index)) | print("Running test {}...".format(index)) | ||||
| success = self.run_test(index) | print("Test name {}\n".format(test_name)) | ||||
| success = self.run_test(test_name) | |||||
| if not success: | if not success: | ||||
| self._failed_tests_list.append(index) | self._failed_tests_list.append(test_name) | ||||
| if len(self._failed_tests_list) != 0: | if len(self._failed_tests_list) != 0: | ||||
| print("Following tests failed: {}".format(self._failed_tests_list)) | print("Following tests failed: {}".format(self._failed_tests_list)) | ||||
| blender_path = bpy.app.binary_path | blender_path = bpy.app.binary_path | ||||
| blend_path = bpy.data.filepath | blend_path = bpy.data.filepath | ||||
| frame = inspect.stack()[1] | frame = inspect.stack()[1] | ||||
| 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_name>")) | ||||
| raise Exception("Tests {} failed".format(self._failed_tests_list)) | raise Exception("Tests {} failed".format(self._failed_tests_list)) | ||||
| class ModifierTest: | class ModifierTest: | ||||
| """ | """ | ||||
| Helper class that stores and executes modifier tests. | Helper class that stores and executes modifier tests. | ||||
| Example usage: | Example usage: | ||||
| >>> modifier_list = [ | >>> modifier_list = [ | ||||
| >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), | >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), | ||||
| >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) | >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) | ||||
| >>> ] | >>> ] | ||||
| >>> tests = [ | >>> tests = [ | ||||
| >>> ["testCube", "expectedCube", modifier_list], | >>> ["Test1","testCube", "expectedCube", modifier_list], | ||||
| >>> ["testCube_2", "expectedCube_2", modifier_list] | >>> ["Test2","testCube_2", "expectedCube_2", modifier_list] | ||||
| >>> ] | >>> ] | ||||
| >>> modifiers_test = ModifierTest(tests) | >>> modifiers_test = ModifierTest(tests) | ||||
| >>> modifiers_test.run_all_tests() | >>> modifiers_test.run_all_tests() | ||||
| """ | """ | ||||
| def __init__(self, modifier_tests: list, apply_modifiers=False, threshold=None): | def __init__(self, modifier_tests: list, apply_modifiers=False, threshold=None): | ||||
| """ | """ | ||||
| Construct a modifier test. | Construct a modifier test. | ||||
| :param modifier_tests: list - list of modifier test cases. Each element in the list must contain the following | :param modifier_tests: list - list of modifier test cases. Each element in the list must contain the following | ||||
| in the correct order: | in the correct order: | ||||
| 0) test_name: str - unique test name | |||||
| 1) test_object_name: bpy.Types.Object - test object | 1) test_object_name: bpy.Types.Object - test object | ||||
| 2) expected_object_name: bpy.Types.Object - expected object | 2) expected_object_name: bpy.Types.Object - expected object | ||||
| 3) modifiers: list - list of mesh_test.ModifierSpec objects. | 3) modifiers: list - list of mesh_test.ModifierSpec objects. | ||||
| """ | """ | ||||
| self.modifier_tests = modifier_tests | self.modifier_tests = modifier_tests | ||||
| self._check_for_unique_test_name() | |||||
| self.apply_modifiers = apply_modifiers | self.apply_modifiers = apply_modifiers | ||||
| self.threshold = threshold | self.threshold = threshold | ||||
| self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | ||||
| self._failed_tests_list = [] | self._failed_tests_list = [] | ||||
| def run_test(self, index: int): | def _check_for_unique_test_name(self): | ||||
| """ | |||||
| Check if the test name is unique | |||||
| """ | |||||
| all_test_names = [] | |||||
| for index, _ in enumerate(self.modifier_tests): | |||||
| test_name = self.modifier_tests[index][0] | |||||
| all_test_names.append(test_name) | |||||
| seen_name = set() | |||||
| for ele in all_test_names: | |||||
| if ele in seen_name: | |||||
| raise ValueError("{} is a duplicate, write a new unique name.".format(ele)) | |||||
| else: | |||||
| seen_name.add(ele) | |||||
| def run_test(self, test_name: str): | |||||
| """ | """ | ||||
| Run a single test from self.modifier_tests list | Run a single test from self.modifier_tests list | ||||
| :param index: int - index of test | :param test_name: str - name of test | ||||
| :return: bool - True if test passed, False otherwise. | :return: bool - True if test passed, False otherwise. | ||||
| """ | """ | ||||
| case = None | |||||
| len_test = len(self.modifier_tests) | |||||
| count = 0 | |||||
| for index, _ in enumerate(self.modifier_tests): | |||||
| if test_name == self.modifier_tests[index][0]: | |||||
| case = self.modifier_tests[index] | case = self.modifier_tests[index] | ||||
| if len(case) != 3: | break | ||||
| raise ValueError("Expected exactly 3 parameters for each test case, got {}".format(len(case))) | count = count+1 | ||||
| test_object_name = case[0] | if count == len_test: | ||||
| expected_object_name = case[1] | raise Exception("No test {} found!".format(test_name)) | ||||
| spec_list = case[2] | |||||
| if len(case) != 4: | |||||
| raise ValueError("Expected exactly 4 parameters for each test case, got {}".format(len(case))) | |||||
| test_name = case[0] | |||||
| test_object_name = case[1] | |||||
| expected_object_name = case[2] | |||||
| spec_list = case[3] | |||||
| test = MeshTest(test_object_name, expected_object_name, threshold=self.threshold) | test = MeshTest(test_name, test_object_name, expected_object_name, threshold=self.threshold) | ||||
| if self.apply_modifiers: | if self.apply_modifiers: | ||||
| test.apply_modifier = True | test.apply_modifier = True | ||||
| for modifier_spec in spec_list: | for modifier_spec in spec_list: | ||||
| test.add_modifier(modifier_spec) | test.add_modifier_to_stack(modifier_spec) | ||||
| success = test.run_test() | success = test.run_test() | ||||
| if test.is_test_updated(): | if test.is_test_updated(): | ||||
| # Run the test again if the blend file has been updated. | # Run the test again if the blend file has been updated. | ||||
| success = test.run_test() | success = test.run_test() | ||||
| return success | return success | ||||
| def run_all_tests(self): | def run_all_tests(self): | ||||
| """ | """ | ||||
| Run all tests in self.modifiers_tests list. Raises an exception if one the tests fails. | Run all tests in self.modifiers_tests list. Raises an exception if one the tests fails. | ||||
| """ | """ | ||||
| for index, _ in enumerate(self.modifier_tests): | for index, _ in enumerate(self.modifier_tests): | ||||
| test_name = self.modifier_tests[index][0] | |||||
| if self.verbose: | if self.verbose: | ||||
| print() | print() | ||||
| print("Running test {}...\n".format(index)) | print("Running test {}...".format(index)) | ||||
| success = self.run_test(index) | print("Test name {}\n".format(test_name)) | ||||
| success = self.run_test(test_name) | |||||
| if not success: | |||||
| self._failed_tests_list.append(test_name) | |||||
| if len(self._failed_tests_list) != 0: | |||||
| print("Following tests failed: {}".format(self._failed_tests_list)) | |||||
| blender_path = bpy.app.binary_path | |||||
| blend_path = bpy.data.filepath | |||||
| frame = inspect.stack()[1] | |||||
| module = inspect.getmodule(frame[0]) | |||||
| python_path = module.__file__ | |||||
| print("Run following command to open Blender and run the failing test:") | |||||
| print("{} {} --python {} -- {} {}" | |||||
| .format(blender_path, blend_path, python_path, "--run-test", "<test_name>")) | |||||
| raise Exception("Tests {} failed".format(self._failed_tests_list)) | |||||
| class DeformModifierTest: | |||||
| """ | |||||
| Helper class that stores and executes deform modifier tests. | |||||
| Example usage: | |||||
| >>> deform_modifier_list = [ | |||||
| >>> MeshTest("WarpPlane", "testObjPlaneWarp", "expObjPlaneWarp", | |||||
| >>> [DeformModifierSpec(10, [ModifierSpec('warp', 'WARP', | |||||
| >>> {'object_from': bpy.data.objects["From"], | |||||
| >>> 'object_to': bpy.data.objects["To"],})])]), | |||||
| >>> ] | |||||
| >>> deform_test = DeformModifierTest(deform_modifier_list) | |||||
| >>> deform_test.run_all_tests() | |||||
| """ | |||||
| def __init__(self, deform_tests: list, apply_modifiers=False): | |||||
| """ | |||||
| Construct a deform modifier test. | |||||
| Each test is made up of a MeshTest Class with its parameters | |||||
| :param: deform_test: list: List of modifiers can be added. | |||||
| Tt consists of a ModifierSpec and ObjectOperatorSpec | |||||
| """ | |||||
| self.deform_tests = deform_tests | |||||
| self._check_for_unique_test_name() | |||||
| self.apply_modifiers = apply_modifiers | |||||
| self.verbose = os.environ.get("BLENDER_VERBOSE") is not None | |||||
| self._failed_tests_list = [] | |||||
| def _check_for_unique_test_name(self): | |||||
| """ | |||||
| Check if the test name is unique | |||||
| """ | |||||
| all_test_names = [] | |||||
| for index, _ in enumerate(self.deform_tests): | |||||
| test_name = self.deform_tests[index].test_name | |||||
| all_test_names.append(test_name) | |||||
| seen_name = set() | |||||
| for ele in all_test_names: | |||||
| if ele in seen_name: | |||||
| raise ValueError("{} is a duplicate, write a new unique name.".format(ele)) | |||||
| else: | |||||
| seen_name.add(ele) | |||||
| def run_test(self, test_name: str): | |||||
| """ | |||||
| Run a single test from self.deform_tests list | |||||
| :param test_name: int - name of test | |||||
| :return: bool - True if test passed, False otherwise. | |||||
| """ | |||||
| case = self.deform_tests[0] | |||||
| len_test = len(self.deform_tests) | |||||
| count = 0 | |||||
| for index, _ in enumerate(self.deform_tests): | |||||
| if test_name == self.deform_tests[index].test_name: | |||||
| case = self.deform_tests[index] | |||||
| break | |||||
| count = count+1 | |||||
| if count == len_test: | |||||
| raise Exception('No test called {} found!'.format(test_name)) | |||||
| test = case | |||||
| if self.apply_modifiers: | |||||
| test.apply_modifier = True | |||||
| 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): | |||||
| """ | |||||
| Run all tests in self.modifiers_tests list. Raises an exception if one the tests fails. | |||||
| """ | |||||
| for index, _ in enumerate(self.deform_tests): | |||||
| test_name = self.deform_tests[index].test_name | |||||
| if self.verbose: | |||||
| print() | |||||
| print("Running test {}...".format(index)) | |||||
| print("Test name {}\n".format(test_name)) | |||||
| success = self.run_test(test_name) | |||||
| if not success: | if not success: | ||||
| self._failed_tests_list.append(index) | self._failed_tests_list.append(test_name) | ||||
| if len(self._failed_tests_list) != 0: | if len(self._failed_tests_list) != 0: | ||||
| print("Following tests failed: {}".format(self._failed_tests_list)) | print("Following tests failed: {}".format(self._failed_tests_list)) | ||||
| blender_path = bpy.app.binary_path | blender_path = bpy.app.binary_path | ||||
| blend_path = bpy.data.filepath | blend_path = bpy.data.filepath | ||||
| frame = inspect.stack()[1] | frame = inspect.stack()[1] | ||||
| 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_name>")) | ||||
| raise Exception("Tests {} failed".format(self._failed_tests_list)) | raise Exception("Tests {} failed".format(self._failed_tests_list)) | ||||