Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'tests/python')
-rw-r--r--tests/python/CMakeLists.txt27
-rw-r--r--tests/python/bl_blendfile_library_overrides.py105
-rw-r--r--tests/python/bl_load_addons.py3
-rw-r--r--tests/python/bl_pyapi_bpy_path.py12
-rw-r--r--tests/python/bl_pyapi_bpy_utils_units.py4
-rw-r--r--tests/python/bl_pyapi_idprop.py52
-rw-r--r--tests/python/bl_pyapi_mathutils.py31
-rw-r--r--tests/python/bl_run_operators_event_simulate.py597
-rw-r--r--tests/python/bl_test.py13
-rw-r--r--tests/python/compositor_render_tests.py68
-rw-r--r--tests/python/operators.py122
11 files changed, 1007 insertions, 27 deletions
diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt
index 969b748e973..92cebb7d274 100644
--- a/tests/python/CMakeLists.txt
+++ b/tests/python/CMakeLists.txt
@@ -713,6 +713,33 @@ if(WITH_CYCLES OR WITH_OPENGL_RENDER_TESTS)
endif()
endif()
+if(WITH_COMPOSITOR)
+ set(compositor_tests
+ color
+ converter
+ distort
+ filter
+ input
+ matte
+ output
+ vector
+
+ multiple_node_setups
+ )
+
+ foreach(comp_test ${compositor_tests})
+ add_python_test(
+ compositor_${comp_test}_test
+ ${CMAKE_CURRENT_LIST_DIR}/compositor_render_tests.py
+ -blender "${TEST_BLENDER_EXE}"
+ -testdir "${TEST_SRC_DIR}/compositor/${comp_test}"
+ -idiff "${OPENIMAGEIO_IDIFF}"
+ -outdir "${TEST_OUT_DIR}/compositor"
+ )
+ endforeach()
+
+endif()
+
if(WITH_OPENGL_DRAW_TESTS)
if(NOT OPENIMAGEIO_IDIFF)
MESSAGE(STATUS "Disabling OpenGL draw tests because OIIO idiff does not exist")
diff --git a/tests/python/bl_blendfile_library_overrides.py b/tests/python/bl_blendfile_library_overrides.py
index ab75a410590..48625a1ecdb 100644
--- a/tests/python/bl_blendfile_library_overrides.py
+++ b/tests/python/bl_blendfile_library_overrides.py
@@ -1,6 +1,6 @@
# Apache License, Version 2.0
-# ./blender.bin --background -noaudio --python tests/python/bl_blendfile_library_overrides.py
+# ./blender.bin --background -noaudio --python tests/python/bl_blendfile_library_overrides.py -- --output-dir=/tmp/
import pathlib
import bpy
import sys
@@ -16,6 +16,8 @@ class TestLibraryOverrides(TestHelper, unittest.TestCase):
OBJECT_LIBRARY_PARENT = "LibMeshParent"
MESH_LIBRARY_CHILD = "LibMeshChild"
OBJECT_LIBRARY_CHILD = "LibMeshChild"
+ MESH_LIBRARY_PERMISSIVE = "LibMeshPermissive"
+ OBJECT_LIBRARY_PERMISSIVE = "LibMeshPermissive"
def __init__(self, args):
self.args = args
@@ -33,6 +35,14 @@ class TestLibraryOverrides(TestHelper, unittest.TestCase):
obj_child = bpy.data.objects.new(TestLibraryOverrides.OBJECT_LIBRARY_CHILD, object_data=mesh_child)
obj_child.parent = obj
bpy.context.collection.objects.link(obj_child)
+
+ mesh = bpy.data.meshes.new(TestLibraryOverrides.MESH_LIBRARY_PERMISSIVE)
+ obj = bpy.data.objects.new(TestLibraryOverrides.OBJECT_LIBRARY_PERMISSIVE, object_data=mesh)
+ bpy.context.collection.objects.link(obj)
+ obj.override_template_create()
+ prop = obj.override_library.properties.add(rna_path='scale')
+ prop.operations.add(operation='NOOP')
+
bpy.ops.wm.save_as_mainfile(filepath=str(self.output_path), check_existing=False, compress=False)
def __ensure_override_library_updated(self):
@@ -47,29 +57,109 @@ class TestLibraryOverrides(TestHelper, unittest.TestCase):
bpy.ops.wm.link(directory=str(link_dir), filename=TestLibraryOverrides.OBJECT_LIBRARY_PARENT)
obj = bpy.data.objects[TestLibraryOverrides.OBJECT_LIBRARY_PARENT]
- assert(obj.override_library is None)
+ self.assertIsNone(obj.override_library)
local_id = obj.override_create()
- assert(local_id.override_library)
- assert(local_id.data.override_library is None)
+ self.assertIsNotNone(local_id.override_library)
+ self.assertIsNone(local_id.data.override_library)
assert(len(local_id.override_library.properties) == 0)
local_id.location.y = 1.0
self.__ensure_override_library_updated()
- assert (len(local_id.override_library.properties) == 1)
assert(len(local_id.override_library.properties) == 1)
override_prop = local_id.override_library.properties[0]
assert(override_prop.rna_path == "location");
assert(len(override_prop.operations) == 1)
override_operation = override_prop.operations[0]
- assert (override_operation.operation == 'REPLACE')
+ assert(override_operation.operation == 'REPLACE')
# Setting location.y overridded all elements in the location array. -1 is a wildcard.
assert(override_operation.subitem_local_index == -1)
-
+
+ def test_link_permissive(self):
+ """
+ Linked assets with a permissive template.
+
+ - Checks if the NOOP is properly handled.
+ - Checks if the correct properties and operations are created/updated.
+ """
+ bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
+ bpy.data.orphans_purge()
+
+ link_dir = self.output_path / "Object"
+ bpy.ops.wm.link(directory=str(link_dir), filename=TestLibraryOverrides.OBJECT_LIBRARY_PERMISSIVE)
+
+ obj = bpy.data.objects[TestLibraryOverrides.OBJECT_LIBRARY_PERMISSIVE]
+ self.assertIsNotNone(obj.override_library)
+ local_id = obj.override_create()
+ self.assertIsNotNone(local_id.override_library)
+ self.assertIsNone(local_id.data.override_library)
+ assert(len(local_id.override_library.properties) == 1)
+ override_prop = local_id.override_library.properties[0]
+ assert(override_prop.rna_path == "scale");
+ assert(len(override_prop.operations) == 1)
+ override_operation = override_prop.operations[0]
+ assert(override_operation.operation == 'NOOP')
+ assert(override_operation.subitem_local_index == -1)
+
+ local_id.location.y = 1.0
+ local_id.scale.x = 0.5
+ # `scale.x` will apply, but will be reverted when the library overrides
+ # are updated. This is by design so python scripts can still alter the
+ # properties locally what is a typical usecase in productions.
+ assert(local_id.scale.x == 0.5)
+ assert(local_id.location.y == 1.0)
+
+ self.__ensure_override_library_updated()
+ assert(local_id.scale.x == 1.0)
+ assert(local_id.location.y == 1.0)
+
+ assert(len(local_id.override_library.properties) == 2)
+ override_prop = local_id.override_library.properties[0]
+ assert(override_prop.rna_path == "scale");
+ assert(len(override_prop.operations) == 1)
+ override_operation = override_prop.operations[0]
+ assert(override_operation.operation == 'NOOP')
+ assert(override_operation.subitem_local_index == -1)
+
+ override_prop = local_id.override_library.properties[1]
+ assert(override_prop.rna_path == "location");
+ assert(len(override_prop.operations) == 1)
+ override_operation = override_prop.operations[0]
+ assert(override_operation.operation == 'REPLACE')
+ assert (override_operation.subitem_local_index == -1)
+
+
+class TestLibraryTemplate(TestHelper, unittest.TestCase):
+ MESH_LIBRARY_PERMISSIVE = "LibMeshPermissive"
+ OBJECT_LIBRARY_PERMISSIVE = "LibMeshPermissive"
+
+ def __init__(self, args):
+ pass
+
+ def test_permissive_template(self):
+ """
+ Test setting up a permissive template.
+ """
+ bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
+ mesh = bpy.data.meshes.new(TestLibraryTemplate.MESH_LIBRARY_PERMISSIVE)
+ obj = bpy.data.objects.new(TestLibraryTemplate.OBJECT_LIBRARY_PERMISSIVE, object_data=mesh)
+ bpy.context.collection.objects.link(obj)
+ assert(obj.override_library is None)
+ obj.override_template_create()
+ assert(obj.override_library is not None)
+ assert(len(obj.override_library.properties) == 0)
+ prop = obj.override_library.properties.add(rna_path='scale')
+ assert(len(obj.override_library.properties) == 1)
+ assert(len(prop.operations) == 0)
+ operation = prop.operations.add(operation='NOOP')
+ assert(len(prop.operations) == 1)
+ assert(operation.operation == 'NOOP')
+
TESTS = (
TestLibraryOverrides,
+ TestLibraryTemplate,
)
@@ -95,6 +185,7 @@ def main():
# Don't write thumbnails into the home directory.
bpy.context.preferences.filepaths.use_save_preview_images = False
+ bpy.context.preferences.experimental.use_override_templates = True
for Test in TESTS:
Test(args).run_all_tests()
diff --git a/tests/python/bl_load_addons.py b/tests/python/bl_load_addons.py
index 01f0b4d72d8..60f44c9452a 100644
--- a/tests/python/bl_load_addons.py
+++ b/tests/python/bl_load_addons.py
@@ -59,7 +59,8 @@ def _init_addon_blacklist():
def addon_modules_sorted():
- modules = addon_utils.modules({})
+ # Pass in an empty module cache to prevent `addon_utils` local module cache being manipulated.
+ modules = addon_utils.modules(module_cache={})
modules[:] = [
mod for mod in modules
if not (mod.__file__.startswith(BLACKLIST_DIRS))
diff --git a/tests/python/bl_pyapi_bpy_path.py b/tests/python/bl_pyapi_bpy_path.py
index 2d6019fbb07..ddb13cfe2ce 100644
--- a/tests/python/bl_pyapi_bpy_path.py
+++ b/tests/python/bl_pyapi_bpy_path.py
@@ -26,12 +26,12 @@ class TestBpyPath(unittest.TestCase):
self.assertEqual(ensure_ext('', ''), '')
self.assertEqual(ensure_ext('', '.blend'), '.blend')
- # Test case-sensitive behaviour.
- self.assertEqual(ensure_ext('demo', '.blend', True), 'demo.blend')
- self.assertEqual(ensure_ext('demo.BLEND', '.blend', True), 'demo.BLEND.blend')
- self.assertEqual(ensure_ext('demo', 'Blend', True), 'demoBlend')
- self.assertEqual(ensure_ext('demoBlend', 'blend', True), 'demoBlendblend')
- self.assertEqual(ensure_ext('demo', '', True), 'demo')
+ # Test case-sensitive behavior.
+ self.assertEqual(ensure_ext('demo', '.blend', case_sensitive=True), 'demo.blend')
+ self.assertEqual(ensure_ext('demo.BLEND', '.blend', case_sensitive=True), 'demo.BLEND.blend')
+ self.assertEqual(ensure_ext('demo', 'Blend', case_sensitive=True), 'demoBlend')
+ self.assertEqual(ensure_ext('demoBlend', 'blend', case_sensitive=True), 'demoBlendblend')
+ self.assertEqual(ensure_ext('demo', '', case_sensitive=True), 'demo')
if __name__ == '__main__':
diff --git a/tests/python/bl_pyapi_bpy_utils_units.py b/tests/python/bl_pyapi_bpy_utils_units.py
index d5d9c9c707b..bdb64fc361e 100644
--- a/tests/python/bl_pyapi_bpy_utils_units.py
+++ b/tests/python/bl_pyapi_bpy_utils_units.py
@@ -54,7 +54,7 @@ class UnitsTesting(unittest.TestCase):
return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e)
for usys, utype, ref, inpt, val in self.INPUT_TESTS:
- opt_val = units.to_value(usys, utype, inpt, ref)
+ opt_val = units.to_value(usys, utype, inpt, str_ref_unit=ref)
# Note: almostequal is not good here, precision is fixed on decimal digits, not variable with
# magnitude of numbers (i.e. 1609.4416 ~= 1609.4456 fails even at 5 of 'places'...).
self.assertTrue(similar_values(opt_val, val, 1e-7),
@@ -63,7 +63,7 @@ class UnitsTesting(unittest.TestCase):
def test_units_outputs(self):
for usys, utype, prec, sep, compat, val, output in self.OUTPUT_TESTS:
- opt_str = units.to_string(usys, utype, val, prec, sep, compat)
+ opt_str = units.to_string(usys, utype, val, precision=prec, split_unit=sep, compatible_unit=compat)
self.assertEqual(
opt_str, output,
msg=(
diff --git a/tests/python/bl_pyapi_idprop.py b/tests/python/bl_pyapi_idprop.py
index 3d0cbd2a7bb..1e570bf9a7f 100644
--- a/tests/python/bl_pyapi_idprop.py
+++ b/tests/python/bl_pyapi_idprop.py
@@ -2,6 +2,7 @@
# ./blender.bin --background -noaudio --python tests/python/bl_pyapi_idprop.py -- --verbose
import bpy
+import idprop
import unittest
import numpy as np
from array import array
@@ -15,12 +16,12 @@ class TestHelper:
def setUp(self):
self._id = bpy.context.scene
- assert(len(self._id.keys()) == 0 or self._id.keys() == ["cycles"])
+ self._id.pop("cycles", None)
+ assert(len(self._id.keys()) == 0)
def tearDown(self):
for key in list(self._id.keys()):
- if key != "cycles":
- del self._id[key]
+ del self._id[key]
def assertAlmostEqualSeq(self, list1, list2):
self.assertEqual(len(list1), len(list2))
@@ -139,6 +140,51 @@ class TestIdPropertyCreation(TestHelper, unittest.TestCase):
with self.assertRaises(TypeError):
self.id["a"] = self
+class TestIdPropertyGroupView(TestHelper, unittest.TestCase):
+
+ def test_type(self):
+ self.assertEqual(type(self.id.keys()), idprop.types.IDPropertyGroupViewKeys)
+ self.assertEqual(type(self.id.values()), idprop.types.IDPropertyGroupViewValues)
+ self.assertEqual(type(self.id.items()), idprop.types.IDPropertyGroupViewItems)
+
+ self.assertEqual(type(iter(self.id.keys())), idprop.types.IDPropertyGroupIterKeys)
+ self.assertEqual(type(iter(self.id.values())), idprop.types.IDPropertyGroupIterValues)
+ self.assertEqual(type(iter(self.id.items())), idprop.types.IDPropertyGroupIterItems)
+
+ def test_basic(self):
+ text = ["A", "B", "C"]
+ for i, ch in enumerate(text):
+ self.id[ch] = i
+ self.assertEqual(len(self.id.keys()), len(text))
+ self.assertEqual(list(self.id.keys()), text)
+ self.assertEqual(list(reversed(self.id.keys())), list(reversed(text)))
+
+ self.assertEqual(len(self.id.values()), len(text))
+ self.assertEqual(list(self.id.values()), list(range(len(text))))
+ self.assertEqual(list(reversed(self.id.values())), list(reversed(range(len(text)))))
+
+ self.assertEqual(len(self.id.items()), len(text))
+ self.assertEqual(list(self.id.items()), [(k, v) for v, k in enumerate(text)])
+ self.assertEqual(list(reversed(self.id.items())), list(reversed([(k, v) for v, k in enumerate(text)])))
+
+ def test_contains(self):
+ # Check `idprop.types.IDPropertyGroupView{Keys/Values/Items}.__contains__`
+ text = ["A", "B", "C"]
+ for i, ch in enumerate(text):
+ self.id[ch] = i
+
+ self.assertIn("A", self.id)
+ self.assertNotIn("D", self.id)
+
+ self.assertIn("A", self.id.keys())
+ self.assertNotIn("D", self.id.keys())
+
+ self.assertIn(2, self.id.values())
+ self.assertNotIn(3, self.id.values())
+
+ self.assertIn(("A", 0), self.id.items())
+ self.assertNotIn(("D", 3), self.id.items())
+
class TestBufferProtocol(TestHelper, unittest.TestCase):
diff --git a/tests/python/bl_pyapi_mathutils.py b/tests/python/bl_pyapi_mathutils.py
index 9dfc6c159cc..c3a22be421a 100644
--- a/tests/python/bl_pyapi_mathutils.py
+++ b/tests/python/bl_pyapi_mathutils.py
@@ -2,7 +2,7 @@
# ./blender.bin --background -noaudio --python tests/python/bl_pyapi_mathutils.py -- --verbose
import unittest
-from mathutils import Matrix, Vector, Quaternion
+from mathutils import Matrix, Vector, Quaternion, Euler
from mathutils import kdtree, geometry
import math
@@ -233,6 +233,27 @@ class MatrixTesting(unittest.TestCase):
self.assertEqual(mat @ mat, prod_mat)
+ def test_loc_rot_scale(self):
+ euler = Euler((math.radians(90), 0, math.radians(90)), 'ZYX')
+ expected = Matrix(((0, -5, 0, 1),
+ (0, 0, -6, 2),
+ (4, 0, 0, 3),
+ (0, 0, 0, 1)))
+
+ result = Matrix.LocRotScale((1, 2, 3), euler, (4, 5, 6))
+ self.assertAlmostEqualMatrix(result, expected, 4)
+
+ result = Matrix.LocRotScale((1, 2, 3), euler.to_quaternion(), (4, 5, 6))
+ self.assertAlmostEqualMatrix(result, expected, 4)
+
+ result = Matrix.LocRotScale((1, 2, 3), euler.to_matrix(), (4, 5, 6))
+ self.assertAlmostEqualMatrix(result, expected, 4)
+
+ def assertAlmostEqualMatrix(self, first, second, size, *, places=6, msg=None, delta=None):
+ for i in range(size):
+ for j in range(size):
+ self.assertAlmostEqual(first[i][j], second[i][j], places=places, msg=msg, delta=delta)
+
class VectorTesting(unittest.TestCase):
@@ -442,20 +463,20 @@ class KDTreeTesting(unittest.TestCase):
ret_regular = k_odd.find(co)
self.assertEqual(ret_regular[1] % 2, 1)
- ret_filter = k_all.find(co, lambda i: (i % 2) == 1)
+ ret_filter = k_all.find(co, filter=lambda i: (i % 2) == 1)
self.assertAlmostEqualVector(ret_regular, ret_filter)
ret_regular = k_evn.find(co)
self.assertEqual(ret_regular[1] % 2, 0)
- ret_filter = k_all.find(co, lambda i: (i % 2) == 0)
+ ret_filter = k_all.find(co, filter=lambda i: (i % 2) == 0)
self.assertAlmostEqualVector(ret_regular, ret_filter)
# filter out all values (search odd tree for even values and the reverse)
co = (0,) * 3
- ret_filter = k_odd.find(co, lambda i: (i % 2) == 0)
+ ret_filter = k_odd.find(co, filter=lambda i: (i % 2) == 0)
self.assertEqual(ret_filter[1], None)
- ret_filter = k_evn.find(co, lambda i: (i % 2) == 1)
+ ret_filter = k_evn.find(co, filter=lambda i: (i % 2) == 1)
self.assertEqual(ret_filter[1], None)
def test_kdtree_invalid_size(self):
diff --git a/tests/python/bl_run_operators_event_simulate.py b/tests/python/bl_run_operators_event_simulate.py
new file mode 100644
index 00000000000..92315d3e853
--- /dev/null
+++ b/tests/python/bl_run_operators_event_simulate.py
@@ -0,0 +1,597 @@
+# ##### 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>
+
+r"""
+Overview
+========
+
+This is a utility to generate events from the command line,
+so reproducible test cases can be written without having to create a custom script each time.
+
+The key differentiating feature for this utility as is that it's able to control modal operators.
+
+Possible use cases for this script include:
+
+- Creating reproducible user interactions for the purpose of benchmarking and profiling.
+
+ Note that cursor-motion actions report the update time between events
+ which can be helpful when measuring optimizations.
+
+- As a convenient way to replay interactive actions that reproduce a bug.
+
+- For writing tests (although some extra functionality may be necessary in this case).
+
+
+Actions
+=======
+
+You will notice most of the functionality is supported using the actions command line argument,
+this is a kind of mini-language to drive Blender.
+
+While the current set of commands is fairly limited more can be added as needed.
+
+To see a list of actions as well as their arguments run:
+
+ ./blender.bin --python tests/python/bl_run_operators_event_simulate.py -- --help
+
+
+Examples
+========
+
+Rotate in edit-mode examples:
+
+ ./blender.bin \
+ --factory-startup \
+ --enable-event-simulate \
+ --python tests/python/bl_run_operators_event_simulate.py \
+ -- \
+ --actions \
+ 'area_maximize(ui_type="VIEW_3D")' \
+ 'operator("object.mode_set", mode="EDIT")' \
+ 'operator("mesh.select_all", action="SELECT")' \
+ 'operator("mesh.subdivide", number_cuts=5)' \
+ 'operator("transform.rotate")' \
+ 'cursor_motion(path="CIRCLE", radius=300, steps=100, repeat=2)'
+
+Sculpt stroke:
+
+ ./blender.bin \
+ --factory-startup \
+ --enable-event-simulate \
+ --python tests/python/bl_run_operators_event_simulate.py \
+ -- \
+ --actions \
+ 'area_maximize(ui_type="VIEW_3D")' \
+ 'event(type="FIVE", value="TAP", ctrl=True)' \
+ 'menu("Visual Geometry to Mesh")' \
+ 'menu("Frame Selected")' \
+ 'menu("Toggle Sculpt Mode")' \
+ 'event(type="WHEELDOWNMOUSE", value="TAP", repeat=2)' \
+ 'event(type="LEFTMOUSE", value="PRESS")' \
+ 'cursor_motion(path="CIRCLE", radius=300, steps=100, repeat=5)' \
+ 'event(type="LEFTMOUSE", value="RELEASE")'
+
+
+Implementation
+==============
+
+While most of the operations listed above can be executed in Python directly,
+either the event loop won't be handled between actions (the case for typical Python script),
+or the context for executing the actions is not properly set (the case for timers).
+
+This utility executes actions as if the user initiated them from a key shortcut.
+"""
+
+
+import os
+import sys
+import argparse
+from argparse import ArgumentTypeError
+
+import bpy
+
+
+# -----------------------------------------------------------------------------
+# Constants
+
+EVENT_TYPES = tuple(bpy.types.Event.bl_rna.properties["type"].enum_items.keys())
+EVENT_VALUES = tuple(bpy.types.Event.bl_rna.properties["value"].enum_items.keys())
+# `TAP` is just convenience for (`PRESS`, `RELEASE`).
+EVENT_VALUES_EXTRA = EVENT_VALUES + ('TAP',)
+
+
+# -----------------------------------------------------------------------------
+# Globals
+
+# Assign a global since this script is not going to be loading new files (which would free the window).
+win = bpy.context.window_manager.windows[0]
+
+
+# -----------------------------------------------------------------------------
+# Utilities
+
+def find_main_area(ui_type=None):
+ """
+ Find the largest area from the current screen.
+ """
+ area_best = None
+ size_best = -1
+ for area in win.screen.areas:
+ if ui_type is not None:
+ if ui_type != area.ui_type:
+ continue
+
+ size = area.width * area.height
+ if size > size_best:
+ size_best = size
+ area_best = area
+ return area_best
+
+
+def gen_events_type_text(text):
+ """
+ Generate events to type in `text`.
+ """
+ for ch in text:
+ kw_extra = {}
+ # The event type in this case is ignored as only the unicode value is used for text input.
+ type = 'SPACE'
+ if ch == '\t':
+ type = 'TAB'
+ elif ch == '\n':
+ type = 'RET'
+ else:
+ kw_extra["unicode"] = ch
+
+ yield dict(type=type, value='PRESS', **kw_extra)
+ kw_extra.pop("unicode", None)
+ yield dict(type=type, value='RELEASE', **kw_extra)
+
+
+# -----------------------------------------------------------------------------
+# Simulate Events
+
+def mouse_location_get():
+ return (
+ run_event_simulate.last_event["x"],
+ run_event_simulate.last_event["y"],
+ )
+
+
+def run_event_simulate(*, event_iter, exit_fn):
+ """
+ Pass events from event_iter into Blender.
+ """
+ last_event = run_event_simulate.last_event
+
+ def event_step():
+ win = bpy.context.window_manager.windows[0]
+
+ val = next(event_step.run_events, Ellipsis)
+ if val is Ellipsis:
+ bpy.app.use_event_simulate = False
+ print("Finished simulation")
+ exit_fn()
+ return None
+
+ # Run event simulation.
+ for attr in ("x", "y"):
+ if attr in val:
+ last_event[attr] = val[attr]
+ else:
+ val[attr] = last_event[attr]
+
+ # Fake event value, since press, release is so common.
+ if val.get("value") == 'TAP':
+ del val["value"]
+ win.event_simulate(**val, value='PRESS')
+ # Needed if new files are loaded.
+ # win = bpy.context.window_manager.windows[0]
+ win.event_simulate(**val, value='RELEASE')
+ else:
+ # print("val", val)
+ win.event_simulate(**val)
+ return 0.0
+
+ event_step.run_events = iter(event_iter)
+
+ bpy.app.timers.register(event_step, first_interval=0.0, persistent=True)
+
+
+run_event_simulate.last_event = dict(
+ x=win.width // 2,
+ y=win.height // 2,
+)
+
+
+# -----------------------------------------------------------------------------
+# Action Implementations
+
+# Static methods from this class are automatically exposed as actions and included in the help text.
+class action_handlers:
+
+ @staticmethod
+ def area_maximize(*, ui_type=None, only_validate=False):
+ """
+ ui_type:
+ Select the area type (typically 'VIEW_3D').
+ Note that this area type needs to exist in the current screen.
+ """
+ if not ((ui_type is None) or (isinstance(ui_type, str))):
+ raise ArgumentTypeError("'type' argument %r not None or a string type")
+
+ if only_validate:
+ return
+
+ area = find_main_area(ui_type=ui_type)
+ if area is None:
+ raise ArgumentTypeError("Area with ui_type=%r not found" % ui_type)
+
+ x = area.x + (area.width // 2)
+ y = area.y + (area.height // 2)
+
+ yield dict(type='MOUSEMOVE', value='NOTHING', x=x, y=y)
+ yield dict(type='SPACE', value='TAP', ctrl=True, alt=True)
+
+ x = win.width // 2
+ y = win.height // 2
+
+ yield dict(type='MOUSEMOVE', value='NOTHING', x=x, y=y)
+
+ @staticmethod
+ def menu(text, *, only_validate=False):
+ """
+ text: Menu item to search for and execute.
+ """
+ if not isinstance(text, str):
+ raise ArgumentTypeError("'text' argument not a string")
+
+ if only_validate:
+ return
+
+ yield dict(type='F3', value='TAP')
+ yield from gen_events_type_text(text)
+ yield dict(type='RET', value='TAP')
+
+ @staticmethod
+ def event(*, value, type, ctrl=False, alt=False, shift=False, repeat=1, only_validate=False):
+ """
+ value: The event, typically key, e.g. 'ESC', 'RET', 'SPACE', 'A'.
+ type: The event type, valid values include: 'PRESS', 'RELEASE', 'TAP'.
+ ctrl: Control modifier.
+ alt: Alt modifier.
+ shift: Shift modifier.
+ """
+ valid_items = EVENT_VALUES_EXTRA
+ if value not in valid_items:
+ raise ArgumentTypeError("'value' argument %r not in %r" % (value, valid_items))
+ valid_items = EVENT_TYPES
+ if type not in valid_items:
+ raise ArgumentTypeError("'type' argument %r not in %r" % (value, valid_items))
+ valid_items = range(1, sys.maxsize)
+ if repeat not in valid_items:
+ raise ArgumentTypeError("'repeat' argument %r not in %r" % (repeat, valid_items))
+ del valid_items
+
+ if only_validate:
+ return
+
+ for _ in range(repeat):
+ yield dict(type=type, ctrl=ctrl, alt=alt, shift=shift, value=value)
+
+ @staticmethod
+ def cursor_motion(*, path, steps, radius=100, repeat=1, only_validate=False):
+ """
+ path: The path type to use in ('CIRCLE').
+ steps: The number of events to generate.
+ radius: The radius in pixels.
+ repeat: Number of times to repeat the cursor rotation.
+ """
+
+ import time
+ from math import sin, cos, pi
+
+ valid_items = range(1, sys.maxsize)
+ if steps not in valid_items:
+ raise ArgumentTypeError("'steps' argument %r not in %r" % (steps, valid_items))
+
+ valid_items = range(1, sys.maxsize)
+ if radius not in valid_items:
+ raise ArgumentTypeError("'radius' argument %r not in %r" % (steps, valid_items))
+
+ valid_items = ('CIRCLE',)
+ if path not in valid_items:
+ raise ArgumentTypeError("'path' argument %r not in %r" % (path, valid_items))
+
+ valid_items = range(1, sys.maxsize)
+ if repeat not in valid_items:
+ raise ArgumentTypeError("'repeat' argument %r not in %r" % (repeat, valid_items))
+ del valid_items
+
+ if only_validate:
+ return
+
+ x_init, y_init = mouse_location_get()
+
+ y_init_ofs = y_init + radius
+
+ yield dict(type='MOUSEMOVE', value='NOTHING', x=x_init, y=y_init_ofs)
+
+ print("\n" "Times for: %s" % os.path.basename(bpy.data.filepath))
+
+ t = time.time()
+ step_total = 0
+
+ if path == 'CIRCLE':
+ for _ in range(repeat):
+ for i in range(1, steps + 1):
+ phi = (i / steps) * 2.0 * pi
+ x_ofs = -radius * sin(phi)
+ y_ofs = +radius * cos(phi)
+ step_total += 1
+ yield dict(
+ type='MOUSEMOVE',
+ value='NOTHING',
+ x=int(x_init + x_ofs),
+ y=int(y_init + y_ofs),
+ )
+
+ delta = time.time() - t
+ delta_step = delta / step_total
+ print(
+ "Average:",
+ ("%.6f FPS" % (1 / delta_step)).rjust(10),
+ )
+
+ yield dict(type='MOUSEMOVE', value='NOTHING', x=x_init, y=y_init)
+
+ @staticmethod
+ def operator(idname, *, only_validate=False, **kw):
+ """
+ idname: The operator identifier (positional argument only).
+ kw: Passed to the operator.
+ """
+
+ # Create a temporary key binding to call the operator.
+ wm = bpy.context.window_manager
+ keyconf = wm.keyconfigs.user
+
+ keymap_id = "Screen"
+ key_to_map = 'F24'
+
+ if only_validate:
+ op_mod, op_submod = idname.partition(".")[0::2]
+ op = getattr(getattr(bpy.ops, op_mod), op_submod)
+ try:
+ # The poll result doesn't matter we only want to know if the operator exists or not.
+ op.poll()
+ except AttributeError:
+ raise ArgumentTypeError("Operator %r does not exist" % (idname))
+
+ keymap = keyconf.keymaps[keymap_id]
+ kmi = keymap.keymap_items.new(idname=idname, type=key_to_map, value='PRESS')
+ kmi.idname = idname
+ props = kmi.properties
+ for key, value in kw.items():
+ if not hasattr(props, key):
+ raise ArgumentTypeError("Operator %r does not have a %r property" % (idname, key))
+
+ try:
+ setattr(props, key, value)
+ except Exception as ex:
+ raise ArgumentTypeError("Operator %r assign %r property with error %s" % (idname, key, str(ex)))
+
+ keymap.keymap_items.remove(kmi)
+ return
+
+ keymap = keyconf.keymaps[keymap_id]
+ kmi = keymap.keymap_items.new(idname=idname, type=key_to_map, value='PRESS')
+ kmi.idname = idname
+ props = kmi.properties
+ for key, value in kw.items():
+ setattr(props, key, value)
+
+ yield dict(type=key_to_map, value='TAP')
+
+ keymap = keyconf.keymaps[keymap_id]
+ kmi = keymap.keymap_items[-1]
+ keymap.keymap_items.remove(kmi)
+
+
+ACTION_DIR = tuple([
+ key for key in sorted(action_handlers.__dict__.keys())
+ if not key.startswith("_")
+])
+
+
+def handle_action(op, args, kwargs, only_validate=False):
+ fn = getattr(action_handlers, op, None)
+ if fn is None:
+ raise ArgumentTypeError("Action %r is not found in %r" % (op, ACTION_DIR))
+ yield from fn(*args, **kwargs, only_validate=only_validate)
+
+
+# -----------------------------------------------------------------------------
+# Argument Parsing
+
+
+class BlenderAction(argparse.Action):
+ """
+ This class is used to extract positional & keyword arguments from
+ a string, validate them, and return the (action, positional_args, keyword_args).
+
+ All of this happens during argument parsing so any errors in the actions
+ show useful error messages instead of failing to execute part way through.
+ """
+
+ @staticmethod
+ def _parse_value(value, index):
+ """
+ Convert:
+ "value(1, 2, a=1, b='', c=None)"
+ To:
+ ("value", (1, 2), {"a": 1, "b": "", "c": None})
+ """
+ split = value.find("(")
+ if split == -1:
+ op = value
+ args = None
+ kwargs = None
+ else:
+ op = value[:split]
+ namespace = {op: lambda *args, **kwargs: (args, kwargs)}
+ expr = value
+ try:
+ args, kwargs = eval(expr, namespace, namespace)
+ except Exception as ex:
+ raise ArgumentTypeError("Unable to parse \"%s\" at index %d, error: %s" % (expr, index, str(ex)))
+
+ # Creating a list is necessary since this is a generator.
+ try:
+ dummy_result = list(handle_action(op, args, kwargs, only_validate=True))
+ except ArgumentTypeError as ex:
+ raise ArgumentTypeError("Invalid 'action' arguments \"%s\" at index %d, %s" % (value, index, str(ex)))
+ # Validation should never yield any events.
+ assert(not dummy_result)
+
+ return (op, args, kwargs)
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ setattr(
+ namespace,
+ self.dest, [
+ self._parse_value(value, index)
+ for index, value in enumerate(values)
+ ],
+ )
+
+
+def argparse_create():
+ import inspect
+ import textwrap
+
+ # When --help or no args are given, print this help
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawTextHelpFormatter,
+ )
+
+ parser.add_argument(
+ "--keep-open",
+ dest="keep_open",
+ default=False,
+ action="store_true",
+ help=(
+ "Keep the window open instead of exiting once event simulation is complete.\n"
+ "This can be useful to inspect the state of the file once the simulation is complete."
+ ),
+ required=False,
+ )
+
+ # Collect doc-strings from static methods in `actions`.
+ actions_docstring = []
+ for action_key in ACTION_DIR:
+ action = getattr(action_handlers, action_key)
+ args = str(inspect.signature(action))
+ args = "(" + args[1:].removeprefix("*, ")
+ args = args.replace(", *, ", ", ") # Needed in case the are positional arguments.
+ args = args.replace(", only_validate=False", "")
+
+ actions_docstring.append("- %s%s\n" % (action_key, args))
+ docs = textwrap.dedent((action.__doc__ or "").lstrip("\n").rstrip()) + "\n\n"
+
+ actions_docstring.append(textwrap.indent(docs, " "))
+
+ parser.add_argument(
+ "--actions",
+ dest="actions",
+ metavar='ACTIONS', type=str,
+ help=(
+ "\n" "Arguments must use one of the following prefix:\n"
+ "\n" + "".join(actions_docstring)
+ ),
+ nargs='+',
+ required=True,
+ action=BlenderAction,
+ )
+
+ return parser
+
+
+# -----------------------------------------------------------------------------
+# Default Startup
+
+
+def setup_default_preferences(prefs):
+ """
+ Set preferences useful for automation.
+ """
+ prefs.view.show_splash = False
+ prefs.view.smooth_view = 0
+ prefs.view.use_save_prompt = False
+ prefs.view.show_developer_ui = True
+ prefs.filepaths.use_auto_save_temporary_files = False
+
+
+# -----------------------------------------------------------------------------
+# Main Function
+
+
+def main_event_iter(*, action_list):
+ """
+ Yield all events from action handlers.
+ """
+ area = find_main_area()
+
+ x_init = area.x + (area.width // 2)
+ y_init = area.y + (area.height // 2)
+
+ yield dict(type='MOUSEMOVE', value='NOTHING', x=x_init, y=y_init)
+
+ for (op, args, kwargs) in action_list:
+ yield from handle_action(op, args, kwargs)
+
+
+def main():
+ from sys import argv
+ argv = argv[argv.index("--") + 1:] if "--" in argv else []
+
+ try:
+ args = argparse_create().parse_args(argv)
+ except ArgumentTypeError as ex:
+ print(ex)
+ sys.exit(1)
+
+ setup_default_preferences(bpy.context.preferences)
+
+ def exit_fn():
+ if not args.keep_open:
+ sys.exit(0)
+ else:
+ bpy.app.use_event_simulate = False
+
+ run_event_simulate(
+ event_iter=main_event_iter(action_list=args.actions),
+ exit_fn=exit_fn,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/python/bl_test.py b/tests/python/bl_test.py
index 6315ffbfa9d..110b4238f6c 100644
--- a/tests/python/bl_test.py
+++ b/tests/python/bl_test.py
@@ -32,9 +32,18 @@ def replace_bpy_app_version():
app = bpy.app
app_fake = type(bpy)("bpy.app")
+ app_attr_exclude = {
+ # This causes a noisy warning every time.
+ "binary_path_python",
+ }
+
for attr in dir(app):
- if not attr.startswith("_"):
- setattr(app_fake, attr, getattr(app, attr))
+ if attr.startswith("_"):
+ continue
+ if attr in app_attr_exclude:
+ continue
+
+ setattr(app_fake, attr, getattr(app, attr))
app_fake.version = 0, 0, 0
app_fake.version_string = "0.00 (sub 0)"
diff --git a/tests/python/compositor_render_tests.py b/tests/python/compositor_render_tests.py
new file mode 100644
index 00000000000..057d4a2e6dd
--- /dev/null
+++ b/tests/python/compositor_render_tests.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# Apache License, Version 2.0
+
+import argparse
+import os
+import shlex
+import shutil
+import subprocess
+import sys
+
+
+# When run from inside Blender, render and exit.
+try:
+ import bpy
+ inside_blender = True
+except ImportError:
+ inside_blender = False
+
+def get_arguments(filepath, output_filepath):
+ return [
+ "--background",
+ "-noaudio",
+ "--factory-startup",
+ "--enable-autoexec",
+ "--debug-memory",
+ "--debug-exit-on-error",
+ filepath,
+ "-P",
+ os.path.realpath(__file__),
+ "-o", output_filepath,
+ "-F", "PNG",
+ "-f", "1"]
+
+
+def create_argparse():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-blender", nargs="+")
+ parser.add_argument("-testdir", nargs=1)
+ parser.add_argument("-outdir", nargs=1)
+ parser.add_argument("-idiff", nargs=1)
+ return parser
+
+
+def main():
+ parser = create_argparse()
+ args = parser.parse_args()
+
+ blender = args.blender[0]
+ test_dir = args.testdir[0]
+ idiff = args.idiff[0]
+ output_dir = args.outdir[0]
+
+ from modules import render_report
+ report = render_report.Report("Compositor", output_dir, idiff)
+ report.set_pixelated(True)
+ report.set_reference_dir("compositor_renders")
+
+ # Temporary change to pass OpenImageDenoise test with both 1.3 and 1.4.
+ if os.path.basename(test_dir) == 'filter':
+ report.set_fail_threshold(0.05)
+
+ ok = report.run(test_dir, blender, get_arguments, batch=True)
+
+ sys.exit(not ok)
+
+
+if not inside_blender and __name__ == "__main__":
+ main()
diff --git a/tests/python/operators.py b/tests/python/operators.py
index 461880ec214..c209b01c20c 100644
--- a/tests/python/operators.py
+++ b/tests/python/operators.py
@@ -137,7 +137,6 @@ def main():
MeshTest("CubeEdgeSplit", "testCubeEdgeSplit", "expectedCubeEdgeSplit",
[OperatorSpecEditMode("edge_split", {}, "EDGE", {2, 5, 8, 11, 14, 17, 20, 23})]),
- ### 25
# edge ring select - Cannot be tested. Need user input.
# MeshTest("CubeEdgeRingSelect", "testCubeEdgeRingSelect", "expectedCubeEdgeRingSelect",
# [OperatorSpecEditMode("edgering_select", {}, "EDGE", {5, 20, 25, 26})]),
@@ -146,6 +145,16 @@ def main():
# MeshTest("EmptyMeshEdgeRingSelect", "testEmptyMeshdgeRingSelect", "expectedEmptyMeshEdgeRingSelect",
# [OperatorSpecEditMode("edgering_select", {}, "VERT", {})]),
+ # edges select sharp
+ MeshTest("CubeEdgesSelectSharp", "testCubeEdgeSelectSharp", "expectedCubeEdgeSelectSharp",
+ [OperatorSpecEditMode("edges_select_sharp", {}, "EDGE", {20})]),
+ MeshTest("SphereEdgesSelectSharp", "testSphereEdgesSelectSharp", "expectedSphereEdgeSelectSharp",
+ [OperatorSpecEditMode("edges_select_sharp", {"sharpness": 0.25}, "EDGE", {288})]),
+ MeshTest("HoledSphereEdgesSelectSharp", "testHoledSphereEdgesSelectSharp", "expectedHoledSphereEdgeSelectSharp",
+ [OperatorSpecEditMode("edges_select_sharp", {"sharpness": 0.18}, "VERT", {})]),
+ MeshTest("EmptyMeshEdgesSelectSharp", "testEmptyMeshEdgeSelectSharp", "expectedEmptyMeshEdgeSelectSharp",
+ [OperatorSpecEditMode("edges_select_sharp", {}, "VERT", {})]),
+
# face make planar
MeshTest("MonkeyFaceMakePlanar", "testMonkeyFaceMakePlanar",
"expectedMonkeyFaceMakePlanar",
@@ -187,6 +196,14 @@ def main():
MeshTest("SphereFillHoles", "testSphereFillHoles", "expectedSphereFillHoles",
[OperatorSpecEditMode("fill_holes", {"sides": 9}, "VERT", {i for i in range(481)})]),
+ # face shade smooth (not a real test)
+ MeshTest("CubeShadeSmooth", "testCubeShadeSmooth", "expectedCubeShadeSmooth",
+ [OperatorSpecEditMode("faces_shade_smooth", {}, "VERT", {i for i in range(8)})]),
+
+ # faces shade flat (not a real test)
+ MeshTest("CubeShadeFlat", "testCubeShadeFlat", "expectedCubeShadeFlat",
+ [OperatorSpecEditMode("faces_shade_flat", {}, "FACE", {i for i in range(6)})]),
+
# inset faces
MeshTest("CubeInset",
"testCubeInset", "expectedCubeInset", [OperatorSpecEditMode("inset", {"thickness": 0.2}, "VERT",
@@ -208,6 +225,109 @@ def main():
[OperatorSpecEditMode("inset", {"thickness": 0.4,
"use_relative_offset": True}, "FACE",
{35, 36, 37, 45, 46, 47, 55, 56, 57})]),
+
+ # loop multi select
+ MeshTest("MokeyLoopMultiSelect", "testMonkeyLoopMultiSelect", "expectedMonkeyLoopMultiSelect",
+ [OperatorSpecEditMode("loop_multi_select", {}, "VERT", {355, 359, 73, 301, 302})]),
+ MeshTest("HoledGridLoopMultiSelect", "testGridLoopMultiSelect", "expectedGridLoopMultiSelect",
+ [OperatorSpecEditMode("loop_multi_select", {}, "VERT", {257, 169, 202, 207, 274, 278, 63})]),
+ MeshTest("EmptyMeshLoopMultiSelect", "testEmptyMeshLoopMultiSelect", "expectedEmptyMeshLoopMultiSelect",
+ [OperatorSpecEditMode("loop_multi_select", {}, "VERT", {})]),
+
+ # mark seam
+ MeshTest("CubeMarkSeam", "testCubeMarkSeam", "expectedCubeMarkSeam",
+ [OperatorSpecEditMode("mark_seam", {}, "EDGE", {1})]),
+
+ # select all
+ MeshTest("CircleSelectAll", "testCircleSelectAll", "expectedCircleSelectAll",
+ [OperatorSpecEditMode("select_all", {}, "VERT", {1})]),
+ MeshTest("IsolatedVertsSelectAll", "testIsolatedVertsSelectAll", "expectedIsolatedVertsSelectAll",
+ [OperatorSpecEditMode("select_all", {}, "VERT", {})]),
+ MeshTest("EmptyMeshSelectAll", "testEmptyMeshSelectAll", "expectedEmptyMeshSelectAll",
+ [OperatorSpecEditMode("select_all", {}, "VERT", {})]),
+
+ # select axis - Cannot be tested. Needs active vert selection
+ # MeshTest("MonkeySelectAxisX", "testMonkeySelectAxisX", "expectedMonkeySelectAxisX",
+ # [OperatorSpecEditMode("select_axis", {"axis": "X"}, "VERT", {13})]),
+ # MeshTest("MonkeySelectAxisY", "testMonkeySelectAxisY", "expectedMonkeySelectAxisY",
+ # [OperatorSpecEditMode("select_axis", {"axis": "Y", "sign": "NEG"}, "FACE", {317})]),
+ # MeshTest("MonkeySelectAxisXYZ", "testMonkeySelectAxisXYZ", "expectedMonkeySelectAxisXYZ",
+ # [OperatorSpecEditMode("select_axis", {"axis": "X", "sign": "NEG"}, "FACE", {317}),
+ # OperatorSpecEditMode("select_axis", {"axis": "Y", "sign": "POS"}, "FACE", {}),
+ # OperatorSpecEditMode("select_axis", {"axis": "Z", "sign": "NEG"}, "FACE", {})]),
+
+ # select faces by sides
+ MeshTest("CubeSelectFacesBySide", "testCubeSelectFacesBySide", "expectedCubeSelectFacesBySide",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4}, "FACE", {})]),
+ MeshTest("CubeSelectFacesBySideGreater", "testCubeSelectFacesBySideGreater", "expectedCubeSelectFacesBySideGreater",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4, "type": "GREATER", "extend": True}, "FACE", {})]),
+ MeshTest("CubeSelectFacesBySideLess", "testCubeSelectFacesBySideLess", "expectedCubeSelectFacesBySideLess",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4, "type": "GREATER", "extend": True}, "FACE", {})]),
+
+ # select interior faces
+ MeshTest("CubeSelectInteriorFaces", "testCubeSelectInteriorFaces", "expectedCubeSelectInteriorFaces",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4}, "FACE", {})]),
+ MeshTest("HoledCubeSelectInteriorFaces", "testHoledCubeSelectInteriorFaces", "expectedHoledCubeSelectInteriorFaces",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4}, "FACE", {})]),
+ MeshTest("EmptyMeshSelectInteriorFaces", "testEmptyMeshSelectInteriorFaces", "expectedEmptyMeshSelectInteriorFaces",
+ [OperatorSpecEditMode("select_face_by_sides", {"number": 4}, "FACE", {})]),
+
+ # select less
+ MeshTest("MonkeySelectLess", "testMonkeySelectLess", "expectedMonkeySelectLess",
+ [OperatorSpecEditMode("select_less", {}, "VERT", {2, 8, 24, 34, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 68,
+ 69, 70, 71, 74, 75, 78, 80, 81, 82, 83, 90, 91, 93, 95, 97, 99,
+ 101, 109, 111, 115, 117, 119, 121, 123, 125, 127, 129, 130, 131,
+ 132, 133, 134, 135, 136, 138, 141, 143, 145, 147, 149, 151, 153,
+ 155, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174,
+ 175, 176, 177, 178, 181, 182, 184, 185, 186, 187, 188, 189, 190,
+ 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204,
+ 206, 207, 208, 210, 216, 217, 218, 219, 220, 221, 222, 229, 230,
+ 231, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255,
+ 257, 259, 263, 267, 269, 271, 275, 277, 289, 291, 293, 295, 309,
+ 310, 311, 312, 316, 317, 318, 319, 320, 323, 325, 327, 329, 331,
+ 341, 347, 349, 350, 351, 354, 356, 359, 361, 363, 365, 367, 369,
+ 375, 379, 381, 382, 385, 386, 387, 388, 389, 390, 391, 392, 393,
+ 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406,
+ 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419,
+ 420, 421, 423, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434,
+ 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447,
+ 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459, 460, 461,
+ 462, 463, 464, 471, 473, 474, 475, 476, 477, 478, 479, 480, 481,
+ 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 495,
+ 496, 497, 498, 499, 502, 505})]),
+ MeshTest("HoledCubeSelectLess", "testHoledCubeSelectLess", "expectedHoledCubeSelectLess",
+ [OperatorSpecEditMode("select_face_by_sides", {}, "FACE", {})]),
+ MeshTest("EmptyMeshSelectLess", "testEmptyMeshSelectLess", "expectedEmptyMeshSelectLess",
+ [OperatorSpecEditMode("select_face_by_sides", {}, "VERT", {})]),
+
+ # select linked
+ MeshTest("PlanesSelectLinked", "testPlanesSelectLinked", "expectedPlanesSelectedLinked",
+ [OperatorSpecEditMode("select_linked", {}, "VERT", {7})]),
+ MeshTest("CubesSelectLinked", "testCubesSelectLinked", "expectedCubesSelectLinked",
+ [OperatorSpecEditMode("select_linked", {}, "VERT", {11})]),
+ MeshTest("EmptyMeshSelectLinked", "testEmptyMeshSelectLinked", "expectedEmptyMeshSelectLinked",
+ [OperatorSpecEditMode("select_linked", {}, "VERT", {})]),
+
+ # select nth (checkered deselect)
+ MeshTest("CircleSelect2nd", "testCircleSelect2nd", "expectedCircleSelect2nd",
+ [OperatorSpecEditMode("select_nth", {}, "VERT", {i for i in range(32)})]),
+
+ # unsubdivide
+ # normal case
+ MeshTest("CubeFaceUnsubdivide", "testCubeUnsubdivide", "expectedCubeUnsubdivide",
+ [OperatorSpecEditMode("unsubdivide", {}, "FACE", {i for i in range(6)})]),
+
+ # T87259 - test cases
+ MeshTest("CubeEdgeUnsubdivide", "testCubeEdgeUnsubdivide", "expectedCubeEdgeUnsubdivide",
+ [OperatorSpecEditMode("unsubdivide", {}, "EDGE", {i for i in range(6)})]),
+ MeshTest("UVSphereUnsubdivide", "testUVSphereUnsubdivide", "expectedUVSphereUnsubdivide",
+ [OperatorSpecEditMode("unsubdivide", {'iterations': 9}, "FACE", {i for i in range(512)})]),
+
+ # vert connect path
+ # Tip: It works only if there is an already existing face or more than 2 vertices.
+ MeshTest("CubeVertConnectPath", "testCubeVertConnectPath", "expectedCubeVertConnectPath",
+ [OperatorSpecEditMode("vert_connect_path", {}, "VERT", {0, 5})]),
+
]
operators_test = RunTest(tests)