Skip to content

Commit 9daa6bd

Browse files
committed
gh-157242: Add support.inject_memory_error() function (#157431)
Add inject_memory_error() and memory_error_cm() functions to test.support to inject memory errors: make the memory allocator fail. (cherry picked from commit 9a1733d)
1 parent 1ebc13b commit 9daa6bd

10 files changed

Lines changed: 66 additions & 54 deletions

File tree

‎Lib/test/support/__init__.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3453,3 +3453,38 @@ def built_with_c_assertions():
34533453
return Py_DEBUG
34543454
else:
34553455
return bool(_testlimitedcapi._py_getbuiltwithassert())
3456+
3457+
3458+
def inject_memory_error(start=0, stop=0):
3459+
"""
3460+
Memory allocation fails after 'start' allocation requests, and until 'stop'
3461+
allocation requests except when 'stop' is negative or equal to 0 (default)
3462+
in which case allocation failures never stop.
3463+
3464+
Raise SkipTest if the _testcapi extension module is missing
3465+
"""
3466+
try:
3467+
import _testcapi
3468+
except ImportError:
3469+
raise unittest.SkipTest("_testcapi required")
3470+
3471+
_testcapi.set_nomemory(start, stop)
3472+
3473+
3474+
@contextlib.contextmanager
3475+
def memory_error_cm(start=0, stop=0):
3476+
"""
3477+
Similar to inject_memory_error() but can be used as a context manager.
3478+
3479+
Raise SkipTest if the _testcapi extension module is missing
3480+
"""
3481+
try:
3482+
import _testcapi
3483+
except ImportError:
3484+
raise unittest.SkipTest("_testcapi required")
3485+
3486+
try:
3487+
_testcapi.set_nomemory(start, stop)
3488+
yield
3489+
finally:
3490+
_testcapi.remove_mem_hooks()

‎Lib/test/test_atexit.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,14 @@ def test_atexit_with_low_memory(self):
197197
# callback doesn't cause an infinite loop during finalization.
198198
code = textwrap.dedent("""
199199
import atexit
200-
import _testcapi
200+
from test.support import inject_memory_error
201201
202202
def callback():
203203
print("hello")
204204
205205
atexit.register(callback)
206206
# Simulate low memory condition
207-
_testcapi.set_nomemory(0)
207+
inject_memory_error()
208208
""")
209209

210210
with os_helper.temp_dir() as temp_dir:

‎Lib/test/test_bytes.py‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,10 @@ def __index__(self):
5151

5252

5353
@contextlib.contextmanager
54-
def inject_memory_error(testcase, start):
55-
# Raise SkipTest if _testcapi extension module is missing
56-
_testcapi = import_helper.import_module('_testcapi')
57-
54+
def inject_memory_error(testcase, start=0):
5855
with testcase.assertRaises(MemoryError):
59-
try:
60-
_testcapi.set_nomemory(start)
56+
with support.memory_error_cm(start):
6157
yield
62-
finally:
63-
_testcapi.remove_mem_hooks()
6458

6559

6660
class BaseBytesTest:
@@ -1607,7 +1601,7 @@ def test_resize_error(self):
16071601
del ba[:offset]
16081602
else:
16091603
expected = ba.copy()
1610-
with inject_memory_error(self, 0):
1604+
with inject_memory_error(self):
16111605
ba.resize(1024)
16121606
self.assertEqual(ba, expected)
16131607

@@ -1618,7 +1612,7 @@ def test_resize_error(self):
16181612
del ba[:offset]
16191613
else:
16201614
expected = ba.copy()
1621-
with inject_memory_error(self, 0):
1615+
with inject_memory_error(self):
16221616
ba.resize(1)
16231617
self.assertEqual(ba, expected)
16241618

@@ -1691,21 +1685,20 @@ def test_take_bytes_error(self):
16911685
# gh-157242: If bytearray.take_bytes() fails (MemoryError),
16921686
# the bytearray must be left unchanged.
16931687

1694-
for logical_offset, to_take, mem_errors in (
1688+
for logical_offset, to_take, start_list in (
16951689
(True, 5, (0, 1)),
16961690
(False, 5, (0, 1)),
16971691
(True, None, (0,)),
16981692
):
1699-
for mem_error in mem_errors:
1700-
with self.subTest(logical_offset=logical_offset,
1701-
to_take=to_take, mem_error=mem_error):
1693+
for start in start_list:
1694+
with self.subTest(logical_offset=logical_offset, start=start):
17021695
ba = bytearray(b'0123456789')
17031696
if logical_offset:
17041697
expected = ba[3:]
17051698
del ba[:3]
17061699
else:
17071700
expected = ba.copy()
1708-
with inject_memory_error(self, mem_error):
1701+
with inject_memory_error(self, start):
17091702
ba.take_bytes(to_take)
17101703
self.assertEqual(ba, expected)
17111704

‎Lib/test/test_class.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,8 +1016,6 @@ class C:
10161016
@support.nomemtest
10171017
@isolation.runInSubprocess()
10181018
def test_detach_materialized_dict_no_memory(self):
1019-
import _testcapi
1020-
10211019
class A:
10221020
def __init__(self):
10231021
self.a = 1
@@ -1032,11 +1030,8 @@ def __init__(self):
10321030
d = a.__dict__
10331031
try:
10341032
with support.catch_unraisable_exception() as ex:
1035-
_testcapi.set_nomemory(n, n + 1)
1036-
try:
1033+
with support.memory_error_cm(n, n + 1):
10371034
del a
1038-
finally:
1039-
_testcapi.remove_mem_hooks()
10401035
exc_type = ex.unraisable and ex.unraisable.exc_type
10411036
except MemoryError:
10421037
# The failing allocation was not in the deallocation code.

‎Lib/test/test_exceptions.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,14 +1663,14 @@ def test_recursion_normalizing_with_no_memory(self):
16631663
# the size of the list of preallocated MemoryError instances, the
16641664
# Fatal Python error message mentions MemoryError.
16651665
code = """if 1:
1666-
import _testcapi
1666+
from test import support
16671667
class C(): pass
16681668
def recurse(cnt):
16691669
cnt -= 1
16701670
if cnt:
16711671
recurse(cnt)
16721672
else:
1673-
_testcapi.set_nomemory(0)
1673+
support.inject_memory_error()
16741674
C()
16751675
recurse(16)
16761676
"""
@@ -1846,9 +1846,10 @@ def test_unhandled(self):
18461846
@support.nomemtest
18471847
def test_memory_error_in_PyErr_PrintEx(self):
18481848
code = """if 1:
1849-
import _testcapi
1849+
from test import support
1850+
stop = %d
18501851
class C(): pass
1851-
_testcapi.set_nomemory(0, %d)
1852+
support.inject_memory_error(0, stop)
18521853
C()
18531854
"""
18541855

@@ -2013,8 +2014,8 @@ def test_exec_set_nomemory_hang(self):
20132014
warmup_code = "a = list(range(0, 1))\n" * 60
20142015
user_input = warmup_code + dedent("""
20152016
try:
2016-
import _testcapi
2017-
_testcapi.set_nomemory(0)
2017+
from test import support
2018+
support.inject_memory_error()
20182019
b = list(range(1000, 2000))
20192020
except Exception as e:
20202021
import traceback

‎Lib/test/test_list.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,12 @@ def test_tier2_invalidates_iterator(self):
329329
def test_no_memory(self):
330330
# gh-118331: Make sure we don't crash if list allocation fails
331331
code = textwrap.dedent("""
332-
import _testcapi, sys
332+
from test import support
333+
import sys
333334
# Prime the freelist
334335
l = [None]
335336
del l
336-
_testcapi.set_nomemory(0)
337+
support.inject_memory_error()
337338
l = [None]
338339
""")
339340
rc, _, _ = assert_python_failure("-c", code)

‎Lib/test/test_pyexpat.py‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,18 +1028,9 @@ def test_error_path_no_crash(self):
10281028
parser.buffer_text = True
10291029
rc_before = sys.getrefcount(parser)
10301030

1031-
# We avoid self.assertRaises(MemoryError) here because the
1032-
# context manager itself needs memory allocations that fail
1033-
# while the nomemory hook is active.
1034-
self.testcapi.set_nomemory(1, 10)
1035-
raised = False
1036-
try:
1037-
parser.ExternalEntityParserCreate(None)
1038-
except MemoryError:
1039-
raised = True
1040-
finally:
1041-
self.testcapi.remove_mem_hooks()
1042-
self.assertTrue(raised, "MemoryError not raised")
1031+
with self.assertRaises(MemoryError):
1032+
with support.memory_error_cm(1, 10):
1033+
parser.ExternalEntityParserCreate(None)
10431034

10441035
rc_after = sys.getrefcount(parser)
10451036
self.assertEqual(rc_after, rc_before)

‎Lib/test/test_repl.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ def test_no_memory(self):
104104
# no memory. Check also that the fix does not break the interactive
105105
# loop when an exception is raised.
106106
user_input = """
107-
import sys, _testcapi
107+
import sys
108+
from test import support
108109
1/0
109110
print('After the exception.')
110-
_testcapi.set_nomemory(0)
111+
support.inject_memory_error()
111112
sys.exit(0)
112113
"""
113114
user_input = dedent(user_input)

‎Lib/test/test_str.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -613,14 +613,9 @@ def test_replace_oom(self):
613613
s1 = "轘" * 4
614614
s2 = "&"
615615
s3 = "&"
616-
assertion = self.assertRaises(MemoryError)
617-
_testcapi.set_nomemory(0, 0)
618-
try:
619-
# No allocations made in the test itself:
620-
with assertion:
616+
with self.assertRaises(MemoryError):
617+
with support.memory_error_cm():
621618
s1.replace(s2, s3) # this line used to crash before
622-
finally:
623-
_testcapi.remove_mem_hooks()
624619

625620
def test_repeat_id_preserving(self):
626621
a = '123abc1@'

‎Lib/test/test_weakref.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,8 +1035,8 @@ def test_no_memory_when_clearing(self):
10351035
# gh-118331: Make sure we do not raise an exception from the destructor
10361036
# when clearing weakrefs if allocating the intermediate tuple fails.
10371037
code = textwrap.dedent("""
1038-
import _testcapi
10391038
import weakref
1039+
from test import support
10401040
10411041
class TestObj:
10421042
pass
@@ -1048,7 +1048,7 @@ def callback(obj):
10481048
# The choice of 50 is arbitrary, but must be large enough to ensure
10491049
# the allocation won't be serviced by the free list.
10501050
wrs = [weakref.ref(obj, callback) for _ in range(50)]
1051-
_testcapi.set_nomemory(0)
1051+
support.inject_memory_error()
10521052
del obj
10531053
""").strip()
10541054
res, _ = script_helper.run_python_until_end("-c", code)

0 commit comments

Comments
 (0)