diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 7bd6d966e8536b..0ba3956be065a8 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -2,7 +2,7 @@ import unittest from test import support -from test.support import cpython_only, import_helper, isolation +from test.support import cpython_only, import_helper testmeths = [ @@ -1014,7 +1014,6 @@ class C: C.a = X() @support.nomemtest - @isolation.runInSubprocess() def test_detach_materialized_dict_no_memory(self): import _testcapi @@ -1026,31 +1025,27 @@ def __init__(self): # The failing allocation should be the one which detaches the # dictionary from the object, but other allocations can happen # first, so try to fail every one of the first allocations. - raised = False + # Drop the last reference from C, so that nothing else (such as + # GC) runs between arming the failure and the deallocation. + seen = [] for n in range(20): - a = A() - d = a.__dict__ - try: - with support.catch_unraisable_exception() as ex: - _testcapi.set_nomemory(n, n + 1) - try: - del a - finally: - _testcapi.remove_mem_hooks() - exc_type = ex.unraisable and ex.unraisable.exc_type - except MemoryError: - # The failing allocation was not in the deallocation code. - continue - if exc_type is not MemoryError: - continue - raised = True - if "a" not in d: - # The dictionary was cleared, as expected. + lst = [A()] + d = lst[0].__dict__ + with support.catch_unraisable_exception() as ex: + _testcapi.call_with_nomemory(n, n + 1, lst.clear) + if ex.unraisable is None: + continue + exc_type = ex.unraisable.exc_type + err_msg = ex.unraisable.err_msg + seen.append((n, exc_type, err_msg)) + if (exc_type is MemoryError and err_msg == + 'Exception ignored while clearing an object managed dict'): + # The dictionary should have been cleared. + self.assertNotIn("a", d) break else: - if not raised: - self.fail("MemoryError was not raised during deallocation") - self.fail("the dictionary was not cleared") + self.fail("MemoryError was not raised while detaching " + f"the dictionary: {seen}") class DefinitionOrderTests(unittest.TestCase): # PEP 520: Preserving Class Attribute Definition Order diff --git a/Modules/_testcapi/mem.c b/Modules/_testcapi/mem.c index 4ae6a60ff39d15..aeb31e3ab7678e 100644 --- a/Modules/_testcapi/mem.c +++ b/Modules/_testcapi/mem.c @@ -210,6 +210,39 @@ remove_mem_hooks(PyObject *self, PyObject *Py_UNUSED(ignored)) Py_RETURN_NONE; } +static PyObject * +call_with_nomemory(PyObject *self, PyObject *args) +{ + /* Call func(*args) with memory allocation failing as in set_nomemory(). + * No bytecode is executed between arming the failure and the call. */ + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + if (nargs < 3) { + PyErr_SetString(PyExc_TypeError, + "call_with_nomemory() requires at least 3 arguments"); + return NULL; + } + int start = PyLong_AsInt(PyTuple_GET_ITEM(args, 0)); + if (start == -1 && PyErr_Occurred()) { + return NULL; + } + int stop = PyLong_AsInt(PyTuple_GET_ITEM(args, 1)); + if (stop == -1 && PyErr_Occurred()) { + return NULL; + } + PyObject *func = PyTuple_GET_ITEM(args, 2); + /* PyObject_Call() with a prebuilt tuple does not allocate, + * unlike PyObject_Vectorcall() for a callee without vectorcall. */ + PyObject *callargs = PyTuple_GetSlice(args, 3, nargs); + if (callargs == NULL) { + return NULL; + } + fm_set_nomemory(start, stop); + PyObject *res = PyObject_Call(func, callargs, NULL); + fm_remove_hooks(); + Py_DECREF(callargs); + return res; +} + static PyObject * test_setallocators(PyMemAllocatorDomain domain) { @@ -964,6 +997,8 @@ static PyMethodDef test_methods[] = { PyDoc_STR("Remove memory hooks.")}, {"set_nomemory", set_nomemory, METH_VARARGS, PyDoc_STR("set_nomemory(start:int, stop:int = 0)")}, + {"call_with_nomemory", call_with_nomemory, METH_VARARGS, + PyDoc_STR("call_with_nomemory(start:int, stop:int, func, /, *args)")}, {"test_pymem_alloc0", test_pymem_alloc0, METH_NOARGS}, {"test_pymem_setallocators", test_pymem_setallocators, METH_NOARGS}, {"test_pymem_setrawallocators", test_pymem_setrawallocators, METH_NOARGS},