Files
test2/tests/files/modeling/mesh_ops_test.py
Sergey Sharybin bbfc97ad6f Move tests/data and assets to the main repository
This change moves the tests data files and publish folder of assets
repository to the main blender.git repository as LFS files.

The goal of this change is to eliminate toil of modifying tests,
cherry-picking changes to LFS branches, adding tests as part of a
PR which brings new features or fixes.

More detailed explanation and conversation can be found in the
design task.

Ref #137215

Pull Request: https://projects.blender.org/blender/blender/pulls/137219
2025-05-05 15:10:22 +02:00

293 lines
9.2 KiB
Python

# SPDX-FileCopyrightText: 2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# A framework to run regression tests on mesh operators
#
# General idea:
# A test is:
# Object mode
# Select <test object>
# Duplicate the object
# Edit mode
# Select None
# Select according to <select spec>
# Apply <mesh operator> with <test params>
# Object mode
# test_mesh = <test object>.data
# run test_mesh.validate()
# 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 TestSpec.
#
# 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
class TestSpec:
"""Test specification.
Holds names of test and expected result objects,
a selection specification,
a mesh op to run and its arguments
"""
def __init__(self, op, test_obj, expected_obj, select, params):
"""Construct a test spec.
Args:
op: string - name of a function in bpy.ops.mesh
test_obj: string - name of the object to apply the test to
expected_obj: string - name of the object that has the expected result
select: string - should be V, E, or F followed by space seperated indices of desired selection
params: string - space-separated name=val pairs giving operator arguments
"""
self.test_obj = test_obj
self.expected_obj = expected_obj
self.select = select
self.op = op
self.params = params
def __str__(self):
return self.op + "(" + self.params + ") on " + self.test_obj + " selecting " + self.select
def ParseParams(self, verbose=False):
"""Parse a space-separated list of name=value pairs.
Args:
self: TestSpec
Returns:
dict - the parsed self.params
"""
ans = {}
nvs = self.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
# This only works if there is a 3D View area visible somewhere in current window
def GetView3DContext(verbose=False):
"""Get a context dictionary for a View3D window.
This can be used as a context override to ensure that an operator will
be executed with a View3D window and other variables implied by that context.
Args:
verbose: bool - should we be wordy about errors
Returns:
dict - with keys for window, screen, area, region, and scene
"""
win = bpy.context.window
scr = win.screen
a3d = None
for a in scr.areas:
if a.type == 'VIEW_3D':
a3d = a
break
if a3d is None:
if verbose:
print('No 3d view area')
return None
rwin = None
for r in a3d.regions:
if r.type == 'WINDOW':
rwin = r
break
if rwin is None:
if verbose:
print('No window in 3d view area')
return None
return {'window': win, 'screen': scr, 'area': a3d, 'region': rwin, 'scene': bpy.context.scene}
def DoMeshSelect(select, verbose=False):
"""Given a selection spec string, switch to the desired selection mode and do the select.
Assume we are in Object mode on a mesh object.
The selection spec string should start with V, E, or F (for vertex, edge, face) to choose
the kind of element selected, and the is followed with a list of element indices
(use --debug to Blender to visualize these).
Args:
select: string - see comment above for syntax
verbose: bool - should we be wordy
Returns:
string - which select_mode to switch to
"""
m = bpy.context.active_object.data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
if m.is_editmode:
if verbose:
print('Select failed, not in Object mode')
return 'VERT'
if not select:
if verbose:
print('No select spec')
return 'VERT'
if select[0] == 'V':
seltype = 'VERT'
elif select[0] == 'E':
seltype = 'EDGE'
elif select[0] == 'F':
seltype = 'FACE'
else:
if verbose:
print('Bad select type', select[0])
return 'VERT'
bpy.context.tool_settings.mesh_select_mode = (seltype == 'VERT', seltype == 'EDGE', seltype == 'FACE')
parts = select[1:].split()
try:
elems = set([int(p) for p in parts])
except ValueError:
if verbose:
print('Bad syntax in select spec', select)
return
for i in elems:
if seltype == 'VERT':
m.vertices[i].select = True
elif seltype == 'EDGE':
m.edges[i].select = True
else:
m.polygons[i].select = True
return seltype
def RunTest(t, cleanup=True, verbose=False, update_expected=False):
"""Run the test specified by given TestSpec.
Args:
t: TestSpec
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:", t)
objs = bpy.data.objects
if t.test_obj in objs:
otest = objs[t.test_obj]
else:
if verbose:
print('No test object', t.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()
otestdup = bpy.context.active_object
smode = DoMeshSelect(t.select, verbose=verbose)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_mode(type=smode)
f = getattr(bpy.ops.mesh, t.op)
if not f:
if verbose:
print('No mesh op', t.op)
if cleanup:
bpy.ops.object.delete()
return False
kw = t.ParseParams(verbose)
retval = f(**kw)
if retval != {'FINISHED'}:
if verbose:
print('unexpected operator return value', retval)
if cleanup:
bpy.ops.object.delete()
return False
bpy.ops.object.mode_set(mode='OBJECT')
if t.expected_obj in objs:
oexpected = objs[t.expected_obj]
else:
# If no expected object, test is run just for effect
if verbose:
print('No expected object', t.expected_obj)
return True
mtest = otestdup.data
mexpected = oexpected.data
cmpret = mtest.unit_test_compare(mesh=mexpected)
success = (cmpret == 'Same')
if success:
if verbose:
print('Success')
else:
if verbose:
print('Fail', cmpret)
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', t.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]
ecoll.objects.link(otestdup)
tcoll.objects.unlink(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 = t.expected_obj
bpy.context.view_layer.objects.active = otest
return success
def RunAllTests(tests, cleanup=True, verbose=False, update_expected=False):
"""Run all tests.
Args:
tests: list of TestSpec - tests to run
cleanup: bool - if True, don't leave result objects lying around
verbose: bool - if True, chatter about running tests and failures
update_expected: bool - if True, replace all expected objects with
current results
Returns:
bool - True if all tests pass
"""
tot = 0
failed = 0
for t in tests:
tot += 1
if not RunTest(t, cleanup=cleanup, verbose=verbose, update_expected=update_expected):
failed += 1
if verbose:
print('Ran', tot, 'tests,' if tot > 1 else 'test,', failed, 'failed')
if failed > 0:
print('Tests Failed')
return failed == 0