https://github.com/python/cpython/commit/742798d177f1aefa76e907d15d9363915e85df33
commit: 742798d177f1aefa76e907d15d9363915e85df33
branch: 3.14
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-17T00:05:26+02:00
summary:
[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 9a1733d5712545db0703c712b158ae5e2a19e1d0)
* 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 a92e0f9b9fb2569776e9c8451dddeea589423de9)
* Use support.inject_memory_error_cm()
(cherry picked from commit c1508fb11b460829c0aef4511e6d0490c3ff2585)
files:
M Lib/test/support/__init__.py
M Lib/test/test_atexit.py
M Lib/test/test_bytes.py
M Lib/test/test_class.py
M Lib/test/test_exceptions.py
M Lib/test/test_interpreters/test_stress.py
M Lib/test/test_list.py
M Lib/test/test_memoryio.py
M Lib/test/test_pyexpat.py
M Lib/test/test_repl.py
M Lib/test/test_str.py
M Lib/test/test_weakref.py
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index ef87bf9fb5a83e7..780f7895361749c 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -3363,6 +3363,22 @@ def built_with_c_assertions():
return bool(_testlimitedcapi._py_getbuiltwithassert())
+def inject_memory_error(start=0, stop=0):
+ """
+ Memory allocation fails after 'start' allocation requests, and until 'stop'
+ allocation requests except when 'stop' is negative or equal to 0 (default)
+ in which case allocation failures never stop.
+
+ Raise SkipTest if the _testcapi extension module is missing
+ """
+ try:
+ import _testcapi
+ except ImportError:
+ raise unittest.SkipTest("_testcapi required")
+
+ _testcapi.set_nomemory(start, stop)
+
+
@contextlib.contextmanager
def inject_memory_error_cm(start=0, stop=0):
"""
diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py
index d6d39e7683363a5..0407cb6fa18d050 100644
--- a/Lib/test/test_atexit.py
+++ b/Lib/test/test_atexit.py
@@ -141,14 +141,14 @@ def test_atexit_with_low_memory(self):
# callback doesn't cause an infinite loop during finalization.
code = textwrap.dedent("""
import atexit
- import _testcapi
+ from test.support import inject_memory_error
def callback():
print("hello")
atexit.register(callback)
# Simulate low memory condition
- _testcapi.set_nomemory(0)
+ inject_memory_error()
""")
with os_helper.temp_dir() as temp_dir:
diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py
index 9e91d9664ef8a91..0e4127588ad9e34 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -5,6 +5,7 @@
"""
import array
+import contextlib
import operator
import os
import re
@@ -48,6 +49,13 @@ def __index__(self):
return self.value
[email protected]
+def inject_memory_error(testcase, start=0):
+ with testcase.assertRaises(MemoryError):
+ with support.inject_memory_error_cm(start):
+ yield
+
+
class BaseBytesTest:
def assertTypedEqual(self, actual, expected):
@@ -1529,6 +1537,36 @@ def test_resize(self):
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
+ @support.nomemtest
+ def test_resize_error(self):
+ # gh-157242: If bytearray.resize() fails (MemoryError),
+ # the bytearray must be left unchanged.
+
+ offset = 3
+ for logical_offset in (False, True):
+ with self.subTest(logical_offset=logical_offset):
+ # grow bytearray
+ ba = bytearray(b'0123456789')
+ if logical_offset:
+ expected = ba[offset:]
+ del ba[:offset]
+ else:
+ expected = ba.copy()
+ with inject_memory_error(self):
+ ba.resize(1024)
+ self.assertEqual(ba, expected)
+
+ # shrink bytearray
+ ba = bytearray(b'0123456789')
+ if logical_offset:
+ expected = ba[offset:]
+ del ba[:offset]
+ else:
+ expected = ba.copy()
+ with inject_memory_error(self):
+ ba.resize(1)
+ self.assertEqual(ba, expected)
+
def test_setitem(self):
def setitem_as_mapping(b, i, val):
b[i] = val
diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py
index 8fb32cd0d7f9464..3f9b593b0aea93d 100644
--- a/Lib/test/test_class.py
+++ b/Lib/test/test_class.py
@@ -1015,8 +1015,6 @@ class C:
@support.nomemtest
@isolation.runInSubprocess()
def test_detach_materialized_dict_no_memory(self):
- import _testcapi
-
class A:
def __init__(self):
self.a = 1
@@ -1031,11 +1029,8 @@ def __init__(self):
d = a.__dict__
try:
with support.catch_unraisable_exception() as ex:
- _testcapi.set_nomemory(n, n + 1)
- try:
+ with support.inject_memory_error_cm(n, n + 1):
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.
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 20d03d99dab5837..86cc3f23959198f 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -1655,14 +1655,14 @@ def test_recursion_normalizing_with_no_memory(self):
# the size of the list of preallocated MemoryError instances, the
# Fatal Python error message mentions MemoryError.
code = """if 1:
- import _testcapi
+ from test import support
class C(): pass
def recurse(cnt):
cnt -= 1
if cnt:
recurse(cnt)
else:
- _testcapi.set_nomemory(0)
+ support.inject_memory_error()
C()
recurse(16)
"""
@@ -1838,9 +1838,10 @@ def test_unhandled(self):
@support.nomemtest
def test_memory_error_in_PyErr_PrintEx(self):
code = """if 1:
- import _testcapi
+ from test import support
+ stop = %d
class C(): pass
- _testcapi.set_nomemory(0, %d)
+ support.inject_memory_error(0, stop)
C()
"""
@@ -2005,8 +2006,8 @@ def test_exec_set_nomemory_hang(self):
warmup_code = "a = list(range(0, 1))\n" * 60
user_input = warmup_code + dedent("""
try:
- import _testcapi
- _testcapi.set_nomemory(0)
+ from test import support
+ support.inject_memory_error()
b = list(range(1000, 2000))
except Exception as e:
import traceback
diff --git a/Lib/test/test_interpreters/test_stress.py
b/Lib/test/test_interpreters/test_stress.py
index 50d2444a4c72d31..56bfc12f951f795 100644
--- a/Lib/test/test_interpreters/test_stress.py
+++ b/Lib/test/test_interpreters/test_stress.py
@@ -78,15 +78,9 @@ def run():
@support.nomemtest
def test_create_interpreter_no_memory(self):
- import _testcapi
-
- assertion = self.assertRaises(InterpreterError)
- try:
- _testcapi.set_nomemory(0, 1)
- with assertion:
+ with self.assertRaises(InterpreterError):
+ with support.inject_memory_error_cm(0, 1):
_interpreters.create()
- finally:
- _testcapi.remove_mem_hooks()
if __name__ == '__main__':
diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py
index 4d5cb11bddb769d..8657b759dd18b2f 100644
--- a/Lib/test/test_list.py
+++ b/Lib/test/test_list.py
@@ -329,11 +329,12 @@ def test_tier2_invalidates_iterator(self):
def test_no_memory(self):
# gh-118331: Make sure we don't crash if list allocation fails
code = textwrap.dedent("""
- import _testcapi, sys
+ from test import support
+ import sys
# Prime the freelist
l = [None]
del l
- _testcapi.set_nomemory(0)
+ support.inject_memory_error()
l = [None]
""")
rc, _, _ = assert_python_failure("-c", code)
diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py
index ffe66fcf732f295..0d6186955ec9ddf 100644
--- a/Lib/test/test_memoryio.py
+++ b/Lib/test/test_memoryio.py
@@ -5,7 +5,6 @@
import unittest
from test import support
-from test.support import import_helper
import gc
import io
@@ -655,18 +654,14 @@ def __buffer__(self, flags):
@support.nomemtest
def test_memory_error(self):
# gh-157242: io.BytesIO() must not close the file on MemoryError
- _testcapi = import_helper.import_module('_testcapi')
# write()
stream = self.ioclass()
stream.write(self.buftype('abc'))
+ data = self.buftype('def')
with self.assertRaises(MemoryError):
- try:
- data = self.buftype('def')
- _testcapi.set_nomemory(0)
+ with support.inject_memory_error_cm():
stream.write(data)
- finally:
- _testcapi.remove_mem_hooks()
stream.write(self.buftype('123'))
self.assertEqual(stream.getvalue(), self.buftype('abc123'))
@@ -675,11 +670,8 @@ def test_memory_error(self):
stream = self.ioclass()
stream.write(data)
with self.assertRaises(MemoryError):
- try:
- _testcapi.set_nomemory(0)
+ with support.inject_memory_error_cm():
stream.truncate(5)
- finally:
- _testcapi.remove_mem_hooks()
self.assertEqual(stream.getvalue(), data)
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index 581efee6f4c1ff1..c0a45764cc47ba4 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -988,18 +988,9 @@ def test_error_path_no_crash(self):
parser.buffer_text = True
rc_before = sys.getrefcount(parser)
- # We avoid self.assertRaises(MemoryError) here because the
- # context manager itself needs memory allocations that fail
- # while the nomemory hook is active.
- self.testcapi.set_nomemory(1, 10)
- raised = False
- try:
- parser.ExternalEntityParserCreate(None)
- except MemoryError:
- raised = True
- finally:
- self.testcapi.remove_mem_hooks()
- self.assertTrue(raised, "MemoryError not raised")
+ with self.assertRaises(MemoryError):
+ with support.inject_memory_error_cm(1, 10):
+ parser.ExternalEntityParserCreate(None)
rc_after = sys.getrefcount(parser)
self.assertEqual(rc_after, rc_before)
diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py
index 9115e8cebabaca8..9f91e122bb5e85d 100644
--- a/Lib/test/test_repl.py
+++ b/Lib/test/test_repl.py
@@ -104,10 +104,11 @@ def test_no_memory(self):
# no memory. Check also that the fix does not break the interactive
# loop when an exception is raised.
user_input = """
- import sys, _testcapi
+ import sys
+ from test import support
1/0
print('After the exception.')
- _testcapi.set_nomemory(0)
+ support.inject_memory_error()
sys.exit(0)
"""
user_input = dedent(user_input)
diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py
index 444841ddd470aa7..3530d555fa725e2 100644
--- a/Lib/test/test_str.py
+++ b/Lib/test/test_str.py
@@ -606,14 +606,9 @@ def test_replace_oom(self):
s1 = "轘" * 4
s2 = "&"
s3 = "&"
- assertion = self.assertRaises(MemoryError)
- _testcapi.set_nomemory(0, 0)
- try:
- # No allocations made in the test itself:
- with assertion:
+ with self.assertRaises(MemoryError):
+ with support.inject_memory_error_cm():
s1.replace(s2, s3) # this line used to crash before
- finally:
- _testcapi.remove_mem_hooks()
def test_repeat_id_preserving(self):
a = '123abc1@'
diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py
index 36a0003079c1e9a..e5d3756355b98b1 100644
--- a/Lib/test/test_weakref.py
+++ b/Lib/test/test_weakref.py
@@ -1035,8 +1035,8 @@ def test_no_memory_when_clearing(self):
# gh-118331: Make sure we do not raise an exception from the destructor
# when clearing weakrefs if allocating the intermediate tuple fails.
code = textwrap.dedent("""
- import _testcapi
import weakref
+ from test import support
class TestObj:
pass
@@ -1048,7 +1048,7 @@ def callback(obj):
# The choice of 50 is arbitrary, but must be large enough to ensure
# the allocation won't be serviced by the free list.
wrs = [weakref.ref(obj, callback) for _ in range(50)]
- _testcapi.set_nomemory(0)
+ support.inject_memory_error()
del obj
""").strip()
res, _ = script_helper.run_python_until_end("-c", code)
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]