mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Migrate threading functions and class to helper
This commit is contained in:
193
Lib/test/support/__init__.py
vendored
193
Lib/test/support/__init__.py
vendored
@@ -104,8 +104,6 @@ __all__ = [
|
||||
'temp_umask', "reap_children",
|
||||
# logging
|
||||
"TestHandler",
|
||||
# threads
|
||||
"threading_setup", "threading_cleanup", "reap_threads", "start_threads",
|
||||
# miscellaneous
|
||||
"check_warnings", "check_no_resource_warning", "check_no_warnings",
|
||||
"EnvironmentVarGuard",
|
||||
@@ -1541,104 +1539,6 @@ print_warning.orig_stderr = sys.stderr
|
||||
# to cleanup threads.
|
||||
environment_altered = False
|
||||
|
||||
# NOTE: we use thread._count() rather than threading.enumerate() (or the
|
||||
# moral equivalent thereof) because a threading.Thread object is still alive
|
||||
# until its __bootstrap() method has returned, even after it has been
|
||||
# unregistered from the threading module.
|
||||
# thread._count(), on the other hand, only gets decremented *after* the
|
||||
# __bootstrap() method has returned, which gives us reliable reference counts
|
||||
# at the end of a test run.
|
||||
|
||||
def threading_setup():
|
||||
return _thread._count(), threading._dangling.copy()
|
||||
|
||||
def threading_cleanup(*original_values):
|
||||
global environment_altered
|
||||
|
||||
_MAX_COUNT = 100
|
||||
|
||||
for count in range(_MAX_COUNT):
|
||||
values = _thread._count(), threading._dangling
|
||||
if values == original_values:
|
||||
break
|
||||
|
||||
if not count: # Display a warning at the first iteration
|
||||
environment_altered = True
|
||||
dangling_threads = values[1]
|
||||
print_warning(f"threading_cleanup() failed to cleanup "
|
||||
f"{values[0] - original_values[0]} threads "
|
||||
f"(count: {values[0]}, "
|
||||
f"dangling: {len(dangling_threads)})")
|
||||
for thread in dangling_threads:
|
||||
print_warning(f"Dangling thread: {thread!r}")
|
||||
|
||||
# Don't hold references to threads
|
||||
dangling_threads = None
|
||||
values = None
|
||||
|
||||
time.sleep(0.01)
|
||||
gc_collect()
|
||||
|
||||
|
||||
def reap_threads(func):
|
||||
"""Use this function when threads are being used. This will
|
||||
ensure that the threads are cleaned up even when the test fails.
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def decorator(*args):
|
||||
key = threading_setup()
|
||||
try:
|
||||
return func(*args)
|
||||
finally:
|
||||
threading_cleanup(*key)
|
||||
return decorator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def wait_threads_exit(timeout=60.0):
|
||||
"""
|
||||
bpo-31234: Context manager to wait until all threads created in the with
|
||||
statement exit.
|
||||
|
||||
Use _thread.count() to check if threads exited. Indirectly, wait until
|
||||
threads exit the internal t_bootstrap() C function of the _thread module.
|
||||
|
||||
threading_setup() and threading_cleanup() are designed to emit a warning
|
||||
if a test leaves running threads in the background. This context manager
|
||||
is designed to cleanup threads started by the _thread.start_new_thread()
|
||||
which doesn't allow to wait for thread exit, whereas thread.Thread has a
|
||||
join() method.
|
||||
"""
|
||||
old_count = _thread._count()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
start_time = time.monotonic()
|
||||
deadline = start_time + timeout
|
||||
while True:
|
||||
count = _thread._count()
|
||||
if count <= old_count:
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
dt = time.monotonic() - start_time
|
||||
msg = (f"wait_threads() failed to cleanup {count - old_count} "
|
||||
f"threads after {dt:.1f} seconds "
|
||||
f"(count: {count}, old count: {old_count})")
|
||||
raise AssertionError(msg)
|
||||
time.sleep(0.010)
|
||||
gc_collect()
|
||||
|
||||
|
||||
def join_thread(thread, timeout=30.0):
|
||||
"""Join a thread. Raise an AssertionError if the thread is still alive
|
||||
after timeout seconds.
|
||||
"""
|
||||
thread.join(timeout)
|
||||
if thread.is_alive():
|
||||
msg = f"failed to join the thread in {timeout:.1f} seconds"
|
||||
raise AssertionError(msg)
|
||||
|
||||
|
||||
def reap_children():
|
||||
"""Use this function at the end of test_main() whenever sub-processes
|
||||
are started. This will help ensure that no extra children (zombies)
|
||||
@@ -1667,42 +1567,6 @@ def reap_children():
|
||||
environment_altered = True
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def start_threads(threads, unlock=None):
|
||||
threads = list(threads)
|
||||
started = []
|
||||
try:
|
||||
try:
|
||||
for t in threads:
|
||||
t.start()
|
||||
started.append(t)
|
||||
except:
|
||||
if verbose:
|
||||
print("Can't start %d threads, only %d threads started" %
|
||||
(len(threads), len(started)))
|
||||
raise
|
||||
yield
|
||||
finally:
|
||||
try:
|
||||
if unlock:
|
||||
unlock()
|
||||
endtime = starttime = time.monotonic()
|
||||
for timeout in range(1, 16):
|
||||
endtime += 60
|
||||
for t in started:
|
||||
t.join(max(endtime - time.monotonic(), 0.01))
|
||||
started = [t for t in started if t.is_alive()]
|
||||
if not started:
|
||||
break
|
||||
if verbose:
|
||||
print('Unable to join %d threads during a period of '
|
||||
'%d minutes' % (len(started), timeout))
|
||||
finally:
|
||||
started = [t for t in started if t.is_alive()]
|
||||
if started:
|
||||
faulthandler.dump_traceback(sys.stdout)
|
||||
raise AssertionError('Unable to join %d threads' % len(started))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def swap_attr(obj, attr, new_val):
|
||||
"""Temporary swap out an attribute with a new object.
|
||||
@@ -2507,63 +2371,6 @@ class catch_unraisable_exception:
|
||||
del self.unraisable
|
||||
|
||||
|
||||
class catch_threading_exception:
|
||||
"""
|
||||
Context manager catching threading.Thread exception using
|
||||
threading.excepthook.
|
||||
|
||||
Attributes set when an exception is catched:
|
||||
|
||||
* exc_type
|
||||
* exc_value
|
||||
* exc_traceback
|
||||
* thread
|
||||
|
||||
See threading.excepthook() documentation for these attributes.
|
||||
|
||||
These attributes are deleted at the context manager exit.
|
||||
|
||||
Usage:
|
||||
|
||||
with support.catch_threading_exception() as cm:
|
||||
# code spawning a thread which raises an exception
|
||||
...
|
||||
|
||||
# check the thread exception, use cm attributes:
|
||||
# exc_type, exc_value, exc_traceback, thread
|
||||
...
|
||||
|
||||
# exc_type, exc_value, exc_traceback, thread attributes of cm no longer
|
||||
# exists at this point
|
||||
# (to avoid reference cycles)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.exc_type = None
|
||||
self.exc_value = None
|
||||
self.exc_traceback = None
|
||||
self.thread = None
|
||||
self._old_hook = None
|
||||
|
||||
def _hook(self, args):
|
||||
self.exc_type = args.exc_type
|
||||
self.exc_value = args.exc_value
|
||||
self.exc_traceback = args.exc_traceback
|
||||
self.thread = args.thread
|
||||
|
||||
def __enter__(self):
|
||||
self._old_hook = threading.excepthook
|
||||
threading.excepthook = self._hook
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
threading.excepthook = self._old_hook
|
||||
del self.exc_type
|
||||
del self.exc_value
|
||||
del self.exc_traceback
|
||||
del self.thread
|
||||
|
||||
|
||||
def wait_process(pid, *, exitcode, timeout=None):
|
||||
"""
|
||||
Wait until process pid completes and check that the process exit code is
|
||||
|
||||
Reference in New Issue
Block a user