| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| from __future__ import annotations |
| |
| import sys |
| from typing import Any, Sequence |
| |
| import pytest |
| import tvm_ffi |
| import tvm_ffi.testing |
| from tvm_ffi.core import TypeInfo |
| |
| |
| def test_make_object() -> None: |
| # with default values |
| obj0 = tvm_ffi.testing.create_object("testing.TestObjectBase") |
| assert isinstance(obj0, tvm_ffi.testing.TestObjectBase) |
| assert obj0.v_i64 == 10 |
| assert obj0.v_f64 == 10.0 |
| assert obj0.v_str == "hello" |
| |
| |
| def test_make_object_via_init() -> None: |
| obj0 = tvm_ffi.testing.TestIntPair(1, 2) |
| assert obj0.a == 1 |
| assert obj0.b == 2 |
| |
| |
| def test_method() -> None: |
| obj0 = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=12) |
| assert isinstance(obj0, tvm_ffi.testing.TestObjectBase) |
| assert obj0.add_i64(1) == 13 |
| assert type(obj0).add_i64.__doc__ == "add_i64 method" |
| assert type(obj0).v_i64.__doc__ == "i64 field" |
| |
| |
| def test_attribute() -> None: |
| obj = tvm_ffi.testing.TestIntPair(3, 4) |
| assert obj.a == 3 |
| assert obj.b == 4 |
| assert type(obj).a.__doc__ == "Field `a`" |
| assert type(obj).b.__doc__ == "Field `b`" |
| |
| |
| def test_attribute_no_doc() -> None: |
| from tvm_ffi.testing import TestObjectBase # noqa: PLC0415 |
| |
| # The docs for v_f64 and v_str are None because they are not explicitly set. |
| assert TestObjectBase.v_i64.__doc__ == "i64 field" |
| assert TestObjectBase.v_f64.__doc__ is None |
| assert TestObjectBase.v_str.__doc__ is None |
| |
| |
| def test_setter() -> None: |
| # test setter |
| obj0 = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=10, v_str="hello") |
| assert isinstance(obj0, tvm_ffi.testing.TestObjectBase) |
| assert obj0.v_i64 == 10 |
| obj0.v_i64 = 11 |
| assert obj0.v_i64 == 11 |
| obj0.v_str = "world" |
| assert obj0.v_str == "world" |
| |
| with pytest.raises(TypeError): |
| obj0.v_str = 1 # ty: ignore[invalid-assignment] |
| |
| with pytest.raises(TypeError): |
| obj0.v_i64 = "hello" # ty: ignore[invalid-assignment] |
| |
| |
| def test_derived_object() -> None: |
| with pytest.raises(TypeError): |
| obj0 = tvm_ffi.testing.create_object("testing.TestObjectDerived") |
| |
| v_map = tvm_ffi.convert({"a": 1}) |
| v_array = tvm_ffi.convert([1, 2, 3]) |
| |
| obj0 = tvm_ffi.testing.create_object( |
| "testing.TestObjectDerived", v_i64=20, v_map=v_map, v_array=v_array |
| ) |
| assert isinstance(obj0, tvm_ffi.testing.TestObjectDerived) |
| assert obj0.v_map.same_as(v_map) # ty: ignore[unresolved-attribute] |
| assert obj0.v_array.same_as(v_array) # ty: ignore[unresolved-attribute] |
| assert obj0.v_i64 == 20 |
| assert obj0.v_f64 == 10.0 |
| assert obj0.v_str == "hello" |
| |
| obj0.v_i64 = 21 |
| assert obj0.v_i64 == 21 |
| |
| |
| class MyObject: |
| def __init__(self, value: Any) -> None: |
| self.value = value |
| |
| |
| def test_opaque_object() -> None: |
| obj0 = MyObject("hello") |
| base_count = sys.getrefcount(obj0) |
| ref_count = tvm_ffi.get_global_func("testing.object_use_count") |
| assert sys.getrefcount(obj0) == base_count |
| obj0_converted = tvm_ffi.convert(obj0) |
| assert ref_count(obj0_converted) == 1 |
| assert sys.getrefcount(obj0) == base_count + 1 |
| assert isinstance(obj0_converted, tvm_ffi.core.OpaquePyObject) |
| obj0_cpy = obj0_converted.pyobject() |
| assert obj0_cpy is obj0 |
| assert sys.getrefcount(obj0) == base_count + 2 |
| obj0_converted = None |
| assert sys.getrefcount(obj0) == base_count + 1 |
| obj0_cpy = None |
| assert sys.getrefcount(obj0) == base_count |
| |
| |
| def test_opaque_type_error() -> None: |
| obj0 = MyObject("hello") |
| with pytest.raises(TypeError) as e: |
| tvm_ffi.testing.add_one(obj0) # ty: ignore[invalid-argument-type] |
| assert ( |
| "Mismatched type on argument #0 when calling: `testing.add_one(0: int) -> int`. Expected `int` but got `ffi.OpaquePyObject`" |
| in str(e.value) |
| ) |
| |
| |
| def test_object_init() -> None: |
| # Registered class with auto-generated __ffi_init__ (all fields have defaults) |
| obj = tvm_ffi.testing.TestObjectBase() |
| assert obj.v_i64 == 10 |
| assert obj.v_f64 == 10.0 |
| assert obj.v_str == "hello" |
| |
| # Registered class with __c_ffi_init__ should work fine |
| pair = tvm_ffi.testing.TestIntPair(3, 4) |
| assert pair.a == 3 and pair.b == 4 |
| |
| # FFI-returned objects should work fine |
| obj = tvm_ffi.testing.create_object("testing.TestObjectBase", v_i64=7) |
| assert obj.__chandle__() != 0 |
| assert obj.v_i64 == 7 # ty: ignore[unresolved-attribute] |
| |
| |
| def test_register_object_wires_init() -> None: |
| """Regression: register_object must wire __init__ from __ffi_init__. |
| |
| Without _install_init in register_object, calling TestIntPair(1, 2) |
| falls through to object.__init__ which silently ignores args on |
| Cython extension types (CObject has custom tp_new). The result is |
| chandle = NULL. Any subsequent field access dereferences |
| chandle + offset → segfault. |
| """ |
| # Construction must produce a live handle, not NULL. |
| pair = tvm_ffi.testing.TestIntPair(3, 7) |
| assert pair.__chandle__() != 0 |
| assert pair.a == 3 |
| assert pair.b == 7 |
| |
| # __new__ alone must NOT produce a usable object (chandle stays NULL). |
| bare = tvm_ffi.testing.TestIntPair.__new__(tvm_ffi.testing.TestIntPair) |
| assert bare.__chandle__() == 0 |
| |
| |
| def test_id_and_is() -> None: |
| x = tvm_ffi.testing.TestIntPair(1, 2) |
| y = tvm_ffi.testing.TestIntPair(1, 2) |
| |
| # id_ is the integer handle, aliasing __chandle__. |
| assert x.id_ == x.__chandle__() |
| assert x.id_ != 0 |
| assert x.id_ != y.id_ |
| |
| # is_ tests handle identity, aliasing same_as. |
| assert x.is_(x) |
| assert not x.is_(y) |
| assert x.is_(x) == x.same_as(x) |
| assert not x.is_(object()) |
| assert not x.is_(None) |
| |
| |
| def test_object_protocol() -> None: |
| class CompactObject: |
| def __init__(self, backend_obj: Any) -> None: |
| self.backend_obj = backend_obj |
| |
| def __tvm_ffi_object__(self) -> Any: |
| return self.backend_obj |
| |
| x = tvm_ffi.convert([]) |
| assert isinstance(x, tvm_ffi.Object) |
| x_compact = CompactObject(x) |
| test_echo = tvm_ffi.get_global_func("testing.echo") |
| y = test_echo(x_compact) |
| assert y.__chandle__() == x.__chandle__() |
| |
| |
| def test_unregistered_object_fallback() -> None: |
| def _check_type(x: Any) -> None: |
| type_info: TypeInfo = type(x).__tvm_ffi_type_info__ |
| assert type_info.type_key == "testing.TestUnregisteredObject" |
| assert x.v1 == 41 |
| assert x.v2 == 42 |
| assert x.get_v1_plus_one() == 42 |
| assert x.get_v2_plus_two() == 44 |
| assert type(x).__name__ == "TestUnregisteredObject" |
| assert type(x).__module__ == "testing" |
| assert type(x).__qualname__ == "testing.TestUnregisteredObject" |
| assert "Auto-generated fallback class" in type(x).__doc__ |
| assert "Get (v1 + 1) from TestUnregisteredBaseObject" in type(x).get_v1_plus_one.__doc__ |
| assert "Get (v2 + 2) from TestUnregisteredObject" in type(x).get_v2_plus_two.__doc__ |
| |
| obj = tvm_ffi.testing.make_unregistered_object() |
| _check_type(obj) |
| for _ in range(5): |
| obj = tvm_ffi.testing.make_unregistered_object() |
| _check_type(obj) |
| |
| |
| @pytest.mark.parametrize( |
| ("test_cls", "make_instance"), |
| [ |
| ( |
| tvm_ffi.testing.TestObjectBase, |
| lambda: tvm_ffi.testing.create_object("testing.TestObjectBase"), |
| ), |
| ( |
| tvm_ffi.testing.TestIntPair, |
| lambda: tvm_ffi.testing.TestIntPair(1, 2), |
| ), |
| ( |
| tvm_ffi.testing.TestObjectDerived, |
| lambda: tvm_ffi.testing.create_object( |
| "testing.TestObjectDerived", |
| v_i64=20, |
| v_map=tvm_ffi.convert({"a": 1}), |
| v_array=tvm_ffi.convert([1, 2]), |
| ), |
| ), |
| ], |
| ) |
| def test_object_subclass_slots(test_cls: type, make_instance: Any) -> None: |
| slots = test_cls.__dict__.get("__slots__") |
| assert slots == () |
| assert "__dict__" not in test_cls.__dict__ |
| assert "__weakref__" not in test_cls.__dict__ |
| obj = make_instance() |
| with pytest.raises(AttributeError): |
| obj._tvm_ffi_test_attr = "nope" |
| |
| |
| @pytest.mark.parametrize( |
| "test_cls, type_key, parent_cls", |
| [ |
| (tvm_ffi.Object, "ffi.Object", None), |
| (tvm_ffi.Tensor, "ffi.Tensor", tvm_ffi.Object), |
| (tvm_ffi.core.DataType, "DataType", None), |
| (tvm_ffi.Device, "Device", None), |
| (tvm_ffi.Shape, "ffi.Shape", tvm_ffi.Object), |
| (tvm_ffi.Module, "ffi.Module", tvm_ffi.Object), |
| (tvm_ffi.Function, "ffi.Function", tvm_ffi.Object), |
| (tvm_ffi.core.Error, "ffi.Error", tvm_ffi.Object), |
| (tvm_ffi.core.String, "ffi.String", tvm_ffi.Object), |
| (tvm_ffi.core.Bytes, "ffi.Bytes", tvm_ffi.Object), |
| (tvm_ffi.Tensor, "ffi.Tensor", tvm_ffi.Object), |
| (tvm_ffi.Array, "ffi.Array", tvm_ffi.Object), |
| (tvm_ffi.List, "ffi.List", tvm_ffi.Object), |
| (tvm_ffi.Map, "ffi.Map", tvm_ffi.Object), |
| (tvm_ffi.access_path.AccessStep, "ffi.reflection.AccessStep", tvm_ffi.Object), |
| (tvm_ffi.access_path.AccessPath, "ffi.reflection.AccessPath", tvm_ffi.Object), |
| (tvm_ffi.testing.TestIntPair, "testing.TestIntPair", tvm_ffi.Object), |
| (tvm_ffi.testing.TestObjectBase, "testing.TestObjectBase", tvm_ffi.Object), |
| ( |
| tvm_ffi.testing.TestObjectDerived, |
| "testing.TestObjectDerived", |
| tvm_ffi.testing.TestObjectBase, |
| ), |
| (tvm_ffi.testing._TestCxxClassBase, "testing.TestCxxClassBase", tvm_ffi.Object), |
| ( |
| tvm_ffi.testing._TestCxxClassDerived, |
| "testing.TestCxxClassDerived", |
| tvm_ffi.testing._TestCxxClassBase, |
| ), |
| ( |
| tvm_ffi.testing._TestCxxClassDerivedDerived, |
| "testing.TestCxxClassDerivedDerived", |
| tvm_ffi.testing._TestCxxClassDerived, |
| ), |
| (tvm_ffi.testing._TestCxxInitSubset, "testing.TestCxxInitSubset", tvm_ffi.Object), |
| (tvm_ffi.testing._SchemaAllTypes, "testing.SchemaAllTypes", tvm_ffi.Object), |
| ], |
| ) |
| def test_type_info_attachment(test_cls: type, type_key: str, parent_cls: type | None) -> None: |
| type_info = tvm_ffi.core._type_cls_to_type_info(test_cls) |
| assert type_info is not None |
| assert type_info.type_cls is test_cls |
| assert type_info.type_key == type_key, ( |
| f"Expected type key `{type_key}` but got `{type_info.type_key}`" |
| ) |
| parent_type_info = type_info.parent_type_info |
| if parent_type_info is None: |
| assert parent_cls is None |
| else: |
| assert parent_type_info.type_cls is parent_cls, ( |
| f"Expected parent type {parent_cls}, but got {parent_type_info.type_cls}" |
| ) |
| |
| |
| def test_get_registered_type_keys() -> None: |
| keys = tvm_ffi.registry.get_registered_type_keys() |
| assert isinstance(keys, Sequence) |
| assert all(isinstance(k, str) for k in keys) |
| keys = set(keys) |
| assert "ffi.Object" in keys |
| assert "ffi.String" in keys |
| for ty in [ |
| "None", |
| "int", |
| "bool", |
| "float", |
| "void*", |
| "DataType", |
| "Device", |
| "DLTensor*", |
| "const char*", |
| "TVMFFIByteArray*", |
| "ObjectRValueRef", |
| ]: |
| assert ty in keys, f"Expected to find `{ty}` in registered type keys, but it was not found." |
| keys.remove(ty) |
| for ty in keys: |
| assert ty.startswith("ffi.") or ty.startswith("testing."), ( |
| f"Expected type key `{ty}` to start with `ffi.` or `testing.`" |
| ) |
| |
| |
| # --------------------------------------------------------------------------- |
| # isinstance / issubclass correctness for the Object type hierarchy |
| # --------------------------------------------------------------------------- |
| |
| |
| class TestIsinstanceIssubclass: |
| """Verify that isinstance/issubclass respect the actual type hierarchy. |
| |
| Regression tests for a bug where _ObjectSlotsMeta.__instancecheck__ |
| and __subclasscheck__ returned True for *any* CObject against *any* |
| Object subclass, making e.g. isinstance(Map(...), Array) == True. |
| """ |
| |
| # -- containers: sibling types must not match each other ---------------- |
| |
| def test_map_not_isinstance_array(self) -> None: |
| """Map instance should not pass isinstance check for Array.""" |
| m = tvm_ffi.Map({"a": 1}) |
| assert not isinstance(m, tvm_ffi.Array) |
| |
| def test_array_not_isinstance_map(self) -> None: |
| """Array instance should not pass isinstance check for Map.""" |
| a = tvm_ffi.Array([1, 2]) |
| assert not isinstance(a, tvm_ffi.Map) |
| |
| def test_list_not_isinstance_dict(self) -> None: |
| """List instance should not pass isinstance check for Dict.""" |
| lst = tvm_ffi.List([1, 2]) |
| assert not isinstance(lst, tvm_ffi.Dict) |
| |
| def test_dict_not_isinstance_list(self) -> None: |
| """Dict instance should not pass isinstance check for List.""" |
| d = tvm_ffi.Dict({"k": 1}) |
| assert not isinstance(d, tvm_ffi.List) |
| |
| def test_array_not_isinstance_list(self) -> None: |
| """Array instance should not pass isinstance check for List.""" |
| a = tvm_ffi.Array([1]) |
| assert not isinstance(a, tvm_ffi.List) |
| |
| def test_map_not_isinstance_dict(self) -> None: |
| """Map instance should not pass isinstance check for Dict.""" |
| m = tvm_ffi.Map({"a": 1}) |
| assert not isinstance(m, tvm_ffi.Dict) |
| |
| # -- containers: positive isinstance ------------------------------------ |
| |
| def test_array_isinstance_object(self) -> None: |
| """Array instance should pass isinstance check for Object.""" |
| a = tvm_ffi.Array([1]) |
| assert isinstance(a, tvm_ffi.Object) |
| |
| def test_map_isinstance_object(self) -> None: |
| """Map instance should pass isinstance check for Object.""" |
| m = tvm_ffi.Map({"a": 1}) |
| assert isinstance(m, tvm_ffi.Object) |
| |
| def test_list_isinstance_object(self) -> None: |
| """List instance should pass isinstance check for Object.""" |
| lst = tvm_ffi.List([1]) |
| assert isinstance(lst, tvm_ffi.Object) |
| |
| def test_dict_isinstance_object(self) -> None: |
| """Dict instance should pass isinstance check for Object.""" |
| d = tvm_ffi.Dict({"k": 1}) |
| assert isinstance(d, tvm_ffi.Object) |
| |
| # -- registered user types: parent / child ------------------------------ |
| |
| def test_derived_isinstance_base(self) -> None: |
| """Derived object should pass isinstance check for its base class.""" |
| obj = tvm_ffi.testing.TestObjectDerived( |
| v_map={"a": 1}, |
| v_array=[1], |
| ) |
| assert isinstance(obj, tvm_ffi.testing.TestObjectBase) |
| assert isinstance(obj, tvm_ffi.Object) |
| |
| def test_base_not_isinstance_derived(self) -> None: |
| """Base object should not pass isinstance check for a derived class.""" |
| obj = tvm_ffi.testing.TestObjectBase() |
| assert not isinstance(obj, tvm_ffi.testing.TestObjectDerived) |
| |
| def test_derived_isinstance_own_class(self) -> None: |
| """Derived object should pass isinstance check for its own class.""" |
| obj = tvm_ffi.testing.TestObjectDerived( |
| v_map={"a": 1}, |
| v_array=[1], |
| ) |
| assert isinstance(obj, tvm_ffi.testing.TestObjectDerived) |
| |
| # -- cross-hierarchy: user object vs container -------------------------- |
| |
| def test_user_object_not_isinstance_array(self) -> None: |
| """User-defined object should not pass isinstance check for Array.""" |
| obj = tvm_ffi.testing.TestObjectBase() |
| assert not isinstance(obj, tvm_ffi.Array) |
| |
| def test_array_not_isinstance_user_object(self) -> None: |
| """Array should not pass isinstance check for a user-defined type.""" |
| a = tvm_ffi.Array([1]) |
| assert not isinstance(a, tvm_ffi.testing.TestObjectBase) |
| |
| # -- issubclass mirrors isinstance -------------------------------------- |
| |
| def test_issubclass_derived_base(self) -> None: |
| """Derived class should be a subclass of its base.""" |
| assert issubclass(tvm_ffi.testing.TestObjectDerived, tvm_ffi.testing.TestObjectBase) |
| |
| def test_issubclass_base_not_derived(self) -> None: |
| """Base class should not be a subclass of its derived class.""" |
| assert not issubclass(tvm_ffi.testing.TestObjectBase, tvm_ffi.testing.TestObjectDerived) |
| |
| def test_issubclass_array_not_map(self) -> None: |
| """Array should not be a subclass of Map.""" |
| assert not issubclass(tvm_ffi.Array, tvm_ffi.Map) |
| |
| def test_issubclass_map_not_array(self) -> None: |
| """Map should not be a subclass of Array.""" |
| assert not issubclass(tvm_ffi.Map, tvm_ffi.Array) |
| |
| def test_issubclass_all_containers_are_object(self) -> None: |
| """All container types should be subclasses of Object.""" |
| for cls in (tvm_ffi.Array, tvm_ffi.List, tvm_ffi.Map, tvm_ffi.Dict): |
| assert issubclass(cls, tvm_ffi.Object) |
| |
| def test_issubclass_user_type_is_object(self) -> None: |
| """User-defined types should be subclasses of Object.""" |
| assert issubclass(tvm_ffi.testing.TestObjectBase, tvm_ffi.Object) |
| assert issubclass(tvm_ffi.testing.TestObjectDerived, tvm_ffi.Object) |
| |
| def test_issubclass_sibling_containers(self) -> None: |
| """Sibling container types should not be subclasses of each other.""" |
| assert not issubclass(tvm_ffi.List, tvm_ffi.Array) |
| assert not issubclass(tvm_ffi.Dict, tvm_ffi.Map) |
| assert not issubclass(tvm_ffi.Array, tvm_ffi.List) |
| assert not issubclass(tvm_ffi.Map, tvm_ffi.Dict) |