Skip to content

Commit 532e063

Browse files
authored
bpo-41718: regrtest saved_test_environment avoids imports (GH-24934)
Reduce the number of modules imported by libregrtest. saved_test_environment no longer imports modules at startup, but try to get them from sys.modules. If an module is missing, skip the test. It also sets directly support.environment_altered. runtest() now now two saved_test_environment instances: one before importing the test module, one after importing it. Remove imports from test.libregrtest.save_env: * asyncio * logging * multiprocessing * shutil * sysconfig * urllib.request * warnings When a test method imports a module (ex: warnings) and the test has a side effect (ex: add a warnings filter), the side effect is not detected, because the module was not imported when Python enters the saved_test_environment context manager.
1 parent 96eeff5 commit 532e063

2 files changed

Lines changed: 69 additions & 42 deletions

File tree

Lib/test/libregrtest/runtest.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ def _test_module(the_module):
211211
support.run_unittest(tests)
212212

213213

214+
def save_env(ns, test_name):
215+
return saved_test_environment(test_name, ns.verbose, ns.quiet, pgo=ns.pgo)
216+
217+
214218
def _runtest_inner2(ns, test_name):
215219
# Load the test function, run the test function, handle huntrleaks
216220
# and findleaks to detect leaks
@@ -229,12 +233,13 @@ def _runtest_inner2(ns, test_name):
229233
test_runner = functools.partial(_test_module, the_module)
230234

231235
try:
232-
if ns.huntrleaks:
233-
# Return True if the test leaked references
234-
refleak = dash_R(ns, test_name, test_runner)
235-
else:
236-
test_runner()
237-
refleak = False
236+
with save_env(ns, test_name):
237+
if ns.huntrleaks:
238+
# Return True if the test leaked references
239+
refleak = dash_R(ns, test_name, test_runner)
240+
else:
241+
test_runner()
242+
refleak = False
238243
finally:
239244
cleanup_test_droppings(test_name, ns.verbose)
240245

@@ -268,7 +273,7 @@ def _runtest_inner(ns, test_name, display_failure=True):
268273
try:
269274
clear_caches()
270275

271-
with saved_test_environment(test_name, ns.verbose, ns.quiet, pgo=ns.pgo) as environment:
276+
with save_env(ns, test_name):
272277
refleak = _runtest_inner2(ns, test_name)
273278
except support.ResourceDenied as msg:
274279
if not ns.quiet and not ns.pgo:
@@ -298,7 +303,7 @@ def _runtest_inner(ns, test_name, display_failure=True):
298303

299304
if refleak:
300305
return FAILED
301-
if environment.changed:
306+
if support.environment_altered:
302307
return ENV_CHANGED
303308
return PASSED
304309

Lib/test/libregrtest/save_env.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import asyncio
21
import builtins
32
import locale
4-
import logging
53
import os
6-
import shutil
74
import sys
8-
import sysconfig
95
import threading
10-
import urllib.request
11-
import warnings
126
from test import support
137
from test.support import os_helper
148
from test.libregrtest.utils import print_warning
15-
try:
16-
import _multiprocessing, multiprocessing.process
17-
except ImportError:
18-
multiprocessing = None
9+
10+
11+
class SkipTestEnvironment(Exception):
12+
pass
1913

2014

