Skip to content

Commit 05e2821

Browse files
committed
gh-157242: Add support.inject_memory_error() function
Add inject_memory_error() and with_memory_error() functions to test.support to inject memory errors: make the memory allocator fail.
1 parent e41677b commit 05e2821

10 files changed

Lines changed: 65 additions & 41 deletions

File tree

‎Lib/test/support/__init__.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3540,3 +3540,38 @@ def built_with_c_assertions():
35403540
return False
35413541

35423542
return True
3543+
3544+
3545+
def inject_memory_error(start=0, stop=0):
3546+
"""
3547+
Memory allocation fails after 'start' allocation requests, and until 'stop'
3548+
allocation requests except when 'stop' is negative or equal to 0 (default)
3549+
in which case allocation failures never stop.
3550+
3551+
Raise SkipTest if the _testcapi extension module is missing
3552+
"""
3553+
try:
3554+
import _testcapi
3555+
except ImportError:
3556+
raise unittest.SkipTest("_testcapi required")
3557+
3558+
_testcapi.set_nomemory(start, stop)
3559+
3560+
3561+
@contextlib.contextmanager
3562+
def with_memory_error(start=0, stop=0):
3563+
"""
3564+
Similar to inject_memory_error() but can be used as a context manager.
3565+
3566+
Raise SkipTest if the _testcapi extension module is missing
3567+
"""
3568+
try:
3569+
import _testcapi
3570+
except ImportError:
3571+
raise unittest.SkipTest("_testcapi required")
3572+
3573+
try:
3574+
_testcapi.set_nomemory(start, stop)
3575+
yield
3576+
finally:
3577+
_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 & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,13 @@ def __index__(self):
5050

5151

5252
@contextlib.contextmanager
53-
def inject_memory_error(testcase, start):
53+
def inject_memory_error(testcase, start=0):
5454
# Raise SkipTest if _testcapi extension module is missing
5555
_testcapi = import_helper.import_module('_testcapi')
5656

5757
with testcase.assertRaises(MemoryError):
58-
try:
59-
_testcapi.set_nomemory(start)
58+
with support.with_memory_error(start):
6059
yield
61-
finally:
62-
_testcapi.remove_mem_hooks()
6360

6461

6562
class BaseBytesTest:
@@ -1585,7 +1582,7 @@ def test_resize_error(self):
15851582
del ba[:offset]
15861583
else:
15871584
expected = ba.copy()
1588-
with inject_memory_error(self, 0):
1585+
with inject_memory_error(self):
15891586
ba.resize(1024)
15901587
self.assertEqual(ba, expected)
15911588

@@ -1596,7 +1593,7 @@ def test_resize_error(self):
15961593
del ba[:offset]
15971594
else:
15981595
expected = ba.copy()
1599-
with inject_memory_error(self, 0):
1596+
with inject_memory_error(self):
16001597
ba.resize(1)
16011598
self.assertEqual(ba, expected)
16021599

@@ -1669,21 +1666,20 @@ def test_take_bytes_error(self):
16691666
# gh-157242: If bytearray.take_bytes() fails (MemoryError),
16701667
# the bytearray must be left unchanged.
16711668

