From d84d2c4985457733602d46dc4ee77290da4e9539 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 17 Aug 2021 03:18:36 +0800 Subject: bpo-44914: Add tests for some invariants of tp_version_tag (GH-27774) --- Lib/test/test_type_cache.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ Modules/_testcapimodule.c | 18 +++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 Lib/test/test_type_cache.py diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py new file mode 100644 index 0000000..8502f6b --- /dev/null +++ b/Lib/test/test_type_cache.py @@ -0,0 +1,47 @@ +""" Tests for the internal type cache in CPython. """ +import unittest +from test import support +from test.support import import_helper +try: + from sys import _clear_type_cache +except ImportError: + _clear_type_cache = None + +# Skip this test if the _testcapi module isn't available. +type_get_version = import_helper.import_module('_testcapi').type_get_version + + +@support.cpython_only +@unittest.skipIf(_clear_type_cache is None, "requires sys._clear_type_cache") +class TypeCacheTests(unittest.TestCase): + def test_tp_version_tag_unique(self): + """tp_version_tag should be unique assuming no overflow, even after + clearing type cache. + """ + # Check if global version tag has already overflowed. + Y = type('Y', (), {}) + Y.x = 1 + Y.x # Force a _PyType_Lookup, populating version tag + y_ver = type_get_version(Y) + # Overflow, or not enough left to conduct the test. + if y_ver == 0 or y_ver > 0xFFFFF000: + self.skipTest("Out of type version tags") + # Note: try to avoid any method lookups within this loop, + # It will affect global version tag. + all_version_tags = [] + append_result = all_version_tags.append + assertNotEqual = self.assertNotEqual + for _ in range(30): + _clear_type_cache() + X = type('Y', (), {}) + X.x = 1 + X.x + tp_version_tag_after = type_get_version(X) + assertNotEqual(tp_version_tag_after, 0, msg="Version overflowed") + append_result(tp_version_tag_after) + self.assertEqual(len(set(all_version_tags)), 30, + msg=f"{all_version_tags} contains non-unique versions") + + +if __name__ == "__main__": + support.run_unittest(TypeCacheTests) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index f338e89..c35eac7 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -5575,6 +5575,23 @@ test_fatal_error(PyObject *self, PyObject *args) Py_RETURN_NONE; } +// type->tp_version_tag +static PyObject * +type_get_version(PyObject *self, PyObject *type) +{ + if (!PyType_Check(type)) { + PyErr_SetString(PyExc_TypeError, "argument must be a type"); + return NULL; + } + PyObject *res = PyLong_FromUnsignedLong( + ((PyTypeObject *)type)->tp_version_tag); + if (res == NULL) { + assert(PyErr_Occurred()); + return NULL; + } + return res; +} + static PyObject *test_buildvalue_issue38913(PyObject *, PyObject *); static PyObject *getargs_s_hash_int(PyObject *, PyObject *, PyObject*); @@ -5854,6 +5871,7 @@ static PyMethodDef TestMethods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"fatal_error", test_fatal_error, METH_VARARGS, PyDoc_STR("fatal_error(message, release_gil=False): call Py_FatalError(message)")}, + {"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")}, {NULL, NULL} /* sentinel */ }; -- cgit v0.12