2115
# Unit tests are supposed to leave the execution environment unchanged
@@ -33,15 +27,13 @@ class saved_test_environment:
3327
#stuff
3428
3529
Unless quiet is True, a warning is printed to stderr if any of
36-
the saved items was changed by the test. The attribute 'changed'
37-
is initially False, but is set to True if a change is detected.
30+
the saved items was changed by the test. The support.environment_altered
31+
attribute is set to True if a change is detected.
3832
3933
If verbose is more than 1, the before and after state of changed
4034
items is also printed.
4135
"""
4236

43-
changed = False
44-
4537
def __init__(self, testname, verbose=0, quiet=False, *, pgo=False):
4638
self.testname = testname
4739
self.verbose = verbose
@@ -73,20 +65,36 @@ def __init__(self, testname, verbose=0, quiet=False, *, pgo=False):
7365
'urllib.requests._url_tempfiles', 'urllib.requests._opener',
7466
)
7567

68+
def get_module(self, name):
69+
# function for restore() methods
70+
return sys.modules[name]
71+
72+
def try_get_module(self, name):
73+
# function for get() methods
74+
try:
75+
return self.get_module(name)
76+
except KeyError:
77+
raise SkipTestEnvironment
78+
7679
def get_urllib_requests__url_tempfiles(self):
77-
return list(urllib.request._url_tempfiles)
80+
urllib_request = self.try_get_module('urllib.request')
81+
return list(urllib_request._url_tempfiles)
7882
def restore_urllib_requests__url_tempfiles(self, tempfiles):
7983
for filename in tempfiles:
8084
os_helper.unlink(filename)
8185

8286
def get_urllib_requests__opener(self):
83-
return urllib.request._opener
87+
urllib_request = self.try_get_module('urllib.request')
88+
return urllib_request._opener
8489
def restore_urllib_requests__opener(self, opener):
85-
urllib.request._opener = opener
90+
urllib_request = self.get_module('urllib.request')
91+
urllib_request._opener = opener
8692

8793
def get_asyncio_events__event_loop_policy(self):
94+
self.try_get_module('asyncio')
8895
return support.maybe_get_event_loop_policy()
8996
def restore_asyncio_events__event_loop_policy(self, policy):
97+
asyncio = self.get_module('asyncio')
9098
asyncio.set_event_loop_policy(policy)
9199

92100
def get_sys_argv(self):
@@ -145,8 +153,10 @@ def restore___import__(self, import_):
145153
builtins.__import__ = import_
146154

147155
def get_warnings_filters(self):
156+
warnings = self.try_get_module('warnings')
148157
return id(warnings.filters), warnings.filters, warnings.filters[:]
149158
def restore_warnings_filters(self, saved_filters):
159+
warnings = self.get_module('warnings')
150160
warnings.filters = saved_filters[1]
151161
warnings.filters[:] = saved_filters[2]
152162

@@ -161,30 +171,36 @@ def restore_asyncore_socket_map(self, saved_map):
161171
asyncore.socket_map.update(saved_map)
162172

163173
def get_shutil_archive_formats(self):
174+
shutil = self.try_get_module('shutil')
164175
# we could call get_archives_formats() but that only returns the
165176
# registry keys; we want to check the values too (the functions that
166177
# are registered)
167178
return shutil._ARCHIVE_FORMATS, shutil._ARCHIVE_FORMATS.copy()
168179
def restore_shutil_archive_formats(self, saved):
180+
shutil = self.get_module('shutil')
169181
shutil._ARCHIVE_FORMATS = saved[0]
170182
shutil._ARCHIVE_FORMATS.clear()
171183
shutil._ARCHIVE_FORMATS.update(saved[1])
172184

173185
def get_shutil_unpack_formats(self):
186+
shutil = self.try_get_module('shutil')
174187
return shutil._UNPACK_FORMATS, shutil._UNPACK_FORMATS.copy()
175188
def restore_shutil_unpack_formats(self, saved):
189+
shutil = self.get_module('shutil')
176190
shutil._UNPACK_FORMATS = saved[0]
177191
shutil._UNPACK_FORMATS.clear()
178192
shutil._UNPACK_FORMATS.update(saved[1])
179193

180194
def get_logging__handlers(self):
195+
logging = self.try_get_module('logging')
181196
# _handlers is a WeakValueDictionary
182197
return id(logging._handlers), logging._handlers, logging._handlers.copy()
183198
def restore_logging__handlers(self, saved_handlers):
184199
# Can't easily revert the logging state
185200
pass
186201

187202
def get_logging__handlerList(self):
203+
logging = self.try_get_module('logging')
188204
# _handlerList is a list of weakrefs to handlers
189205
return id(logging._handlerList), logging._handlerList, logging._handlerList[:]
190206
def restore_logging__handlerList(self, saved_handlerList):
@@ -208,32 +224,34 @@ def restore_threading__dangling(self, saved):
208224

209225
# Same for Process objects
210226
def get_multiprocessing_process__dangling(self):
211-
if not multiprocessing:
212-
return None
227+
multiprocessing_process = self.try_get_module('multiprocessing.process')
213228
# Unjoined process objects can survive after process exits
214-
multiprocessing.process._cleanup()
229+
multiprocessing_process._cleanup()
215230
# This copies the weakrefs without making any strong reference
216-
return multiprocessing.process._dangling.copy()
231+
return multiprocessing_process._dangling.copy()
217232
def restore_multiprocessing_process__dangling(self, saved):
218-
if not multiprocessing:
219-
return
220-
multiprocessing.process._dangling.clear()
221-
multiprocessing.process._dangling.update(saved)
233+
multiprocessing_process = self.get_module('multiprocessing.process')
234+
multiprocessing_process._dangling.clear()
235+
multiprocessing_process._dangling.update(saved)
222236

223237
def get_sysconfig__CONFIG_VARS(self):
224238
# make sure the dict is initialized
239+
sysconfig = self.try_get_module('sysconfig')
225240
sysconfig.get_config_var('prefix')
226241
return (id(sysconfig._CONFIG_VARS), sysconfig._CONFIG_VARS,
227242
dict(sysconfig._CONFIG_VARS))
228243
def restore_sysconfig__CONFIG_VARS(self, saved):
244+
sysconfig = self.get_module('sysconfig')
229245
sysconfig._CONFIG_VARS = saved[1]
230246
sysconfig._CONFIG_VARS.clear()
231247
sysconfig._CONFIG_VARS.update(saved[2])
232248

233249
def get_sysconfig__INSTALL_SCHEMES(self):
250+
sysconfig = self.try_get_module('sysconfig')
234251
return (id(sysconfig._INSTALL_SCHEMES), sysconfig._INSTALL_SCHEMES,
235252
sysconfig._INSTALL_SCHEMES.copy())
236253
def restore_sysconfig__INSTALL_SCHEMES(self, saved):
254+
sysconfig = self.get_module('sysconfig')
237255
sysconfig._INSTALL_SCHEMES = saved[1]
238256
sysconfig._INSTALL_SCHEMES.clear()
239257
sysconfig._INSTALL_SCHEMES.update(saved[2])
@@ -264,8 +282,10 @@ def restore_locale(self, saved):
264282
locale.setlocale(lc, setting)
265283

266284
def get_warnings_showwarning(self):
285+
warnings = self.try_get_module('warnings')
267286
return warnings.showwarning
268287
def restore_warnings_showwarning(self, fxn):
288+
warnings = self.get_module('warnings')
269289
warnings.showwarning = fxn
270290

271291
def resource_info(self):
@@ -276,26 +296,28 @@ def resource_info(self):
276296
yield name, getattr(self, get_name), getattr(self, restore_name)
277297

278298
def __enter__(self):
279-
self.saved_values = dict((name, get()) for name, get, restore
280-
in self.resource_info())
299+
self.saved_values = []
300+
for name, get, restore in self.resource_info():
301+
try:
302+
original = get()
303+
except SkipTestEnvironment:
304+
continue
305+
306+
self.saved_values.append((name, get, restore, original))
281307
return self
282308

283309
def __exit__(self, exc_type, exc_val, exc_tb):
284310
saved_values = self.saved_values
285-
del self.saved_values
311+
self.saved_values = None
286312

287313
# Some resources use weak references
288314
support.gc_collect()
289315

290-
# Read support.environment_altered, set by support helper functions
291-
self.changed |= support.environment_altered
292-
293-
for name, get, restore in self.resource_info():
316+
for name, get, restore, original in saved_values:
294317
current = get()
295-
original = saved_values.pop(name)
296318
# Check for changes to the resource's value
297319
if current != original:
298-
self.changed = True
320+
support.environment_altered = True
299321
restore(original)
300322
if not self.quiet and not self.pgo:
301323
print_warning(f"{name} was modified by {self.testname}")

0 commit comments

Comments
 (0)