Changeset View
Standalone View
tests/python/modules/mesh_test.py
| Show All 39 Lines | |||||
| # Tests are verbose when the environment variable BLENDER_VERBOSE is set. | # Tests are verbose when the environment variable BLENDER_VERBOSE is set. | ||||
| import bpy | import bpy | ||||
| import functools | import functools | ||||
| import inspect | import inspect | ||||
| import os | import os | ||||
| # 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. | ||||
zazizizou: Can you specify which modifiers are supported here? | |||||
| """ | """ | ||||
| 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. | |||||
Done Inline ActionsMissing a space before int mont29: Missing a space before `int` | |||||
| """ | """ | ||||
| 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 | ||||
Done Inline ActionsAgain, space before int mont29: Again, space before `int` | |||||
| """ | """ | ||||
| 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 OperatorSpecEditMode: | ||||
Done Inline ActionsShould be renamed to something like OperatorSpecEditMode mont29: Should be renamed to something like `OperatorSpecEditMode` | |||||
| """ | """ | ||||
| Holds one operator and its parameters. | Holds one operator and its parameters. | ||||
Done Inline ActionsCan be more specific now that the name has changed zazizizou: Can be more specific now that the name has changed | |||||
| """ | """ | ||||
| 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 OperatorSpecEditMode. Raises ValueError if selec_mode is invalid. | ||||
| :param operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" | :param operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" | ||||
| :param operator_parameters: dict - {name : val} dictionary containing operator parameters. | :param operator_parameters: dict - {name : val} dictionary containing operator parameters. | ||||
| :param select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' | :param select_mode: str - mesh selection mode, must be either 'VERT', 'EDGE' or 'FACE' | ||||
| :param selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. | :param selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. | ||||
| """ | """ | ||||
| self.operator_name = operator_name | self.operator_name = operator_name | ||||
| self.operator_parameters = operator_parameters | self.operator_parameters = operator_parameters | ||||
| if select_mode not in ['VERT', 'EDGE', 'FACE']: | if select_mode not in ['VERT', 'EDGE', 'FACE']: | ||||
| raise ValueError("select_mode must be either {}, {} or {}".format('VERT', 'EDGE', 'FACE')) | raise ValueError("select_mode must be either {}, {} or {}".format('VERT', 'EDGE', 'FACE')) | ||||
| 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 OperatorSpecObjectMode: | |||||
Done Inline ActionsShould be renamed to something like OperatorSpecObjectMode mont29: Should be renamed to something like `OperatorSpecObjectMode` | |||||
| """ | |||||
| Holds an object operator and its parameters. Helper class for DeformModifierSpec. | |||||
Done Inline ActionsCould you also briefly explain why this is needed, e.g. why not use OperatorSpec? zazizizou: Could you also briefly explain why this is needed, e.g. why not use OperatorSpec? | |||||
| Needed to support operations in Object Mode and not Edit Mode which is supported by OperatorSpecEditMode. | |||||
| """ | |||||
| def __init__(self, operator_name: str, operator_parameters: dict): | |||||
| """ | |||||
| :param operator_name: str - name of the object operator from bpy.ops.object, e.g. "shade_smooth" or "shape_keys" | |||||
Done Inline Actionsdoesn't seem right zazizizou: doesn't seem right | |||||
| :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 OperatorSpecObjectMode. | |||||
Done Inline ActionsNot really accurate. Can you explain why this class is needed? zazizizou: Not really accurate. Can you explain why this class is needed? | |||||
| For deform modifiers which have an object operator | |||||
| """ | |||||
| def __init__(self, frame_number: int, modifier_list: list, object_operator_spec: OperatorSpecObjectMode = 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: OperatorSpecObjectMode - 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__( | def __init__( | ||||
| self, | self, | ||||
| test_name: str, | |||||
| test_object_name: str, | test_object_name: str, | ||||
| expected_object_name: str, | expected_object_name: str, | ||||
| operations_stack=None, | operations_stack=None, | ||||
| apply_modifiers=False, | apply_modifiers=False, | ||||
| threshold=None, | do_compare=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_object: str - Name of object of mesh type to run the operations on. | :param test_name: str - unique test name identifier. | ||||
| :param expected_object: str - Name of object of mesh type that has the expected | :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 | |||||
| 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_modifiers: 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 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: | 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, OperatorSpecEditMode) | ||||
| raise ValueError("Expected operation of type {} or {}. Got {}". | or isinstance(operation, OperatorSpecObjectMode) or isinstance(operation, DeformModifierSpec) | ||||
| format(type(ModifierSpec), type(OperatorSpec), | 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))) | type(operation))) | ||||
| self.operations_stack = operations_stack | self.operations_stack = operations_stack | ||||
| self.apply_modifier = apply_modifiers | self.apply_modifier = apply_modifiers | ||||
| self.do_compare = do_compare | |||||
| 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 14 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): | |||||
| """ | |||||
| Add a modifier to the operations stack. | |||||
| :param modifier_spec: modifier to add to the operations stack | |||||
| """ | |||||
| self.operations_stack.append(modifier_spec) | |||||
| if self.verbose: | |||||
| print("Added modififier {}".format(modifier_spec)) | |||||
| def add_operator(self, operator_spec: OperatorSpec): | |||||
| """ | |||||
| Adds an operator to the operations stack. | |||||
| :param operator_spec: OperatorSpec - operator to add to the operations stack. | |||||
| """ | |||||
| 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): | ||||
Done Inline ActionsI think the way the framework was extended makes these public functions confusing and unnecessary. There are also no checks for valid input, e.g. cloth modifier can't be added twice or so.. zazizizou: I think the way the framework was extended makes these public functions confusing and… | |||||
Done Inline Actionsnever mind, this is used in the helper classes like ModifierTest. So only input check is missing zazizizou: never mind, this is used in the helper classes like `ModifierTest`. So only input check is… | |||||
| 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.location = self.expected_object.location | ||||
| expected_object_name = self.expected_object.name | expected_object_name = self.expected_object.name | ||||
| Show All 19 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_impl(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 or sequence, a simple/nested dictionary of modifier parameters. | |||||
| :param: nested_settings_path : list(stack): helps in tracing path to each node. | |||||
| """ | |||||
Done Inline ActionsShould be dict or sequence ? campbellbarton: Should be `dict or sequence` ? | |||||
| if not isinstance(modifier_parameters, dict): | |||||
| param_setting = None | |||||
| for i, setting in enumerate(nested_settings_path): | |||||
| # 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) | |||||
| elif hasattr(modifier, setting): | |||||
| 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 | |||||
Not Done Inline ActionsThis exception seems like something we should avoid, as-is, it seems fairly harmless. Suggest to be more specific in this case, this could check the modifier type too for example. campbellbarton: This exception seems like something we should avoid, as-is, it seems fairly harmless.
OTOH, if… | |||||
Done Inline ActionsI am not sure how to handle it in a better way? since modifier has become a setting, I can't access modifier.type anymore. IMO we can keep it like this. I am up for suggestions to make it better. calra: I am not sure how to handle it in a better way? since `modifier` has become a setting, I can't… | |||||
| else: | |||||
| # Clean up first | |||||
| bpy.ops.object.delete() | |||||
Done Inline ActionsIt's not obvious which attribute is expected to fail (although I'd assume setattr(modifier, setting, modifier_parameters)). Is there any reason the try/except can't be replaced with a call to hasattr? This is normally preferable as try/except that contain more than a short snippet of code can hide mistakes/errors. campbellbarton: It's not obvious which attribute is expected to fail (although I'd assume `setattr(modifier… | |||||
| raise Exception("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_impl(modifier, modifier_parameters[key], nested_settings_path, modifier_name) | |||||
| if nested_settings_path: | |||||
| nested_settings_path.pop() | |||||
Done Inline ActionsCan be written as: if nested_settings_path: campbellbarton: Can be written as: `if nested_settings_path:` | |||||
| def set_parameters(self, modifier, modifier_parameters): | |||||
| """ | |||||
| Wrapper for _set_parameters_util | |||||
Done Inline ActionsI think 'wrapper' is more commonly used zazizizou: I think 'wrapper' is more commonly used | |||||
| """ | """ | ||||
| Add modifier to object and apply (if modifier_spec.apply_modifier is True) | settings = [] | ||||
| modifier_name = modifier.name | |||||
| self._set_parameters_impl(modifier, modifier_parameters, settings, modifier_name) | |||||
Not Done Inline ActionsThis isn't a copy and it seems the newly defined variable doesn't serve any purpose, should it be removed. campbellbarton: This isn't a copy and it seems the newly defined variable doesn't serve any purpose, should it… | |||||
Done Inline ActionsThe newly defined variable modifier_copy is sent to set_parameters_impl because the modifier is changed to become a setting. Are you suggesting that the function creates a copy within its scope and the original modifier is intact? calra: The newly defined variable `modifier_copy` is sent to `set_parameters_impl` because the… | |||||
Not Done Inline ActionsIn this case assigning the copy won't do what I think you intend it to do. As assigning it to a new variable wont make a copy. This would only make sense if you overwrote the modifier variable and wanted to access the previous value. campbellbarton: In this case assigning the copy won't do what I think you intend it to do. As assigning it to a… | |||||
| def _add_modifier(self, test_object, modifier_spec: ModifierSpec): | |||||
| """ | |||||
| Add modifier to object. | |||||
Done Inline ActionsSuggest to call _set_parameters_impl, (typically used when the internal implementation needs to be separate). campbellbarton: Suggest to call `_set_parameters_impl`, (typically used when the internal implementation needs… | |||||
| :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'] | |||||
Done Inline ActionsThis can be a tuple (preferred if it's not modified). campbellbarton: This can be a tuple (preferred if it's not modified). | |||||
| scene = bpy.context.scene | |||||
| scene.frame_set(1) | |||||
Done Inline ActionsAny reason to set the frame to zero instead of 1? (as was done previously). Suggest to use 1 since it's more common, or add a comment why zero is important. campbellbarton: Any reason to set the frame to zero instead of 1? (as was done previously).
Suggest to use 1… | |||||
| 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): | |||||
| # Modifier automatically gets applied when converting from Curve to Mesh. | |||||
| 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!") | |||||
| if self.apply_modifier: | def _bake_current_simulation(self, test_object, test_modifier_name, frame_end): | ||||
| bpy.ops.object.modifier_apply(modifier=modifier_spec.modifier_name) | """ | ||||
| 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} | |||||
Not Done Inline ActionsThis can be checked at construction time. If the object type is not supported, there is no point in trying to add a modifier zazizizou: This can be checked at construction time. If the object type is not supported, there is no… | |||||
| 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(1) | ||||
| 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 physics_spec.modifier_parameters: | for param_name in particle_sys_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( | bpy.ops.object.duplicates_make_real() | ||||
| test_object, | test_object.select_set(True) | ||||
| physics_spec.modifier_type, | bpy.ops.object.join() | ||||
| physics_spec.modifier_name, | |||||
| physics_spec.frame_end, | |||||
| ) | |||||
| 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_edit_mode(self, test_object, operator: OperatorSpecEditMode): | ||||
Done Inline Actionsshould be renamed _apply_operator_edit_mode() zazizizou: should be renamed `_apply_operator_edit_mode()` | |||||
| """ | """ | ||||
| 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: OperatorSpecEditMode - OperatorSpecEditMode object with parameters. | ||||
| """ | """ | ||||
| mesh = test_object.data | mesh = test_object.data | ||||
| bpy.ops.object.mode_set(mode='EDIT') | bpy.ops.object.mode_set(mode='EDIT') | ||||
| bpy.ops.mesh.select_all(action='DESELECT') | bpy.ops.mesh.select_all(action='DESELECT') | ||||
| bpy.ops.object.mode_set(mode='OBJECT') | bpy.ops.object.mode_set(mode='OBJECT') | ||||
| # Do selection. | # Do selection. | ||||
| bpy.context.tool_settings.mesh_select_mode = (operator.select_mode == 'VERT', | bpy.context.tool_settings.mesh_select_mode = (operator.select_mode == 'VERT', | ||||
| operator.select_mode == 'EDGE', | operator.select_mode == 'EDGE', | ||||
| operator.select_mode == 'FACE') | operator.select_mode == 'FACE') | ||||
| for index in operator.selection: | for index in operator.selection: | ||||
| if operator.select_mode == 'VERT': | if operator.select_mode == 'VERT': | ||||
| mesh.vertices[index].select = True | mesh.vertices[index].select = True | ||||
| elif operator.select_mode == 'EDGE': | elif operator.select_mode == 'EDGE': | ||||
| mesh.edges[index].select = True | mesh.edges[index].select = True | ||||
| elif operator.select_mode == 'FACE': | elif operator.select_mode == 'FACE': | ||||
| 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: | ||||
| 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 as ex: | |||||
| raise TypeError("Incorrect operator parameters {!r} raised {!r}".format(operator.operator_parameters, ex)) | |||||
Done Inline ActionsThis exceptions could hide useful information, suggest including the exception when re-raising. eg: except TypeError as ex:
raise TypeError(
"Incorrect operator parameters {!r}, "
"raised exception {!r}".format(operator.operator_parameters, ex))Same for _apply_operator_object_mode. campbellbarton: This exceptions could hide useful information, suggest including the exception when re-raising. | |||||
| if retval != {'FINISHED'}: | |||||
| raise RuntimeError("Unexpected operator return value: {}".format(retval)) | |||||
| if self.verbose: | |||||
| print("Applied {}".format(operator)) | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| def _apply_operator_object_mode(self, operator: OperatorSpecObjectMode): | |||||
Done Inline Actionsfor consistency, you could rename to _apply_operator_object_mode() zazizizou: for consistency, you could rename to `_apply_operator_object_mode()`
| |||||
| """ | |||||
| Applies the object operator. | |||||
| """ | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| object_operator = getattr(bpy.ops.object, operator.operator_name) | |||||
| try: | |||||
| retval = object_operator(**operator.operator_parameters) | |||||
| except AttributeError: | |||||
| raise AttributeError("bpy.ops.mesh has no attribute {}".format(operator.operator_name)) | |||||
| except TypeError as ex: | |||||
| raise TypeError("Incorrect operator parameters {!r} raised {!r}".format(operator.operator_parameters, ex)) | |||||
| 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)) | ||||
| 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') | 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, OperatorSpecObjectMode): | |||||
| self._apply_operator_object_mode(object_operations) | |||||
| scene.frame_set(operation.frame_number) | |||||
Done Inline ActionsWas this left in on purpose? campbellbarton: Was this left in on purpose? | |||||
| 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 | ||||
Not Done Inline ActionsThis might need a try..except if the error message from blender/python is not clear enogh zazizizou: This might need a `try..except` if the error message from blender/python is not clear enogh | |||||
Done Inline ActionsIt gets checked in later in another try..except calra: It gets checked in later in another `try..except` | |||||
| bpy.context.view_layer.objects.active = self.test_object | bpy.context.view_layer.objects.active = self.test_object | ||||
Done Inline Actionswhat is this for? zazizizou: what is this for? | |||||
| # 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 | ||||
Done Inline ActionsI think it's best to rename this to something like _apply_deform_modifier because that's the only case where it's used. zazizizou: I think it's best to rename this to something like `_apply_deform_modifier` because that's the… | |||||
Done Inline Actions'operation' is used in this file to refer to either a 'modifier' or an 'operator' but not to a list or a mixture of both. So could you rename this one? You should at least add documentation and/or type hints zazizizou: 'operation' is used in this file to refer to either a 'modifier' or an 'operator' but not to a… | |||||
| evaluated_test_object.name = "evaluated_object" | evaluated_test_object.name = "evaluated_object" | ||||
| if self.verbose: | if self.verbose: | ||||
| print() | |||||
Not Done Inline ActionsWas this left in on purpose? campbellbarton: Was this left in on purpose? | |||||
Done Inline ActionsYes, for formatting reasons (output on the console) calra: Yes, for formatting reasons (output on the console) | |||||
| 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, OperatorSpecEditMode): | |||||
| self._apply_operator_edit_mode(evaluated_test_object, operation) | |||||
| elif isinstance(operation, OperatorSpec): | elif isinstance(operation, OperatorSpecObjectMode): | ||||
| self._apply_operator(evaluated_test_object, operation) | 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) | |||||
| elif isinstance(operation, PhysicsSpec): | |||||
| self._apply_physics_settings(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(OperatorSpecEditMode), | ||||
| type(operation))) | type(OperatorSpecObjectMode), type(ParticleSystemSpec), type(operation))) | ||||
| # Compare resulting mesh with expected one. | # 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: | 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) | ||||
| else: | else: | ||||
| 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') | ||||
Done Inline ActionsThe check could be inverted, using an early return instead, seeing as the result is to skip all comparison code. campbellbarton: The check could be inverted, using an early return instead, seeing as the result is to skip all… | |||||
| # 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... | ||||
Done Inline Actionsare these tabs? if so you should replace with spaces zazizizou: are these tabs? if so you should replace with spaces | |||||
| 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...") | ||||
| # Delete evaluated_test_object. | # Delete evaluated_test_object. | ||||
| bpy.ops.object.delete() | bpy.ops.object.delete() | ||||
| return True | return True | ||||
| else: | else: | ||||
| return self._on_failed_test(compare_result, validation_success, evaluated_test_object) | return self._on_failed_test(compare_result, validation_success, evaluated_test_object) | ||||
| class OperatorTest: | class RunTest: | ||||
| """ | """ | ||||
| Helper class that stores and executes operator tests. | Helper class that stores and executes modifier tests. | ||||
| Example usage: | Example usage: | ||||
| >>> modifier_list = [ | |||||
| >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), | |||||
| >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) | |||||
| >>> ] | |||||
| >>> operator_list = [ | |||||
| >>> OperatorSpecEditMode("delete_edgeloop", {}, "EDGE", MONKEY_LOOP_EDGE), | |||||
| >>> ] | |||||
| >>> tests = [ | >>> tests = [ | ||||
| >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_1', 'intersect_boolean', {'operation': 'UNION'}], | >>> MeshTest("Test1", "testCube", "expectedCube", modifier_list), | ||||
| >>> ['FACE', {0, 1, 2, 3, 4, 5}, 'Cubecube', 'Cubecube_result_2', 'intersect_boolean', {'operation': 'INTERSECT'}], | >>> MeshTest("Test2", "testCube_2", "expectedCube_2", modifier_list), | ||||
| >>> MeshTest("MonkeyDeleteEdge", "testMonkey","expectedMonkey", operator_list) | |||||
| >>> ] | >>> ] | ||||
| >>> operator_test = OperatorTest(tests) | >>> modifiers_test = RunTest(tests) | ||||
| >>> operator_test.run_all_tests() | >>> modifiers_test.run_all_tests() | ||||
| """ | """ | ||||
| def __init__(self, operator_tests): | def __init__(self, tests, apply_modifiers=False, do_compare=False): | ||||
| """ | """ | ||||
| Constructs an operator test. | Construct a modifier test. | ||||
| :param operator_tests: list - list of operator test cases. Each element in the list must contain the following | :param tests: list - list of modifier or 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' | 0) test_name: str - unique test name | ||||
| 2) selection: set - set of vertices/edges/faces indices to select, e.g. [0, 9, 10]. | 1) test_object_name: bpy.Types.Object - test object | ||||
| 3) test_object_name: bpy.Types.Object - test object | 2) expected_object_name: bpy.Types.Object - expected object | ||||
| 4) expected_object_name: bpy.Types.Object - expected object | 3) modifiers or operators: list - list of mesh_test.ModifierSpec objects or | ||||
| 5) operator_name: str - name of mesh operator from bpy.ops.mesh, e.g. "bevel" or "fill" | mesh_test.OperatorSpecEditMode objects | ||||
| 6) operator_parameters: dict - {name : val} dictionary containing operator parameters. | |||||
| """ | """ | ||||
| self.operator_tests = operator_tests | self.tests = tests | ||||
| self._ensure_unique_test_name_or_raise_error() | |||||
| self.apply_modifiers = apply_modifiers | |||||
| self.do_compare = do_compare | |||||
| 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 _ensure_unique_test_name_or_raise_error(self): | ||||
Done Inline ActionsName is misleading, this raises an exception when not unique. Could be called: _ensure_unique_test_name_or_raise_error campbellbarton: Name is misleading, this raises an exception when not unique.
Could be called… | |||||
| """ | """ | ||||
| Run a single test from operator_tests list | Check if the test name is unique else raise an error. | ||||
| :param index: int - index of test | """ | ||||
| :return: bool - True if test is successful. False otherwise. | all_test_names = [] | ||||
| """ | for each_test in self.tests: | ||||
| case = self.operator_tests[index] | test_name = each_test.test_name | ||||
| if len(case) != 6: | all_test_names.append(test_name) | ||||
| raise ValueError("Expected exactly 6 parameters for each test case, got {}".format(len(case))) | |||||
| select_mode = case[0] | seen_name = set() | ||||
| selection = case[1] | for ele in all_test_names: | ||||
| test_object_name = case[2] | if ele in seen_name: | ||||
| expected_object_name = case[3] | raise ValueError("{} is a duplicate, write a new unique name.".format(ele)) | ||||
Done Inline ActionsEnumerate here seems redundant, looping on the value would make more sense. campbellbarton: Enumerate here seems redundant, looping on the value would make more sense. | |||||
| operator_name = case[4] | else: | ||||
| operator_parameters = case[5] | seen_name.add(ele) | ||||
| operator_spec = OperatorSpec(operator_name, operator_parameters, select_mode, selection) | |||||
| 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 | |||||
Not Done Inline Actionsthis should be at the very top, otherwise line 642 might fail zazizizou: this should be at the very top, otherwise line 642 might fail | |||||
Done Inline Actionsinitialized case with None now, if I have shifted it up, then we would have to check again because it gets reassigned calra: initialized `case` with `None` now, if I have shifted it up, then we would have to check again… | |||||
| def run_all_tests(self): | def run_all_tests(self): | ||||
| for index, _ in enumerate(self.operator_tests): | """ | ||||
| Run all tests in self.tests list. Raises an exception if one the tests fails. | |||||
| """ | |||||
| for test_number, each_test in enumerate(self.tests): | |||||
| test_name = each_test.test_name | |||||
| if self.verbose: | if self.verbose: | ||||
| print() | print() | ||||
| print("Running test {}...".format(index)) | print("Running test {}...".format(test_number)) | ||||
| 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("\nFollowing 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)) | ||||
Done Inline ActionsNow that you have only one class for ModifierTest and you have no plans of merging OperatorTest I don't see how defining this abstract class is useful. Why not include everything in ModifierTest and delete RunTest? zazizizou: Now that you have only one class for `ModifierTest` and you have no plans of merging… | |||||
| def run_test(self, test_name: str): | |||||
| class ModifierTest: | |||||
| """ | |||||
| Helper class that stores and executes modifier tests. | |||||
| Example usage: | |||||
| >>> modifier_list = [ | |||||
| >>> ModifierSpec("firstSUBSURF", "SUBSURF", {"quality": 5}), | |||||
| >>> ModifierSpec("firstSOLIDIFY", "SOLIDIFY", {"thickness_clamp": 0.9, "thickness": 1}) | |||||
| >>> ] | |||||
| >>> tests = [ | |||||
| >>> ["testCube", "expectedCube", modifier_list], | |||||
| >>> ["testCube_2", "expectedCube_2", modifier_list] | |||||
| >>> ] | |||||
| >>> modifiers_test = ModifierTest(tests) | |||||
| >>> modifiers_test.run_all_tests() | |||||
| """ | |||||
| def __init__(self, modifier_tests: list, apply_modifiers=False, threshold=None): | |||||
| """ | |||||
| Construct a modifier test. | |||||
| :param modifier_tests: list - list of modifier 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) modifiers: list - list of mesh_test.ModifierSpec objects. | |||||
| """ | |||||
| self.modifier_tests = modifier_tests | |||||
| self.apply_modifiers = apply_modifiers | |||||
| self.threshold = threshold | |||||
| 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 self.modifier_tests list | Run a single test from self.tests list | ||||
Done Inline Actionsmight as well name it _check_for_unique_test_name() zazizizou: might as well name it `_check_for_unique_test_name()` | |||||
| :param index: int - index of test | :param test_name: int - name of test | ||||
| :return: bool - True if test passed, False otherwise. | :return: bool - True if test passed, False otherwise. | ||||
| """ | """ | ||||
| case = self.modifier_tests[index] | case = None | ||||
| if len(case) != 3: | for index, each_test in enumerate(self.tests): | ||||
| raise ValueError("Expected exactly 3 parameters for each test case, got {}".format(len(case))) | if test_name == each_test.test_name: | ||||
| test_object_name = case[0] | case = self.tests[index] | ||||
| expected_object_name = case[1] | break | ||||
| spec_list = case[2] | |||||
| test = MeshTest(test_object_name, expected_object_name, threshold=self.threshold) | if case is None: | ||||
| raise Exception('No test called {} found!'.format(test_name)) | |||||
Done Inline ActionsThis block of code reads strangely.
campbellbarton: This block of code reads strangely.
- initializing case to the first test doesn't seem… | |||||
| test = case | |||||
| if self.apply_modifiers: | if self.apply_modifiers: | ||||
| test.apply_modifier = True | test.apply_modifier = True | ||||
| for modifier_spec in spec_list: | if self.do_compare: | ||||
| test.add_modifier(modifier_spec) | test.do_compare = True | ||||
| 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 | ||||
Done Inline Actionsinput parameters need documentation, especially threshold needs some explanation zazizizou: input parameters need documentation, especially threshold needs some explanation | |||||
| 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.modifier_tests): | |||||
| if self.verbose: | |||||
| print() | |||||
| print("Running test {}...\n".format(index)) | |||||
| success = self.run_test(index) | |||||
| if not success: | |||||
| self._failed_tests_list.append(index) | |||||
| 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_index>")) | |||||
| raise Exception("Tests {} failed".format(self._failed_tests_list)) | |||||
Can you specify which modifiers are supported here?