Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f203efd

Browse files
gh-157429: Make test_detach_materialized_dict_no_memory deterministic
Add _testcapi.call_with_nomemory() which arms the allocation failure, calls a function and removes the memory hooks without executing any bytecode in between, so that pending work at the eval breaker (GC, deferred frees on free-threading builds) cannot consume the failing allocation. The test now drops the last reference from C with this helper, accepts only the unraisable exception reported while clearing the managed dict, and no longer needs to run in a subprocess. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
1 parent ad447fa commit f203efd

2 files changed

Lines changed: 54 additions & 24 deletions

File tree

Lib/test/test_class.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import unittest
44
from test import support
5-
from test.support import cpython_only, import_helper, isolation
5+
from test.support import cpython_only, import_helper
66

77
testmeths = [
88

@@ -1014,7 +1014,6 @@ class C:
10141014
C.a = X()
10151015

10161016
@support.nomemtest
1017-
@isolation.runInSubprocess()
10181017
def test_detach_materialized_dict_no_memory(self):
10191018
import _testcapi
10201019

@@ -1026,31 +1025,27 @@ def __init__(self):
10261025
# The failing allocation should be the one which detaches the
10271026
# dictionary from the object, but other allocations can happen
10281027
# first, so try to fail every one of the first allocations.
1029-
raised = False
1028+
# Drop the last reference from C, so that nothing else (such as
1029+
# GC) runs between arming the failure and the deallocation.
1030+
seen = []
10301031
for n in range(20):
1031-
a = A()
1032-
d = a.__dict__
1033-
try:
1034-
with support.catch_unraisable_exception() as ex:
1035-
_testcapi.set_nomemory(n, n + 1)
1036-
try:
1037-
del a
1038-
finally:
1039-
_testcapi.remove_mem_hooks()
1040-
exc_type = ex.unraisable and ex.unraisable.exc_type
1041-
except MemoryError:
1042-
# The failing allocation was not in the deallocation code.
1043-
continue
1044-
if exc_type is not MemoryError:
1045-
continue
1046-
raised = True
1047-
if "a" not in d:
1048-
# The dictionary was cleared, as expected.
1032+
lst = [A()]
1033+
d = lst[0].__dict__
1034+
with support.catch_unraisable_exception() as ex:
1035+
_testcapi.call_with_nomemory(n, n + 1, lst.clear)
1036+
if ex.unraisable is None:
1037+
continue
1038+
exc_type = ex.unraisable.exc_type
1039+
err_msg = ex.unraisable.err_msg
1040+
seen.append((n, exc_type, err_msg))
1041+
if (exc_type is MemoryError and err_msg ==
1042+
'Exception ignored while clearing an object managed dict'):
1043+
# The dictionary should have been cleared.
1044+
self.assertNotIn("a", d)
10491045
break
10501046
else:
1051-
if not raised:
1052-
self.fail("MemoryError was not raised during deallocation")
1053-
self.fail("the dictionary was not cleared")
1047+
self.fail("MemoryError was not raised while detaching "
1048+
f"the dictionary: {seen}")
10541049

10551050
class DefinitionOrderTests(unittest.TestCase):
10561051
# PEP 520: Preserving Class Attribute Definition Order

Modules/_testcapi/mem.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,39 @@ remove_mem_hooks(PyObject *self, PyObject *Py_UNUSED(ignored))
210210
Py_RETURN_NONE;
211211
}
212212

213+
static PyObject *
214+
call_with_nomemory(PyObject *self, PyObject *args)
215+
{
216+
/* Call func(*args) with memory allocation failing as in set_nomemory().
217+
* No bytecode is executed between arming the failure and the call. */
218+
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
219+
if (nargs < 3) {
220+
PyErr_SetString(PyExc_TypeError,
221+
"call_with_nomemory() requires at least 3 arguments");
222+
return NULL;
223+
}
224+
int start = PyLong_AsInt(PyTuple_GET_ITEM(args, 0));
225+
if (start == -1 && PyErr_Occurred()) {
226+
return NULL;
227+
}
228+
int stop = PyLong_AsInt(PyTuple_GET_ITEM(args, 1));
229+
if (stop == -1 && PyErr_Occurred()) {
230+
return NULL;
231+
}
232+
PyObject *func = PyTuple_GET_ITEM(args, 2);
233+
/* PyObject_Call() with a prebuilt tuple does not allocate,
234+
* unlike PyObject_Vectorcall() for a callee without vectorcall. */
235+
PyObject *callargs = PyTuple_GetSlice(args, 3, nargs);
236+
if (callargs == NULL) {
237+
return NULL;
238+
}
239+
fm_set_nomemory(start, stop);
240+
PyObject *res = PyObject_Call(func, callargs, NULL);
241+
fm_remove_hooks();
242+
Py_DECREF(callargs);
243+
return res;
244+
}
245+
213246
static PyObject *
214247
test_setallocators(PyMemAllocatorDomain domain)
215248
{
@@ -964,6 +997,8 @@ static PyMethodDef test_methods[] = {
964997
PyDoc_STR("Remove memory hooks.")},
965998
{"set_nomemory", set_nomemory, METH_VARARGS,
966999
PyDoc_STR("set_nomemory(start:int, stop:int = 0)")},
1000+
{"call_with_nomemory", call_with_nomemory, METH_VARARGS,
1001+
PyDoc_STR("call_with_nomemory(start:int, stop:int, func, /, *args)")},
9671002
{"test_pymem_alloc0", test_pymem_alloc0, METH_NOARGS},
9681003
{"test_pymem_setallocators", test_pymem_setallocators, METH_NOARGS},
9691004
{"test_pymem_setrawallocators", test_pymem_setrawallocators, METH_NOARGS},

0 commit comments

Comments
 (0)