Changeset View
Changeset View
Standalone View
Standalone View
tests/python/modules/mesh_modifiers_test.py
- This file was added.
| # ##### BEGIN GPL LICENSE BLOCK ##### | |||||
| # | |||||
| # This program is free software; you can redistribute it and/or | |||||
| # modify it under the terms of the GNU General Public License | |||||
| # as published by the Free Software Foundation; either version 2 | |||||
| # of the License, or (at your option) any later version. | |||||
| # | |||||
| # This program is distributed in the hope that it will be useful, | |||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
| # GNU General Public License for more details. | |||||
| # | |||||
| # You should have received a copy of the GNU General Public License | |||||
| # along with this program; if not, write to the Free Software Foundation, | |||||
| # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||
| # | |||||
| # ##### END GPL LICENSE BLOCK ##### | |||||
| # <pep8 compliant> | |||||
| # A framework to run regression tests on mesh modifiers based on howardt's mesh_ops_test.py | |||||
| # | |||||
| # General idea: | |||||
| # A test is: | |||||
| # Object mode | |||||
| # Select <test object> | |||||
| # Duplicate the object | |||||
| # Select the object | |||||
| # Apply case.modifier with case.params for case in <modifiers list> | |||||
| # test_mesh = <test object>.data | |||||
| # run test_mesh.unit_test_compare(<expected object>.data) | |||||
| # delete the duplicate object | |||||
| # | |||||
| # The things in angle brackets are parameters of the test, and are specified in | |||||
| # a declarative ModifierTestSpec. | |||||
| # | |||||
| # If tests fail and it is because of a known and OK change due to things that have | |||||
| # changed in Blender, we can use the 'update_expected' parameter of RunTest | |||||
| # to update the <expected object>. | |||||
| import bpy | |||||
| def ParseParameters(params, verbose=False): | |||||
brecht: Can we avoid doing custom parameter parsing here? Any reason not to use a Python dictionary? | |||||
| """Parse a space-separated list of name=value pairs. | |||||
| Args: | |||||
| self: TestSpec | |||||
| Returns: | |||||
| dict - the parsed self.params | |||||
| """ | |||||
| ans = {} | |||||
| nvs = params.split() | |||||
| for nv in nvs: | |||||
| parts = nv.split('=') | |||||
| if len(parts) != 2: | |||||
| if verbose: | |||||
| print('Parameter syntax error at', nv) | |||||
| break | |||||
| name = parts[0] | |||||
| try: | |||||
| val = eval(parts[1]) | |||||
| except SyntaxError: | |||||
| if verbose: | |||||
| print('Parameter value syntax error at', nv) | |||||
| break | |||||
| ans[name] = val | |||||
| return ans | |||||
| class ModifierSpec: | |||||
| """ | |||||
| Holds one modifier and its parameters. | |||||
| """ | |||||
| def __init__(self, modifierName: str, modifierType: str, params: str): | |||||
| """ | |||||
| Constructs a modifier spec. | |||||
| Args: | |||||
| modifierName: str - name of object modifier | |||||
| modifierType: str - type of object modifier, e.g. "SUBSURF" | |||||
| params: string - space-separated name=val pairs giving operator arguments | |||||
| """ | |||||
| self.modifierName = modifierName | |||||
brechtUnsubmitted Not Done Inline ActionsFollow Blender Python naming conventions, camel case only for class names: https://wiki.blender.org/wiki/Style_Guide/Python brecht: Follow Blender Python naming conventions, camel case only for class names: https://wiki.blender. | |||||
| self.modifierType = modifierType | |||||
| self.params = ParseParameters(params) | |||||
| def __str__(self): | |||||
| return self.modifier + "(" + self.params + ") on " + self.test_obj + " selecting " + self.select | |||||
| class ModifierTestSpec: | |||||
| """ | |||||
| Holds names of test and expected result objects, | |||||
| a list of mesh modifiers and their arguments. | |||||
| """ | |||||
| def __init__(self, modifiers_list, test_obj, expected_obj): | |||||
| """ | |||||
| Constructs a modifier test spec. | |||||
| Args: | |||||
| modififiers_list: list - list of ModifierSpec | |||||
| test_obj: string - name of the object to apply the test to | |||||
| expected_obj: string - name of the object that has the expected result | |||||
| """ | |||||
| self.modifiers_list = modifiers_list | |||||
| self.test_obj = test_obj | |||||
| self.expected_obj = expected_obj | |||||
| def RunTest(modifierTestSpec, cleanup=True, verbose=True, update_expected=False): | |||||
| """ | |||||
| Run the test specified by modifierTestSpec | |||||
| Args: | |||||
| modifierTestSpec: ModifierTestSpec | |||||
| cleanup: bool - should we clean up duplicate after the test | |||||
| verbose: bool - should be we wordy | |||||
| update_expected: bool - should we replace the golden expected object | |||||
| with the result of current run? | |||||
| Only has effect if cleanup is false. | |||||
| Returns: | |||||
| bool - True if test passes, False otherwise | |||||
| """ | |||||
| if verbose: | |||||
| print("Run test:", ModifierTestSpec) | |||||
| objs = bpy.data.objects | |||||
| if modifierTestSpec.test_obj in objs: | |||||
| otest = objs[modifierTestSpec.test_obj] | |||||
| if verbose: | |||||
| print("Found test object", otest) | |||||
| else: | |||||
| if verbose: | |||||
| print('No test object', modifierTestSpec.test_obj) | |||||
| return False | |||||
| bpy.ops.object.mode_set(mode='OBJECT') | |||||
| bpy.ops.object.select_all(action='DESELECT') | |||||
| bpy.context.view_layer.objects.active = otest | |||||
| otest.select_set(True) | |||||
| bpy.ops.object.duplicate() | |||||
| duplicateTestObject = bpy.context.active_object | |||||
| otestdup = bpy.context.active_object | |||||
| if verbose: | |||||
| print(duplicateTestObject, "is set to active") | |||||
| # create modifiers in modifiers_list | |||||
| for modifierSpec in modifierTestSpec.modifiers_list: | |||||
| modifier = duplicateTestObject.modifiers.new(modifierSpec.modifierName, | |||||
| modifierSpec.modifierType) | |||||
| if verbose: | |||||
| print("created modifier", modifierSpec.modifierName, "of type", modifierSpec.modifierType) | |||||
| for paramName in modifierSpec.params: | |||||
| try: | |||||
| setattr(modifier, paramName, modifierSpec.params[paramName]) | |||||
| if verbose: | |||||
| print("set parameter '", paramName, "' with value", modifierSpec.params[paramName]) | |||||
| except AttributeError: | |||||
| if verbose: | |||||
| print('No modifier parameter', paramName) | |||||
| if cleanup: | |||||
| bpy.ops.object.delete() | |||||
| return False | |||||
| # apply modifiers | |||||
| for modifierSpec in modifierTestSpec.modifiers_list: | |||||
| bpy.ops.object.modifier_apply(modifier=modifierSpec.modifierName) | |||||
| if verbose: | |||||
| print("applied modifier", modifierSpec.modifierName) | |||||
| if modifierTestSpec.expected_obj in objs: | |||||
| oexpected = objs[modifierTestSpec.expected_obj] | |||||
| if verbose: | |||||
| print("found expected test object", oexpected) | |||||
| else: | |||||
| # If no expected object, test is ran just for effect | |||||
| if verbose: | |||||
| print('No expected object', modifierTestSpec.expected_obj) | |||||
| return True | |||||
| # now compare resulting mesh with expected one | |||||
| meshTest = duplicateTestObject.data | |||||
| meshExpected = oexpected.data | |||||
| compareResult = meshTest.unit_test_compare(mesh=meshExpected) | |||||
| success = (compareResult == 'Same') | |||||
| if success: | |||||
| if verbose: | |||||
| print('Success') | |||||
| else: | |||||
| if verbose: | |||||
| print('Fail', compareResult) | |||||
| if cleanup: | |||||
| bpy.ops.object.delete() | |||||
| otest.select_set(state=True, view_layer=None) | |||||
| bpy.context.view_layer.objects.active = otest | |||||
| elif update_expected: | |||||
| if verbose: | |||||
| print('Updating expected object', modifierTestSpec.expected_obj) | |||||
| oexpected.name = oexpected.name + '_pendingdelete' | |||||
| otestdup.location = oexpected.location | |||||
| expected_collections = oexpected.users_collection | |||||
| testdup_collections = otestdup.users_collection | |||||
| # should be exactly 1 collection each for otestdup and oexpected | |||||
| tcoll = testdup_collections[0] | |||||
| ecoll = expected_collections[0] | |||||
| tcoll.objects.unlink(otestdup) | |||||
| ecoll.objects.link(otestdup) | |||||
| bpy.context.view_layer.objects.active = oexpected | |||||
| bpy.ops.object.select_all(action='DESELECT') | |||||
| oexpected.select_set(state=True, view_layer=None) | |||||
| bpy.ops.object.delete() | |||||
| otestdup.name = modifierTestSpec.expected_obj | |||||
| bpy.context.view_layer.objects.active = otest | |||||
| return success | |||||
Can we avoid doing custom parameter parsing here? Any reason not to use a Python dictionary?