diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 6ae8bc0db23..fec4dc24f92 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -260,6 +260,11 @@ add_blender_test( --python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_idprop_datablock.py ) +add_blender_test( + script_pyapi_prop + --python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_prop.py +) + add_blender_test( script_pyapi_prop_array --python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_prop_array.py diff --git a/tests/python/bl_pyapi_idprop.py b/tests/python/bl_pyapi_idprop.py index 3fdc280bef5..c14a4f001b9 100644 --- a/tests/python/bl_pyapi_idprop.py +++ b/tests/python/bl_pyapi_idprop.py @@ -254,6 +254,11 @@ class TestIdPropertyUIData(TestHelper, unittest.TestCase): self.assertEqual(self.id.path_resolve('["%s"]' % bpy.utils.escape_identifier(self.key_id)), 'A') +# NOTE: the tests below are fairly deep checks on expected consistency in the py-defined dynamic RNA properties, +# between their python representations (as RNA properties) and their underlying (IDProperty-based) storage. +# See `bl_pyapi_prop.py` and `bl_pyapi_prop_array.py` for more basic but systematic testing +# of all `bpy.props`-defined property types. + # Check statically typed underlying IDProperties storage for dynamic RNA properties. class TestIdPropertyDynamicRNA(TestHelper, unittest.TestCase): diff --git a/tests/python/bl_pyapi_prop.py b/tests/python/bl_pyapi_prop.py new file mode 100644 index 00000000000..a742d1cbb14 --- /dev/null +++ b/tests/python/bl_pyapi_prop.py @@ -0,0 +1,329 @@ +# SPDX-FileCopyrightText: 2020-2023 Blender Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# NOTE: See also `bl_pyapi_prop_array.py` for the `Vector` bpy.props similar tests, +# and `bl_pyapi_idprop.py` for some deeper testing of the consistency between +# the underlying IDProperty storage, and the property data exposed in Python. + +# ./blender.bin --background --python tests/python/bl_pyapi_prop.py -- --verbose +import bpy +from bpy.props import ( + BoolProperty, + IntProperty, + FloatProperty, + EnumProperty, + StringProperty, + PointerProperty, + CollectionProperty, +) + +import unittest +import functools + +id_inst = bpy.context.scene +id_type = bpy.types.Scene + + +# ----------------------------------------------------------------------------- +# Utility Types + +class TestPropertyGroup(bpy.types.PropertyGroup): + test_prop: IntProperty() + + +# ----------------------------------------------------------------------------- +# Tests + +class TestPropNumerical(unittest.TestCase): + default_value = 0 + custom_value = 1 + + def setUp(self): + id_type.test_bool = BoolProperty(default=bool(self.default_value)) + id_type.test_int = IntProperty(default=int(self.default_value)) + id_type.test_float = FloatProperty(default=float(self.default_value)) + + self.test_bool_storage = bool(self.custom_value) + + def set_(s, v): + self.test_bool_storage = v + id_type.test_bool_getset = BoolProperty( + default=bool(self.default_value), + get=lambda s: self.test_bool_storage, + set=set_, + ) + + self.test_int_storage = int(self.custom_value) + + def set_(s, v): + self.test_int_storage = v + id_type.test_int_getset = IntProperty( + default=int(self.default_value), + get=lambda s: self.test_int_storage, + set=set_, + ) + + self.test_float_storage = float(self.custom_value) + + def set_(s, v): + self.test_float_storage = v + id_type.test_float_getset = FloatProperty( + default=float(self.default_value), + get=lambda s: self.test_float_storage, + set=set_, + ) + + def tearDown(self): + del id_type.test_float + del id_type.test_int + del id_type.test_bool + + del id_type.test_float_getset + del id_type.test_int_getset + del id_type.test_bool_getset + + def do_test_access(self, prop_name, py_type, expected_value): + v = getattr(id_inst, prop_name) + self.assertIsInstance(v, py_type) + self.assertEqual(v, expected_value) + setattr(id_inst, prop_name, v) + + def test_access_bool(self): + self.do_test_access("test_bool", bool, bool(self.default_value)) + + def test_access_int(self): + self.do_test_access("test_int", int, int(self.default_value)) + + def test_access_float(self): + self.do_test_access("test_float", float, float(self.default_value)) + + def test_access_bool_getset(self): + self.do_test_access("test_bool_getset", bool, bool(self.custom_value)) + + def test_access_int_getset(self): + self.do_test_access("test_int_getset", int, int(self.custom_value)) + + def test_access_float_getset(self): + self.do_test_access("test_float_getset", float, float(self.custom_value)) + + # TODO: Add expected failure cases (e.g. handling of out-of range values). + + +class TestPropString(unittest.TestCase): + default_value = "" + custom_value = "Blender" + + def setUp(self): + id_type.test_string = StringProperty(default=self.default_value) + + self.test_string_storage = self.custom_value + + def set_(s, v): + self.test_string_storage = v + id_type.test_string_getset = StringProperty( + default=self.default_value, + get=lambda s: self.test_string_storage, + set=set_, + ) + + def tearDown(self): + del id_type.test_string + del id_type.test_string_getset + + def do_test_access(self, prop_name, py_type, expected_value): + v = getattr(id_inst, prop_name) + self.assertIsInstance(v, py_type) + self.assertEqual(v, expected_value) + setattr(id_inst, prop_name, v) + + def test_access_string(self): + self.do_test_access("test_string", str, self.default_value) + + def test_access_string_getset(self): + self.do_test_access("test_string_getset", str, self.custom_value) + + # TODO: Add expected failure cases (e.g. handling of too long values, invalid utf8 sequences, etc.). + + +class TestPropEnum(unittest.TestCase): + # FIXME: Auto-generated enum values do not play well with partially specifying some values. + # This won't work, generating (1, 1, 2, 16): + # enum_items = (("1", "1", "", 1), ("2", "2", ""), ("3", "3", ""), ("16", "16", "", 16),) + enum_items = (("1", "1", ""), ("2", "2", ""), ("3", "3", ""), ("16", "16", "", 16),) + enum_expected_values = {"1": 0, "2": 1, "3": 2, "16": 16} + enum_expected_bitflag_values = {"1": 2**0, "2": 2**1, "3": 2**2, "16": 2**4} + + default_value = "1" + custom_value = "2" + default_bitflag_value = {"1", "3"} + custom_bitflag_value = {"16", "3"} + + def setUp(self): + id_type.test_enum = EnumProperty(items=self.enum_items, default=self.default_value) + id_type.test_enum_bitflag = EnumProperty( + items=self.enum_items, + default=self.default_bitflag_value, + options={"ENUM_FLAG"}, + ) + + self.test_enum_storage = self.enum_expected_values[self.custom_value] + + def set_(s, v): + self.test_enum_storage = v + id_type.test_enum_getset = EnumProperty( + items=self.enum_items, + default=self.default_value, + get=lambda s: self.test_enum_storage, + set=set_, + ) + + self.test_enum_bitflag_storage = functools.reduce( + lambda a, b: a | b, + (self.enum_expected_bitflag_values[bf] for bf in self.custom_bitflag_value)) + + def set_(s, v): + self.test_enum_bitflag_storage = v + id_type.test_enum_bitflag_getset = EnumProperty( + items=self.enum_items, + default=self.default_bitflag_value, + options={"ENUM_FLAG"}, + get=lambda s: self.test_enum_bitflag_storage, + set=set_, + ) + + def tearDown(self): + del id_type.test_enum + del id_type.test_enum_bitflag + del id_type.test_enum_getset + del id_type.test_enum_bitflag_getset + + # Test expected generated values for enum items. + def do_test_enum_values(self, prop_name, expected_item_values): + enum_items = id_inst.bl_rna.properties[prop_name].enum_items + self.assertEqual(len(expected_item_values), len(enum_items)) + for (expected_identifier, expected_value), item in zip(expected_item_values.items(), enum_items): + self.assertEqual(expected_identifier, item.identifier) + self.assertEqual(expected_value, item.value) + + def test_enum_item_values(self): + self.do_test_enum_values("test_enum", self.enum_expected_values) + + def test_enum_bitflag_item_values(self): + self.do_test_enum_values("test_enum_bitflag", self.enum_expected_bitflag_values) + + # Test basic access to enum values. + def do_test_access(self, prop_name, py_type, expected_value): + v = getattr(id_inst, prop_name) + self.assertIsInstance(v, py_type) + self.assertEqual(v, expected_value) + setattr(id_inst, prop_name, v) + + def test_access_enum(self): + self.do_test_access("test_enum", str, self.default_value) + + def test_access_enum_bitflag(self): + self.do_test_access("test_enum_bitflag", set, self.default_bitflag_value) + + def test_access_enum_getset(self): + self.do_test_access("test_enum_getset", str, self.custom_value) + + def test_access_enum_bitflag_getset(self): + self.do_test_access("test_enum_bitflag_getset", set, self.custom_bitflag_value) + + # TODO: Add expected failure cases (e.g. handling of invalid items identifiers). + + +class TestPropCollectionAndPointer(unittest.TestCase): + + def setUp(self): + bpy.utils.register_class(TestPropertyGroup) + + id_type.test_pointer = PointerProperty(type=TestPropertyGroup) + id_type.test_pointer_ID = PointerProperty(type=bpy.types.ID) + id_type.test_pointer_ID_poll = PointerProperty(type=bpy.types.ID, poll=lambda s, v: v.id_type == 'OBJECT') + id_type.test_collection = CollectionProperty(type=TestPropertyGroup) + + def tearDown(self): + del id_type.test_pointer + del id_type.test_pointer_ID + del id_type.test_pointer_ID_poll + del id_type.test_collection + + bpy.utils.unregister_class(TestPropertyGroup) + + def test_access_pointer(self): + v = id_inst.test_pointer + self.assertIsInstance(v, TestPropertyGroup) + self.assertTrue(hasattr(v, "test_prop")) + self.assertEqual(v.test_prop, 0) + v.test_prop = 42 + self.assertEqual(id_inst.test_pointer.test_prop, 42) + + def test_access_pointer_ID(self): + self.assertEqual(id_inst.test_pointer_ID, None) + + # Non-refcounting ID type + win_man = bpy.data.window_managers[0] + win_man_users = win_man.users + id_inst.test_pointer_ID = win_man + self.assertEqual(id_inst.test_pointer_ID, win_man) + self.assertEqual(win_man.users, win_man_users) + id_inst.test_pointer_ID = None + self.assertEqual(id_inst.test_pointer_ID, None) + self.assertEqual(win_man.users, win_man_users) + + # Refcounting ID type + ma = bpy.data.materials[0] + ma_users = ma.users + id_inst.test_pointer_ID = ma + self.assertEqual(id_inst.test_pointer_ID, ma) + self.assertEqual(win_man.users, ma_users + 1) + id_inst.test_pointer_ID = None + self.assertEqual(id_inst.test_pointer_ID, None) + self.assertEqual(ma.users, ma_users) + + def test_access_pointer_ID_poll(self): + # Poll callback is only used for UI, in scripts it's still possible to assign an 'invalid' ID. + self.assertEqual(id_inst.test_pointer_ID_poll, None) + win_man = bpy.data.window_managers[0] + id_inst.test_pointer_ID_poll = win_man + self.assertEqual(id_inst.test_pointer_ID_poll, win_man) + id_inst.test_pointer_ID_poll = None + self.assertEqual(id_inst.test_pointer_ID_poll, None) + + def test_access_collection(self): + self.assertEqual(len(id_inst.test_collection), 0) + + test_item = id_inst.test_collection.add() + self.assertEqual(len(id_inst.test_collection), 1) + self.assertIsInstance(test_item, TestPropertyGroup) + self.assertTrue(hasattr(test_item, "test_prop")) + self.assertEqual(test_item.test_prop, 0) + self.assertEqual(id_inst.test_collection[0], test_item) + test_item.test_prop = 42 + self.assertEqual(test_item.test_prop, 42) + self.assertEqual(id_inst.test_collection[0].test_prop, 42) + + test_item_2 = id_inst.test_collection.add() + test_item_3 = id_inst.test_collection.add() + test_item_3.test_prop = 24 + self.assertEqual(len(id_inst.test_collection), 3) + self.assertEqual(id_inst.test_collection[0], test_item) + self.assertEqual(id_inst.test_collection[1], test_item_2) + self.assertEqual(id_inst.test_collection[2], test_item_3) + + id_inst.test_collection.remove(1) + self.assertEqual(len(id_inst.test_collection), 2) + self.assertEqual(id_inst.test_collection[0], test_item) + # Removing the second item re-allocates the third one, so no equality anymore. + self.assertNotEqual(id_inst.test_collection[1], test_item_3) + self.assertEqual(id_inst.test_collection[1].test_prop, 24) + + # TODO: Add expected failure cases (e.g. assigning propertygroup to a Pointer property, etc.). + + +if __name__ == '__main__': + import sys + sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) + unittest.main() diff --git a/tests/python/bl_pyapi_prop_array.py b/tests/python/bl_pyapi_prop_array.py index fa5b733a012..2aaf4e0ba7c 100644 --- a/tests/python/bl_pyapi_prop_array.py +++ b/tests/python/bl_pyapi_prop_array.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: Apache-2.0 +# NOTE: See also `bl_pyapi_prop.py` for the non-`Vector` bpy.props similar tests, +# and `bl_pyapi_idprop.py` for some deeper testing of the consistency between +# the underlying IDProperty storage, and the property data exposed in Python. + # ./blender.bin --background --python tests/python/bl_pyapi_prop_array.py -- --verbose import bpy from bpy.props import (