| # 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. |
| |
| |
| import gc |
| import weakref |
| from typing import NoReturn |
| |
| import pytest |
| import tvm_ffi |
| |
| |
| def test_parse_backtrace() -> None: |
| backtrace = """ |
| File "test.py", line 1, in <module> |
| File "test.py", line 3, in run_test |
| """ |
| parsed = tvm_ffi.error._parse_backtrace(backtrace) |
| assert len(parsed) == 2 |
| assert parsed[0] == ("test.py", 1, "<module>") |
| assert parsed[1] == ("test.py", 3, "run_test") |
| |
| |
| def test_error_from_cxx() -> None: |
| test_raise_error = tvm_ffi.get_global_func("testing.test_raise_error") |
| |
| try: |
| test_raise_error("ValueError", "error XYZ") |
| except ValueError as e: |
| assert e.__tvm_ffi_error__.kind == "ValueError" # ty: ignore[unresolved-attribute] |
| assert e.__tvm_ffi_error__.message == "error XYZ" # ty: ignore[unresolved-attribute] |
| assert e.__tvm_ffi_error__.backtrace.find("TestRaiseError") != -1 # ty: ignore[unresolved-attribute] |
| |
| fapply = tvm_ffi.convert(lambda f, *args: f(*args)) |
| |
| with pytest.raises(TypeError): |
| fapply(test_raise_error, "TypeError", "error XYZ") |
| |
| # wrong number of arguments |
| with pytest.raises(TypeError): |
| tvm_ffi.convert(lambda x: x)() |
| |
| |
| def test_error_from_nested_pyfunc() -> None: |
| fapply = tvm_ffi.convert(lambda f, *args: f(*args)) |
| cxx_test_raise_error = tvm_ffi.get_global_func("testing.test_raise_error") |
| cxx_test_apply = tvm_ffi.get_global_func("testing.apply") |
| |
| record_object = [] |
| |
| def raise_error() -> None: |
| try: |
| fapply(cxx_test_raise_error, "ValueError", "error XYZ") |
| except ValueError as e: |
| assert e.__tvm_ffi_error__.kind == "ValueError" # ty: ignore[unresolved-attribute] |
| assert e.__tvm_ffi_error__.message == "error XYZ" # ty: ignore[unresolved-attribute] |
| assert e.__tvm_ffi_error__.backtrace.find("TestRaiseError") != -1 # ty: ignore[unresolved-attribute] |
| record_object.append(e.__tvm_ffi_error__) # ty: ignore[unresolved-attribute] |
| raise e |
| |
| try: |
| cxx_test_apply(raise_error) |
| except ValueError as e: |
| backtrace = e.__tvm_ffi_error__.backtrace # ty: ignore[unresolved-attribute] |
| assert e.__tvm_ffi_error__.same_as(record_object[0]) # ty: ignore[unresolved-attribute] |
| assert backtrace.count("TestRaiseError") == 1 |
| # The following lines may fail if debug symbols are missing |
| try: |
| assert backtrace.count("TestApply") == 1 |
| assert backtrace.count("<lambda>") == 1 |
| pos_cxx_raise = backtrace.find("TestRaiseError") |
| pos_cxx_apply = backtrace.find("TestApply") |
| pos_lambda = backtrace.find("<lambda>") |
| assert pos_cxx_raise < pos_lambda |
| assert pos_lambda < pos_cxx_apply |
| except Exception as e: |
| pytest.xfail("May fail if debug symbols are missing") # ty: ignore[invalid-argument-type, too-many-positional-arguments] |
| |
| |
| def test_error_traceback_update() -> None: |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| |
| def raise_error() -> NoReturn: |
| raise ValueError("error XYZ") |
| |
| try: |
| raise_error() |
| except ValueError as e: |
| ffi_error = tvm_ffi.convert(e) |
| assert ffi_error.backtrace.find("raise_error") != -1 |
| |
| def raise_cxx_error() -> None: |
| cxx_test_raise_error = tvm_ffi.get_global_func("testing.test_raise_error") |
| cxx_test_raise_error("ValueError", "error XYZ") |
| |
| try: |
| raise_cxx_error() |
| except ValueError as e: |
| assert e.__tvm_ffi_error__.backtrace.find("raise_cxx_error") == -1 # ty: ignore[unresolved-attribute] |
| ffi_error1 = tvm_ffi.convert(e) |
| ffi_error2 = fecho(e) |
| assert ffi_error1.backtrace.find("raise_cxx_error") != -1 |
| assert ffi_error2.backtrace.find("raise_cxx_error") != -1 |
| |
| |
| def test_error_no_cyclic_reference() -> None: |
| # This test case ensures that when an error is raised from C++ side, |
| # there is no cyclic reference that slows down the garbage collection. |
| # Please see `_with_append_backtrace` in error.py |
| |
| # temporarily disable gc |
| gc.disable() |
| |
| try: |
| # We should create a class as a probe to detect gc activity |
| # beacuse weakref doesn't support list, dict or other trivial types |
| class SampleObject: ... |
| |
| # trigger a C++ side KeyError by accessing a non-existent key |
| def trigger_cpp_side_error() -> None: |
| try: |
| tmp_map: tvm_ffi.Map = tvm_ffi.Map(dict()) |
| tmp_map["a"] |
| except KeyError: |
| pass |
| |
| def may_create_cyclic_reference() -> weakref.ReferenceType: |
| obj = SampleObject() |
| trigger_cpp_side_error() |
| return weakref.ref(obj) |
| |
| wref = may_create_cyclic_reference() |
| |
| # if the object is not collected, wref() will return the object |
| assert wref() is None, "Cyclic reference occurs inside error handling pipeline" |
| |
| finally: |
| # re-enable gc whenever exception occurs |
| gc.enable() |