blob: 9ad7b3c4ffb59b72b35ee94bff926085ac19a9bc [file]
# 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)