Skip to content

Commit 742798d

Browse files
authored
[3.14] gh-157242: Add support.inject_memory_error() function (#159582)
[3.15] gh-157242: Add support.inject_memory_error() function (#159553) * 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) * gh-157242: Use support.inject_memory_error_cm() in tests (#157811) Rename support.memory_error_cm() to support.inject_memory_error_cm(). (cherry picked from commit a92e0f9) * Use support.inject_memory_error_cm() (cherry picked from commit c1508fb)
1 parent 9b3e52f commit 742798d

12 files changed

Lines changed: 82 additions & 58 deletions

File tree

‎Lib/test/support/__init__.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,6 +3363,22 @@ def built_with_c_assertions():
33633363
return bool(_testlimitedcapi._py_getbuiltwithassert())
33643364

33653365

3366+
def inject_memory_error(start=0, stop=0):
3367+
"""
3368+
Memory allocation fails after 'start' allocation requests, and until 'stop'
3369+
allocation requests except when 'stop' is negative or equal to 0 (default)
3370+
in which case allocation failures never stop.
3371+
3372+
Raise SkipTest if the _testcapi extension module is missing
3373+
"""
3374+
try:
3375+
import _testcapi
3376+
except ImportError:
3377+
raise unittest.SkipTest("_testcapi required")
3378+
3379+
_testcapi.set_nomemory(start, stop)
3380+
3381+
33663382
@contextlib.contextmanager
33673383
def inject_memory_error_cm(start=0, stop=0):
33683384
"""

‎Lib/test/test_atexit.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ def test_atexit_with_low_memory(self):
141141
# callback doesn't cause an infinite loop during finalization.
142142
code = textwrap.dedent("""
143143
import atexit
144-
import _testcapi
144+
from test.support import inject_memory_error
145145
146146
def callback():
147147
print("hello")
148148
149149
atexit.register(callback)
150150
# Simulate low memory condition
151-
_testcapi.set_nomemory(0)
151+
inject_memory_error()
152152
""")
153153

154154
with os_helper.temp_dir() as temp_dir:

‎Lib/test/test_bytes.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import array
8+
import contextlib
89
import operator
910
import os
1011
import re
@@ -48,6 +49,13 @@ def __index__(self):
4849
return self.value
4950

5051

52+
@contextlib.contextmanager
53+
def inject_memory_error(testcase, start=0):
54+
with testcase.assertRaises(MemoryError):
55+
with support.inject_memory_error_cm(start):
56+
yield
57+
58+
5159
class BaseBytesTest:
5260

5361
def assertTypedEqual(self, actual, expected):
@@ -1529,6 +1537,36 @@ def test_resize(self):
15291537
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
15301538
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
15311539

1540+
@support.nomemtest
1541+
def test_resize_error(self):
1542+
# gh-157242: If bytearray.resize() fails (MemoryError),
1543+
# the bytearray must be left unchanged.
1544+
1545+
offset = 3
1546+
for logical_offset in (False, True):
1547+
with self.subTest(logical_offset=logical_offset):
1548+
# grow bytearray
1549+
ba = bytearray(b'0123456789')
1550+
if logical_offset:
1551+
expected = ba[offset:]
1552+
del ba[:offset]
1553+
else:
1554+
expected = ba.copy()
1555+
with inject_memory_error(self):
1556+
ba.resize(1024)
1557+
self.assertEqual(ba, expected)
1558+
1559+
# shrink bytearray
1560+
ba = bytearray(b'0123456789')
1561+
if logical_offset:
1562+
expected = ba[offset:]
1563+
del ba[:offset]
1564+
else:
1565+
expected = ba.copy()
1566+
with inject_memory_error(self):
1567+
ba.resize(1)
1568+
self.assertEqual(ba, expected)
1569+
15321570
def test_setitem(self):
15331571
def setitem_as_mapping(b, i, val):
15341572
b[i] = val

‎Lib/test/test_class.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,8 +1015,6 @@ class C:
10151015
@support.nomemtest
10161016
@isolation.runInSubprocess()
10171017
def test_detach_materialized_dict_no_memory(self):
1018-
import _testcapi
1019-
10201018
class A:
10211019
def __init__(self):
10221020
self.a = 1
@@ -1031,11 +1029,8 @@ def __init__(self):
10311029
d = a.__dict__
10321030
try:
10331031
with support.catch_unraisable_exception() as ex:
1034-
_testcapi.set_nomemory(n, n + 1)
1035-
try:
1032+
with support.inject_memory_error_cm(n, n + 1):
10361033
del a
1037-
finally:
1038-
_testcapi.remove_mem_hooks()
10391034
exc_type = ex.unraisable and ex.unraisable.exc_type
10401035
except MemoryError:
10411036
# 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
@@ -1655,14 +1655,14 @@ def test_recursion_normalizing_with_no_memory(self):
16551655
# the size of the list of preallocated MemoryError instances, the
16561656
# Fatal Python error message mentions MemoryError.
16571657
code = """if 1:
1658-
import _testcapi
1658+
from test import support
16591659
class C(): pass
16601660
def recurse(cnt):
16611661
cnt -= 1
16621662
if cnt:
16631663
recurse(cnt)
16641664
else:
1665-
_testcapi.set_nomemory(0)
1665+
support.inject_memory_error()
16661666
C()
16671667
recurse(16)
16681668
"""
@@ -1838,9 +1838,10 @@ def test_unhandled(self):
18381838
@support.nomemtest
18391839
def test_memory_error_in_PyErr_PrintEx(self):
18401840
code = """if 1:
1841-
import _testcapi
1841+
from test import support
1842+
stop = %d
18421843
class C(): pass
1843-
_testcapi.set_nomemory(0, %d)
1844+
support.inject_memory_error(0, stop)
18441845
C()
18451846
"""
18461847

@@ -2005,8 +2006,8 @@ def test_exec_set_nomemory_hang(self):
20052006
warmup_code = "a = list(range(0, 1))\n" * 60
20062007
user_input = warmup_code + dedent("""
20072008
try:
2008-
import _testcapi
2009-
_testcapi.set_nomemory(0)
2009+
from test import support
2010+
support.inject_memory_error()
20102011
b = list(range(1000, 2000))
20112012
except Exception as e:
20122013
import traceback

‎Lib/test/test_interpreters/test_stress.py‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,9 @@ def run():
7878

7979
@support.nomemtest
8080
def test_create_interpreter_no_memory(self):
81-
import _testcapi
82-
83-
assertion = self.assertRaises(InterpreterError)
84-
try:
85-
_testcapi.set_nomemory(0, 1)
86-
with assertion:
81+
with self.assertRaises(InterpreterError):
82+
with support.inject_memory_error_cm(0, 1):
8783
_interpreters.create()
88-
finally:
89-
_testcapi.remove_mem_hooks()
9084

9185

9286
if __name__ == '__main__':

‎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_memoryio.py‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import unittest
77
from test import support
8-
from test.support import import_helper
98

109
import gc
1110
import io
@@ -655,18 +654,14 @@ def __buffer__(self, flags):
655654
@support.nomemtest
656655
def test_memory_error(self):
657656
# gh-157242: io.BytesIO() must not close the file on MemoryError
658-
_testcapi = import_helper.import_module('_testcapi')
659657

660658
# write()
661659
stream = self.ioclass()
662660
stream.write(self.buftype('abc'))
661+
data = self.buftype('def')
663662
with self.assertRaises(MemoryError):
664-
try:
665-
data = self.buftype('def')
666-
_testcapi.set_nomemory(0)
663+
with support.inject_memory_error_cm():
667664
stream.write(data)
668-
finally:
669-
_testcapi.remove_mem_hooks()
670665
stream.write(self.buftype('123'))
671666
self.assertEqual(stream.getvalue(), self.buftype('abc123'))
672667

@@ -675,11 +670,8 @@ def test_memory_error(self):
675670
stream = self.ioclass()
676671
stream.write(data)
677672
with self.assertRaises(MemoryError):
678-
try:
679-
_testcapi.set_nomemory(0)
673+
with support.inject_memory_error_cm():
680674
stream.truncate(5)
681-
finally:
682-
_testcapi.remove_mem_hooks()
683675
self.assertEqual(stream.getvalue(), data)
684676

685677

‎Lib/test/test_pyexpat.py‎

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

991-
# We avoid self.assertRaises(MemoryError) here because the
992-
# context manager itself needs memory allocations that fail
993-
# while the nomemory hook is active.
994-
self.testcapi.set_nomemory(1, 10)
995-
raised = False
996-
try:
997-
parser.ExternalEntityParserCreate(None)
998-
except MemoryError:
999-
raised = True
1000-
finally:
1001-
self.testcapi.remove_mem_hooks()
1002-
self.assertTrue(raised, "MemoryError not raised")
991+
with self.assertRaises(MemoryError):
992+
with support.inject_memory_error_cm(1, 10):
993+
parser.ExternalEntityParserCreate(None)
1003994

1004995
rc_after = sys.getrefcount(parser)
1005996
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)

0 commit comments

Comments
 (0)