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.
This commit is contained in:
Victor Stinner 2021-03-22 23:52:13 +01:00 committed by GitHub
parent 96eeff5162
commit 532e063fc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 42 deletions

View File

@ -211,6 +211,10 @@ def _test_module(the_module):
support.run_unittest(tests)
def save_env(ns, test_name):
return saved_test_environment(test_name, ns.verbose, ns.quiet, pgo=ns.pgo)
def _runtest_inner2(ns, test_name):
# Load the test function, run the test function, handle huntrleaks
# and findleaks to detect leaks
@ -229,12 +233,13 @@ def _runtest_inner2(ns, test_name):
test_runner = functools.partial(_test_module, the_module)
try:
if ns.huntrleaks:
# Return True if the test leaked references
refleak = dash_R(ns, test_name, test_runner)
else:
test_runner()
refleak = False
with save_env(ns, test_name):
if ns.huntrleaks:
# Return True if the test leaked references
refleak = dash_R(ns, test_name, test_runner)
else:
test_runner()
refleak = False
finally:
cleanup_test_droppings(test_name, ns.verbose)
@ -268,7 +273,7 @@ def _runtest_inner(ns, test_name, display_failure=True):
try:
clear_caches()
with saved_test_environment(test_name, ns.verbose, ns.quiet, pgo=ns.pgo) as environment:
with save_env(ns, test_name):
refleak = _runtest_inner2(ns, test_name)
except support.ResourceDenied as msg:
if not ns.quiet and not ns.pgo:
@ -298,7 +303,7 @@ def _runtest_inner(ns, test_name, display_failure=True):
if refleak:
return FAILED
if environment.changed:
if support.environment_altered:
return ENV_CHANGED
return PASSED

View File