1672-
for logical_offset, to_take, mem_errors in (
1669+
for logical_offset, to_take, start_list in (
16731670
(True, 5, (0, 1)),
16741671
(False, 5, (0, 1)),
16751672
(True, None, (0,)),
16761673
):
1677-
for mem_error in mem_errors:
1678-
with self.subTest(logical_offset=logical_offset,
1679-
to_take=to_take, mem_error=mem_error):
1674+
for start in start_list:
1675+
with self.subTest(logical_offset=logical_offset, start=start):
16801676
ba = bytearray(b'0123456789')
16811677
if logical_offset:
16821678
expected = ba[3:]
16831679
del ba[:3]
16841680
else:
16851681
expected = ba.copy()
1686-
with inject_memory_error(self, mem_error):
1682+
with inject_memory_error(self, start):
16871683
ba.take_bytes(to_take)
16881684
self.assertEqual(ba, expected)
16891685

‎Lib/test/test_class.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,11 +1032,8 @@ def __init__(self):
10321032
d = a.__dict__
10331033
try:
10341034
with support.catch_unraisable_exception() as ex:
1035-
_testcapi.set_nomemory(n, n + 1)
1036-
try:
1035+
with support.with_memory_error(n, n + 1):
10371036
del a
1038-
finally:
1039-
_testcapi.remove_mem_hooks()
10401037
exc_type = ex.unraisable and ex.unraisable.exc_type
10411038
except MemoryError:
10421039
# 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
@@ -1660,14 +1660,14 @@ def test_recursion_normalizing_with_no_memory(self):
16601660
# the size of the list of preallocated MemoryError instances, the
16611661
# Fatal Python error message mentions MemoryError.
16621662
code = """if 1:
1663-
import _testcapi
1663+
from test import support
16641664
class C(): pass
16651665
def recurse(cnt):
16661666
cnt -= 1
16671667
if cnt:
16681668
recurse(cnt)
16691669
else:
1670-
_testcapi.set_nomemory(0)
1670+
support.inject_memory_error()
16711671
C()
16721672
recurse(16)
16731673
"""
@@ -1843,9 +1843,10 @@ def test_unhandled(self):
18431843
@support.nomemtest
18441844
def test_memory_error_in_PyErr_PrintEx(self):
18451845
code = """if 1:
1846-
import _testcapi
1846+
from test import support
1847+
stop = %d
18471848
class C(): pass
1848-
_testcapi.set_nomemory(0, %d)
1849+
support.inject_memory_error(0, stop)
18491850
C()
18501851
"""
18511852

@@ -2010,8 +2011,8 @@ def test_exec_set_nomemory_hang(self):
20102011
warmup_code = "a = list(range(0, 1))\n" * 60
20112012
user_input = warmup_code + dedent("""
20122013
try:
2013-
import _testcapi
2014-
_testcapi.set_nomemory(0)
2014+
from test import support
2015+
support.inject_memory_error()
20152016
b = list(range(1000, 2000))
20162017
except Exception as e:
20172018
import traceback

‎Lib/test/test_list.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,12 @@ def test_tier2_invalidates_iterator(self):
377377
def test_no_memory(self):
378378
# gh-118331: Make sure we don't crash if list allocation fails
379379
code = textwrap.dedent("""
380-
import _testcapi, sys
380+
from test import support
381+
import sys
381382
# Prime the freelist
382383
l = [None]
383384
del l
384-
_testcapi.set_nomemory(0)
385+
support.inject_memory_error()
385386
l = [None]
386387
""")
387388
rc, _, _ = assert_python_failure("-c", code)

‎Lib/test/test_pyexpat.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,14 +1080,12 @@ def test_error_path_no_crash(self):
10801080
# We avoid self.assertRaises(MemoryError) here because the
10811081
# context manager itself needs memory allocations that fail
10821082
# while the nomemory hook is active.
1083-
self.testcapi.set_nomemory(1, 10)
10841083
raised = False
10851084
try:
1086-
parser.ExternalEntityParserCreate(None)
1085+
with support.with_memory_error(1, 10):
1086+
parser.ExternalEntityParserCreate(None)
10871087
except MemoryError:
10881088
raised = True
1089-
finally:
1090-
self.testcapi.remove_mem_hooks()
10911089
self.assertTrue(raised, "MemoryError not raised")
10921090

10931091
rc_after = sys.getrefcount(parser)

‎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.with_memory_error():
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
@@ -1029,8 +1029,8 @@ def test_no_memory_when_clearing(self):
10291029
# gh-118331: Make sure we do not raise an exception from the destructor
10301030
# when clearing weakrefs if allocating the intermediate tuple fails.
10311031
code = textwrap.dedent("""
1032-
import _testcapi
10331032
import weakref
1033+
from test import support
10341034
10351035
class TestObj:
10361036
pass
@@ -1042,7 +1042,7 @@ def callback(obj):
10421042
# The choice of 50 is arbitrary, but must be large enough to ensure
10431043
# the allocation won't be serviced by the free list.
10441044
wrs = [weakref.ref(obj, callback) for _ in range(50)]
1045-
_testcapi.set_nomemory(0)
1045+
support.inject_memory_error()
10461046
del obj
10471047
""").strip()
10481048
res, _ = script_helper.run_python_until_end("-c", code)

0 commit comments

Comments
 (0)