| import enum | |
| import os | |
| import pickle | |
| import sys | |
| import textwrap | |
| import unittest | |
| from test import support | |
| from test.support import import_helper | |
| from test.support import os_helper | |
| from test.support import threading_helper | |
| from test.support.script_helper import assert_python_failure | |
| _testlimitedcapi = import_helper.import_module('_testlimitedcapi') | |
| _testcapi = import_helper.import_module('_testcapi') | |
| _testinternalcapi = import_helper.import_module('_testinternalcapi') | |
| NULL = None | |
| STDERR_FD = 2 | |
| class Constant(enum.IntEnum): | |
| Py_CONSTANT_NONE = 0 | |
| Py_CONSTANT_FALSE = 1 | |
| Py_CONSTANT_TRUE = 2 | |
| Py_CONSTANT_ELLIPSIS = 3 | |
| Py_CONSTANT_NOT_IMPLEMENTED = 4 | |
| Py_CONSTANT_ZERO = 5 | |
| Py_CONSTANT_ONE = 6 | |
| Py_CONSTANT_EMPTY_STR = 7 | |
| Py_CONSTANT_EMPTY_BYTES = 8 | |
| Py_CONSTANT_EMPTY_TUPLE = 9 | |
| INVALID_CONSTANT = Py_CONSTANT_EMPTY_TUPLE + 1 | |
| class GetConstantTest(unittest.TestCase): | |
| def check_get_constant(self, get_constant): | |
| self.assertIs(get_constant(Constant.Py_CONSTANT_NONE), None) | |
| self.assertIs(get_constant(Constant.Py_CONSTANT_FALSE), False) | |
| self.assertIs(get_constant(Constant.Py_CONSTANT_TRUE), True) | |
| self.assertIs(get_constant(Constant.Py_CONSTANT_ELLIPSIS), Ellipsis) | |
| self.assertIs(get_constant(Constant.Py_CONSTANT_NOT_IMPLEMENTED), NotImplemented) | |
| for constant_id, constant_type, value in ( | |
| (Constant.Py_CONSTANT_ZERO, int, 0), | |
| (Constant.Py_CONSTANT_ONE, int, 1), | |
| (Constant.Py_CONSTANT_EMPTY_STR, str, ""), | |
| (Constant.Py_CONSTANT_EMPTY_BYTES, bytes, b""), | |
| (Constant.Py_CONSTANT_EMPTY_TUPLE, tuple, ()), | |
| ): | |
| with self.subTest(constant_id=constant_id): | |
| obj = get_constant(constant_id) | |
| self.assertEqual(type(obj), constant_type, obj) | |
| self.assertEqual(obj, value) | |
| with self.assertRaises(SystemError): | |
| get_constant(Constant.INVALID_CONSTANT) | |
| def test_get_constant(self): | |
| self.check_get_constant(_testlimitedcapi.get_constant) | |
| def test_get_constant_borrowed(self): | |
| self.check_get_constant(_testlimitedcapi.get_constant_borrowed) | |
| class SentinelTest(unittest.TestCase): | |
| def test_pysentinel_new(self): | |
| marker = _testcapi.pysentinel_new("CAPI_SENTINEL", __name__) | |
| self.assertIs(type(marker), sentinel) | |
| self.assertTrue(_testcapi.pysentinel_check(marker)) | |
| self.assertFalse(_testcapi.pysentinel_check(object())) | |
| self.assertTrue(_testcapi.pysentinel_checkexact(marker)) | |
| self.assertFalse(_testcapi.pysentinel_checkexact(object())) | |
| self.assertEqual(marker.__name__, "CAPI_SENTINEL") | |
| self.assertEqual(marker.__module__, __name__) | |
| self.assertEqual(repr(marker), "CAPI_SENTINEL") | |
| no_module = _testcapi.pysentinel_new("NO_MODULE") | |
| self.assertIs(type(no_module), sentinel) | |
| self.assertEqual(no_module.__name__, "NO_MODULE") | |
| self.assertIs(no_module.__module__, None) | |
| with_repr = _testcapi.pysentinel_new("WITH_REPR", __name__, "custom repr") | |
| self.assertIs(type(with_repr), sentinel) | |
| self.assertEqual(with_repr.__name__, "WITH_REPR") | |
| self.assertEqual(with_repr.__module__, __name__) | |
| self.assertEqual(repr(with_repr), "custom repr") | |
| globals()["CAPI_SENTINEL"] = marker | |
| self.addCleanup(globals().pop, "CAPI_SENTINEL", None) | |
| self.assertIs(pickle.loads(pickle.dumps(marker)), marker) | |
| class PrintTest(unittest.TestCase): | |
| def testPyObjectPrintObject(self): | |
| class PrintableObject: | |
| def __repr__(self): | |
| return "spam spam spam" | |
| def __str__(self): | |
| return "egg egg egg" | |
| obj = PrintableObject() | |
| output_filename = os_helper.TESTFN | |
| self.addCleanup(os_helper.unlink, output_filename) | |
| # Test repr printing | |
| _testcapi.call_pyobject_print(obj, output_filename, False) | |
| with open(output_filename, 'r') as output_file: | |
| self.assertEqual(output_file.read(), repr(obj)) | |
| # Test str printing | |
| _testcapi.call_pyobject_print(obj, output_filename, True) | |
| with open(output_filename, 'r') as output_file: | |
| self.assertEqual(output_file.read(), str(obj)) | |
| def testPyObjectPrintNULL(self): | |
| output_filename = os_helper.TESTFN | |
| self.addCleanup(os_helper.unlink, output_filename) | |
| # Test repr printing | |
| _testcapi.pyobject_print_null(output_filename) | |
| with open(output_filename, 'r') as output_file: | |
| self.assertEqual(output_file.read(), '<nil>') | |
| def testPyObjectPrintNoRefObject(self): | |
| output_filename = os_helper.TESTFN | |
| self.addCleanup(os_helper.unlink, output_filename) | |
| # Test repr printing | |
| correct_output = _testcapi.pyobject_print_noref_object(output_filename) | |
| with open(output_filename, 'r') as output_file: | |
| self.assertEqual(output_file.read(), correct_output) | |
| def testPyObjectPrintOSError(self): | |
| output_filename = os_helper.TESTFN | |
| self.addCleanup(os_helper.unlink, output_filename) | |
| open(output_filename, "w+").close() | |
| with self.assertRaises(OSError): | |
| _testcapi.pyobject_print_os_error(output_filename) | |
| class ClearWeakRefsNoCallbacksTest(unittest.TestCase): | |
| """Test PyUnstable_Object_ClearWeakRefsNoCallbacks""" | |
| def test_ClearWeakRefsNoCallbacks(self): | |
| """Ensure PyUnstable_Object_ClearWeakRefsNoCallbacks works""" | |
| import weakref | |
| import gc | |
| class C: | |
| pass | |
| obj = C() | |
| messages = [] | |
| ref = weakref.ref(obj, lambda: messages.append("don't add this")) | |
| self.assertIs(ref(), obj) | |
| self.assertFalse(messages) | |
| _testcapi.pyobject_clear_weakrefs_no_callbacks(obj) | |
| self.assertIsNone(ref()) | |
| gc.collect() | |
| self.assertFalse(messages) | |
| def test_ClearWeakRefsNoCallbacks_no_weakref_support(self): | |
| """Don't fail on objects that don't support weakrefs""" | |
| import weakref | |
| obj = object() | |
| with self.assertRaises(TypeError): | |
| ref = weakref.ref(obj) | |
| _testcapi.pyobject_clear_weakrefs_no_callbacks(obj) | |
| @threading_helper.requires_working_threading() | |
| class EnableDeferredRefcountingTest(unittest.TestCase): | |
| """Test PyUnstable_Object_EnableDeferredRefcount""" | |
| @support.requires_resource("cpu") | |
| def test_enable_deferred_refcount(self): | |
| from threading import Thread | |
| import gc | |
| self.assertEqual(_testcapi.pyobject_enable_deferred_refcount("not tracked"), 0) | |
| foo = [] | |
| self.assertEqual(_testcapi.pyobject_enable_deferred_refcount(foo), int(support.Py_GIL_DISABLED)) | |
| # The object must be tracked by the GC | |
| not_gc_tracked = tuple([1, 2]) | |
| self.assertFalse(gc.is_tracked(not_gc_tracked)) | |
| self.assertEqual(_testcapi.pyobject_enable_deferred_refcount(not_gc_tracked), 0) | |
| # Make sure reference counting works on foo now | |
| self.assertEqual(foo, []) | |
| if support.Py_GIL_DISABLED: | |
| self.assertTrue(_testinternalcapi.has_deferred_refcount(foo)) | |
| # Make sure that PyUnstable_Object_EnableDeferredRefcount is thread safe | |
| def silly_func(obj): | |
| self.assertIn( | |
| _testcapi.pyobject_enable_deferred_refcount(obj), | |
| (0, 1) | |
| ) | |
| silly_list = [1, 2, 3] | |
| threads = [ | |
| Thread(target=silly_func, args=(silly_list,)) for _ in range(4) | |
| ] | |
| with threading_helper.start_threads(threads): | |
| for i in range(10): | |
| silly_list.append(i) | |
| if support.Py_GIL_DISABLED: | |
| self.assertTrue(_testinternalcapi.has_deferred_refcount(silly_list)) | |
| class IsUniquelyReferencedTest(unittest.TestCase): | |
| """Test PyUnstable_Object_IsUniquelyReferenced""" | |
| def test_is_uniquely_referenced(self): | |
| self.assertTrue(_testcapi.is_uniquely_referenced(object())) | |
| self.assertTrue(_testcapi.is_uniquely_referenced([])) | |
| # Immortals | |
| self.assertFalse(_testcapi.is_uniquely_referenced(())) | |
| self.assertFalse(_testcapi.is_uniquely_referenced(42)) | |
| # CRASHES is_uniquely_referenced(NULL) | |
| class CAPITest(unittest.TestCase): | |
| def check_negative_refcount(self, code): | |
| # bpo-35059: Check that Py_DECREF() reports the correct filename | |
| # when calling _Py_NegativeRefcount() to abort Python. | |
| code = textwrap.dedent(code) | |
| rc, out, err = assert_python_failure('-c', code) | |
| self.assertRegex(err, | |
| br'object\.c:[0-9]+: ' | |
| br'_Py_NegativeRefcount: Assertion failed: ' | |
| br'object has negative ref count') | |
| @unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'), | |
| 'need _testcapi.negative_refcount()') | |
| def test_negative_refcount(self): | |
| code = """ | |
| import _testcapi | |
| from test import support | |
| with support.SuppressCrashReport(): | |
| _testcapi.negative_refcount() | |
| """ | |
| self.check_negative_refcount(code) | |
| @unittest.skipUnless(hasattr(_testcapi, 'decref_freed_object'), | |
| 'need _testcapi.decref_freed_object()') | |
| @support.skip_if_sanitizer("use after free on purpose", | |
| address=True, memory=True, ub=True) | |
| def test_decref_freed_object(self): | |
| code = """ | |
| import _testcapi | |
| from test import support | |
| with support.SuppressCrashReport(): | |
| _testcapi.decref_freed_object() | |
| """ | |
| self.check_negative_refcount(code) | |
| @support.requires_resource('cpu') | |
| def test_decref_delayed(self): | |
| # gh-130519: Test that _PyObject_XDecRefDelayed() and QSBR code path | |
| # handles destructors that are possibly re-entrant or trigger a GC. | |
| import gc | |
| class MyObj: | |
| def __del__(self): | |
| gc.collect() | |
| for _ in range(1000): | |
| obj = MyObj() | |
| _testinternalcapi.incref_decref_delayed(obj) | |
| def test_decref_delayed_immortal(self): | |
| # _PyObject_XDecRefDelayed() must handle immortal objects, including | |
| # objects that become immortal after they were queued. The GC | |
| # processes the queue with the world stopped. | |
| import gc | |
| _testinternalcapi.incref_decref_delayed(None) | |
| gc.collect() | |
| s = ''.join(['decref', '_delayed', '_immortal']) | |
| _testinternalcapi.incref_decref_delayed(s) | |
| s = sys.intern(s) | |
| gc.collect() | |
| def test_is_unique_temporary(self): | |
| self.assertTrue(_testcapi.pyobject_is_unique_temporary(object())) | |
| obj = object() | |
| self.assertFalse(_testcapi.pyobject_is_unique_temporary(obj)) | |
| def func(x): | |
| # This relies on the LOAD_FAST_BORROW optimization (gh-130704) | |
| self.assertEqual(sys.getrefcount(x), 1) | |
| self.assertFalse(_testcapi.pyobject_is_unique_temporary(x)) | |
| func(object()) | |
| # Test that a newly created object in C is not considered | |
| # a uniquely referenced temporary, because it's not on the stack. | |
| # gh-142586: do the test in a loop over a list to test for handling | |
| # tagged ints on the stack. | |
| for i in [0, 1, 2]: | |
| self.assertFalse(_testcapi.pyobject_is_unique_temporary_new_object()) | |
| def pyobject_dump(self, obj, release_gil=False): | |
| pyobject_dump = _testcapi.pyobject_dump | |
| try: | |
| old_stderr = os.dup(STDERR_FD) | |
| except OSError as exc: | |
| # os.dup(STDERR_FD) is not supported on WASI | |
| self.skipTest(f"os.dup() failed with {exc!r}") | |
| filename = os_helper.TESTFN | |
| try: | |
| try: | |
| with open(filename, "wb") as fp: | |
| fd = fp.fileno() | |
| os.dup2(fd, STDERR_FD) | |
| pyobject_dump(obj, release_gil) | |
| finally: | |
| os.dup2(old_stderr, STDERR_FD) | |
| os.close(old_stderr) | |
| with open(filename) as fp: | |
| return fp.read().rstrip() | |
| finally: | |
| os_helper.unlink(filename) | |
| def test_pyobject_dump(self): | |
| # test string object | |
| str_obj = 'test string' | |
| output = self.pyobject_dump(str_obj) | |
| hex_regex = r'(0x)?[0-9a-fA-F]+' | |
| regex = ( | |
| fr"object address : {hex_regex}\n" | |
| r"object refcount : [0-9]+\n" | |
| fr"object type : {hex_regex}\n" | |
| r"object type name: str\n" | |
| r"object repr : 'test string'" | |
| ) | |
| self.assertRegex(output, regex) | |
| # release the GIL | |
| output = self.pyobject_dump(str_obj, release_gil=True) | |
| self.assertRegex(output, regex) | |
| # test NULL object | |
| output = self.pyobject_dump(NULL) | |
| self.assertRegex(output, r'<object at .* is freed>') | |
| class RefTracerTest(unittest.TestCase): | |
| @support.requires_resource('cpu') | |
| @support.skip_emscripten_stack_overflow() | |
| @support.skip_wasi_stack_overflow() | |
| def test_destroy_traced_for_trashcan_deferred_objects(self): | |
| depth = 200_000 | |
| chain = None | |
| for _ in range(depth): | |
| chain = [chain] | |
| _testcapi.start_counting_list_destroys() | |
| del chain | |
| destroys = _testcapi.stop_counting_list_destroys() | |
| self.assertEqual(destroys, depth) | |
| if __name__ == "__main__": | |
| unittest.main() |