@ -1,21 +1,15 @@
import asyncio
import builtins
import locale
import logging
import os
import shutil
import sys
import sysconfig
import threading
import urllib.request
import warnings
from test import support
from test.support import os_helper
from test.libregrtest.utils import print_warning
try:
import _multiprocessing, multiprocessing.process
except ImportError:
multiprocessing = None
class SkipTestEnvironment(Exception):
pass
# Unit tests are supposed to leave the execution environment unchanged
@ -33,15 +27,13 @@ class saved_test_environment:
#stuff
Unless quiet is True, a warning is printed to stderr if any of
the saved items was changed by the test. The attribute 'changed'
is initially False, but is set to True if a change is detected.
the saved items was changed by the test. The support.environment_altered
attribute is set to True if a change is detected.
If verbose is more than 1, the before and after state of changed
items is also printed.
"""
changed = False
def __init__(self, testname, verbose=0, quiet=False, *, pgo=False):
self.testname = testname
self.verbose = verbose
@ -73,20 +65,36 @@ class saved_test_environment:
'urllib.requests._url_tempfiles', 'urllib.requests._opener',
)
def get_module(self, name):
# function for restore() methods
return sys.modules[name]
def try_get_module(self, name):
# function for get() methods
try:
return self.get_module(name)
except KeyError:
raise SkipTestEnvironment
def get_urllib_requests__url_tempfiles(self):
return list(urllib.request._url_tempfiles)
urllib_request = self.try_get_module('urllib.request')
return list(urllib_request._url_tempfiles)
def restore_urllib_requests__url_tempfiles(self, tempfiles):
for filename in tempfiles:
os_helper.unlink(filename)
def get_urllib_requests__opener(self):
return urllib.request._opener
urllib_request = self.try_get_module('urllib.request')
return urllib_request._opener
def restore_urllib_requests__opener(self, opener):
urllib.request._opener = opener
urllib_request = self.get_module('urllib.request')
urllib_request._opener = opener
def get_asyncio_events__event_loop_policy(self):
self.try_get_module('asyncio')
return support.maybe_get_event_loop_policy()
def restore_asyncio_events__event_loop_policy(self, policy):
asyncio = self.get_module('asyncio')
asyncio.set_event_loop_policy(policy)
def get_sys_argv(self):
@ -145,8 +153,10 @@ class saved_test_environment:
builtins.__import__ = import_
def get_warnings_filters(self):
warnings = self.try_get_module('warnings')
return id(warnings.filters), warnings.filters, warnings.filters[:]
def restore_warnings_filters(self, saved_filters):
warnings = self.get_module('warnings')
warnings.filters = saved_filters[1]
warnings.filters[:] = saved_filters[2]
@ -161,23 +171,28 @@ class saved_test_environment:
asyncore.socket_map.update(saved_map)
def get_shutil_archive_formats(self):
shutil = self.try_get_module('shutil')
# we could call get_archives_formats() but that only returns the
# registry keys; we want to check the values too (the functions that
# are registered)
return shutil._ARCHIVE_FORMATS, shutil._ARCHIVE_FORMATS.copy()
def restore_shutil_archive_formats(self, saved):
shutil = self.get_module('shutil')
shutil._ARCHIVE_FORMATS = saved[0]
shutil._ARCHIVE_FORMATS.clear()
shutil._ARCHIVE_FORMATS.update(saved[1])
def get_shutil_unpack_formats(self):
shutil = self.try_get_module('shutil')
return shutil._UNPACK_FORMATS, shutil._UNPACK_FORMATS.copy()
def restore_shutil_unpack_formats(self, saved):
shutil = self.get_module('shutil')
shutil._UNPACK_FORMATS = saved[0]
shutil._UNPACK_FORMATS.clear()
shutil._UNPACK_FORMATS.update(saved[1])
def get_logging__handlers(self):
logging = self.try_get_module('logging')
# _handlers is a WeakValueDictionary
return id(logging._handlers), logging._handlers, logging._handlers.copy()
def restore_logging__handlers(self, saved_handlers):
@ -185,6 +200,7 @@ class saved_test_environment:
pass
def get_logging__handlerList(self):
logging = self.try_get_module('logging')
# _handlerList is a list of weakrefs to handlers
return id(logging._handlerList), logging._handlerList, logging._handlerList[:]
def restore_logging__handlerList(self, saved_handlerList):
@ -208,32 +224,34 @@ class saved_test_environment:
# Same for Process objects
def get_multiprocessing_process__dangling(self):
if not multiprocessing:
return None
multiprocessing_process = self.try_get_module('multiprocessing.process')
# Unjoined process objects can survive after process exits
multiprocessing.process._cleanup()
multiprocessing_process._cleanup()
# This copies the weakrefs without making any strong reference
return multiprocessing.process._dangling.copy()
return multiprocessing_process._dangling.copy()
def restore_multiprocessing_process__dangling(self, saved):
if not multiprocessing:
return
multiprocessing.process._dangling.clear()
multiprocessing.process._dangling.update(saved)
multiprocessing_process = self.get_module('multiprocessing.process')
multiprocessing_process._dangling.clear()
multiprocessing_process._dangling.update(saved)
def get_sysconfig__CONFIG_VARS(self):
# make sure the dict is initialized
sysconfig = self.try_get_module('sysconfig')
sysconfig.get_config_var('prefix')
return (id(sysconfig._CONFIG_VARS), sysconfig._CONFIG_VARS,
dict(sysconfig._CONFIG_VARS))
def restore_sysconfig__CONFIG_VARS(self, saved):
sysconfig = self.get_module('sysconfig')
sysconfig._CONFIG_VARS = saved[1]
sysconfig._CONFIG_VARS.clear()
sysconfig._CONFIG_VARS.update(saved[2])
def get_sysconfig__INSTALL_SCHEMES(self):
sysconfig = self.try_get_module('sysconfig')
return (id(sysconfig._INSTALL_SCHEMES), sysconfig._INSTALL_SCHEMES,
sysconfig._INSTALL_SCHEMES.copy())
def restore_sysconfig__INSTALL_SCHEMES(self, saved):
sysconfig = self.get_module('sysconfig')
sysconfig._INSTALL_SCHEMES = saved[1]
sysconfig._INSTALL_SCHEMES.clear()
sysconfig._INSTALL_SCHEMES.update(saved[2])
@ -264,8 +282,10 @@ class saved_test_environment:
locale.setlocale(lc, setting)
def get_warnings_showwarning(self):
warnings = self.try_get_module('warnings')
return warnings.showwarning
def restore_warnings_showwarning(self, fxn):
warnings = self.get_module('warnings')
warnings.showwarning = fxn
def resource_info(self):
@ -276,26 +296,28 @@ class saved_test_environment:
yield name, getattr(self, get_name), getattr(self, restore_name)
def __enter__(self):
self.saved_values = dict((name, get()) for name, get, restore
in self.resource_info())
self.saved_values = []
for name, get, restore in self.resource_info():
try:
original = get()
except SkipTestEnvironment:
continue
self.saved_values.append((name, get, restore, original))
return self
def __exit__(self, exc_type, exc_val, exc_tb):
saved_values = self.saved_values
del self.saved_values
self.saved_values = None
# Some resources use weak references
support.gc_collect()
# Read support.environment_altered, set by support helper functions
self.changed |= support.environment_altered
for name, get, restore in self.resource_info():
for name, get, restore, original in saved_values:
current = get()
original = saved_values.pop(name)
# Check for changes to the resource's value
if current != original:
self.changed = True
support.environment_altered = True
restore(original)
if not self.quiet and not self.pgo:
print_warning(f"{name} was modified by {self.testname}")