From 00fe666c6387a2a669eb3956108f315102470c43 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:36:00 +0000 Subject: [PATCH 01/21] gh-141044: Fix ASan leak with small threading.stack_size() AddressSanitizer instrumentation consumes extra C stack, so the old minimum left no working space above the soft recursion limit. threading.Thread bootstrap then leaked thread objects. Require 6 stack margins under ASan, matching TSan. Cover the unjoined original repro and confirm the new minimum does not leak. Move the NEWS blurb to Library, matching gh-143191. --- Include/internal/pycore_pythonrun.h | 7 +- Lib/test/test_thread.py | 6 + Lib/test/test_threading.py | 134 ++++++++++++++++++ ...-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst | 7 + 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index d333eb2ccf7c413..234337416dd17eb 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -62,7 +62,12 @@ extern PyObject* _PyRun_SimpleString( # define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2) #endif -#ifdef _Py_THREAD_SANITIZER +#if defined(_Py_THREAD_SANITIZER) || defined(_Py_ADDRESS_SANITIZER) +/* Sanitizer builds need more than the default 3 margins: + * - TSan: tstate_set_stack() only uses half the stack. + * - ASan (gh-141044): instrumentation consumes extra C stack, so 3 + * margins leave threading.Thread bootstrap with no working space + * above the soft recursion limit and leak thread objects. */ # define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6) #else # define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3) diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index ac924728febc991..07904dde764453b 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -84,6 +84,12 @@ def test_stack_size(self): # size must be positive thread.stack_size(-4096) + if support.check_sanitizer(address=True, function=False): + # gh-141044: 127 KiB used to be accepted but leaked under ASan + with self.assertRaises(ValueError): + thread.stack_size(127 * 1024) + self.assertEqual(thread.stack_size(), 0) + @unittest.skipIf(os.name not in ("nt", "posix"), 'test meant for nt and posix') def test_nt_and_posix_stack_size(self): try: diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 96b43936be92cda..563098d8f1da8c1 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -269,6 +269,140 @@ def test_various_ops_large_stack(self): self.test_various_ops() threading.stack_size(0) + def test_stack_size_no_leak(self): + # gh-141044: a custom thread stack size used to leak Thread objects + # when the size passed _thread.stack_size() but was too small for + # threading.Thread bootstrap under AddressSanitizer. The reported + # repro used 127 KiB and did not join the thread. + try: + threading.stack_size(262144) + except _thread.error: + self.skipTest( + 'platform does not support changing thread stack size') + threading.stack_size(0) + + def run_script(script): + _, _, err = assert_python_ok( + "-c", textwrap.dedent(script), + ASAN_OPTIONS="detect_leaks=1:halt_on_error=1") + err_s = err.decode("utf-8", "replace") + self.assertNotIn("LeakSanitizer", err_s, err_s) + + min_stack_helper = """ + import os + import threading + import _thread + + def min_stack_size(): + page = os.sysconf("SC_PAGESIZE") if hasattr(os, "sysconf") else 4096 + size = page + while size <= 4 * 1024 * 1024: + try: + threading.stack_size(size) + except ValueError: + size += page + continue + except _thread.error: + return None + return size + return None + """ + + # Original reproducer: start, do not join. 127 KiB is rejected on + # ASan builds; if a build still accepts it, the thread must not leak. + run_script(""" + import threading + try: + threading.stack_size(127 * 1024) + except ValueError: + raise SystemExit(0) + def worker(): + pass + t = threading.Thread(target=worker, name="worker-thread") + t.start() + threading.stack_size(0) + """) + + # Smallest accepted size, unjoined. Wait until the worker finishes + # without join(); process shutdown also joins. LSan plus (on debug + # builds) gettotalrefcount() must stay clean at this new minimum. + run_script(min_stack_helper + """ + import gc + import sys + import time + + def worker(): + pass + size = min_stack_size() + if size is None: + raise SystemExit(0) + + def wait_unjoined(threads, timeout=30): + deadline = time.monotonic() + timeout + for t in threads: + while t.is_alive(): + if time.monotonic() > deadline: + raise SystemExit("unjoined worker did not finish") + time.sleep(0.001) + + if hasattr(sys, "gettotalrefcount"): + gc.collect() + gc.collect() + start = sys.gettotalrefcount() + threads = [] + for _ in range(8): + t = threading.Thread(target=worker) + t.start() + threads.append(t) + wait_unjoined(threads) + del threads + threading.stack_size(0) + gc.collect() + gc.collect() + delta = sys.gettotalrefcount() - start + if delta > 50: + raise SystemExit(f"refcount leak: {delta}") + else: + t = threading.Thread(target=worker, name="min-stack-worker") + t.start() + threading.stack_size(0) + """) + + # Joined threads at the minimum must not leak references. + run_script(min_stack_helper + """ + import gc + import sys + + def worker(): + pass + size = min_stack_size() + if size is None: + raise SystemExit(0) + for _ in range(3): + t = threading.Thread(target=worker) + t.start() + t.join() + if hasattr(sys, "gettotalrefcount"): + gc.collect() + gc.collect() + start = sys.gettotalrefcount() + for _ in range(8): + t = threading.Thread(target=worker) + t.start() + t.join() + threading.stack_size(0) + gc.collect() + gc.collect() + delta = sys.gettotalrefcount() - start + if delta > 50: + raise SystemExit(f"refcount leak: {delta}") + else: + t = threading.Thread(target=worker) + t.start() + t.join() + threading.stack_size(0) + """) + def test_foreign_thread(self): # Check that a "foreign" thread can use the threading module. dummy_thread = None diff --git a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst new file mode 100644 index 000000000000000..95a743d2089cd68 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst @@ -0,0 +1,7 @@ +Fix a reference leak when creating a :class:`threading.Thread` after +setting a small custom stack size with :func:`threading.stack_size` in +AddressSanitizer builds. ASan instrumentation needs extra C stack, so +:func:`_thread.stack_size` now requires the same 6 stack margins used +with ThreadSanitizer. Sizes that previously appeared to work under ASan +(for example 127 KiB) now raise :exc:`ValueError`. Non-ASan builds are +unchanged. Patch by Kailash Nelson. From 3aef3107dabcdade64a9567dd6b707598a90632f Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:15:36 -0600 Subject: [PATCH 02/21] gh-141044: Require 6 stack margins on debug builds too Py_DEBUG (and UBSan) share the larger stack margin with ASan/TSan, but still used a 3-margin minimum. threading.Thread bootstrap then leaked references at that floor; the new refcount test failed on Ubuntu/macOS CI. Align the minimum with ASan/TSan. Release builds stay at 3 margins. --- Include/internal/pycore_pythonrun.h | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index 234337416dd17eb..c6143dba3d61d5b 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -62,12 +62,18 @@ extern PyObject* _PyRun_SimpleString( # define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2) #endif -#if defined(_Py_THREAD_SANITIZER) || defined(_Py_ADDRESS_SANITIZER) -/* Sanitizer builds need more than the default 3 margins: +#if (defined(Py_DEBUG) \ + || defined(_Py_ADDRESS_SANITIZER) \ + || defined(_Py_THREAD_SANITIZER) \ + || defined(_Py_UNDEFINED_BEHAVIOR_SANITIZER)) +/* Debug and sanitizer builds need more than the default 3 margins: * - TSan: tstate_set_stack() only uses half the stack. - * - ASan (gh-141044): instrumentation consumes extra C stack, so 3 - * margins leave threading.Thread bootstrap with no working space - * above the soft recursion limit and leak thread objects. */ + * - ASan (gh-141044): instrumentation consumes extra C stack. + * - Py_DEBUG / UBSan: larger C frames leave threading.Thread bootstrap + * with no working space above the soft recursion limit at 3 margins, + * leaking thread objects (same failure mode as ASan). + * Require 6 margins, matching the builds that already use a larger + * _PyOS_LOG2_STACK_MARGIN above. Release builds stay at 3. */ # define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6) #else # define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3) @@ -78,4 +84,3 @@ extern PyObject* _PyRun_SimpleString( } #endif #endif // !Py_INTERNAL_PYTHONRUN_H - From 09dc0cb75c6553bf6c3cca6b4a81be733116839e Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:15:55 -0600 Subject: [PATCH 03/21] gh-141044: Update NEWS for debug/sanitizer 6-margin floor --- .../2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst index 95a743d2089cd68..9eb845fdd6980aa 100644 --- a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst +++ b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst @@ -1,7 +1,7 @@ Fix a reference leak when creating a :class:`threading.Thread` after setting a small custom stack size with :func:`threading.stack_size` in -AddressSanitizer builds. ASan instrumentation needs extra C stack, so -:func:`_thread.stack_size` now requires the same 6 stack margins used -with ThreadSanitizer. Sizes that previously appeared to work under ASan -(for example 127 KiB) now raise :exc:`ValueError`. Non-ASan builds are -unchanged. Patch by Kailash Nelson. +debug and sanitizer builds. Those builds need extra C stack, so +:func:`_thread.stack_size` now requires 6 stack margins (as ThreadSanitizer +already did). Sizes that previously appeared to work (for example 127 KiB +on 64-bit debug/ASan builds) now raise :exc:`ValueError`. Release builds +are unchanged. Patch by Kailash Nelson. From 04ebedb92dd23d4b170785008abac888d3bd1c70 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:16:23 -0600 Subject: [PATCH 04/21] gh-141044: Portable 127 KiB stack_size rejection check --- Lib/test/test_thread.py | 470 +--------------------------------------- 1 file changed, 1 insertion(+), 469 deletions(-) diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index 07904dde764453b..311c8dd0658759f 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -1,469 +1 @@ -import os -import unittest -import random -from test import support -from test.support import threading_helper -import _thread as thread -import time -import warnings -import weakref - -from test import lock_tests - -threading_helper.requires_working_threading(module=True) - -NUMTASKS = 10 -NUMTRIPS = 3 - -_print_mutex = thread.allocate_lock() - -def verbose_print(arg): - """Helper function for printing out debugging output.""" - if support.verbose: - with _print_mutex: - print(arg) - - -class BasicThreadTest(unittest.TestCase): - - def setUp(self): - self.done_mutex = thread.allocate_lock() - self.done_mutex.acquire() - self.running_mutex = thread.allocate_lock() - self.random_mutex = thread.allocate_lock() - self.created = 0 - self.running = 0 - self.next_ident = 0 - - key = threading_helper.threading_setup() - self.addCleanup(threading_helper.threading_cleanup, *key) - - -class ThreadRunningTests(BasicThreadTest): - - def newtask(self): - with self.running_mutex: - self.next_ident += 1 - verbose_print("creating task %s" % self.next_ident) - thread.start_new_thread(self.task, (self.next_ident,)) - self.created += 1 - self.running += 1 - - def task(self, ident): - with self.random_mutex: - delay = random.random() / 10000.0 - verbose_print("task %s will run for %sus" % (ident, round(delay*1e6))) - time.sleep(delay) - verbose_print("task %s done" % ident) - with self.running_mutex: - self.running -= 1 - if self.created == NUMTASKS and self.running == 0: - self.done_mutex.release() - - def test_starting_threads(self): - with threading_helper.wait_threads_exit(): - # Basic test for thread creation. - for i in range(NUMTASKS): - self.newtask() - verbose_print("waiting for tasks to complete...") - self.done_mutex.acquire() - verbose_print("all tasks done") - - def test_stack_size(self): - # Various stack size tests. - self.assertEqual(thread.stack_size(), 0, "initial stack size is not 0") - - thread.stack_size(0) - self.assertEqual(thread.stack_size(), 0, "stack_size not reset to default") - - with self.assertRaises(ValueError): - # 123 bytes is too small - thread.stack_size(123) - - with self.assertRaises(ValueError): - # size must be positive - thread.stack_size(-4096) - - if support.check_sanitizer(address=True, function=False): - # gh-141044: 127 KiB used to be accepted but leaked under ASan - with self.assertRaises(ValueError): - thread.stack_size(127 * 1024) - self.assertEqual(thread.stack_size(), 0) - - @unittest.skipIf(os.name not in ("nt", "posix"), 'test meant for nt and posix') - def test_nt_and_posix_stack_size(self): - try: - thread.stack_size(4096) - except ValueError: - verbose_print("caught expected ValueError setting " - "stack_size(4096)") - except thread.error: - self.skipTest("platform does not support changing thread stack " - "size") - - fail_msg = "stack_size(%d) failed - should succeed" - for tss in (262144, 0x100000, 0): - thread.stack_size(tss) - self.assertEqual(thread.stack_size(), tss, fail_msg % tss) - verbose_print("successfully set stack_size(%d)" % tss) - - for tss in (262144, 0x100000): - verbose_print("trying stack_size = (%d)" % tss) - self.next_ident = 0 - self.created = 0 - with threading_helper.wait_threads_exit(): - for i in range(NUMTASKS): - self.newtask() - - verbose_print("waiting for all tasks to complete") - self.done_mutex.acquire() - verbose_print("all tasks done") - - thread.stack_size(0) - - def test__count(self): - # Test the _count() function. - orig = thread._count() - mut = thread.allocate_lock() - mut.acquire() - started = [] - - def task(): - started.append(None) - mut.acquire() - mut.release() - - with threading_helper.wait_threads_exit(): - thread.start_new_thread(task, ()) - for _ in support.sleeping_retry(support.LONG_TIMEOUT): - if started: - break - self.assertEqual(thread._count(), orig + 1) - - # Allow the task to finish. - mut.release() - - # The only reliable way to be sure that the thread ended from the - # interpreter's point of view is to wait for the function object to - # be destroyed. - done = [] - wr = weakref.ref(task, lambda _: done.append(None)) - del task - - for _ in support.sleeping_retry(support.LONG_TIMEOUT): - if done: - break - support.gc_collect() # For PyPy or other GCs. - self.assertEqual(thread._count(), orig) - - def test_unraisable_exception(self): - def task(): - started.release() - raise ValueError("task failed") - - started = thread.allocate_lock() - with support.catch_unraisable_exception() as cm: - with threading_helper.wait_threads_exit(): - started.acquire() - thread.start_new_thread(task, ()) - started.acquire() - - self.assertEqual(str(cm.unraisable.exc_value), "task failed") - self.assertIsNone(cm.unraisable.object) - self.assertEqual(cm.unraisable.err_msg, - f"Exception ignored in thread started by {task!r}") - self.assertIsNotNone(cm.unraisable.exc_traceback) - - def test_join_thread(self): - finished = [] - - def task(): - time.sleep(0.05) - finished.append(thread.get_ident()) - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - handle.join() - self.assertEqual(len(finished), 1) - self.assertEqual(handle.ident, finished[0]) - - def test_join_thread_already_exited(self): - def task(): - pass - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - time.sleep(0.05) - handle.join() - - def test_join_several_times(self): - def task(): - pass - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - handle.join() - # Subsequent join() calls should succeed - handle.join() - - def test_joinable_not_joined(self): - handle_destroyed = thread.allocate_lock() - handle_destroyed.acquire() - - def task(): - handle_destroyed.acquire() - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - del handle - handle_destroyed.release() - - def test_join_from_self(self): - errors = [] - handles = [] - start_joinable_thread_returned = thread.allocate_lock() - start_joinable_thread_returned.acquire() - task_tried_to_join = thread.allocate_lock() - task_tried_to_join.acquire() - - def task(): - start_joinable_thread_returned.acquire() - try: - handles[0].join() - except Exception as e: - errors.append(e) - finally: - task_tried_to_join.release() - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - handles.append(handle) - start_joinable_thread_returned.release() - # Can still join after joining failed in other thread - task_tried_to_join.acquire() - handle.join() - - assert len(errors) == 1 - with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): - raise errors[0] - - def test_join_then_self_join(self): - # make sure we can't deadlock in the following scenario with - # threads t0 and t1 (see comment in `ThreadHandle_join()` for more - # details): - # - # - t0 joins t1 - # - t1 self joins - def make_lock(): - lock = thread.allocate_lock() - lock.acquire() - return lock - - error = None - self_joiner_handle = None - self_joiner_started = make_lock() - self_joiner_barrier = make_lock() - def self_joiner(): - nonlocal error - - self_joiner_started.release() - self_joiner_barrier.acquire() - - try: - self_joiner_handle.join() - except Exception as e: - error = e - - joiner_started = make_lock() - def joiner(): - joiner_started.release() - self_joiner_handle.join() - - with threading_helper.wait_threads_exit(): - self_joiner_handle = thread.start_joinable_thread(self_joiner) - # Wait for the self-joining thread to start - self_joiner_started.acquire() - - # Start the thread that joins the self-joiner - joiner_handle = thread.start_joinable_thread(joiner) - - # Wait for the joiner to start - joiner_started.acquire() - - # Not great, but I don't think there's a deterministic way to make - # sure that the self-joining thread has been joined. - time.sleep(0.1) - - # Unblock the self-joiner - self_joiner_barrier.release() - - self_joiner_handle.join() - joiner_handle.join() - - with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): - raise error - - def test_join_with_timeout(self): - lock = thread.allocate_lock() - lock.acquire() - - def thr(): - lock.acquire() - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(thr) - handle.join(0.1) - self.assertFalse(handle.is_done()) - lock.release() - handle.join() - self.assertTrue(handle.is_done()) - - def test_join_unstarted(self): - handle = thread._ThreadHandle() - with self.assertRaisesRegex(RuntimeError, "thread not started"): - handle.join() - - def test_set_done_unstarted(self): - handle = thread._ThreadHandle() - with self.assertRaisesRegex(RuntimeError, "thread not started"): - handle._set_done() - - def test_start_duplicate_handle(self): - lock = thread.allocate_lock() - lock.acquire() - - def func(): - lock.acquire() - - handle = thread._ThreadHandle() - with threading_helper.wait_threads_exit(): - thread.start_joinable_thread(func, handle=handle) - with self.assertRaisesRegex(RuntimeError, "thread already started"): - thread.start_joinable_thread(func, handle=handle) - lock.release() - handle.join() - - def test_start_with_none_handle(self): - def func(): - pass - - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(func, handle=None) - handle.join() - - -class Barrier: - def __init__(self, num_threads): - self.num_threads = num_threads - self.waiting = 0 - self.checkin_mutex = thread.allocate_lock() - self.checkout_mutex = thread.allocate_lock() - self.checkout_mutex.acquire() - - def enter(self): - self.checkin_mutex.acquire() - self.waiting = self.waiting + 1 - if self.waiting == self.num_threads: - self.waiting = self.num_threads - 1 - self.checkout_mutex.release() - return - self.checkin_mutex.release() - - self.checkout_mutex.acquire() - self.waiting = self.waiting - 1 - if self.waiting == 0: - self.checkin_mutex.release() - return - self.checkout_mutex.release() - - -class BarrierTest(BasicThreadTest): - - def test_barrier(self): - with threading_helper.wait_threads_exit(): - self.bar = Barrier(NUMTASKS) - self.running = NUMTASKS - for i in range(NUMTASKS): - thread.start_new_thread(self.task2, (i,)) - verbose_print("waiting for tasks to end") - self.done_mutex.acquire() - verbose_print("tasks done") - - def task2(self, ident): - for i in range(NUMTRIPS): - if ident == 0: - # give it a good chance to enter the next - # barrier before the others are all out - # of the current one - delay = 0 - else: - with self.random_mutex: - delay = random.random() / 10000.0 - verbose_print("task %s will run for %sus" % - (ident, round(delay * 1e6))) - time.sleep(delay) - verbose_print("task %s entering %s" % (ident, i)) - self.bar.enter() - verbose_print("task %s leaving barrier" % ident) - with self.running_mutex: - self.running -= 1 - # Must release mutex before releasing done, else the main thread can - # exit and set mutex to None as part of global teardown; then - # mutex.release() raises AttributeError. - finished = self.running == 0 - if finished: - self.done_mutex.release() - -class LockTests(lock_tests.LockTests): - locktype = thread.allocate_lock - - -class TestForkInThread(unittest.TestCase): - def setUp(self): - self.read_fd, self.write_fd = os.pipe() - - @support.requires_fork() - @threading_helper.reap_threads - def test_forkinthread(self): - pid = None - - def fork_thread(read_fd, write_fd): - nonlocal pid - - # Ignore the warning about fork with threads. - with warnings.catch_warnings(category=DeprecationWarning, - action="ignore"): - # fork in a thread (DANGER, undefined per POSIX) - if (pid := os.fork()): - # parent process - return - - # child process - try: - os.close(read_fd) - os.write(write_fd, b"OK") - finally: - os._exit(0) - - with threading_helper.wait_threads_exit(): - thread.start_new_thread(fork_thread, (self.read_fd, self.write_fd)) - self.assertEqual(os.read(self.read_fd, 2), b"OK") - os.close(self.write_fd) - - self.assertIsNotNone(pid) - support.wait_process(pid, exitcode=0) - - def tearDown(self): - try: - os.close(self.read_fd) - except OSError: - pass - - try: - os.close(self.write_fd) - except OSError: - pass - - -if __name__ == "__main__": - unittest.main() +PLACEHOLDER \ No newline at end of file From bfbf64c4469ce6ea16abdef2911143d5b1ed1ae9 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:19:34 -0600 Subject: [PATCH 05/21] gh-141044: Restore test_thread.py (undo accidental PLACEHOLDER) --- Lib/test/test_thread.py | 470 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 469 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index 311c8dd0658759f..07904dde764453b 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -1 +1,469 @@ -PLACEHOLDER \ No newline at end of file +import os +import unittest +import random +from test import support +from test.support import threading_helper +import _thread as thread +import time +import warnings +import weakref + +from test import lock_tests + +threading_helper.requires_working_threading(module=True) + +NUMTASKS = 10 +NUMTRIPS = 3 + +_print_mutex = thread.allocate_lock() + +def verbose_print(arg): + """Helper function for printing out debugging output.""" + if support.verbose: + with _print_mutex: + print(arg) + + +class BasicThreadTest(unittest.TestCase): + + def setUp(self): + self.done_mutex = thread.allocate_lock() + self.done_mutex.acquire() + self.running_mutex = thread.allocate_lock() + self.random_mutex = thread.allocate_lock() + self.created = 0 + self.running = 0 + self.next_ident = 0 + + key = threading_helper.threading_setup() + self.addCleanup(threading_helper.threading_cleanup, *key) + + +class ThreadRunningTests(BasicThreadTest): + + def newtask(self): + with self.running_mutex: + self.next_ident += 1 + verbose_print("creating task %s" % self.next_ident) + thread.start_new_thread(self.task, (self.next_ident,)) + self.created += 1 + self.running += 1 + + def task(self, ident): + with self.random_mutex: + delay = random.random() / 10000.0 + verbose_print("task %s will run for %sus" % (ident, round(delay*1e6))) + time.sleep(delay) + verbose_print("task %s done" % ident) + with self.running_mutex: + self.running -= 1 + if self.created == NUMTASKS and self.running == 0: + self.done_mutex.release() + + def test_starting_threads(self): + with threading_helper.wait_threads_exit(): + # Basic test for thread creation. + for i in range(NUMTASKS): + self.newtask() + verbose_print("waiting for tasks to complete...") + self.done_mutex.acquire() + verbose_print("all tasks done") + + def test_stack_size(self): + # Various stack size tests. + self.assertEqual(thread.stack_size(), 0, "initial stack size is not 0") + + thread.stack_size(0) + self.assertEqual(thread.stack_size(), 0, "stack_size not reset to default") + + with self.assertRaises(ValueError): + # 123 bytes is too small + thread.stack_size(123) + + with self.assertRaises(ValueError): + # size must be positive + thread.stack_size(-4096) + + if support.check_sanitizer(address=True, function=False): + # gh-141044: 127 KiB used to be accepted but leaked under ASan + with self.assertRaises(ValueError): + thread.stack_size(127 * 1024) + self.assertEqual(thread.stack_size(), 0) + + @unittest.skipIf(os.name not in ("nt", "posix"), 'test meant for nt and posix') + def test_nt_and_posix_stack_size(self): + try: + thread.stack_size(4096) + except ValueError: + verbose_print("caught expected ValueError setting " + "stack_size(4096)") + except thread.error: + self.skipTest("platform does not support changing thread stack " + "size") + + fail_msg = "stack_size(%d) failed - should succeed" + for tss in (262144, 0x100000, 0): + thread.stack_size(tss) + self.assertEqual(thread.stack_size(), tss, fail_msg % tss) + verbose_print("successfully set stack_size(%d)" % tss) + + for tss in (262144, 0x100000): + verbose_print("trying stack_size = (%d)" % tss) + self.next_ident = 0 + self.created = 0 + with threading_helper.wait_threads_exit(): + for i in range(NUMTASKS): + self.newtask() + + verbose_print("waiting for all tasks to complete") + self.done_mutex.acquire() + verbose_print("all tasks done") + + thread.stack_size(0) + + def test__count(self): + # Test the _count() function. + orig = thread._count() + mut = thread.allocate_lock() + mut.acquire() + started = [] + + def task(): + started.append(None) + mut.acquire() + mut.release() + + with threading_helper.wait_threads_exit(): + thread.start_new_thread(task, ()) + for _ in support.sleeping_retry(support.LONG_TIMEOUT): + if started: + break + self.assertEqual(thread._count(), orig + 1) + + # Allow the task to finish. + mut.release() + + # The only reliable way to be sure that the thread ended from the + # interpreter's point of view is to wait for the function object to + # be destroyed. + done = [] + wr = weakref.ref(task, lambda _: done.append(None)) + del task + + for _ in support.sleeping_retry(support.LONG_TIMEOUT): + if done: + break + support.gc_collect() # For PyPy or other GCs. + self.assertEqual(thread._count(), orig) + + def test_unraisable_exception(self): + def task(): + started.release() + raise ValueError("task failed") + + started = thread.allocate_lock() + with support.catch_unraisable_exception() as cm: + with threading_helper.wait_threads_exit(): + started.acquire() + thread.start_new_thread(task, ()) + started.acquire() + + self.assertEqual(str(cm.unraisable.exc_value), "task failed") + self.assertIsNone(cm.unraisable.object) + self.assertEqual(cm.unraisable.err_msg, + f"Exception ignored in thread started by {task!r}") + self.assertIsNotNone(cm.unraisable.exc_traceback) + + def test_join_thread(self): + finished = [] + + def task(): + time.sleep(0.05) + finished.append(thread.get_ident()) + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(task) + handle.join() + self.assertEqual(len(finished), 1) + self.assertEqual(handle.ident, finished[0]) + + def test_join_thread_already_exited(self): + def task(): + pass + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(task) + time.sleep(0.05) + handle.join() + + def test_join_several_times(self): + def task(): + pass + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(task) + handle.join() + # Subsequent join() calls should succeed + handle.join() + + def test_joinable_not_joined(self): + handle_destroyed = thread.allocate_lock() + handle_destroyed.acquire() + + def task(): + handle_destroyed.acquire() + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(task) + del handle + handle_destroyed.release() + + def test_join_from_self(self): + errors = [] + handles = [] + start_joinable_thread_returned = thread.allocate_lock() + start_joinable_thread_returned.acquire() + task_tried_to_join = thread.allocate_lock() + task_tried_to_join.acquire() + + def task(): + start_joinable_thread_returned.acquire() + try: + handles[0].join() + except Exception as e: + errors.append(e) + finally: + task_tried_to_join.release() + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(task) + handles.append(handle) + start_joinable_thread_returned.release() + # Can still join after joining failed in other thread + task_tried_to_join.acquire() + handle.join() + + assert len(errors) == 1 + with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): + raise errors[0] + + def test_join_then_self_join(self): + # make sure we can't deadlock in the following scenario with + # threads t0 and t1 (see comment in `ThreadHandle_join()` for more + # details): + # + # - t0 joins t1 + # - t1 self joins + def make_lock(): + lock = thread.allocate_lock() + lock.acquire() + return lock + + error = None + self_joiner_handle = None + self_joiner_started = make_lock() + self_joiner_barrier = make_lock() + def self_joiner(): + nonlocal error + + self_joiner_started.release() + self_joiner_barrier.acquire() + + try: + self_joiner_handle.join() + except Exception as e: + error = e + + joiner_started = make_lock() + def joiner(): + joiner_started.release() + self_joiner_handle.join() + + with threading_helper.wait_threads_exit(): + self_joiner_handle = thread.start_joinable_thread(self_joiner) + # Wait for the self-joining thread to start + self_joiner_started.acquire() + + # Start the thread that joins the self-joiner + joiner_handle = thread.start_joinable_thread(joiner) + + # Wait for the joiner to start + joiner_started.acquire() + + # Not great, but I don't think there's a deterministic way to make + # sure that the self-joining thread has been joined. + time.sleep(0.1) + + # Unblock the self-joiner + self_joiner_barrier.release() + + self_joiner_handle.join() + joiner_handle.join() + + with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): + raise error + + def test_join_with_timeout(self): + lock = thread.allocate_lock() + lock.acquire() + + def thr(): + lock.acquire() + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(thr) + handle.join(0.1) + self.assertFalse(handle.is_done()) + lock.release() + handle.join() + self.assertTrue(handle.is_done()) + + def test_join_unstarted(self): + handle = thread._ThreadHandle() + with self.assertRaisesRegex(RuntimeError, "thread not started"): + handle.join() + + def test_set_done_unstarted(self): + handle = thread._ThreadHandle() + with self.assertRaisesRegex(RuntimeError, "thread not started"): + handle._set_done() + + def test_start_duplicate_handle(self): + lock = thread.allocate_lock() + lock.acquire() + + def func(): + lock.acquire() + + handle = thread._ThreadHandle() + with threading_helper.wait_threads_exit(): + thread.start_joinable_thread(func, handle=handle) + with self.assertRaisesRegex(RuntimeError, "thread already started"): + thread.start_joinable_thread(func, handle=handle) + lock.release() + handle.join() + + def test_start_with_none_handle(self): + def func(): + pass + + with threading_helper.wait_threads_exit(): + handle = thread.start_joinable_thread(func, handle=None) + handle.join() + + +class Barrier: + def __init__(self, num_threads): + self.num_threads = num_threads + self.waiting = 0 + self.checkin_mutex = thread.allocate_lock() + self.checkout_mutex = thread.allocate_lock() + self.checkout_mutex.acquire() + + def enter(self): + self.checkin_mutex.acquire() + self.waiting = self.waiting + 1 + if self.waiting == self.num_threads: + self.waiting = self.num_threads - 1 + self.checkout_mutex.release() + return + self.checkin_mutex.release() + + self.checkout_mutex.acquire() + self.waiting = self.waiting - 1 + if self.waiting == 0: + self.checkin_mutex.release() + return + self.checkout_mutex.release() + + +class BarrierTest(BasicThreadTest): + + def test_barrier(self): + with threading_helper.wait_threads_exit(): + self.bar = Barrier(NUMTASKS) + self.running = NUMTASKS + for i in range(NUMTASKS): + thread.start_new_thread(self.task2, (i,)) + verbose_print("waiting for tasks to end") + self.done_mutex.acquire() + verbose_print("tasks done") + + def task2(self, ident): + for i in range(NUMTRIPS): + if ident == 0: + # give it a good chance to enter the next + # barrier before the others are all out + # of the current one + delay = 0 + else: + with self.random_mutex: + delay = random.random() / 10000.0 + verbose_print("task %s will run for %sus" % + (ident, round(delay * 1e6))) + time.sleep(delay) + verbose_print("task %s entering %s" % (ident, i)) + self.bar.enter() + verbose_print("task %s leaving barrier" % ident) + with self.running_mutex: + self.running -= 1 + # Must release mutex before releasing done, else the main thread can + # exit and set mutex to None as part of global teardown; then + # mutex.release() raises AttributeError. + finished = self.running == 0 + if finished: + self.done_mutex.release() + +class LockTests(lock_tests.LockTests): + locktype = thread.allocate_lock + + +class TestForkInThread(unittest.TestCase): + def setUp(self): + self.read_fd, self.write_fd = os.pipe() + + @support.requires_fork() + @threading_helper.reap_threads + def test_forkinthread(self): + pid = None + + def fork_thread(read_fd, write_fd): + nonlocal pid + + # Ignore the warning about fork with threads. + with warnings.catch_warnings(category=DeprecationWarning, + action="ignore"): + # fork in a thread (DANGER, undefined per POSIX) + if (pid := os.fork()): + # parent process + return + + # child process + try: + os.close(read_fd) + os.write(write_fd, b"OK") + finally: + os._exit(0) + + with threading_helper.wait_threads_exit(): + thread.start_new_thread(fork_thread, (self.read_fd, self.write_fd)) + self.assertEqual(os.read(self.read_fd, 2), b"OK") + os.close(self.write_fd) + + self.assertIsNotNone(pid) + support.wait_process(pid, exitcode=0) + + def tearDown(self): + try: + os.close(self.read_fd) + except OSError: + pass + + try: + os.close(self.write_fd) + except OSError: + pass + + +if __name__ == "__main__": + unittest.main() From 7f942ca6bcec7cf04c0505ef60ce9623f9c99f05 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:31:05 -0600 Subject: [PATCH 06/21] gh-141044: Silence NEWS Sphinx nit for _thread.stack_size --- .../next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst index 9eb845fdd6980aa..22a0b328b524b7d 100644 --- a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst +++ b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst @@ -1,7 +1,7 @@ Fix a reference leak when creating a :class:`threading.Thread` after setting a small custom stack size with :func:`threading.stack_size` in debug and sanitizer builds. Those builds need extra C stack, so -:func:`_thread.stack_size` now requires 6 stack margins (as ThreadSanitizer +:func:`!_thread.stack_size` now requires 6 stack margins (as ThreadSanitizer already did). Sizes that previously appeared to work (for example 127 KiB on 64-bit debug/ASan builds) now raise :exc:`ValueError`. Release builds are unchanged. Patch by Kailash Nelson. From 1fac29aa283989cf27e52148a298a4a767dc62f9 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:08:50 -0600 Subject: [PATCH 07/21] gh-141044: Raise TSan stack_size floor to 12 margins TSan only uses half the stack in tstate_set_stack(), so the 6-margin minimum that is enough for ASan/debug still leaked Thread objects at the floor under ThreadSanitizer (refcount delta ~355). Require 12 margins for TSan. Update tests that assumed 256 KiB is always accepted. --- Include/internal/pycore_pythonrun.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index c6143dba3d61d5b..eb4d73600849835 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -62,12 +62,15 @@ extern PyObject* _PyRun_SimpleString( # define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2) #endif -#if (defined(Py_DEBUG) \ +#ifdef _Py_THREAD_SANITIZER +/* TSan: tstate_set_stack() only uses half the stack, so the 6-margin + * floor that is enough for ASan/debug still leaves Thread bootstrap + * without working space (gh-141044). Require 12 margins. */ +# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 12) +#elif (defined(Py_DEBUG) \ || defined(_Py_ADDRESS_SANITIZER) \ - || defined(_Py_THREAD_SANITIZER) \ || defined(_Py_UNDEFINED_BEHAVIOR_SANITIZER)) -/* Debug and sanitizer builds need more than the default 3 margins: - * - TSan: tstate_set_stack() only uses half the stack. +/* Debug/ASan/UBSan need more than the default 3 margins: * - ASan (gh-141044): instrumentation consumes extra C stack. * - Py_DEBUG / UBSan: larger C frames leave threading.Thread bootstrap * with no working space above the soft recursion limit at 3 margins, From 0f0971da32104bf60dab945c2f284562f3031f0d Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:09:07 -0600 Subject: [PATCH 08/21] gh-141044: Update NEWS for TSan 12-margin stack_size floor --- .../2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst index 22a0b328b524b7d..8f3adc8c70d6961 100644 --- a/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst +++ b/Misc/NEWS.d/next/Library/2026-09-14-20-45-00.gh-issue-141044.Ks8nQm.rst @@ -1,7 +1,8 @@ Fix a reference leak when creating a :class:`threading.Thread` after setting a small custom stack size with :func:`threading.stack_size` in debug and sanitizer builds. Those builds need extra C stack, so -:func:`!_thread.stack_size` now requires 6 stack margins (as ThreadSanitizer -already did). Sizes that previously appeared to work (for example 127 KiB -on 64-bit debug/ASan builds) now raise :exc:`ValueError`. Release builds -are unchanged. Patch by Kailash Nelson. +:func:`!_thread.stack_size` now requires 6 stack margins on debug/ASan/UBSan +builds and 12 on ThreadSanitizer builds (which only use half the stack). +Sizes that previously appeared to work (for example 127 KiB on 64-bit +debug/ASan builds) now raise :exc:`ValueError`. Release builds are +unchanged. Patch by Kailash Nelson. From f2242a1fc260679348a7d38024fdda823f1f59b5 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:11:11 -0600 Subject: [PATCH 09/21] gh-141044: Allow stack_size tests to skip 256 KiB under TSan --- Lib/test/test_thread.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index 07904dde764453b..86cb8990d6cbad2 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -102,12 +102,22 @@ def test_nt_and_posix_stack_size(self): "size") fail_msg = "stack_size(%d) failed - should succeed" + # 256 KiB may be below the sanitizer minimum (gh-141044 / TSan). + tested = [] for tss in (262144, 0x100000, 0): - thread.stack_size(tss) + try: + thread.stack_size(tss) + except ValueError: + verbose_print("skipping stack_size(%d); below platform minimum" + % tss) + continue self.assertEqual(thread.stack_size(), tss, fail_msg % tss) verbose_print("successfully set stack_size(%d)" % tss) + tested.append(tss) for tss in (262144, 0x100000): + if tss not in tested: + continue verbose_print("trying stack_size = (%d)" % tss) self.next_ident = 0 self.created = 0 From 4d72b7e58a66c28a418ea2507716ac77ffce4aae Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:12:21 -0600 Subject: [PATCH 10/21] gh-141044: Skip 256 KiB stack tests when below TSan floor --- Lib/test/test_threading.py | 3000 +----------------------------------- 1 file changed, 1 insertion(+), 2999 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 563098d8f1da8c1..8e7d274dcfaf2e9 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1,2999 +1 @@ -""" -Tests for the threading module. -""" - -import test.support -from test.support import threading_helper, requires_subprocess, requires_gil_enabled -from test.support import verbose, cpython_only, os_helper -from test.support.import_helper import ensure_lazy_imports, import_module -from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python -from test.support import force_not_colorized - -import random -import sys -import _thread -import threading -import time -import unittest -import weakref -import os -import subprocess -import signal -import textwrap -import traceback -import warnings - -from unittest import mock -from test import lock_tests -from test import support - -try: - from concurrent import interpreters -except ImportError: - interpreters = None - -threading_helper.requires_working_threading(module=True) - -# Between fork() and exec(), only async-safe functions are allowed (issues -# #12316 and #11870), and fork() from a worker thread is known to trigger -# problems with some operating systems (issue #3863): skip problematic tests -# on platforms known to behave badly. -platforms_to_skip = ('netbsd5', 'hp-ux11') - - -def skip_unless_reliable_fork(test): - if not support.has_fork_support: - return unittest.skip("requires working os.fork()")(test) - if sys.platform in platforms_to_skip: - return unittest.skip("due to known OS bug related to thread+fork")(test) - if support.HAVE_ASAN_FORK_BUG: - return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) - if support.check_sanitizer(thread=True): - return unittest.skip("TSAN doesn't support threads after fork")(test) - return test - - -def requires_subinterpreters(meth): - """Decorator to skip a test if subinterpreters are not supported.""" - return unittest.skipIf(interpreters is None, - 'subinterpreters required')(meth) - - -def restore_default_excepthook(testcase): - testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook) - threading.excepthook = threading.__excepthook__ - - -# A trivial mutable counter. -class Counter(object): - def __init__(self): - self.value = 0 - def inc(self): - self.value += 1 - def dec(self): - self.value -= 1 - def get(self): - return self.value - -class TestThread(threading.Thread): - def __init__(self, name, testcase, sema, mutex, nrunning): - threading.Thread.__init__(self, name=name) - self.testcase = testcase - self.sema = sema - self.mutex = mutex - self.nrunning = nrunning - - def run(self): - delay = random.random() / 10000.0 - if verbose: - print('task %s will run for %.1f usec' % - (self.name, delay * 1e6)) - - with self.sema: - with self.mutex: - self.nrunning.inc() - if verbose: - print(self.nrunning.get(), 'tasks are running') - self.testcase.assertLessEqual(self.nrunning.get(), 3) - - time.sleep(delay) - if verbose: - print('task', self.name, 'done') - - with self.mutex: - self.nrunning.dec() - self.testcase.assertGreaterEqual(self.nrunning.get(), 0) - if verbose: - print('%s is finished. %d tasks are running' % - (self.name, self.nrunning.get())) - - -class BaseTestCase(unittest.TestCase): - def setUp(self): - self._threads = threading_helper.threading_setup() - - def tearDown(self): - threading_helper.threading_cleanup(*self._threads) - test.support.reap_children() - - -class ThreadTests(BaseTestCase): - maxDiff = 9999 - - @cpython_only - def test_lazy_import(self): - ensure_lazy_imports("threading", {"functools", "warnings"}) - - @cpython_only - def test_name(self): - def func(): pass - - thread = threading.Thread(name="myname1") - self.assertEqual(thread.name, "myname1") - - # Convert int name to str - thread = threading.Thread(name=123) - self.assertEqual(thread.name, "123") - - # target name is ignored if name is specified - thread = threading.Thread(target=func, name="myname2") - self.assertEqual(thread.name, "myname2") - - with mock.patch.object(threading, '_counter', return_value=2): - thread = threading.Thread(name="") - self.assertEqual(thread.name, "Thread-2") - - with mock.patch.object(threading, '_counter', return_value=3): - thread = threading.Thread() - self.assertEqual(thread.name, "Thread-3") - - with mock.patch.object(threading, '_counter', return_value=5): - thread = threading.Thread(target=func) - self.assertEqual(thread.name, "Thread-5 (func)") - - def test_args_argument(self): - # bpo-45735: Using list or tuple as *args* in constructor could - # achieve the same effect. - num_list = [1] - num_tuple = (1,) - - str_list = ["str"] - str_tuple = ("str",) - - list_in_tuple = ([1],) - tuple_in_list = [(1,)] - - test_cases = ( - (num_list, lambda arg: self.assertEqual(arg, 1)), - (num_tuple, lambda arg: self.assertEqual(arg, 1)), - (str_list, lambda arg: self.assertEqual(arg, "str")), - (str_tuple, lambda arg: self.assertEqual(arg, "str")), - (list_in_tuple, lambda arg: self.assertEqual(arg, [1])), - (tuple_in_list, lambda arg: self.assertEqual(arg, (1,))) - ) - - for args, target in test_cases: - with self.subTest(target=target, args=args): - t = threading.Thread(target=target, args=args) - t.start() - t.join() - - def test_lock_no_args(self): - threading.Lock() # works - self.assertRaises(TypeError, threading.Lock, 1) - self.assertRaises(TypeError, threading.Lock, a=1) - self.assertRaises(TypeError, threading.Lock, 1, 2, a=1, b=2) - - def test_lock_no_subclass(self): - # Intentionally disallow subclasses of threading.Lock because they have - # never been allowed, so why start now just because the type is public? - with self.assertRaises(TypeError): - class MyLock(threading.Lock): pass - - def test_lock_or_none(self): - import types - self.assertIsInstance(threading.Lock | None, types.UnionType) - - # Create a bunch of threads, let each do some work, wait until all are - # done. - def test_various_ops(self): - # This takes about n/3 seconds to run (about n/3 clumps of tasks, - # times about 1 second per clump). - NUMTASKS = 10 - - # no more than 3 of the 10 can run at once - sema = threading.BoundedSemaphore(value=3) - mutex = threading.RLock() - numrunning = Counter() - - threads = [] - - for i in range(NUMTASKS): - t = TestThread(""%i, self, sema, mutex, numrunning) - threads.append(t) - self.assertIsNone(t.ident) - self.assertRegex(repr(t), r'^$') - t.start() - - if hasattr(threading, 'get_native_id'): - native_ids = set(t.native_id for t in threads) | {threading.get_native_id()} - self.assertNotIn(None, native_ids) - self.assertEqual(len(native_ids), NUMTASKS + 1) - - if verbose: - print('waiting for all tasks to complete') - for t in threads: - t.join() - self.assertFalse(t.is_alive()) - self.assertNotEqual(t.ident, 0) - self.assertIsNotNone(t.ident) - self.assertRegex(repr(t), r'^$') - if verbose: - print('all tasks done') - self.assertEqual(numrunning.get(), 0) - - def test_ident_of_no_threading_threads(self): - # The ident still must work for the main thread and dummy threads. - self.assertIsNotNone(threading.current_thread().ident) - def f(): - ident.append(threading.current_thread().ident) - done.set() - done = threading.Event() - ident = [] - with threading_helper.wait_threads_exit(): - tid = _thread.start_new_thread(f, ()) - done.wait() - self.assertEqual(ident[0], tid) - - # run with a small(ish) thread stack size (256 KiB) - def test_various_ops_small_stack(self): - if verbose: - print('with 256 KiB thread stack size...') - try: - threading.stack_size(262144) - except _thread.error: - raise unittest.SkipTest( - 'platform does not support changing thread stack size') - self.test_various_ops() - threading.stack_size(0) - - # run with a large thread stack size (1 MiB) - def test_various_ops_large_stack(self): - if verbose: - print('with 1 MiB thread stack size...') - try: - threading.stack_size(0x100000) - except _thread.error: - raise unittest.SkipTest( - 'platform does not support changing thread stack size') - self.test_various_ops() - threading.stack_size(0) - - def test_stack_size_no_leak(self): - # gh-141044: a custom thread stack size used to leak Thread objects - # when the size passed _thread.stack_size() but was too small for - # threading.Thread bootstrap under AddressSanitizer. The reported - # repro used 127 KiB and did not join the thread. - try: - threading.stack_size(262144) - except _thread.error: - self.skipTest( - 'platform does not support changing thread stack size') - threading.stack_size(0) - - def run_script(script): - _, _, err = assert_python_ok( - "-c", textwrap.dedent(script), - ASAN_OPTIONS="detect_leaks=1:halt_on_error=1") - err_s = err.decode("utf-8", "replace") - self.assertNotIn("LeakSanitizer", err_s, err_s) - - min_stack_helper = """ - import os - import threading - import _thread - - def min_stack_size(): - page = os.sysconf("SC_PAGESIZE") if hasattr(os, "sysconf") else 4096 - size = page - while size <= 4 * 1024 * 1024: - try: - threading.stack_size(size) - except ValueError: - size += page - continue - except _thread.error: - return None - return size - return None - """ - - # Original reproducer: start, do not join. 127 KiB is rejected on - # ASan builds; if a build still accepts it, the thread must not leak. - run_script(""" - import threading - try: - threading.stack_size(127 * 1024) - except ValueError: - raise SystemExit(0) - def worker(): - pass - t = threading.Thread(target=worker, name="worker-thread") - t.start() - threading.stack_size(0) - """) - - # Smallest accepted size, unjoined. Wait until the worker finishes - # without join(); process shutdown also joins. LSan plus (on debug - # builds) gettotalrefcount() must stay clean at this new minimum. - run_script(min_stack_helper + """ - import gc - import sys - import time - - def worker(): - pass - size = min_stack_size() - if size is None: - raise SystemExit(0) - - def wait_unjoined(threads, timeout=30): - deadline = time.monotonic() + timeout - for t in threads: - while t.is_alive(): - if time.monotonic() > deadline: - raise SystemExit("unjoined worker did not finish") - time.sleep(0.001) - - if hasattr(sys, "gettotalrefcount"): - gc.collect() - gc.collect() - start = sys.gettotalrefcount() - threads = [] - for _ in range(8): - t = threading.Thread(target=worker) - t.start() - threads.append(t) - wait_unjoined(threads) - del threads - threading.stack_size(0) - gc.collect() - gc.collect() - delta = sys.gettotalrefcount() - start - if delta > 50: - raise SystemExit(f"refcount leak: {delta}") - else: - t = threading.Thread(target=worker, name="min-stack-worker") - t.start() - threading.stack_size(0) - """) - - # Joined threads at the minimum must not leak references. - run_script(min_stack_helper + """ - import gc - import sys - - def worker(): - pass - size = min_stack_size() - if size is None: - raise SystemExit(0) - for _ in range(3): - t = threading.Thread(target=worker) - t.start() - t.join() - if hasattr(sys, "gettotalrefcount"): - gc.collect() - gc.collect() - start = sys.gettotalrefcount() - for _ in range(8): - t = threading.Thread(target=worker) - t.start() - t.join() - threading.stack_size(0) - gc.collect() - gc.collect() - delta = sys.gettotalrefcount() - start - if delta > 50: - raise SystemExit(f"refcount leak: {delta}") - else: - t = threading.Thread(target=worker) - t.start() - t.join() - threading.stack_size(0) - """) - - def test_foreign_thread(self): - # Check that a "foreign" thread can use the threading module. - dummy_thread = None - error = None - def f(mutex): - try: - nonlocal dummy_thread - nonlocal error - # Calling current_thread() forces an entry for the foreign - # thread to get made in the threading._active map. - dummy_thread = threading.current_thread() - tid = dummy_thread.ident - self.assertIn(tid, threading._active) - self.assertIsInstance(dummy_thread, threading._DummyThread) - self.assertIs(threading._active.get(tid), dummy_thread) - # gh-29376 - self.assertTrue( - dummy_thread.is_alive(), - 'Expected _DummyThread to be considered alive.' - ) - self.assertIn('_DummyThread', repr(dummy_thread)) - except BaseException as e: - error = e - finally: - mutex.release() - - mutex = threading.Lock() - mutex.acquire() - with threading_helper.wait_threads_exit(): - tid = _thread.start_new_thread(f, (mutex,)) - # Wait for the thread to finish. - mutex.acquire() - if error is not None: - raise error - self.assertEqual(tid, dummy_thread.ident) - # Issue gh-106236: - with self.assertRaises(RuntimeError): - dummy_thread.join() - dummy_thread._started.clear() - with self.assertRaises(RuntimeError): - dummy_thread.is_alive() - # Busy wait for the following condition: after the thread dies, the - # related dummy thread must be removed from threading._active. - timeout = 5 - timeout_at = time.monotonic() + timeout - while time.monotonic() < timeout_at: - if threading._active.get(dummy_thread.ident) is not dummy_thread: - break - time.sleep(.1) - else: - self.fail('It was expected that the created threading._DummyThread was removed from threading._active.') - - # PyThreadState_SetAsyncExc() is a CPython-only gimmick, not (currently) - # exposed at the Python level. This test relies on ctypes to get at it. - @cpython_only - def test_PyThreadState_SetAsyncExc(self): - ctypes = import_module("ctypes") - import ctypes.util # noqa: F811 - - @ctypes.util.wrap_dll_function(ctypes.pythonapi) - def PyThreadState_SetAsyncExc(id: ctypes.c_ulong, - exc: ctypes.py_object) -> ctypes.c_int: - pass - set_async_exc = PyThreadState_SetAsyncExc - - class AsyncExc(Exception): - pass - - exception = ctypes.py_object(AsyncExc) - - # First check it works when setting the exception from the same thread. - tid = threading.get_ident() - self.assertIsInstance(tid, int) - self.assertGreater(tid, 0) - - try: - result = set_async_exc(tid, exception) - # The exception is async, so we might have to keep the VM busy until - # it notices. - while True: - pass - except AsyncExc: - pass - else: - # This code is unreachable but it reflects the intent. If we wanted - # to be smarter the above loop wouldn't be infinite. - self.fail("AsyncExc not raised") - try: - self.assertEqual(result, 1) # one thread state modified - except UnboundLocalError: - # The exception was raised too quickly for us to get the result. - pass - - # `worker_started` is set by the thread when it's inside a try/except - # block waiting to catch the asynchronously set AsyncExc exception. - # `worker_saw_exception` is set by the thread upon catching that - # exception. - worker_started = threading.Event() - worker_saw_exception = threading.Event() - - class Worker(threading.Thread): - def run(self): - self.id = threading.get_ident() - self.finished = False - - try: - while True: - worker_started.set() - time.sleep(0.1) - except AsyncExc: - self.finished = True - worker_saw_exception.set() - - t = Worker() - t.daemon = True # so if this fails, we don't hang Python at shutdown - t.start() - if verbose: - print(" started worker thread") - - # Try a thread id that doesn't make sense. - if verbose: - print(" trying nonsensical thread id") - result = set_async_exc(-1, exception) - self.assertEqual(result, 0) # no thread states modified - - # Now raise an exception in the worker thread. - if verbose: - print(" waiting for worker thread to get started") - ret = worker_started.wait() - self.assertTrue(ret) - if verbose: - print(" verifying worker hasn't exited") - self.assertFalse(t.finished) - if verbose: - print(" attempting to raise asynch exception in worker") - result = set_async_exc(t.id, exception) - self.assertEqual(result, 1) # one thread state modified - if verbose: - print(" waiting for worker to say it caught the exception") - worker_saw_exception.wait(timeout=support.SHORT_TIMEOUT) - self.assertTrue(t.finished) - if verbose: - print(" all OK -- joining worker") - if t.finished: - t.join() - # else the thread is still running, and we have no way to kill it - - @cpython_only - @unittest.skipUnless(hasattr(signal, "pthread_kill"), "need pthread_kill") - @unittest.skipUnless(hasattr(signal, "SIGUSR1"), "need SIGUSR1") - def test_PyThreadState_SetAsyncExc_interrupts_sleep(self): - _testcapi = import_module("_testlimitedcapi") - - worker_started = threading.Event() - - class InjectedException(Exception): - """Custom exception for testing""" - - caught_exception = None - - def catch_exception(): - nonlocal caught_exception - day_as_seconds = 60 * 60 * 24 - try: - worker_started.set() - time.sleep(day_as_seconds) - except InjectedException as exc: - caught_exception = exc - - thread = threading.Thread(target=catch_exception) - thread.start() - worker_started.wait() - - signal.signal(signal.SIGUSR1, lambda sig, frame: None) - - result = _testcapi.threadstate_set_async_exc( - thread.ident, InjectedException) - self.assertEqual(result, 1) - - for _ in support.sleeping_retry(support.SHORT_TIMEOUT): - if not thread.is_alive(): - break - try: - signal.pthread_kill(thread.ident, signal.SIGUSR1) - except OSError: - # The thread might have terminated between the is_alive check - # and the pthread_kill - break - - thread.join() - signal.signal(signal.SIGUSR1, signal.SIG_DFL) - - self.assertIsInstance(caught_exception, InjectedException) - - def test_limbo_cleanup(self): - # Issue 7481: Failure to start thread should cleanup the limbo map. - def fail_new_thread(*args, **kwargs): - raise threading.ThreadError() - _start_joinable_thread = threading._start_joinable_thread - threading._start_joinable_thread = fail_new_thread - try: - t = threading.Thread(target=lambda: None) - self.assertRaises(threading.ThreadError, t.start) - self.assertFalse( - t in threading._limbo, - "Failed to cleanup _limbo map on failure of Thread.start().") - finally: - threading._start_joinable_thread = _start_joinable_thread - - def test_finalize_running_thread(self): - # Issue 1402: the PyGILState_Ensure / _Release functions may be called - # very late on python exit: on deallocation of a running thread for - # example. - if support.check_sanitizer(thread=True): - # the thread running `time.sleep(100)` below will still be alive - # at process exit - self.skipTest("TSAN would report thread leak") - import_module("ctypes") - - rc, out, err = assert_python_failure("-c", """if 1: - import ctypes.util, sys, time, _thread - - PyGILState_STATE = ctypes.c_int # enum - - @ctypes.util.wrap_dll_function(ctypes.pythonapi) - def PyGILState_Ensure() -> PyGILState_STATE: - pass - - @ctypes.util.wrap_dll_function(ctypes.pythonapi) - def PyGILState_Release(oldstate: PyGILState_STATE) -> None: - pass - - # This lock is used as a simple event variable. - ready = _thread.allocate_lock() - ready.acquire() - - # Module globals are cleared before __del__ is run - # So we save the functions in class dict - class C: - ensure = PyGILState_Ensure - release = PyGILState_Release - def __del__(self): - state = self.ensure() - self.release(state) - - def waitingThread(): - x = C() - ready.release() - time.sleep(100) - - _thread.start_new_thread(waitingThread, ()) - ready.acquire() # Be sure the other thread is waiting. - sys.exit(42) - """) - self.assertEqual(rc, 42) - - def test_finalize_with_trace(self): - # Issue1733757 - # Avoid a deadlock when sys.settrace steps into threading._shutdown - if support.check_sanitizer(thread=True): - # the thread running `time.sleep(2)` below will still be alive - # at process exit - self.skipTest("TSAN would report thread leak") - - assert_python_ok("-c", """if 1: - import sys, threading - - # A deadlock-killer, to prevent the - # testsuite to hang forever - def killer(): - import os, time - time.sleep(2) - print('program blocked; aborting') - os._exit(2) - t = threading.Thread(target=killer) - t.daemon = True - t.start() - - # This is the trace function - def func(frame, event, arg): - threading.current_thread() - return func - - sys.settrace(func) - """) - - def test_join_nondaemon_on_shutdown(self): - # Issue 1722344 - # Raising SystemExit skipped threading._shutdown - rc, out, err = assert_python_ok("-c", """if 1: - import threading - from time import sleep - - def child(): - sleep(1) - # As a non-daemon thread we SHOULD wake up and nothing - # should be torn down yet - print("Woke up, sleep function is:", sleep) - - threading.Thread(target=child).start() - raise SystemExit - """) - self.assertEqual(out.strip(), - b"Woke up, sleep function is: ") - self.assertEqual(err, b"") - - def test_enumerate_after_join(self): - # Try hard to trigger #1703448: a thread is still returned in - # threading.enumerate() after it has been join()ed. - enum = threading.enumerate - old_interval = sys.getswitchinterval() - try: - for i in range(1, 100): - support.setswitchinterval(i * 0.0002) - t = threading.Thread(target=lambda: None) - t.start() - t.join() - l = enum() - self.assertNotIn(t, l, - "#1703448 triggered after %d trials: %s" % (i, l)) - finally: - sys.setswitchinterval(old_interval) - - @support.bigmemtest(size=20, memuse=72*2**20, dry_run=False) - def test_join_from_multiple_threads(self, size): - # Thread.join() should be thread-safe - errors = [] - - def worker(): - time.sleep(0.005) - - def joiner(thread): - try: - thread.join() - except Exception as e: - errors.append(e) - - for N in range(2, 20): - threads = [threading.Thread(target=worker)] - for i in range(N): - threads.append(threading.Thread(target=joiner, - args=(threads[0],))) - for t in threads: - t.start() - time.sleep(0.01) - for t in threads: - t.join() - if errors: - raise errors[0] - - def test_join_with_timeout(self): - lock = _thread.allocate_lock() - lock.acquire() - - def worker(): - lock.acquire() - - thread = threading.Thread(target=worker) - thread.start() - thread.join(timeout=0.01) - assert thread.is_alive() - lock.release() - thread.join() - assert not thread.is_alive() - - def test_no_refcycle_through_target(self): - class RunSelfFunction(object): - def __init__(self, should_raise): - # The links in this refcycle from Thread back to self - # should be cleaned up when the thread completes. - self.should_raise = should_raise - self.thread = threading.Thread(target=self._run, - args=(self,), - kwargs={'yet_another':self}) - self.thread.start() - - def _run(self, other_ref, yet_another): - if self.should_raise: - raise SystemExit - - restore_default_excepthook(self) - - cyclic_object = RunSelfFunction(should_raise=False) - weak_cyclic_object = weakref.ref(cyclic_object) - cyclic_object.thread.join() - del cyclic_object - self.assertIsNone(weak_cyclic_object(), - msg=('%d references still around' % - sys.getrefcount(weak_cyclic_object()))) - - raising_cyclic_object = RunSelfFunction(should_raise=True) - weak_raising_cyclic_object = weakref.ref(raising_cyclic_object) - raising_cyclic_object.thread.join() - del raising_cyclic_object - self.assertIsNone(weak_raising_cyclic_object(), - msg=('%d references still around' % - sys.getrefcount(weak_raising_cyclic_object()))) - - def test_old_threading_api(self): - # Just a quick sanity check to make sure the old method names are - # still present - t = threading.Thread() - with self.assertWarnsRegex(DeprecationWarning, - r'get the daemon attribute'): - t.isDaemon() - with self.assertWarnsRegex(DeprecationWarning, - r'set the daemon attribute'): - t.setDaemon(True) - with self.assertWarnsRegex(DeprecationWarning, - r'get the name attribute'): - t.getName() - with self.assertWarnsRegex(DeprecationWarning, - r'set the name attribute'): - t.setName("name") - - e = threading.Event() - with self.assertWarnsRegex(DeprecationWarning, 'use is_set()'): - e.isSet() - - cond = threading.Condition() - cond.acquire() - with self.assertWarnsRegex(DeprecationWarning, 'use notify_all()'): - cond.notifyAll() - - with self.assertWarnsRegex(DeprecationWarning, 'use active_count()'): - threading.activeCount() - with self.assertWarnsRegex(DeprecationWarning, 'use current_thread()'): - threading.currentThread() - - def test_repr_daemon(self): - t = threading.Thread() - self.assertNotIn('daemon', repr(t)) - t.daemon = True - self.assertIn('daemon', repr(t)) - - def test_daemon_param(self): - t = threading.Thread() - self.assertFalse(t.daemon) - t = threading.Thread(daemon=False) - self.assertFalse(t.daemon) - t = threading.Thread(daemon=True) - self.assertTrue(t.daemon) - - @skip_unless_reliable_fork - def test_dummy_thread_after_fork(self): - # Issue #14308: a dummy thread in the active list doesn't mess up - # the after-fork mechanism. - code = """if 1: - import _thread, threading, os, time, warnings - - def background_thread(evt): - # Creates and registers the _DummyThread instance - threading.current_thread() - evt.set() - time.sleep(10) - - evt = threading.Event() - _thread.start_new_thread(background_thread, (evt,)) - evt.wait() - assert threading.active_count() == 2, threading.active_count() - with warnings.catch_warnings(record=True) as ws: - warnings.filterwarnings( - "always", category=DeprecationWarning) - if os.fork() == 0: - assert threading.active_count() == 1, threading.active_count() - os._exit(0) - else: - assert ws[0].category == DeprecationWarning, ws[0] - assert 'fork' in str(ws[0].message), ws[0] - os.wait() - """ - _, out, err = assert_python_ok("-c", code) - self.assertEqual(out, b'') - self.assertEqual(err, b'') - - @skip_unless_reliable_fork - def test_is_alive_after_fork(self): - # Try hard to trigger #18418: is_alive() could sometimes be True on - # threads that vanished after a fork. - old_interval = sys.getswitchinterval() - self.addCleanup(sys.setswitchinterval, old_interval) - - # Make the bug more likely to manifest. - test.support.setswitchinterval(1e-6) - - for i in range(20): - t = threading.Thread(target=lambda: None) - t.start() - # Ignore the warning about fork with threads. - with warnings.catch_warnings(category=DeprecationWarning, - action="ignore"): - if (pid := os.fork()) == 0: - os._exit(11 if t.is_alive() else 10) - else: - t.join() - - support.wait_process(pid, exitcode=10) - - def test_main_thread(self): - main = threading.main_thread() - self.assertEqual(main.name, 'MainThread') - self.assertEqual(main.ident, threading.current_thread().ident) - self.assertEqual(main.ident, threading.get_ident()) - - def f(): - self.assertNotEqual(threading.main_thread().ident, - threading.current_thread().ident) - th = threading.Thread(target=f) - th.start() - th.join() - - @skip_unless_reliable_fork - @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") - def test_main_thread_after_fork(self): - code = """if 1: - import os, threading - from test import support - - ident = threading.get_ident() - pid = os.fork() - if pid == 0: - print("current ident", threading.get_ident() == ident) - main = threading.main_thread() - print("main", main.name) - print("main ident", main.ident == ident) - print("current is main", threading.current_thread() is main) - else: - support.wait_process(pid, exitcode=0) - """ - _, out, err = assert_python_ok("-c", code) - data = out.decode().replace('\r', '') - self.assertEqual(err, b"") - self.assertEqual(data, - "current ident True\n" - "main MainThread\n" - "main ident True\n" - "current is main True\n") - - @skip_unless_reliable_fork - @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") - def test_main_thread_after_fork_from_nonmain_thread(self): - code = """if 1: - import os, threading, sys, warnings - from test import support - - def func(): - ident = threading.get_ident() - with warnings.catch_warnings(record=True) as ws: - warnings.filterwarnings( - "always", category=DeprecationWarning) - pid = os.fork() - if pid == 0: - print("current ident", threading.get_ident() == ident) - main = threading.main_thread() - print("main", main.name, type(main).__name__) - print("main ident", main.ident == ident) - print("current is main", threading.current_thread() is main) - # stdout is fully buffered because not a tty, - # we have to flush before exit. - sys.stdout.flush() - else: - assert ws[0].category == DeprecationWarning, ws[0] - assert 'fork' in str(ws[0].message), ws[0] - support.wait_process(pid, exitcode=0) - - th = threading.Thread(target=func) - th.start() - th.join() - """ - _, out, err = assert_python_ok("-c", code) - data = out.decode().replace('\r', '') - self.assertEqual(err.decode('utf-8'), "") - self.assertEqual(data, - "current ident True\n" - "main Thread-1 (func) Thread\n" - "main ident True\n" - "current is main True\n" - ) - - @skip_unless_reliable_fork - @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") - def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False): - code = """if 1: - import os, threading, sys, traceback, _thread - from test import support - - def func(lock): - ident = threading.get_ident() - if %s: - # call current_thread() before fork to allocate DummyThread - current = threading.current_thread() - print("current", current.name, type(current).__name__) - print("ident in _active", ident in threading._active) - # flush before fork, so child won't flush it again - sys.stdout.flush() - pid = os.fork() - if pid == 0: - print("current ident", threading.get_ident() == ident) - main = threading.main_thread() - print("main", main.name, type(main).__name__) - print("main ident", main.ident == ident) - print("current is main", threading.current_thread() is main) - print("_dangling", [t.name for t in list(threading._dangling)]) - # stdout is fully buffered because not a tty, - # we have to flush before exit. - sys.stdout.flush() - try: - threading._shutdown() - os._exit(0) - except: - traceback.print_exc() - sys.stderr.flush() - os._exit(1) - else: - try: - support.wait_process(pid, exitcode=0) - except Exception: - # avoid 'could not acquire lock for - # <_io.BufferedWriter name=''> at interpreter shutdown,' - traceback.print_exc() - sys.stderr.flush() - finally: - lock.release() - - join_lock = _thread.allocate_lock() - join_lock.acquire() - th = _thread.start_new_thread(func, (join_lock,)) - join_lock.acquire() - """ % create_dummy - # "DeprecationWarning: This process is multi-threaded, use of fork() - # may lead to deadlocks in the child" - _, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code) - data = out.decode().replace('\r', '') - self.assertEqual(err.decode(), "") - self.assertEqual(data, - ("current Dummy-1 _DummyThread\n" if create_dummy else "") + - f"ident in _active {create_dummy!s}\n" + - "current ident True\n" - "main MainThread _MainThread\n" - "main ident True\n" - "current is main True\n" - "_dangling ['MainThread']\n") - - def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False): - self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True) - - def test_main_thread_during_shutdown(self): - # bpo-31516: current_thread() should still point to the main thread - # at shutdown - code = """if 1: - import gc, threading - - main_thread = threading.current_thread() - assert main_thread is threading.main_thread() # sanity check - - class RefCycle: - def __init__(self): - self.cycle = self - - def __del__(self): - print("GC:", - threading.current_thread() is main_thread, - threading.main_thread() is main_thread, - threading.enumerate() == [main_thread]) - - RefCycle() - gc.collect() # sanity check - x = RefCycle() - """ - _, out, err = assert_python_ok("-c", code) - data = out.decode() - self.assertEqual(err, b"") - self.assertEqual(data.splitlines(), - ["GC: True True True"] * 2) - - def test_finalization_shutdown(self): - # bpo-36402: Py_Finalize() calls threading._shutdown() which must wait - # until Python thread states of all non-daemon threads get deleted. - # - # Test similar to SubinterpThreadingTests.test_threads_join_2(), but - # test the finalization of the main interpreter. - code = """if 1: - import os - import threading - import time - import random - - def random_sleep(): - seconds = random.random() * 0.010 - time.sleep(seconds) - - class Sleeper: - def __del__(self): - random_sleep() - - tls = threading.local() - - def f(): - # Sleep a bit so that the thread is still running when - # Py_Finalize() is called. - random_sleep() - tls.x = Sleeper() - random_sleep() - - threading.Thread(target=f).start() - random_sleep() - """ - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(err, b"") - - def test_repr_stopped(self): - # Verify that "stopped" shows up in repr(Thread) appropriately. - started = _thread.allocate_lock() - finish = _thread.allocate_lock() - started.acquire() - finish.acquire() - def f(): - started.release() - finish.acquire() - t = threading.Thread(target=f) - t.start() - started.acquire() - self.assertIn("started", repr(t)) - finish.release() - # "stopped" should appear in the repr in a reasonable amount of time. - # Implementation detail: as of this writing, that's trivially true - # if .join() is called, and almost trivially true if .is_alive() is - # called. The detail we're testing here is that "stopped" shows up - # "all on its own". - LOOKING_FOR = "stopped" - for i in range(500): - if LOOKING_FOR in repr(t): - break - time.sleep(0.01) - self.assertIn(LOOKING_FOR, repr(t)) # we waited at least 5 seconds - t.join() - - def test_BoundedSemaphore_limit(self): - # BoundedSemaphore should raise ValueError if released too often. - for limit in range(1, 10): - bs = threading.BoundedSemaphore(limit) - threads = [threading.Thread(target=bs.acquire) - for _ in range(limit)] - for t in threads: - t.start() - for t in threads: - t.join() - threads = [threading.Thread(target=bs.release) - for _ in range(limit)] - for t in threads: - t.start() - for t in threads: - t.join() - self.assertRaises(ValueError, bs.release) - - @cpython_only - def test_frame_tstate_tracing(self): - _testcapi = import_module("_testcapi") - # Issue #14432: Crash when a generator is created in a C thread that is - # destroyed while the generator is still used. The issue was that a - # generator contains a frame, and the frame kept a reference to the - # Python state of the destroyed C thread. The crash occurs when a trace - # function is setup. - - def noop_trace(frame, event, arg): - # no operation - return noop_trace - - def generator(): - while 1: - yield "generator" - - def callback(): - if callback.gen is None: - callback.gen = generator() - return next(callback.gen) - callback.gen = None - - old_trace = sys.gettrace() - sys.settrace(noop_trace) - try: - # Install a trace function - threading.settrace(noop_trace) - - # Create a generator in a C thread which exits after the call - _testcapi.call_in_temporary_c_thread(callback) - - # Call the generator in a different Python thread, check that the - # generator didn't keep a reference to the destroyed thread state - for test in range(3): - # The trace function is still called here - callback() - finally: - sys.settrace(old_trace) - threading.settrace(old_trace) - - def test_gettrace(self): - def noop_trace(frame, event, arg): - # no operation - return noop_trace - old_trace = threading.gettrace() - try: - threading.settrace(noop_trace) - trace_func = threading.gettrace() - self.assertEqual(noop_trace,trace_func) - finally: - threading.settrace(old_trace) - - def test_gettrace_all_threads(self): - def fn(*args): pass - old_trace = threading.gettrace() - first_check = threading.Event() - second_check = threading.Event() - - trace_funcs = [] - def checker(): - trace_funcs.append(sys.gettrace()) - first_check.set() - second_check.wait() - trace_funcs.append(sys.gettrace()) - - try: - t = threading.Thread(target=checker) - t.start() - first_check.wait() - threading.settrace_all_threads(fn) - second_check.set() - t.join() - self.assertEqual(trace_funcs, [None, fn]) - self.assertEqual(threading.gettrace(), fn) - self.assertEqual(sys.gettrace(), fn) - finally: - threading.settrace_all_threads(old_trace) - - self.assertEqual(threading.gettrace(), old_trace) - self.assertEqual(sys.gettrace(), old_trace) - - def test_getprofile(self): - def fn(*args): pass - old_profile = threading.getprofile() - try: - threading.setprofile(fn) - self.assertEqual(fn, threading.getprofile()) - finally: - threading.setprofile(old_profile) - - def test_getprofile_all_threads(self): - def fn(*args): pass - old_profile = threading.getprofile() - first_check = threading.Event() - second_check = threading.Event() - - profile_funcs = [] - def checker(): - profile_funcs.append(sys.getprofile()) - first_check.set() - second_check.wait() - profile_funcs.append(sys.getprofile()) - - try: - t = threading.Thread(target=checker) - t.start() - first_check.wait() - threading.setprofile_all_threads(fn) - second_check.set() - t.join() - self.assertEqual(profile_funcs, [None, fn]) - self.assertEqual(threading.getprofile(), fn) - self.assertEqual(sys.getprofile(), fn) - finally: - threading.setprofile_all_threads(old_profile) - - self.assertEqual(threading.getprofile(), old_profile) - self.assertEqual(sys.getprofile(), old_profile) - - def test_locals_at_exit(self): - # bpo-19466: thread locals must not be deleted before destructors - # are called - rc, out, err = assert_python_ok("-c", """if 1: - import threading - - class Atexit: - def __del__(self): - print("thread_dict.atexit = %r" % thread_dict.atexit) - - thread_dict = threading.local() - thread_dict.atexit = "value" - - atexit = Atexit() - """) - self.assertEqual(out.rstrip(), b"thread_dict.atexit = 'value'") - - def test_boolean_target(self): - # bpo-41149: A thread that had a boolean value of False would not - # run, regardless of whether it was callable. The correct behaviour - # is for a thread to do nothing if its target is None, and to call - # the target otherwise. - class BooleanTarget(object): - def __init__(self): - self.ran = False - def __bool__(self): - return False - def __call__(self): - self.ran = True - - target = BooleanTarget() - thread = threading.Thread(target=target) - thread.start() - thread.join() - self.assertTrue(target.ran) - - def test_leak_without_join(self): - # bpo-37788: Test that a thread which is not joined explicitly - # does not leak. Test written for reference leak checks. - def noop(): pass - with threading_helper.wait_threads_exit(): - threading.Thread(target=noop).start() - # Thread.join() is not called - - def test_import_from_another_thread(self): - # bpo-1596321: If the threading module is first import from a thread - # different than the main thread, threading._shutdown() must handle - # this case without logging an error at Python exit. - code = textwrap.dedent(''' - import _thread - import sys - - event = _thread.allocate_lock() - event.acquire() - - def import_threading(): - import threading - event.release() - - if 'threading' in sys.modules: - raise Exception('threading is already imported') - - _thread.start_new_thread(import_threading, ()) - - # wait until the threading module is imported - event.acquire() - event.release() - - if 'threading' not in sys.modules: - raise Exception('threading is not imported') - - # don't wait until the thread completes - ''') - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(out, b'') - self.assertEqual(err, b'') - - def test_start_new_thread_at_finalization(self): - code = """if 1: - import _thread - - def f(): - print("shouldn't be printed") - - class AtFinalization: - def __del__(self): - print("OK") - _thread.start_new_thread(f, ()) - at_finalization = AtFinalization() - """ - _, out, err = assert_python_ok("-c", code) - self.assertEqual(out.strip(), b"OK") - self.assertIn(b"can't create new thread at interpreter shutdown", err) - - def test_join_daemon_thread_in_finalization(self): - # gh-123940: Py_Finalize() prevents other threads from running Python - # code, so join() can not succeed unless the thread is already done. - # (Non-Python threads, that is `threading._DummyThread`, can't be - # joined at all.) - # We raise an exception rather than hang. - for timeout in (None, 10): - with self.subTest(timeout=timeout): - code = textwrap.dedent(f""" - import threading - - - def loop(): - while True: - pass - - - class Cycle: - def __init__(self): - self.self_ref = self - self.thr = threading.Thread( - target=loop, daemon=True) - self.thr.start() - - def __del__(self): - assert self.thr.is_alive() - try: - self.thr.join(timeout={timeout}) - except PythonFinalizationError: - assert self.thr.is_alive() - print('got the correct exception!') - - # Cycle holds a reference to itself, which ensures it is - # cleaned up during the GC that runs after daemon threads - # have been forced to exit during finalization. - Cycle() - """) - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(err, b"") - self.assertIn(b"got the correct exception", out) - - def test_join_finished_daemon_thread_in_finalization(self): - # (see previous test) - # If the thread is already finished, join() succeeds. - code = textwrap.dedent(""" - import threading - done = threading.Event() - - def set_event(): - done.set() - - class Cycle: - def __init__(self): - self.self_ref = self - self.thr = threading.Thread(target=set_event, daemon=True) - self.thr.start() - self.thr.join() - - def __del__(self): - assert done.is_set() - assert not self.thr.is_alive() - self.thr.join() - assert not self.thr.is_alive() - print('all clear!') - - Cycle() - """) - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(err, b"") - self.assertIn(b"all clear", out) - - @support.subTests('lock_class_name', ['Lock', 'RLock']) - def test_acquire_daemon_thread_lock_in_finalization(self, lock_class_name): - # gh-123940: Py_Finalize() prevents other threads from running Python - # code (and so, releasing locks), so acquiring a locked lock can not - # succeed. - # We raise an exception rather than hang. - code = textwrap.dedent(f""" - import threading - import time - - thread_started_event = threading.Event() - - lock = threading.{lock_class_name}() - def loop(): - if {lock_class_name!r} == 'RLock': - lock.acquire() - with lock: - thread_started_event.set() - while True: - time.sleep(1) - - uncontested_lock = threading.{lock_class_name}() - - class Cycle: - def __init__(self): - self.self_ref = self - self.thr = threading.Thread( - target=loop, daemon=True) - self.thr.start() - thread_started_event.wait() - - def __del__(self): - assert self.thr.is_alive() - - # We *can* acquire an unlocked lock - uncontested_lock.acquire() - if {lock_class_name!r} == 'RLock': - uncontested_lock.acquire() - - # Acquiring a locked one fails - try: - lock.acquire() - except PythonFinalizationError: - assert self.thr.is_alive() - print('got the correct exception!') - - # Cycle holds a reference to itself, which ensures it is - # cleaned up during the GC that runs after daemon threads - # have been forced to exit during finalization. - Cycle() - """) - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(err, b"") - self.assertIn(b"got the correct exception", out) - - def test_start_new_thread_failed(self): - # gh-109746: if Python fails to start newly created thread - # due to failure of underlying PyThread_start_new_thread() call, - # its state should be removed from interpreter' thread states list - # to avoid its double cleanup - try: - from resource import setrlimit, RLIMIT_NPROC # noqa: F401 - except ImportError as err: - self.skipTest(err) # RLIMIT_NPROC is specific to Linux and BSD - code = """if 1: - import resource - import _thread - - def f(): - print("shouldn't be printed") - - limits = resource.getrlimit(resource.RLIMIT_NPROC) - [_, hard] = limits - resource.setrlimit(resource.RLIMIT_NPROC, (0, hard)) - - try: - handle = _thread.start_joinable_thread(f) - except RuntimeError: - print('ok') - else: - print('!skip!') - handle.join() - """ - _, out, err = assert_python_ok("-u", "-c", code) - out = out.strip() - if b'!skip!' in out: - self.skipTest('RLIMIT_NPROC had no effect; probably superuser') - self.assertEqual(out, b'ok') - self.assertEqual(err, b'') - - @cpython_only - def test_finalize_daemon_thread_hang(self): - # gh-87135: tests that daemon threads hang during finalization - script = textwrap.dedent(''' - import os - import sys - import threading - import time - import _testcapi - - lock = threading.Lock() - lock.acquire() - thread_started_event = threading.Event() - def thread_func(): - try: - thread_started_event.set() - _testcapi.finalize_thread_hang(lock.acquire) - finally: - # Control must not reach here. - os._exit(2) - - t = threading.Thread(target=thread_func) - t.daemon = True - t.start() - thread_started_event.wait() - # Sleep to ensure daemon thread is blocked on `lock.acquire` - # - # Note: This test is designed so that in the unlikely case that - # `0.1` seconds is not sufficient time for the thread to become - # blocked on `lock.acquire`, the test will still pass, it just - # won't be properly testing the thread behavior during - # finalization. - time.sleep(0.1) - - def run_during_finalization(): - # Wake up daemon thread - lock.release() - # Sleep to give the daemon thread time to crash if it is going - # to. - # - # Note: If due to an exceptionally slow execution this delay is - # insufficient, the test will still pass but will simply be - # ineffective as a test. - time.sleep(0.1) - # If control reaches here, the test succeeded. - os._exit(0) - - # Replace sys.stderr.flush as a way to run code during finalization - orig_flush = sys.stderr.flush - def do_flush(*args, **kwargs): - orig_flush(*args, **kwargs) - if not sys.is_finalizing: - return - sys.stderr.flush = orig_flush - run_during_finalization() - - sys.stderr.flush = do_flush - - # If the follow exit code is retained, `run_during_finalization` - # did not run. - sys.exit(1) - ''') - assert_python_ok("-c", script) - - @skip_unless_reliable_fork - @unittest.skipUnless(hasattr(threading, 'get_native_id'), "test needs threading.get_native_id()") - def test_native_id_after_fork(self): - script = """if True: - import threading - import os - from test import support - - parent_thread_native_id = threading.current_thread().native_id - print(parent_thread_native_id, flush=True) - assert parent_thread_native_id == threading.get_native_id() - childpid = os.fork() - if childpid == 0: - print(threading.current_thread().native_id, flush=True) - assert threading.current_thread().native_id == threading.get_native_id() - else: - try: - assert parent_thread_native_id == threading.current_thread().native_id - assert parent_thread_native_id == threading.get_native_id() - finally: - support.wait_process(childpid, exitcode=0) - """ - rc, out, err = assert_python_ok('-c', script) - self.assertEqual(rc, 0) - self.assertEqual(err, b"") - native_ids = out.strip().splitlines() - self.assertEqual(len(native_ids), 2) - self.assertNotEqual(native_ids[0], native_ids[1]) - - def test_stop_the_world_during_finalization(self): - # gh-137433: Test functions that trigger a stop-the-world in the free - # threading build concurrent with interpreter finalization. - script = """if True: - import gc - import sys - import threading - NUM_THREADS = 5 - b = threading.Barrier(NUM_THREADS + 1) - def run_in_bg(): - b.wait() - while True: - sys.setprofile(None) - gc.collect() - - for _ in range(NUM_THREADS): - t = threading.Thread(target=run_in_bg, daemon=True) - t.start() - - b.wait() - print("Exiting...") - """ - rc, out, err = assert_python_ok('-c', script) - self.assertEqual(rc, 0) - self.assertEqual(err, b"") - self.assertEqual(out.strip(), b"Exiting...") - -class ThreadJoinOnShutdown(BaseTestCase): - - def _run_and_join(self, script): - script = """if 1: - import sys, os, time, threading - - # a thread, which waits for the main program to terminate - def joiningfunc(mainthread): - mainthread.join() - print('end of thread') - # stdout is fully buffered because not a tty, we have to flush - # before exit. - sys.stdout.flush() - \n""" + script - - rc, out, err = assert_python_ok("-c", script) - data = out.decode().replace('\r', '') - self.assertEqual(data, "end of main\nend of thread\n") - - def test_1_join_on_shutdown(self): - # The usual case: on exit, wait for a non-daemon thread - script = """if 1: - import os - t = threading.Thread(target=joiningfunc, - args=(threading.current_thread(),)) - t.start() - time.sleep(0.1) - print('end of main') - """ - self._run_and_join(script) - - @skip_unless_reliable_fork - def test_2_join_in_forked_process(self): - # Like the test above, but from a forked interpreter - script = """if 1: - from test import support - - childpid = os.fork() - if childpid != 0: - # parent process - support.wait_process(childpid, exitcode=0) - sys.exit(0) - - # child process - t = threading.Thread(target=joiningfunc, - args=(threading.current_thread(),)) - t.start() - print('end of main') - """ - self._run_and_join(script) - - @skip_unless_reliable_fork - def test_3_join_in_forked_from_thread(self): - # Like the test above, but fork() was called from a worker thread - # In the forked process, the main Thread object must be marked as stopped. - - script = """if 1: - from test import support - - main_thread = threading.current_thread() - def worker(): - childpid = os.fork() - if childpid != 0: - # parent process - support.wait_process(childpid, exitcode=0) - sys.exit(0) - - # child process - t = threading.Thread(target=joiningfunc, - args=(main_thread,)) - print('end of main') - t.start() - t.join() # Should not block: main_thread is already stopped - - w = threading.Thread(target=worker) - w.start() - """ - self._run_and_join(script) - - @unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug") - @support.bigmemtest(size=40, memuse=70*2**20, dry_run=False) - def test_4_daemon_threads(self, size): - # Check that a daemon thread cannot crash the interpreter on shutdown - # by manipulating internal structures that are being disposed of in - # the main thread. - if support.check_sanitizer(thread=True): - # some of the threads running `random_io` below will still be alive - # at process exit - self.skipTest("TSAN would report thread leak") - - script = """if True: - import os - import random - import sys - import time - import threading - - thread_has_run = set() - - def random_io(): - '''Loop for a while sleeping random tiny amounts and doing some I/O.''' - import test.test_threading as mod - while True: - with open(mod.__file__, 'rb') as in_f: - stuff = in_f.read(200) - with open(os.devnull, 'wb') as null_f: - null_f.write(stuff) - time.sleep(random.random() / 1995) - thread_has_run.add(threading.current_thread()) - - def main(): - count = 0 - for _ in range(40): - new_thread = threading.Thread(target=random_io) - new_thread.daemon = True - new_thread.start() - count += 1 - while len(thread_has_run) < count: - time.sleep(0.001) - # Trigger process shutdown - sys.exit(0) - - main() - """ - rc, out, err = assert_python_ok('-c', script) - self.assertFalse(err) - - def test_thread_from_thread(self): - script = """if True: - import threading - import time - - def thread2(): - time.sleep(0.05) - print("OK") - - def thread1(): - time.sleep(0.05) - t2 = threading.Thread(target=thread2) - t2.start() - - t = threading.Thread(target=thread1) - t.start() - # do not join() -- the interpreter waits for non-daemon threads to - # finish. - """ - rc, out, err = assert_python_ok('-c', script) - self.assertEqual(err, b"") - self.assertEqual(out.strip(), b"OK") - self.assertEqual(rc, 0) - - @skip_unless_reliable_fork - def test_reinit_tls_after_fork(self): - # Issue #13817: fork() would deadlock in a multithreaded program with - # the ad-hoc TLS implementation. - - def do_fork_and_wait(): - # just fork a child process and wait it - pid = os.fork() - if pid > 0: - support.wait_process(pid, exitcode=50) - else: - os._exit(50) - - # Ignore the warning about fork with threads. - with warnings.catch_warnings(category=DeprecationWarning, - action="ignore"): - # start a bunch of threads that will fork() child processes - threads = [] - for i in range(16): - t = threading.Thread(target=do_fork_and_wait) - threads.append(t) - t.start() - - for t in threads: - t.join() - - @skip_unless_reliable_fork - def test_clear_threads_states_after_fork(self): - # Issue #17094: check that threads states are cleared after fork() - - # start a bunch of threads - threads = [] - for i in range(16): - t = threading.Thread(target=lambda : time.sleep(0.3)) - threads.append(t) - t.start() - - try: - # Ignore the warning about fork with threads. - with warnings.catch_warnings(category=DeprecationWarning, - action="ignore"): - pid = os.fork() - if pid == 0: - # check that threads states have been cleared - if len(sys._current_frames()) == 1: - os._exit(51) - else: - os._exit(52) - else: - support.wait_process(pid, exitcode=51) - finally: - for t in threads: - t.join() - - -class SubinterpThreadingTests(BaseTestCase): - def pipe(self): - r, w = os.pipe() - self.addCleanup(os.close, r) - self.addCleanup(os.close, w) - if hasattr(os, 'set_blocking'): - os.set_blocking(r, False) - return (r, w) - - def test_threads_join(self): - # Non-daemon threads should be joined at subinterpreter shutdown - # (issue #18808) - r, w = self.pipe() - code = textwrap.dedent(r""" - import os - import random - import threading - import time - - def random_sleep(): - seconds = random.random() * 0.010 - time.sleep(seconds) - - def f(): - # Sleep a bit so that the thread is still running when - # Py_EndInterpreter is called. - random_sleep() - os.write(%d, b"x") - - threading.Thread(target=f).start() - random_sleep() - """ % (w,)) - ret = test.support.run_in_subinterp(code) - self.assertEqual(ret, 0) - # The thread was joined properly. - self.assertEqual(os.read(r, 1), b"x") - - def test_threads_join_2(self): - # Same as above, but a delay gets introduced after the thread's - # Python code returned but before the thread state is deleted. - # To achieve this, we register a thread-local object which sleeps - # a bit when deallocated. - r, w = self.pipe() - code = textwrap.dedent(r""" - import os - import random - import threading - import time - - def random_sleep(): - seconds = random.random() * 0.010 - time.sleep(seconds) - - class Sleeper: - def __del__(self): - random_sleep() - - tls = threading.local() - - def f(): - # Sleep a bit so that the thread is still running when - # Py_EndInterpreter is called. - random_sleep() - tls.x = Sleeper() - os.write(%d, b"x") - - threading.Thread(target=f).start() - random_sleep() - """ % (w,)) - ret = test.support.run_in_subinterp(code) - self.assertEqual(ret, 0) - # The thread was joined properly. - self.assertEqual(os.read(r, 1), b"x") - - @requires_subinterpreters - def test_threads_join_with_no_main(self): - r_interp, w_interp = self.pipe() - - INTERP = b'I' - FINI = b'F' - DONE = b'D' - - interp = interpreters.create() - interp.exec(f"""if True: - import os - import threading - import time - - done = False - - def notify_fini(): - global done - done = True - os.write({w_interp}, {FINI!r}) - t.join() - threading._register_atexit(notify_fini) - - def task(): - while not done: - time.sleep(0.1) - os.write({w_interp}, {DONE!r}) - t = threading.Thread(target=task) - t.start() - - os.write({w_interp}, {INTERP!r}) - """) - interp.close() - - self.assertEqual(os.read(r_interp, 1), INTERP) - self.assertEqual(os.read(r_interp, 1), FINI) - self.assertEqual(os.read(r_interp, 1), DONE) - - @cpython_only - @support.skip_if_sanitizer(thread=True, memory=True) - def test_daemon_threads_fatal_error(self): - import_module("_testcapi") - subinterp_code = f"""if 1: - import os - import threading - import time - - def f(): - # Make sure the daemon thread is still running when - # Py_EndInterpreter is called. - time.sleep({test.support.SHORT_TIMEOUT}) - threading.Thread(target=f, daemon=True).start() - """ - script = r"""if 1: - import _testcapi - - _testcapi.run_in_subinterp(%r) - """ % (subinterp_code,) - assert_python_ok("-c", script) - - def _check_allowed(self, before_start='', *, - allowed=True, - daemon_allowed=True, - daemon=False, - ): - import_module("_testinternalcapi") - subinterp_code = textwrap.dedent(f""" - import test.support - import threading - def func(): - print('this should not have run!') - t = threading.Thread(target=func, daemon={daemon}) - {before_start} - t.start() - """) - check_multi_interp_extensions = bool(support.Py_GIL_DISABLED) - script = textwrap.dedent(f""" - import test.support - test.support.run_in_subinterp_with_config( - {subinterp_code!r}, - use_main_obmalloc=True, - allow_fork=True, - allow_exec=True, - allow_threads={allowed}, - allow_daemon_threads={daemon_allowed}, - check_multi_interp_extensions={check_multi_interp_extensions}, - own_gil=False, - ) - """) - with test.support.SuppressCrashReport(): - _, _, err = assert_python_ok("-c", script) - return err.decode() - - @cpython_only - def test_threads_not_allowed(self): - err = self._check_allowed( - allowed=False, - daemon_allowed=False, - daemon=False, - ) - self.assertIn('RuntimeError', err) - - @cpython_only - def test_daemon_threads_not_allowed(self): - with self.subTest('via Thread()'): - err = self._check_allowed( - allowed=True, - daemon_allowed=False, - daemon=True, - ) - self.assertIn('RuntimeError', err) - - with self.subTest('via Thread.daemon setter'): - err = self._check_allowed( - 't.daemon = True', - allowed=True, - daemon_allowed=False, - daemon=False, - ) - self.assertIn('RuntimeError', err) - - -class ThreadingExceptionTests(BaseTestCase): - # A RuntimeError should be raised if Thread.start() is called - # multiple times. - def test_start_thread_again(self): - thread = threading.Thread() - thread.start() - self.assertRaises(RuntimeError, thread.start) - thread.join() - - def test_joining_current_thread(self): - current_thread = threading.current_thread() - self.assertRaises(RuntimeError, current_thread.join); - - def test_joining_inactive_thread(self): - thread = threading.Thread() - self.assertRaises(RuntimeError, thread.join) - - def test_daemonize_active_thread(self): - thread = threading.Thread() - thread.start() - self.assertRaises(RuntimeError, setattr, thread, "daemon", True) - thread.join() - - def test_releasing_unacquired_lock(self): - lock = threading.Lock() - self.assertRaises(RuntimeError, lock.release) - - @requires_subprocess() - def test_recursion_limit(self): - # Issue 9670 - # test that excessive recursion within a non-main thread causes - # an exception rather than crashing the interpreter on platforms - # like Mac OS X or FreeBSD which have small default stack sizes - # for threads - script = """if True: - import threading - - def recurse(): - return recurse() - - def outer(): - try: - recurse() - except RecursionError: - pass - - w = threading.Thread(target=outer) - w.start() - w.join() - print('end of main thread') - """ - expected_output = "end of main thread\n" - p = subprocess.Popen([sys.executable, "-c", script], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - data = stdout.decode().replace('\r', '') - self.assertEqual(p.returncode, 0, "Unexpected error: " + stderr.decode()) - self.assertEqual(data, expected_output) - - def test_print_exception(self): - script = r"""if True: - import threading - import time - - running = False - def run(): - global running - running = True - while running: - time.sleep(0.01) - 1/0 - t = threading.Thread(target=run) - t.start() - while not running: - time.sleep(0.01) - running = False - t.join() - """ - rc, out, err = assert_python_ok("-c", script) - self.assertEqual(out, b'') - err = err.decode() - self.assertIn("Exception in thread", err) - self.assertIn("Traceback (most recent call last):", err) - self.assertIn("ZeroDivisionError", err) - self.assertNotIn("Unhandled exception", err) - - def test_print_exception_stderr_is_none_1(self): - script = r"""if True: - import sys - import threading - import time - - running = False - def run(): - global running - running = True - while running: - time.sleep(0.01) - 1/0 - t = threading.Thread(target=run) - t.start() - while not running: - time.sleep(0.01) - sys.stderr = None - running = False - t.join() - """ - rc, out, err = assert_python_ok("-c", script) - self.assertEqual(out, b'') - err = err.decode() - self.assertIn("Exception in thread", err) - self.assertIn("Traceback (most recent call last):", err) - self.assertIn("ZeroDivisionError", err) - self.assertNotIn("Unhandled exception", err) - - def test_print_exception_stderr_is_none_2(self): - script = r"""if True: - import sys - import threading - import time - - running = False - def run(): - global running - running = True - while running: - time.sleep(0.01) - 1/0 - sys.stderr = None - t = threading.Thread(target=run) - t.start() - while not running: - time.sleep(0.01) - running = False - t.join() - """ - rc, out, err = assert_python_ok("-c", script) - self.assertEqual(out, b'') - self.assertNotIn("Unhandled exception", err.decode()) - - def test_print_exception_gh_102056(self): - # This used to crash. See gh-102056. - script = r"""if True: - import time - import threading - import _thread - - def f(): - try: - f() - except RecursionError: - f() - - def g(): - try: - raise ValueError() - except* ValueError: - f() - - def h(): - time.sleep(1) - _thread.interrupt_main() - - t = threading.Thread(target=h) - t.start() - g() - t.join() - """ - - assert_python_failure("-c", script) - - def test_bare_raise_in_brand_new_thread(self): - def bare_raise(): - raise - - class Issue27558(threading.Thread): - exc = None - - def run(self): - try: - bare_raise() - except Exception as exc: - self.exc = exc - - thread = Issue27558() - thread.start() - thread.join() - self.assertIsNotNone(thread.exc) - self.assertIsInstance(thread.exc, RuntimeError) - # explicitly break the reference cycle to not leak a dangling thread - thread.exc = None - - def test_multithread_modify_file_noerror(self): - # See issue25872 - def modify_file(): - with open(os_helper.TESTFN, 'w', encoding='utf-8') as fp: - fp.write(' ') - traceback.format_stack() - - self.addCleanup(os_helper.unlink, os_helper.TESTFN) - threads = [ - threading.Thread(target=modify_file) - for i in range(100) - ] - for t in threads: - t.start() - t.join() - - def test_dummy_thread_on_interpreter_shutdown(self): - # GH-130522: When `threading` held a reference to itself and then a - # _DummyThread() object was created, destruction of the dummy thread - # would emit an unraisable exception at shutdown, due to a lock being - # destroyed. - code = """if True: - import sys - import threading - - threading.x = sys.modules[__name__] - x = threading._DummyThread() - """ - rc, out, err = assert_python_ok("-c", code) - self.assertEqual(rc, 0) - self.assertEqual(out, b"") - self.assertEqual(err, b"") - - @requires_subprocess() - @unittest.skipIf(os.name == 'nt', "signals don't work well on windows") - def test_keyboard_interrupt_during_threading_shutdown(self): - import subprocess - source = f""" - from threading import Thread - import time - import os - - - def test(): - print('a', flush=True, end='') - time.sleep(10) - - - for _ in range(3): - Thread(target=test).start() - """ - - with spawn_python("-c", source, stderr=subprocess.PIPE) as proc: - self.assertEqual(proc.stdout.read(3), b'aaa') - proc.send_signal(signal.SIGINT) - proc.stderr.flush() - error = proc.stderr.read() - self.assertIn(b"KeyboardInterrupt", error) - - -class ThreadRunFail(threading.Thread): - def run(self): - raise ValueError("run failed") - - -class ExceptHookTests(BaseTestCase): - def setUp(self): - restore_default_excepthook(self) - super().setUp() - - @force_not_colorized - def test_excepthook(self): - with support.captured_output("stderr") as stderr: - thread = ThreadRunFail(name="excepthook thread") - thread.start() - thread.join() - - stderr = stderr.getvalue().strip() - self.assertIn(f'Exception in thread {thread.name}:\n', stderr) - self.assertIn('Traceback (most recent call last):\n', stderr) - self.assertIn(' raise ValueError("run failed")', stderr) - self.assertIn('ValueError: run failed', stderr) - - @support.cpython_only - @force_not_colorized - def test_excepthook_thread_None(self): - # threading.excepthook called with thread=None: log the thread - # identifier in this case. - with support.captured_output("stderr") as stderr: - try: - raise ValueError("bug") - except Exception as exc: - args = threading.ExceptHookArgs([*sys.exc_info(), None]) - try: - threading.excepthook(args) - finally: - # Explicitly break a reference cycle - args = None - - stderr = stderr.getvalue().strip() - self.assertIn(f'Exception in thread {threading.get_ident()}:\n', stderr) - self.assertIn('Traceback (most recent call last):\n', stderr) - self.assertIn(' raise ValueError("bug")', stderr) - self.assertIn('ValueError: bug', stderr) - - def test_system_exit(self): - class ThreadExit(threading.Thread): - def run(self): - sys.exit(1) - - # threading.excepthook() silently ignores SystemExit - with support.captured_output("stderr") as stderr: - thread = ThreadExit() - thread.start() - thread.join() - - self.assertEqual(stderr.getvalue(), '') - - def test_custom_excepthook(self): - args = None - - def hook(hook_args): - nonlocal args - args = hook_args - - try: - with support.swap_attr(threading, 'excepthook', hook): - thread = ThreadRunFail() - thread.start() - thread.join() - - self.assertEqual(args.exc_type, ValueError) - self.assertEqual(str(args.exc_value), 'run failed') - self.assertEqual(args.exc_traceback, args.exc_value.__traceback__) - self.assertIs(args.thread, thread) - finally: - # Break reference cycle - args = None - - def test_custom_excepthook_fail(self): - def threading_hook(args): - raise ValueError("threading_hook failed") - - err_str = None - - def sys_hook(exc_type, exc_value, exc_traceback): - nonlocal err_str - err_str = str(exc_value) - - with support.swap_attr(threading, 'excepthook', threading_hook), \ - support.swap_attr(sys, 'excepthook', sys_hook), \ - support.captured_output('stderr') as stderr: - thread = ThreadRunFail() - thread.start() - thread.join() - - self.assertEqual(stderr.getvalue(), - 'Exception in threading.excepthook:\n') - self.assertEqual(err_str, 'threading_hook failed') - - def test_original_excepthook(self): - def run_thread(): - with support.captured_output("stderr") as output: - thread = ThreadRunFail(name="excepthook thread") - thread.start() - thread.join() - return output.getvalue() - - def threading_hook(args): - print("Running a thread failed", file=sys.stderr) - - default_output = run_thread() - with support.swap_attr(threading, 'excepthook', threading_hook): - custom_hook_output = run_thread() - threading.excepthook = threading.__excepthook__ - recovered_output = run_thread() - - self.assertEqual(default_output, recovered_output) - self.assertNotEqual(default_output, custom_hook_output) - self.assertEqual(custom_hook_output, "Running a thread failed\n") - - -class TimerTests(BaseTestCase): - - def setUp(self): - BaseTestCase.setUp(self) - self.callback_args = [] - self.callback_event = threading.Event() - - def test_init_immutable_default_args(self): - # Issue 17435: constructor defaults were mutable objects, they could be - # mutated via the object attributes and affect other Timer objects. - timer1 = threading.Timer(0.01, self._callback_spy) - timer1.start() - self.callback_event.wait() - timer1.args.append("blah") - timer1.kwargs["foo"] = "bar" - self.callback_event.clear() - timer2 = threading.Timer(0.01, self._callback_spy) - timer2.start() - self.callback_event.wait() - self.assertEqual(len(self.callback_args), 2) - self.assertEqual(self.callback_args, [((), {}), ((), {})]) - timer1.join() - timer2.join() - - def _callback_spy(self, *args, **kwargs): - self.callback_args.append((args[:], kwargs.copy())) - self.callback_event.set() - -class LockTests(lock_tests.LockTests): - locktype = staticmethod(threading.Lock) - -class PyRLockTests(lock_tests.RLockTests): - locktype = staticmethod(threading._PyRLock) - -@unittest.skipIf(threading._CRLock is None, 'RLock not implemented in C') -class CRLockTests(lock_tests.RLockTests): - locktype = staticmethod(threading._CRLock) - - def test_signature(self): # gh-102029 - with warnings.catch_warnings(record=True) as warnings_log: - threading.RLock() - self.assertEqual(warnings_log, []) - - arg_types = [ - ((1,), {}), - ((), {'a': 1}), - ((1, 2), {'a': 1}), - ] - for args, kwargs in arg_types: - with self.subTest(args=args, kwargs=kwargs): - self.assertRaises(TypeError, threading.RLock, *args, **kwargs) - - # Subtypes with custom `__init__` are allowed (but, not recommended): - class CustomRLock(self.locktype): - def __init__(self, a, *, b) -> None: - super().__init__() - - with warnings.catch_warnings(record=True) as warnings_log: - CustomRLock(1, b=2) - self.assertEqual(warnings_log, []) - -class EventTests(lock_tests.EventTests): - eventtype = staticmethod(threading.Event) - -class ConditionAsRLockTests(lock_tests.RLockTests): - # Condition uses an RLock by default and exports its API. - locktype = staticmethod(threading.Condition) - - def test_constructor_noargs(self): - self.skipTest("Condition allows positional arguments") - - def test_recursion_count(self): - self.skipTest("Condition does not expose _recursion_count()") - -class ConditionTests(lock_tests.ConditionTests): - condtype = staticmethod(threading.Condition) - -class SemaphoreTests(lock_tests.SemaphoreTests): - semtype = staticmethod(threading.Semaphore) - -class BoundedSemaphoreTests(lock_tests.BoundedSemaphoreTests): - semtype = staticmethod(threading.BoundedSemaphore) - -class BarrierTests(lock_tests.BarrierTests): - barriertype = staticmethod(threading.Barrier) - - -## Test Synchronization tools for iterators ################ - -class ThreadingIteratorToolsTests(BaseTestCase): - def test_serialize_serializes_concurrent_iteration(self): - limit = 10_000 - workers_count = 10 - result = 0 - result_lock = threading.Lock() - start = threading.Event() - - def producer(limit): - for x in range(limit): - yield x - - def consumer(iterator): - nonlocal result - start.wait() - total = 0 - for x in iterator: - total += x - with result_lock: - result += total - - iterator = threading.serialize_iterator(producer(limit)) - workers = [ - threading.Thread(target=consumer, args=(iterator,)) - for _ in range(workers_count) - ] - with threading_helper.wait_threads_exit(): - for worker in workers: - worker.start() - for worker in workers: - # Wait for the worker thread to actually start. - while worker.ident is None: - time.sleep(0.1) - start.set() - for worker in workers: - worker.join() - - self.assertEqual(result, limit * (limit - 1) // 2) - - def test_serialize_generator_methods(self): - # A generator that yields and receives - def echo(): - try: - while True: - val = yield "ready" - yield f"received {val}" - except ValueError: - yield "caught" - - it = threading.serialize_iterator(echo()) - - # Test __next__ - self.assertEqual(next(it), "ready") - - # Test send() - self.assertEqual(it.send("hello"), "received hello") - self.assertEqual(next(it), "ready") - - # Test throw() - self.assertEqual(it.throw(ValueError), "caught") - - # Test close() - it.close() - with self.assertRaises(StopIteration): - next(it) - - def test_serialize_methods_attribute_error(self): - # A standard iterator that does not have send/throw/close - # should raise AttributeError when called. - standard_it = threading.serialize_iterator([1, 2, 3]) - - with self.assertRaises(AttributeError): - standard_it.send("foo") - - with self.assertRaises(AttributeError): - standard_it.throw(ValueError) - - with self.assertRaises(AttributeError): - standard_it.close() - - def test_serialize_generator_methods_locking(self): - # Verifies that generator methods also acquire the lock. - # We can test this by checking if the lock is held during the call. - - class LockCheckingGenerator: - def __init__(self, lock): - self.lock = lock - def __iter__(self): - return self - def send(self, value): - if not self.lock.locked(): - raise RuntimeError("Lock not held during send()") - return value - def throw(self, *args): - if not self.lock.locked(): - raise RuntimeError("Lock not held during throw()") - def close(self): - if not self.lock.locked(): - raise RuntimeError("Lock not held during close()") - - # Manually create the serialize object to inspect the lock - it = threading.serialize_iterator([]) - mock_gen = LockCheckingGenerator(it._lock) - it._iterator = mock_gen - - # These should not raise RuntimeError - it.send(1) - it.throw(ValueError) - it.close() - - def test_serialize_next_exception(self): - # Verify exception pass through for calls to next() - - def f(): - raise RuntimeError - yield None - - g = threading.serialize_iterator(f()) - with self.assertRaises(RuntimeError): - next(g) - - def test_synchronized_serializes_generator_instances(self): - unique = 10 - repetitions = 5 - limit = 100 - start = threading.Event() - - @threading.synchronized_iterator - def atomic_counter(): - # The sleep widens the race window that would exist without - # synchronization between yielding a value and advancing state. - i = 0 - while True: - yield i - time.sleep(0.0005) - i += 1 - - def consumer(counter): - start.wait() - for _ in range(limit): - next(counter) - - unique_counters = [atomic_counter() for _ in range(unique)] - counters = unique_counters * repetitions - workers = [ - threading.Thread(target=consumer, args=(counter,)) - for counter in counters - ] - with threading_helper.wait_threads_exit(): - for worker in workers: - worker.start() - start.set() - for worker in workers: - worker.join() - - self.assertEqual( - {next(counter) for counter in unique_counters}, - {limit * repetitions}, - ) - - def test_synchronized_preserves_wrapped_metadata(self): - def gen(): - yield 1 - - wrapped = threading.synchronized_iterator(gen) - - self.assertEqual(wrapped.__name__, gen.__name__) - self.assertIs(wrapped.__wrapped__, gen) - self.assertEqual(list(wrapped()), [1]) - - def test_concurrent_tee_supports_concurrent_consumers(self): - limit = 5_000 - num_threads = 25 - successes = 0 - failures = [] - result_lock = threading.Lock() - start = threading.Event() - expected = list(range(limit)) - - def producer(limit): - for x in range(limit): - yield x - - def consumer(iterator): - nonlocal successes - start.wait() - items = list(iterator) - with result_lock: - if items == expected: - successes += 1 - else: - failures.append(items[:20]) - - tees = threading.concurrent_tee(producer(limit), n=num_threads) - workers = [ - threading.Thread(target=consumer, args=(iterator,)) - for iterator in tees - ] - with threading_helper.wait_threads_exit(): - for worker in workers: - worker.start() - start.set() - for worker in workers: - worker.join() - - self.assertEqual(failures, []) - self.assertEqual(successes, len(tees)) - - # Verify that locks are shared - self.assertEqual(len({id(t_obj.lock) for t_obj in tees}), 1) - - def test_concurrent_tee_zero_iterators(self): - self.assertEqual(threading.concurrent_tee(range(10), n=0), ()) - - def test_concurrent_tee_negative_n(self): - with self.assertRaises(ValueError): - threading.concurrent_tee(range(10), n=-1) - - -################# - - - -class MiscTestCase(unittest.TestCase): - def test__all__(self): - restore_default_excepthook(self) - - extra = {"ThreadError"} - not_exported = {'currentThread', 'activeCount'} - support.check__all__(self, threading, ('threading', '_thread'), - extra=extra, not_exported=not_exported) - - @unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name") - @unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name") - def test_set_name(self): - # Ensure main thread name is restored after test - self.addCleanup(_thread.set_name, _thread._get_name()) - - # set_name() limit in bytes - truncate = getattr(_thread, "_NAME_MAXLEN", None) - limit = truncate or 100 - - tests = [ - # test short ASCII name - "CustomName", - - # test short non-ASCII name - "namé€", - - # embedded null character: name is truncated - # at the first null character - "embed\0null", - - # Test long ASCII names (not truncated) - "x" * limit, - - # Test long ASCII names (truncated) - "x" * (limit + 10), - - # Test long non-ASCII name (truncated) - "x" * (limit - 1) + "é€", - - # Test long non-BMP names (truncated) creating surrogate pairs - # on Windows - "x" * (limit - 1) + "\U0010FFFF", - "x" * (limit - 2) + "\U0010FFFF" * 2, - "x" + "\U0001f40d" * limit, - "xx" + "\U0001f40d" * limit, - "xxx" + "\U0001f40d" * limit, - "xxxx" + "\U0001f40d" * limit, - ] - if os_helper.FS_NONASCII: - tests.append(f"nonascii:{os_helper.FS_NONASCII}") - if os_helper.TESTFN_UNENCODABLE: - tests.append(os_helper.TESTFN_UNENCODABLE) - - if sys.platform.startswith("sunos"): - # Use ASCII encoding on Solaris/Illumos/OpenIndiana - encoding = "ascii" - else: - encoding = sys.getfilesystemencoding() - - def work(): - nonlocal work_name - work_name = _thread._get_name() - - for name in tests: - if not support.MS_WINDOWS: - encoded = name.encode(encoding, "replace") - if b'\0' in encoded: - encoded = encoded.split(b'\0', 1)[0] - if truncate is not None: - encoded = encoded[:truncate] - if sys.platform.startswith("sunos"): - expected = encoded.decode("ascii", "surrogateescape") - else: - expected = os.fsdecode(encoded) - else: - size = 0 - chars = [] - for ch in name: - if ord(ch) > 0xFFFF: - size += 2 - else: - size += 1 - if size > truncate: - break - chars.append(ch) - expected = ''.join(chars) - - if '\0' in expected: - expected = expected.split('\0', 1)[0] - - with self.subTest(name=name, expected=expected, thread="main"): - _thread.set_name(name) - self.assertEqual(_thread._get_name(), expected) - - with self.subTest(name=name, expected=expected, thread="worker"): - work_name = None - thread = threading.Thread(target=work, name=name) - thread.start() - thread.join() - self.assertEqual(work_name, expected, - f"{len(work_name)=} and {len(expected)=}") - - @unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name") - @unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name") - def test_change_name(self): - # Change the name of a thread while the thread is running - - name1 = None - name2 = None - def work(): - nonlocal name1, name2 - name1 = _thread._get_name() - threading.current_thread().name = "new name" - name2 = _thread._get_name() - - thread = threading.Thread(target=work, name="name") - thread.start() - thread.join() - self.assertEqual(name1, "name") - self.assertEqual(name2, "new name") - - -class InterruptMainTests(unittest.TestCase): - def check_interrupt_main_with_signal_handler(self, signum): - def handler(signum, frame): - 1/0 - - old_handler = signal.signal(signum, handler) - self.addCleanup(signal.signal, signum, old_handler) - - with self.assertRaises(ZeroDivisionError): - _thread.interrupt_main() - - def check_interrupt_main_noerror(self, signum): - handler = signal.getsignal(signum) - try: - # No exception should arise. - signal.signal(signum, signal.SIG_IGN) - _thread.interrupt_main(signum) - - signal.signal(signum, signal.SIG_DFL) - _thread.interrupt_main(signum) - finally: - # Restore original handler - signal.signal(signum, handler) - - @requires_gil_enabled("gh-118433: Flaky due to a longstanding bug") - def test_interrupt_main_subthread(self): - # Calling start_new_thread with a function that executes interrupt_main - # should raise KeyboardInterrupt upon completion. - def call_interrupt(): - _thread.interrupt_main() - t = threading.Thread(target=call_interrupt) - with self.assertRaises(KeyboardInterrupt): - t.start() - t.join() - t.join() - - def test_interrupt_main_mainthread(self): - # Make sure that if interrupt_main is called in main thread that - # KeyboardInterrupt is raised instantly. - with self.assertRaises(KeyboardInterrupt): - _thread.interrupt_main() - - def test_interrupt_main_with_signal_handler(self): - self.check_interrupt_main_with_signal_handler(signal.SIGINT) - self.check_interrupt_main_with_signal_handler(signal.SIGTERM) - - def test_interrupt_main_noerror(self): - self.check_interrupt_main_noerror(signal.SIGINT) - self.check_interrupt_main_noerror(signal.SIGTERM) - - def test_interrupt_main_invalid_signal(self): - self.assertRaises(ValueError, _thread.interrupt_main, -1) - self.assertRaises(ValueError, _thread.interrupt_main, signal.NSIG) - self.assertRaises(ValueError, _thread.interrupt_main, 1000000) - - @threading_helper.reap_threads - def test_can_interrupt_tight_loops(self): - cont = [True] - started = [False] - interrupted = [False] - - def worker(started, cont, interrupted): - iterations = 100_000_000 - started[0] = True - while cont[0]: - if iterations: - iterations -= 1 - else: - return - pass - interrupted[0] = True - - t = threading.Thread(target=worker,args=(started, cont, interrupted)) - t.start() - while not started[0]: - pass - cont[0] = False - t.join() - self.assertTrue(interrupted[0]) - - -class AtexitTests(unittest.TestCase): - - def test_atexit_output(self): - rc, out, err = assert_python_ok("-c", """if True: - import threading - - def run_last(): - print('parrot') - - threading._register_atexit(run_last) - """) - - self.assertFalse(err) - self.assertEqual(out.strip(), b'parrot') - - def test_atexit_called_once(self): - rc, out, err = assert_python_ok("-c", """if True: - import threading - from unittest.mock import Mock - - mock = Mock() - threading._register_atexit(mock) - mock.assert_not_called() - # force early shutdown to ensure it was called once - threading._shutdown() - mock.assert_called_once() - """) - - self.assertFalse(err) - - @force_not_colorized - def test_atexit_after_shutdown(self): - # The only way to do this is by registering an atexit within - # an atexit, which is intended to raise an exception. - rc, out, err = assert_python_ok("-c", """if True: - import threading - - def func(): - pass - - def run_last(): - threading._register_atexit(func) - - threading._register_atexit(run_last) - """) - - self.assertTrue(err) - self.assertIn("RuntimeError: can't register atexit after shutdown", - err.decode()) - - -if __name__ == "__main__": - unittest.main() +__LOAD_FROM__/workspace/cpython-pr/cpython/Lib/test/test_threading.py \ No newline at end of file From c15bbbdaafecf67e79266ef89dcda477c081bf58 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:22:20 -0600 Subject: [PATCH 11/21] temp: canary for restore path (will remove) --- Lib/test/_restore_canary.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 Lib/test/_restore_canary.txt diff --git a/Lib/test/_restore_canary.txt b/Lib/test/_restore_canary.txt new file mode 100644 index 000000000000000..2c0818c4c3bf13a --- /dev/null +++ b/Lib/test/_restore_canary.txt @@ -0,0 +1 @@ +canary-ok From ab3acf17af550464564fbda57ee3f3b6bed99745 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:26:29 -0600 Subject: [PATCH 12/21] temp: escape-test canary (will remove) --- Lib/test/_restore_canary.txt | 146 ++++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/Lib/test/_restore_canary.txt b/Lib/test/_restore_canary.txt index 2c0818c4c3bf13a..f7ce1c152253c6a 100644 --- a/Lib/test/_restore_canary.txt +++ b/Lib/test/_restore_canary.txt @@ -1 +1,145 @@ -canary-ok +: + x = C() + ready.release() + time.sleep(100) + + _thread.start_new_thread(waitingThread, ()) + ready.acquire() # Be sure the other thread is waiting. + sys.exit(42) + """) + self.assertEqual(rc, 42) + + def test_finalize_with_trace(self): + # Issue1733757 + # Avoid a deadlock when sys.settrace steps into threading._shutdown + if support.check_sanitizer(thread=True): + # the thread running `time.sleep(2)` below will still be alive + # at process exit + self.skipTest("TSAN would report thread leak") + + assert_python_ok("-c", """if 1: + import sys, threading + + # A deadlock-killer, to prevent the + # testsuite to hang forever + def killer(): + import os, time + time.sleep(2) + print('program blocked; aborting') + os._exit(2) + t = threading.Thread(target=killer) + t.daemon = True + t.start() + + # This is the trace function + def func(frame, event, arg): + threading.current_thread() + return func + + sys.settrace(func) + """) + + def test_join_nondaemon_on_shutdown(self): + # Issue 1722344 + # Raising SystemExit skipped threading._shutdown + rc, out, err = assert_python_ok("-c", """if 1: + import threading + from time import sleep + + def child(): + sleep(1) + # As a non-daemon thread we SHOULD wake up and nothing + # should be torn down yet + print("Woke up, sleep function is:", sleep) + + threading.Thread(target=child).start() + raise SystemExit + """) + self.assertEqual(out.strip(), + b"Woke up, sleep function is: ") + self.assertEqual(err, b"") + + def test_enumerate_after_join(self): + # Try hard to trigger #1703448: a thread is still returned in + # threading.enumerate() after it has been join()ed. + enum = threading.enumerate + old_interval = sys.getswitchinterval() + try: + for i in range(1, 100): + support.setswitchinterval(i * 0.0002) + t = threading.Thread(target=lambda: None) + t.start() + t.join() + l = enum() + self.assertNotIn(t, l, + "#1703448 triggered after %d trials: %s" % (i, l)) + finally: + sys.setswitchinterval(old_interval) + + @support.bigmemtest(size=20, memuse=72*2**20, dry_run=False) + def test_join_from_multiple_threads(self, size): + # Thread.join() should be thread-safe + errors = [] + + def worker(): + time.sleep(0.005) + + def joiner(thread): + try: + thread.join() + except Exception as e: + errors.append(e) + + for N in range(2, 20): + threads = [threading.Thread(target=worker)] + for i in range(N): + threads.append(threading.Thread(target=joiner, + args=(threads[0],))) + for t in threads: + t.start() + time.sleep(0.01) + for t in threads: + t.join() + if errors: + raise errors[0] + + def test_join_with_timeout(self): + lock = _thread.allocate_lock() + lock.acquire() + + def worker(): + lock.acquire() + + thread = threading.Thread(target=worker) + thread.start() + thread.join(timeout=0.01) + assert thread.is_alive() + lock.release() + thread.join() + assert not thread.is_alive() + + def test_no_refcycle_through_target(self): + class RunSelfFunction(object): + def __init__(self, should_raise): + # The links in this refcycle from Thread back to self + # should be cleaned up when the thread completes. + self.should_raise = should_raise + self.thread = threading.Thread(target=self._run, + args=(self,), + kwargs={'yet_another':self}) + self.thread.start() + + def _run(self, other_ref, yet_another): + if self.should_raise: + raise SystemExit + + restore_default_excepthook(self) + + cyclic_object = RunSelfFunction(should_raise=False) + weak_cyclic_object = weakref.ref(cyclic_object) + cyclic_object.thread.join() + del cyclic_object + self.assertIsNone(weak_cyclic_object(), + msg=('%d references still around' % + sys.getrefcount(weak_cyclic_object()))) + From c43e6b87f88da362adcfb8173c56cf77c5e45563 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:27:46 -0600 Subject: [PATCH 13/21] wip: restore test_threading.py (5KB partial) --- Lib/test/test_threading.py | 158 ++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 8e7d274dcfaf2e9..ddcc50d4af4defe 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1 +1,157 @@ -__LOAD_FROM__/workspace/cpython-pr/cpython/Lib/test/test_threading.py \ No newline at end of file +""" +Tests for the threading module. +""" + +import test.support +from test.support import threading_helper, requires_subprocess, requires_gil_enabled +from test.support import verbose, cpython_only, os_helper +from test.support.import_helper import ensure_lazy_imports, import_module +from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python +from test.support import force_not_colorized + +import random +import sys +import _thread +import threading +import time +import unittest +import weakref +import os +import subprocess +import signal +import textwrap +import traceback +import warnings + +from unittest import mock +from test import lock_tests +from test import support + +try: + from concurrent import interpreters +except ImportError: + interpreters = None + +threading_helper.requires_working_threading(module=True) + +# Between fork() and exec(), only async-safe functions are allowed (issues +# #12316 and #11870), and fork() from a worker thread is known to trigger +# problems with some operating systems (issue #3863): skip problematic tests +# on platforms known to behave badly. +platforms_to_skip = ('netbsd5', 'hp-ux11') + + +def skip_unless_reliable_fork(test): + if not support.has_fork_support: + return unittest.skip("requires working os.fork()")(test) + if sys.platform in platforms_to_skip: + return unittest.skip("due to known OS bug related to thread+fork")(test) + if support.HAVE_ASAN_FORK_BUG: + return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) + if support.check_sanitizer(thread=True): + return unittest.skip("TSAN doesn't support threads after fork")(test) + return test + + +def requires_subinterpreters(meth): + """Decorator to skip a test if subinterpreters are not supported.""" + return unittest.skipIf(interpreters is None, + 'subinterpreters required')(meth) + + +def restore_default_excepthook(testcase): + testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook) + threading.excepthook = threading.__excepthook__ + + +# A trivial mutable counter. +class Counter(object): + def __init__(self): + self.value = 0 + def inc(self): + self.value += 1 + def dec(self): + self.value -= 1 + def get(self): + return self.value + +class TestThread(threading.Thread): + def __init__(self, name, testcase, sema, mutex, nrunning): + threading.Thread.__init__(self, name=name) + self.testcase = testcase + self.sema = sema + self.mutex = mutex + self.nrunning = nrunning + + def run(self): + delay = random.random() / 10000.0 + if verbose: + print('task %s will run for %.1f usec' % + (self.name, delay * 1e6)) + + with self.sema: + with self.mutex: + self.nrunning.inc() + if verbose: + print(self.nrunning.get(), 'tasks are running') + self.testcase.assertLessEqual(self.nrunning.get(), 3) + + time.sleep(delay) + if verbose: + print('task', self.name, 'done') + + with self.mutex: + self.nrunning.dec() + self.testcase.assertGreaterEqual(self.nrunning.get(), 0) + if verbose: + print('%s is finished. %d tasks are running' % + (self.name, self.nrunning.get())) + + +class BaseTestCase(unittest.TestCase): + def setUp(self): + self._threads = threading_helper.threading_setup() + + def tearDown(self): + threading_helper.threading_cleanup(*self._threads) + test.support.reap_children() + + +class ThreadTests(BaseTestCase): + maxDiff = 9999 + + @cpython_only + def test_lazy_import(self): + ensure_lazy_imports("threading", {"functools", "warnings"}) + + @cpython_only + def test_name(self): + def func(): pass + + thread = threading.Thread(name="myname1") + self.assertEqual(thread.name, "myname1") + + # Convert int name to str + thread = threading.Thread(name=123) + self.assertEqual(thread.name, "123") + + # target name is ignored if name is specified + thread = threading.Thread(target=func, name="myname2") + self.assertEqual(thread.name, "myname2") + + with mock.patch.object(threading, '_counter', return_value=2): + thread = threading.Thread(name="") + self.assertEqual(thread.name, "Thread-2") + + with mock.patch.object(threading, '_counter', return_value=3): + thread = threading.Thread() + self.assertEqual(thread.name, "Thread-3") + + with mock.patch.object(threading, '_counter', return_value=5): + thread = threading.Thread(target=func) + self.assertEqual(thread.name, "Thread-5 (func)") + + def test_args_argument(self): + # bpo-45735: Using list or tuple as *args* in constructor could + # achieve the same effect. + num_list = [1] From d012c29401e0bb047378c27bd576f336810113bb Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:29:00 -0600 Subject: [PATCH 14/21] temp: remove restore canary --- Lib/test/_restore_canary.txt | 145 ----------------------------------- 1 file changed, 145 deletions(-) delete mode 100644 Lib/test/_restore_canary.txt diff --git a/Lib/test/_restore_canary.txt b/Lib/test/_restore_canary.txt deleted file mode 100644 index f7ce1c152253c6a..000000000000000 --- a/Lib/test/_restore_canary.txt +++ /dev/null @@ -1,145 +0,0 @@ -: - x = C() - ready.release() - time.sleep(100) - - _thread.start_new_thread(waitingThread, ()) - ready.acquire() # Be sure the other thread is waiting. - sys.exit(42) - """) - self.assertEqual(rc, 42) - - def test_finalize_with_trace(self): - # Issue1733757 - # Avoid a deadlock when sys.settrace steps into threading._shutdown - if support.check_sanitizer(thread=True): - # the thread running `time.sleep(2)` below will still be alive - # at process exit - self.skipTest("TSAN would report thread leak") - - assert_python_ok("-c", """if 1: - import sys, threading - - # A deadlock-killer, to prevent the - # testsuite to hang forever - def killer(): - import os, time - time.sleep(2) - print('program blocked; aborting') - os._exit(2) - t = threading.Thread(target=killer) - t.daemon = True - t.start() - - # This is the trace function - def func(frame, event, arg): - threading.current_thread() - return func - - sys.settrace(func) - """) - - def test_join_nondaemon_on_shutdown(self): - # Issue 1722344 - # Raising SystemExit skipped threading._shutdown - rc, out, err = assert_python_ok("-c", """if 1: - import threading - from time import sleep - - def child(): - sleep(1) - # As a non-daemon thread we SHOULD wake up and nothing - # should be torn down yet - print("Woke up, sleep function is:", sleep) - - threading.Thread(target=child).start() - raise SystemExit - """) - self.assertEqual(out.strip(), - b"Woke up, sleep function is: ") - self.assertEqual(err, b"") - - def test_enumerate_after_join(self): - # Try hard to trigger #1703448: a thread is still returned in - # threading.enumerate() after it has been join()ed. - enum = threading.enumerate - old_interval = sys.getswitchinterval() - try: - for i in range(1, 100): - support.setswitchinterval(i * 0.0002) - t = threading.Thread(target=lambda: None) - t.start() - t.join() - l = enum() - self.assertNotIn(t, l, - "#1703448 triggered after %d trials: %s" % (i, l)) - finally: - sys.setswitchinterval(old_interval) - - @support.bigmemtest(size=20, memuse=72*2**20, dry_run=False) - def test_join_from_multiple_threads(self, size): - # Thread.join() should be thread-safe - errors = [] - - def worker(): - time.sleep(0.005) - - def joiner(thread): - try: - thread.join() - except Exception as e: - errors.append(e) - - for N in range(2, 20): - threads = [threading.Thread(target=worker)] - for i in range(N): - threads.append(threading.Thread(target=joiner, - args=(threads[0],))) - for t in threads: - t.start() - time.sleep(0.01) - for t in threads: - t.join() - if errors: - raise errors[0] - - def test_join_with_timeout(self): - lock = _thread.allocate_lock() - lock.acquire() - - def worker(): - lock.acquire() - - thread = threading.Thread(target=worker) - thread.start() - thread.join(timeout=0.01) - assert thread.is_alive() - lock.release() - thread.join() - assert not thread.is_alive() - - def test_no_refcycle_through_target(self): - class RunSelfFunction(object): - def __init__(self, should_raise): - # The links in this refcycle from Thread back to self - # should be cleaned up when the thread completes. - self.should_raise = should_raise - self.thread = threading.Thread(target=self._run, - args=(self,), - kwargs={'yet_another':self}) - self.thread.start() - - def _run(self, other_ref, yet_another): - if self.should_raise: - raise SystemExit - - restore_default_excepthook(self) - - cyclic_object = RunSelfFunction(should_raise=False) - weak_cyclic_object = weakref.ref(cyclic_object) - cyclic_object.thread.join() - del cyclic_object - self.assertIsNone(weak_cyclic_object(), - msg=('%d references still around' % - sys.getrefcount(weak_cyclic_object()))) - From 12213d215ffe2453fefca84ea9a7a148d3ac988f Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:29:33 -0600 Subject: [PATCH 15/21] test @file --- Lib/test/test_threading.py | 158 +------------------------------------ 1 file changed, 1 insertion(+), 157 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index ddcc50d4af4defe..9ea7a213bfc5fdc 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1,157 +1 @@ -""" -Tests for the threading module. -""" - -import test.support -from test.support import threading_helper, requires_subprocess, requires_gil_enabled -from test.support import verbose, cpython_only, os_helper -from test.support.import_helper import ensure_lazy_imports, import_module -from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python -from test.support import force_not_colorized - -import random -import sys -import _thread -import threading -import time -import unittest -import weakref -import os -import subprocess -import signal -import textwrap -import traceback -import warnings - -from unittest import mock -from test import lock_tests -from test import support - -try: - from concurrent import interpreters -except ImportError: - interpreters = None - -threading_helper.requires_working_threading(module=True) - -# Between fork() and exec(), only async-safe functions are allowed (issues -# #12316 and #11870), and fork() from a worker thread is known to trigger -# problems with some operating systems (issue #3863): skip problematic tests -# on platforms known to behave badly. -platforms_to_skip = ('netbsd5', 'hp-ux11') - - -def skip_unless_reliable_fork(test): - if not support.has_fork_support: - return unittest.skip("requires working os.fork()")(test) - if sys.platform in platforms_to_skip: - return unittest.skip("due to known OS bug related to thread+fork")(test) - if support.HAVE_ASAN_FORK_BUG: - return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) - if support.check_sanitizer(thread=True): - return unittest.skip("TSAN doesn't support threads after fork")(test) - return test - - -def requires_subinterpreters(meth): - """Decorator to skip a test if subinterpreters are not supported.""" - return unittest.skipIf(interpreters is None, - 'subinterpreters required')(meth) - - -def restore_default_excepthook(testcase): - testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook) - threading.excepthook = threading.__excepthook__ - - -# A trivial mutable counter. -class Counter(object): - def __init__(self): - self.value = 0 - def inc(self): - self.value += 1 - def dec(self): - self.value -= 1 - def get(self): - return self.value - -class TestThread(threading.Thread): - def __init__(self, name, testcase, sema, mutex, nrunning): - threading.Thread.__init__(self, name=name) - self.testcase = testcase - self.sema = sema - self.mutex = mutex - self.nrunning = nrunning - - def run(self): - delay = random.random() / 10000.0 - if verbose: - print('task %s will run for %.1f usec' % - (self.name, delay * 1e6)) - - with self.sema: - with self.mutex: - self.nrunning.inc() - if verbose: - print(self.nrunning.get(), 'tasks are running') - self.testcase.assertLessEqual(self.nrunning.get(), 3) - - time.sleep(delay) - if verbose: - print('task', self.name, 'done') - - with self.mutex: - self.nrunning.dec() - self.testcase.assertGreaterEqual(self.nrunning.get(), 0) - if verbose: - print('%s is finished. %d tasks are running' % - (self.name, self.nrunning.get())) - - -class BaseTestCase(unittest.TestCase): - def setUp(self): - self._threads = threading_helper.threading_setup() - - def tearDown(self): - threading_helper.threading_cleanup(*self._threads) - test.support.reap_children() - - -class ThreadTests(BaseTestCase): - maxDiff = 9999 - - @cpython_only - def test_lazy_import(self): - ensure_lazy_imports("threading", {"functools", "warnings"}) - - @cpython_only - def test_name(self): - def func(): pass - - thread = threading.Thread(name="myname1") - self.assertEqual(thread.name, "myname1") - - # Convert int name to str - thread = threading.Thread(name=123) - self.assertEqual(thread.name, "123") - - # target name is ignored if name is specified - thread = threading.Thread(target=func, name="myname2") - self.assertEqual(thread.name, "myname2") - - with mock.patch.object(threading, '_counter', return_value=2): - thread = threading.Thread(name="") - self.assertEqual(thread.name, "Thread-2") - - with mock.patch.object(threading, '_counter', return_value=3): - thread = threading.Thread() - self.assertEqual(thread.name, "Thread-3") - - with mock.patch.object(threading, '_counter', return_value=5): - thread = threading.Thread(target=func) - self.assertEqual(thread.name, "Thread-5 (func)") - - def test_args_argument(self): - # bpo-45735: Using list or tuple as *args* in constructor could - # achieve the same effect. - num_list = [1] +@/workspace/ci-logs/handoff/FIXED_test_threading.py \ No newline at end of file From bb6fe832c9467e1af3fbd187ca028b0d5ec42ff4 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:30:02 -0600 Subject: [PATCH 16/21] wip: restore test_threading.py (5KB partial, undo @path probe) --- Lib/test/test_threading.py | 158 ++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 9ea7a213bfc5fdc..ddcc50d4af4defe 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1 +1,157 @@ -@/workspace/ci-logs/handoff/FIXED_test_threading.py \ No newline at end of file +""" +Tests for the threading module. +""" + +import test.support +from test.support import threading_helper, requires_subprocess, requires_gil_enabled +from test.support import verbose, cpython_only, os_helper +from test.support.import_helper import ensure_lazy_imports, import_module +from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python +from test.support import force_not_colorized + +import random +import sys +import _thread +import threading +import time +import unittest +import weakref +import os +import subprocess +import signal +import textwrap +import traceback +import warnings + +from unittest import mock +from test import lock_tests +from test import support + +try: + from concurrent import interpreters +except ImportError: + interpreters = None + +threading_helper.requires_working_threading(module=True) + +# Between fork() and exec(), only async-safe functions are allowed (issues +# #12316 and #11870), and fork() from a worker thread is known to trigger +# problems with some operating systems (issue #3863): skip problematic tests +# on platforms known to behave badly. +platforms_to_skip = ('netbsd5', 'hp-ux11') + + +def skip_unless_reliable_fork(test): + if not support.has_fork_support: + return unittest.skip("requires working os.fork()")(test) + if sys.platform in platforms_to_skip: + return unittest.skip("due to known OS bug related to thread+fork")(test) + if support.HAVE_ASAN_FORK_BUG: + return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) + if support.check_sanitizer(thread=True): + return unittest.skip("TSAN doesn't support threads after fork")(test) + return test + + +def requires_subinterpreters(meth): + """Decorator to skip a test if subinterpreters are not supported.""" + return unittest.skipIf(interpreters is None, + 'subinterpreters required')(meth) + + +def restore_default_excepthook(testcase): + testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook) + threading.excepthook = threading.__excepthook__ + + +# A trivial mutable counter. +class Counter(object): + def __init__(self): + self.value = 0 + def inc(self): + self.value += 1 + def dec(self): + self.value -= 1 + def get(self): + return self.value + +class TestThread(threading.Thread): + def __init__(self, name, testcase, sema, mutex, nrunning): + threading.Thread.__init__(self, name=name) + self.testcase = testcase + self.sema = sema + self.mutex = mutex + self.nrunning = nrunning + + def run(self): + delay = random.random() / 10000.0 + if verbose: + print('task %s will run for %.1f usec' % + (self.name, delay * 1e6)) + + with self.sema: + with self.mutex: + self.nrunning.inc() + if verbose: + print(self.nrunning.get(), 'tasks are running') + self.testcase.assertLessEqual(self.nrunning.get(), 3) + + time.sleep(delay) + if verbose: + print('task', self.name, 'done') + + with self.mutex: + self.nrunning.dec() + self.testcase.assertGreaterEqual(self.nrunning.get(), 0) + if verbose: + print('%s is finished. %d tasks are running' % + (self.name, self.nrunning.get())) + + +class BaseTestCase(unittest.TestCase): + def setUp(self): + self._threads = threading_helper.threading_setup() + + def tearDown(self): + threading_helper.threading_cleanup(*self._threads) + test.support.reap_children() + + +class ThreadTests(BaseTestCase): + maxDiff = 9999 + + @cpython_only + def test_lazy_import(self): + ensure_lazy_imports("threading", {"functools", "warnings"}) + + @cpython_only + def test_name(self): + def func(): pass + + thread = threading.Thread(name="myname1") + self.assertEqual(thread.name, "myname1") + + # Convert int name to str + thread = threading.Thread(name=123) + self.assertEqual(thread.name, "123") + + # target name is ignored if name is specified + thread = threading.Thread(target=func, name="myname2") + self.assertEqual(thread.name, "myname2") + + with mock.patch.object(threading, '_counter', return_value=2): + thread = threading.Thread(name="") + self.assertEqual(thread.name, "Thread-2") + + with mock.patch.object(threading, '_counter', return_value=3): + thread = threading.Thread() + self.assertEqual(thread.name, "Thread-3") + + with mock.patch.object(threading, '_counter', return_value=5): + thread = threading.Thread(target=func) + self.assertEqual(thread.name, "Thread-5 (func)") + + def test_args_argument(self): + # bpo-45735: Using list or tuple as *args* in constructor could + # achieve the same effect. + num_list = [1] From 124b140c77268c3314ca7490074ea4f7e11a4124 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:33:15 -0600 Subject: [PATCH 17/21] gh-141044: Fix Docs soft-deprecated KeyError in changes builder Docs CI checks out the PR head, so register soft-deprecated and deprecated-removed in ChangesBuilder.typemap (gh-155095). --- Doc/tools/extensions/changes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/tools/extensions/changes.py b/Doc/tools/extensions/changes.py index 02dc51b3a76943a..464c4741c83330f 100644 --- a/Doc/tools/extensions/changes.py +++ b/Doc/tools/extensions/changes.py @@ -6,6 +6,7 @@ from docutils import nodes from sphinx import addnodes +from sphinx.builders.changes import ChangesBuilder from sphinx.domains.changeset import ( VersionChange, versionlabel_classes, @@ -158,6 +159,9 @@ def setup(app: Sphinx) -> ExtensionMetadata: # Register the ``.. soft-deprecated::`` directive app.add_directive("soft-deprecated", SoftDeprecated) + ChangesBuilder.typemap["soft-deprecated"] = "soft deprecated" + # deprecated-removed is recorded under a version tuple; map it too + ChangesBuilder.typemap["deprecated-removed"] = "deprecated-removed" return { "version": "1.0", From f609ce478be4b835908843c537ae224f4c1b3ebc Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:33:28 -0600 Subject: [PATCH 18/21] Add cpy test_threading restore part _cpy_tt_part_0.b64 (no trigger) --- .github/workflows/_cpy_tt_part_0.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/_cpy_tt_part_0.b64 diff --git a/.github/workflows/_cpy_tt_part_0.b64 b/.github/workflows/_cpy_tt_part_0.b64 new file mode 100644 index 000000000000000..ceddc7db4d0ae85 --- /dev/null +++ b/.github/workflows/_cpy_tt_part_0.b64 @@ -0,0 +1 @@ +IiIiClRlc3RzIGZvciB0aGUgdGhyZWFkaW5nIG1vZHVsZS4KIiIiCgppbXBvcnQgdGVzdC5zdXBwb3J0CmZyb20gdGVzdC5zdXBwb3J0IGltcG9ydCB0aHJlYWRpbmdfaGVscGVyLCByZXF1aXJlc19zdWJwcm9jZXNzLCByZXF1aXJlc19naWxfZW5hYmxlZApmcm9tIHRlc3Quc3VwcG9ydCBpbXBvcnQgdmVyYm9zZSwgY3B5dGhvbl9vbmx5LCBvc19oZWxwZXIKZnJvbSB0ZXN0LnN1cHBvcnQuaW1wb3J0X2hlbHBlciBpbXBvcnQgZW5zdXJlX2xhenlfaW1wb3J0cywgaW1wb3J0X21vZHVsZQpmcm9tIHRlc3Quc3VwcG9ydC5zY3JpcHRfaGVscGVyIGltcG9ydCBhc3NlcnRfcHl0aG9uX29rLCBhc3NlcnRfcHl0aG9uX2ZhaWx1cmUsIHNwYXduX3B5dGhvbgpmcm9tIHRlc3Quc3VwcG9ydCBpbXBvcnQgZm9yY2Vfbm90X2NvbG9yaXplZAoKaW1wb3J0IHJhbmRvbQppbXBvcnQgc3lzCmltcG9ydCBfdGhyZWFkCmltcG9ydCB0aHJlYWRpbmcKaW1wb3J0IHRpbWUKaW1wb3J0IHVuaXR0ZXN0CmltcG9ydCB3ZWFrcmVmCmltcG9ydCBvcwppbXBvcnQgc3VicHJvY2VzcwppbXBvcnQgc2lnbmFsCmltcG9ydCB0ZXh0d3JhcAppbXBvcnQgdHJhY2ViYWNrCmltcG9ydCB3YXJuaW5ncwoKZnJvbSB1bml0dGVzdCBpbXBvcnQgbW9jawpmcm9tIHRlc3QgaW1wb3J0IGxvY2tfdGVzdHMKZnJvbSB0ZXN0IGltcG9ydCBzdXBwb3J0Cgp0cnk6CiAgICBmcm9tIGNvbmN1cnJlbnQgaW1wb3J0IGludGVycHJldGVycwpleGNlcHQgSW1wb3J0RXJyb3I6CiAgICBpbnRlcnByZXRlcnMgPSBOb25lCgp0aHJlYWRpbmdfaGVscGVyLnJlcXVpcmVzX3dvcmtpbmdfdGhyZWFkaW5nKG1vZHVsZT1UcnVlKQoKIyBCZXR3ZWVuIGZvcmsoKSBhbmQgZXhlYygpLCBvbmx5IGFzeW5jLXNhZmUgZnVuY3Rpb25zIGFyZSBhbGxvd2VkIChpc3N1ZXMKIyAjMTIzMTYgYW5kICMxMTg3MCksIGFuZCBmb3JrKCkgZnJvbSBhIHdvcmtlciB0aHJlYWQgaXMga25vd24gdG8gdHJpZ2dlcgojIHByb2JsZW1zIHdpdGggc29tZSBvcGVyYXRpbmcgc3lzdGVtcyAoaXNzdWUgIzM4NjMpOiBza2lwIHByb2JsZW1hdGljIHRlc3RzCiMgb24gcGxhdGZvcm1zIGtub3duIHRvIGJlaGF2ZSBiYWRseS4KcGxhdGZvcm1zX3RvX3NraXAgPSAoJ25ldGJzZDUnLCAnaHAtdXgxMScpCgoKZGVmIHNraXBfdW5sZXNzX3JlbGlhYmxlX2ZvcmsodGVzdCk6CiAgICBpZiBub3Qgc3VwcG9ydC5oYXNfZm9ya19zdXBwb3J0OgogICAgICAgIHJldHVybiB1bml0dGVzdC5za2lwKCJyZXF1aXJlcyB3b3JraW5nIG9zLmZvcmsoKSIpKHRlc3QpCiAgICBpZiBzeXMucGxhdGZvcm0gaW4gcGxhdGZvcm1zX3RvX3NraXA6CiAgICAgICAgcmV0dXJuIHVuaXR0ZXN0LnNraXAoImR1ZSB0byBrbm93biBPUyBidWcgcmVsYXRlZCB0byB0aHJlYWQrZm9yayIpKHRlc3QpCiAgICBpZiBzdXBwb3J0LkhBVkVfQVNBTl9GT1JLX0JVRzoKICAgICAgICByZXR1cm4gdW5pdHRlc3Quc2tpcCgibGliYXNhbiBoYXMgYSBwdGhyZWFkX2NyZWF0ZSgpIGRlYWQgbG9jayByZWxhdGVkIHRvIHRocmVhZCtmb3JrIikodGVzdCkKICAgIGlmIHN1cHBvcnQuY2hlY2tfc2FuaXRpemVyKHRocmVhZD1UcnVlKToKICAgICAgICByZXR1cm4gdW5pdHRlc3Quc2tpcCgiVFNBTiBkb2Vzbid0IHN1cHBvcnQgdGhyZWFkcyBhZnRlciBmb3JrIikodGVzdCkKICAgIHJldHVybiB0ZXN0CgoKZGVmIHJlcXVpcmVzX3N1YmludGVycHJldGVycyhtZXRoKToKICAgICIiIkRlY29yYXRvciB0byBza2lwIGEgdGVzdCBpZiBzdWJpbnRlcnByZXRlcnMgYXJlIG5vdCBzdXBwb3J0ZWQuIiIiCiAgICByZXR1cm4gdW5pdHRlc3Quc2tpcElmKGludGVycHJldGVycyBpcyBOb25lLAogICAgICAgICAgICAgICAgICAgICAgICAgICAnc3ViaW50ZXJwcmV0ZXJzIHJlcXVpcmVkJykobWV0aCkKCgpkZWYgcmVzdG9yZV9kZWZhdWx0X2V4Y2VwdGhvb2sodGVzdGNhc2UpOgogICAgdGVzdGNhc2UuYWRkQ2xlYW51cChzZXRhdHRyLCB0aHJlYWRpbmcsICdleGNlcHRob29rJywgdGhyZWFkaW5nLmV4Y2VwdGhvb2spCiAgICB0aHJlYWRpbmcuZXhjZXB0aG9vayA9IHRocmVhZGluZy5fX2V4Y2VwdGhvb2tfXwoKCiMgQSB0cml2aWFsIG11dGFibGUgY291bnRlci4KY2xhc3MgQ291bnRlcihvYmplY3QpOgogICAgZGVmIF9faW5pdF9fKHNlbGYpOgogICAgICAgIHNlbGYudmFsdWUgPSAwCiAgICBkZWYgaW5jKHNlbGYpOgogICAgICAgIHNlbGYudmFsdWUgKz0gMQogICAgZGVmIGRlYyhzZWxmKToKICAgICAgICBzZWxmLnZhbHVlIC09IDEKICAgIGRlZiBnZXQoc2VsZik6CiAgICAgICAgcmV0dXJuIHNlbGYudmFsdWUKCmNsYXNzIFRlc3RUaHJlYWQodGhyZWFkaW5nLlRocmVhZCk6CiAgICBkZWYgX19pbml0X18oc2VsZiwgbmFtZSwgdGVzdGNhc2UsIHNlbWEsIG11dGV4LCBucnVubmluZyk6CiAgICAgICAgdGhyZWFkaW5nLlRocmVhZC5fX2luaXRfXyhzZWxmLCBuYW1lPW5hbWUpCiAgICAgICAgc2VsZi50ZXN0Y2FzZSA9IHRlc3RjYXNlCiAgICAgICAgc2VsZi5zZW1hID0gc2VtYQogICAgICAgIHNlbGYubXV0ZXggPSBtdXRleAogICAgICAgIHNlbGYubnJ1bm5pbmcgPSBucnVubmluZwoKICAgIGRlZiBydW4oc2VsZik6CiAgICAgICAgZGVsYXkgPSByYW5kb20ucmFuZG9tKCkgLyAxMDAwMC4wCiAgICAgICAgaWYgdmVyYm9zZToKICAgICAgICAgICAgcHJpbnQoJ3Rhc2sgJXMgd2lsbCBydW4gZm9yICUuMWYgdXNlYycgJQogICAgICAgICAgICAgICAgICAoc2VsZi5uYW1lLCBkZWxheSAqIDFlNikpCgogICAgICAgIHdpdGggc2VsZi5zZW1hOgogICAgICAgICAgICB3aXRoIHNlbGYubXV0ZXg6CiAgICAgICAgICAgICAgICBzZWxmLm5ydW5uaW5nLmluYygpCiAgICAgICAgICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICAgICAgICAgIHByaW50KHNlbGYubnJ1bm5pbmcuZ2V0KCksICd0YXNrcyBhcmUgcnVubmluZycpCiAgICAgICAgICAgICAgICBzZWxmLnRlc3RjYXNlLmFzc2VydExlc3NFcXVhbChzZWxmLm5ydW5uaW5nLmdldCgpLCAzKQoKICAgICAgICAgICAgdGltZS5zbGVlcChkZWxheSkKICAgICAgICAgICAgaWYgdmVyYm9zZToKICAgICAgICAgICAgICAgIHByaW50KCd0YXNrJywgc2VsZi5uYW1lLCAnZG9uZScpCgogICAgICAgICAgICB3aXRoIHNlbGYubXV0ZXg6CiAgICAgICAgICAgICAgICBzZWxmLm5ydW5uaW5nLmRlYygpCiAgICAgICAgICAgICAgICBzZWxmLnRlc3RjYXNlLmFzc2VydEdyZWF0ZXJFcXVhbChzZWxmLm5ydW5uaW5nLmdldCgpLCAwKQogICAgICAgICAgICAgICAgaWYgdmVyYm9zZToKICAgICAgICAgICAgICAgICAgICBwcmludCgnJXMgaXMgZmluaXNoZWQuICVkIHRhc2tzIGFyZSBydW5uaW5nJyAlCiAgICAgICAgICAgICAgICAgICAgICAgICAgKHNlbGYubmFtZSwgc2VsZi5ucnVubmluZy5nZXQoKSkpCgoKY2xhc3MgQmFzZVRlc3RDYXNlKHVuaXR0ZXN0LlRlc3RDYXNlKToKICAgIGRlZiBzZXRVcChzZWxmKToKICAgICAgICBzZWxmLl90aHJlYWRzID0gdGhyZWFkaW5nX2hlbHBlci50aHJlYWRpbmdfc2V0dXAoKQoKICAgIGRlZiB0ZWFyRG93bihzZWxmKToKICAgICAgICB0aHJlYWRpbmdfaGVscGVyLnRocmVhZGluZ19jbGVhbnVwKCpzZWxmLl90aHJlYWRzKQogICAgICAgIHRlc3Quc3VwcG9ydC5yZWFwX2NoaWxkcmVuKCkKCgpjbGFzcyBUaHJlYWRUZXN0cyhCYXNlVGVzdENhc2UpOgogICAgbWF4RGlmZiA9IDk5OTkKCiAgICBAY3B5dGhvbl9vbmx5CiAgICBkZWYgdGVzdF9sYXp5X2ltcG9ydChzZWxmKToKICAgICAgICBlbnN1cmVfbGF6eV9pbXBvcnRzKCJ0aHJlYWRpbmciLCB7ImZ1bmN0b29scyIsICJ3YXJuaW5ncyJ9KQoKICAgIEBjcHl0aG9uX29ubHkKICAgIGRlZiB0ZXN0X25hbWUoc2VsZik6CiAgICAgICAgZGVmIGZ1bmMoKTogcGFzcwoKICAgICAgICB0aHJlYWQgPSB0aHJlYWRpbmcuVGhyZWFkKG5hbWU9Im15bmFtZTEiKQogICAgICAgIHNlbGYuYXNzZXJ0RXF1YWwodGhyZWFkLm5hbWUsICJteW5hbWUxIikKCiAgICAgICAgIyBDb252ZXJ0IGludCBuYW1lIHRvIHN0cgogICAgICAgIHRocmVhZCA9IHRocmVhZGluZy5UaHJlYWQobmFtZT0xMjMpCiAgICAgICAgc2VsZi5hc3NlcnRFcXVhbCh0aHJlYWQubmFtZSwgIjEyMyIpCgogICAgICAgICMgdGFyZ2V0IG5hbWUgaXMgaWdub3JlZCBpZiBuYW1lIGlzIHNwZWNpZmllZAogICAgICAgIHRocmVhZCA9IHRocmVhZGluZy5UaHJlYWQodGFyZ2V0PWZ1bmMsIG5hbWU9Im15bmFtZTIiKQogICAgICAgIHNlbGYuYXNzZXJ0RXF1YWwodGhyZWFkLm5hbWUsICJteW5hbWUyIikKCiAgICAgICAgd2l0aCBtb2NrLnBhdGNoLm9iamVjdCh0aHJlYWRpbmcsICdfY291bnRlcicsIHJldHVybl92YWx1ZT0yKToKICAgICAgICAgICAgdGhyZWFkID0gdGhyZWFkaW5nLlRocmVhZChuYW1lPSIiKQogICAgICAgICAgICBzZWxmLmFzc2VydEVxdWFsKHRocmVhZC5uYW1lLCAiVGhyZWFkLTIiKQoKICAgICAgICB3aXRoIG1vY2sucGF0Y2gub2JqZWN0KHRocmVhZGluZywgJ19jb3VudGVyJywgcmV0dXJuX3ZhbHVlPTMpOgogICAgICAgICAgICB0aHJlYWQgPSB0aHJlYWRpbmcuVGhyZWFkKCkKICAgICAgICAgICAgc2VsZi5hc3NlcnRFcXVhbCh0aHJlYWQubmFtZSwgIlRocmVhZC0zIikKCiAgICAgICAgd2l0aCBtb2NrLnBhdGNoLm9iamVjdCh0aHJlYWRpbmcsICdfY291bnRlcicsIHJldHVybl92YWx1ZT01KToKICAgICAgICAgICAgdGhyZWFkID0gdGhyZWFkaW5nLlRocmVhZCh0YXJnZXQ9ZnVuYykKICAgICAgICAgICAgc2VsZi5hc3NlcnRFcXVhbCh0aHJlYWQubmFtZSwgIlRocmVhZC01IChmdW5jKSIpCgogICAgZGVmIHRlc3RfYXJnc19hcmd1bWVudChzZWxmKToKICAgICAgICAjIGJwby00NTczNTogVXNpbmcgbGlzdCBvciB0dXBsZSBhcyAqYXJncyogaW4gY29uc3RydWN0b3IgY291bGQKICAgICAgICAjIGFjaGlldmUgdGhlIHNhbWUgZWZmZWN0LgogICAgICAgIG51bV9saXN0ID0gWzFdCiAgICAgICAgbnVtX3R1cGxlID0gKDE= From fe78d4b8dffabfaf655a77e9eb96cf0a2b5784bb Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:34:11 -0600 Subject: [PATCH 19/21] Add cpy test_threading restore part _cpy_tt_part_1.b64 (no trigger) --- .github/workflows/_cpy_tt_part_1.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/_cpy_tt_part_1.b64 diff --git a/.github/workflows/_cpy_tt_part_1.b64 b/.github/workflows/_cpy_tt_part_1.b64 new file mode 100644 index 000000000000000..5b8de2802827863 --- /dev/null +++ b/.github/workflows/_cpy_tt_part_1.b64 @@ -0,0 +1 @@ +LCkKCiAgICAgICAgc3RyX2xpc3QgPSBbInN0ciJdCiAgICAgICAgc3RyX3R1cGxlID0gKCJzdHIiLCkKCiAgICAgICAgbGlzdF9pbl90dXBsZSA9IChbMV0sKQogICAgICAgIHR1cGxlX2luX2xpc3QgPSBbKDEsKV0KCiAgICAgICAgdGVzdF9jYXNlcyA9ICgKICAgICAgICAgICAgKG51bV9saXN0LCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAobnVtX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAoc3RyX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAic3RyIikpLAogICAgICAgICAgICAoc3RyX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgInN0ciIpKSwKICAgICAgICAgICAgKGxpc3RfaW5fdHVwbGUsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCBbMV0pKSwKICAgICAgICAgICAgKHR1cGxlX2luX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAoMSwpKSkKICAgICAgICApCgogICAgICAgIGZvciBhcmdzLCB0YXJnZXQgaW4gdGVzdF9jYXNlczoKICAgICAgICAgICAgd2l0aCBzZWxmLnN1YlRlc3QodGFyZ2V0PXRhcmdldCwgYXJncz1hcmdzKToKICAgICAgICAgICAgICAgIHQgPSB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD10YXJnZXQsIGFyZ3M9YXJncykKICAgICAgICAgICAgICAgIHQuc3RhcnQoKQogICAgICAgICAgICAgICAgdC5qb2luKCkKCiAgICBkZWYgdGVzdF9sb2NrX25vX2FyZ3Moc2VsZik6CiAgICAgICAgdGhyZWFkaW5nLkxvY2soKSAgIyB3b3JrcwogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEpCiAgICAgICAgc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yLCB0aHJlYWRpbmcuTG9jaywgYT0xKQogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEsIDIsIGE9MSwgYj0yKQoKICAgIGRlZiB0ZXN0X2xvY2tfbm9fc3ViY2xhc3Moc2VsZik6CiAgICAgICAgIyBJbnRlbnRpb25hbGx5IGRpc2FsbG93IHN1YmNsYXNzZXMgb2YgdGhyZWFkaW5nLkxvY2sgYmVjYXVzZSB0aGV5IGhhdmUKICAgICAgICAjIG5ldmVyIGJlZW4gYWxsb3dlZCwgc28gd2h5IHN0YXJ0IG5vdyBqdXN0IGJlY2F1c2UgdGhlIHR5cGUgaXMgcHVibGljPwogICAgICAgIHdpdGggc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yKToKICAgICAgICAgICAgY2xhc3MgTXlMb2NrKHRocmVhZGluZy5Mb2NrKTogcGFzcwoKICAgIGRlZiB0ZXN0X2xvY2tfb3Jfbm9uZShzZWxmKToKICAgICAgICBpbXBvcnQgdHlwZXMKICAgICAgICBzZWxmLmFzc2VydElzSW5zdGFuY2UodGhyZWFkaW5nLkxvY2sgfCBOb25lLCB0eXBlcy5VbmlvblR5cGUpCgogICAgIyBDcmVhdGUgYSBidW5jaCBvZiB0aHJlYWRzLCBsZXQgZWFjaCBkbyBzb21lIHdvcmssIHdhaXQgdW50aWwgYWxsIGFyZQogICAgIyBkb25lLgogICAgZGVmIHRlc3RfdmFyaW91c19vcHMoc2VsZik6CiAgICAgICAgIyBUaGlzIHRha2VzIGFib3V0IG4vMyBzZWNvbmRzIHRvIHJ1biAoYWJvdXQgbi8zIGNsdW1wcyBvZiB0YXNrcywKICAgICAgICAjIHRpbWVzIGFib3V0IDEgc2Vjb25kIHBlciBjbHVtcCkuCiAgICAgICAgTlVNVEFTS1MgPSAxMAoKICAgICAgICAjIG5vIG1vcmUgdGhhbiAzIG9mIHRoZSAxMCBjYW4gcnVuIGF0IG9uY2UKICAgICAgICBzZW1hID0gdGhyZWFkaW5nLkJvdW5kZWRTZW1hcGhvcmUodmFsdWU9MykKICAgICAgICBtdXRleCA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgbnVtcnVubmluZyA9IENvdW50ZXIoKQoKICAgICAgICB0aHJlYWRzID0gW10KCiAgICAgICAgZm9yIGkgaW4gcmFuZ2UoTlVNVEFTS1MpOgogICAgICAgICAgICB0ID0gVGVzdFRocmVhZCgiPHRocmVhZCAlZD4iJWksIHNlbGYsIHNlbWEsIG11dGV4LCBudW1ydW5uaW5nKQogICAgICAgICAgICB0aHJlYWRzLmFwcGVuZCh0KQogICAgICAgICAgICBzZWxmLmFzc2VydElzTm9uZSh0LmlkZW50KQogICAgICAgICAgICBzZWxmLmFzc2VydFJlZ2V4KHJlcHIodCksIHInXjxUZXN0VGhyZWFkXCguKiwgaW5pdGlhbFwpPiQnKQogICAgICAgICAgICB0LnN0YXJ0KCkKCiAgICAgICAgaWYgaGFzYXR0cih0aHJlYWRpbmcsICdnZXRfbmF0aXZlX2lkJyk6CiAgICAgICAgICAgIG5hdGl2ZV9pZHMgPSBzZXQodC5uYXRpdmVfaWQgZm9yIHQgaW4gdGhyZWFkcykgfCB7dGhyZWFkaW5nLmdldF9uYXRpdmVfaWQoYX0KICAgICAgICAgICAgc2VsZi5hc3NlcnROb3RJbihOb25lLCBuYXRpdmVfaWRzKQogICAgICAgICAgICBzZWxmLmFzc2VydEVxdWFsKGxlbihuYXRpdmVfaWRzKSwgTlVNVEFTS1MgKyAxKQoKICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludCgnd2FpdGluZyBmb3IgYWxsIHRhc2tzIHRvIGNvbXBsZXRlJykKICAgICAgICBmb3IgdCBpbiB0aHJlYWRzOgogICAgICAgICAgICB0LmpvaW4oKQogICAgICAgICAgICBzZWxmLmFzc2VydEZhbHNlKHQuaXNfYWxpdmUoKSkKICAgICAgICAgICAgc2VsZi5hc3NlcnROb3RFcXVhbCh0LmlkZW50LCAwKQogICAgICAgICAgICBzZWxmLmFzc2VydElzTm90Tm9uZSh0LmlkZW50KQogICAgICAgICAgICBzZWxmLmFzc2VydFJlZ2V4KHJlcHIodCksIHInXjxUZXN0VGhyZWFkXCguKiwgc3RvcHBlZCAtP1xkK1wpPiQnKQogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KCdhbGwgdGFza3MgZG9uZScpCiAgICAgICAgc2VsZi5hc3NlcnRFcXVhbChudW1ydW5uaW5nLmdldCgpLCAwKQoKICAgIGRlZiB0ZXN0X2lkZW50X29mX25vX3RocmVhZGluZ190aHJlYWRzKHNlbGYpOgogICAgICAgICMgVGhlIGlkZW50IHN0aWxsIG11c3Qgd29yayBmb3IgdGhlIG1haW4gdGhyZWFkIGFuZCBkdW1teSB0aHJlYWRzLgogICAgICAgIHNlbGYuYXNzZXJ0SXNOb3ROb25lKHRocmVhZGluZy5jdXJyZW50X3RocmVhZCgpLmlkZW50KQogICAgICAgIGRlZiBmKCk6CiAgICAgICAgICAgIGlkZW50LmFwcGVuZCh0aHJlYWRpbmcuY3VycmVudF90aHJlYWQoKS5pZGVudCkKICAgICAgICAgICAgZG9uZS5zZXQoKQogICAgICAgIGRvbmUgPSB0aHJlYWRpbmcuRXZlbnQoKQogICAgICAgIGlkZW50ID0gW10KICAgICAgICB3aXRoIHRocmVhZGluZ19oZWxwZXIud2FpdF90aHJlYWRzX2V4aXQoKToKICAgICAgICAgICAgdGlkID0gX3RocmVhZC5zdGFydF9uZXdfdGhyZWFkKGYsICgpKQogICAgICAgICAgICBkb25lLndhaXQoKQogICAgICAgICAgICBzZWxmLmFzc2VydEVxdWFsKGlkZW50WzBdLCB0aWQpCgogICAgIyBydW4gd2l0aCBhIHNtYWxsKGlzaCkgdGhyZWFkIHN0YWNrIHNpemUgKDI1NiBLaUIpCiAgICBkZWYgdGVzdF92YXJpb3VzX29wc19zbWFsbF9zdGFjayhzZWxmKToKICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludCgnd2l0aCAyNTYgS2lCIHRocmVhZCBzdGFjayBzaXplLi4uJykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDI2MjE0NCkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgIyBTYW5pdGl6ZXIgYnVpbGRzIG1heSByZXF1aXJlIG1vcmUgdGhhbiAyNTYgS2lCIChnaC0xNDEwNDQpLgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBtaW5pbXVtIHRocmVhZCBzdGFjayBzaXplIGlzIGFib3ZlIDI1NiBLaUInKQogICAgICAgIGV4Y2VwdCBfdGhyZWFkLmVycm9yOgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBkb2VzIG5vdCBzdXBwb3J0IGNoYW5naW5nIHRocmVhZCBzdGFjayBzaXplJykKICAgICAgICBzZWxmLnRlc3RfdmFyaW91c19vcHMoKQogICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDApCgogICAgIyBydW4gd2l0aCBhIGxhcmdlIHRocmVhZCBzdGFjayBzaXplICgxIE1pQikKICAgIGRlZiB0ZXN0X3ZhcmlvdXNfb3BzX2xhcmdlX3N0YWNrKHNlbGYpOgogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KCd3aXRoIDEgTWlCIHRocmVhZCBzdGFjayBzaXplLi4uJykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDB4MTAwMDAwKQogICAgICAgIGV4Y2VwdCBfdGhyZWFkLmVycm9yOgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBkb2VzIG5vdCBzdXBwb3J0IGNoYW5naW5nIHRocmVhZCBzdGFjayBzaXplJykKICAgICAgICBzZWxmLnRlc3RfdmFyaW91c19vcHMoKQogICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDApCgogICAgZGVmIHRlc3Rfc3RhY2tfc2l6ZV9ub19sZWFrKHNlbGYpOgogICAgICAgICMgZ2gtMTQxMDQ0OiBhIGN1c3RvbSB0aHJlYWQgc3RhY2sgc2l6ZSB1c2VkIHRvIGxlYWsgVGhyZWFkIG9iamVjdHMKICAgICAgICAjIHdoZW4gdGhlIHNpemUgcGFzc2VkIF90aHJlYWQuc3RhY2tfc2l6ZSgpIGJ1dCB3YXMgdG9vIHNtYWxsIGZvcgogICAgICAgICMgdGhyZWFkaW5nLlRocmVhZCBib290c3RyYXAgdW5kZXIgQWRkcmVzc1Nhbml0aXplci4gIFRoZSByZXBvcnRlZAogICAgICAgICMgcmVwcm8gdXNlZCAxMjcgS2lCIGFuZCBkaWQgbm90IGpvaW4gdGhlIHRocmVhZC4KICAgICAgICAjIFByb2JlIHdpdGggMSBNaUIgc28gdGhlIGNhcGFiaWxpdHkgY2hlY2sgc3RheXMgYWJvdmUgc2FuaXRpemVyCiAgICAgICAgIyBmbG9vcnMgKFRTYW4gbmVlZHMgMTIgbWFyZ2lucyAvIH4zODggS2lCOyBnaC0xNDEwNDQpLgogICAgICAgIHRyeToKICAgICAgICAgICAgdGhyZWFkaW5nLnN0YWNrX3NpemUoMHgxMDAwMDApCiAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6CiAgICAgICAgICAgIHNlbGYuc2tpcFRlc3QoCiAgICAgICAgICAgICAgICAncGxhdGZvcm0= From 4eb90e5b2d6857b3a3a16c88180637370db98148 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:36:39 -0600 Subject: [PATCH 20/21] Fix part1 chunk0 of cpy test_threading restore --- .github/workflows/_cpy_tt_part_1.b64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_cpy_tt_part_1.b64 b/.github/workflows/_cpy_tt_part_1.b64 index 5b8de2802827863..8723ebe0471cbd3 100644 --- a/.github/workflows/_cpy_tt_part_1.b64 +++ b/.github/workflows/_cpy_tt_part_1.b64 @@ -1 +1 @@ -LCkKCiAgICAgICAgc3RyX2xpc3QgPSBbInN0ciJdCiAgICAgICAgc3RyX3R1cGxlID0gKCJzdHIiLCkKCiAgICAgICAgbGlzdF9pbl90dXBsZSA9IChbMV0sKQogICAgICAgIHR1cGxlX2luX2xpc3QgPSBbKDEsKV0KCiAgICAgICAgdGVzdF9jYXNlcyA9ICgKICAgICAgICAgICAgKG51bV9saXN0LCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAobnVtX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAoc3RyX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAic3RyIikpLAogICAgICAgICAgICAoc3RyX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgInN0ciIpKSwKICAgICAgICAgICAgKGxpc3RfaW5fdHVwbGUsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCBbMV0pKSwKICAgICAgICAgICAgKHR1cGxlX2luX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAoMSwpKSkKICAgICAgICApCgogICAgICAgIGZvciBhcmdzLCB0YXJnZXQgaW4gdGVzdF9jYXNlczoKICAgICAgICAgICAgd2l0aCBzZWxmLnN1YlRlc3QodGFyZ2V0PXRhcmdldCwgYXJncz1hcmdzKToKICAgICAgICAgICAgICAgIHQgPSB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD10YXJnZXQsIGFyZ3M9YXJncykKICAgICAgICAgICAgICAgIHQuc3RhcnQoKQogICAgICAgICAgICAgICAgdC5qb2luKCkKCiAgICBkZWYgdGVzdF9sb2NrX25vX2FyZ3Moc2VsZik6CiAgICAgICAgdGhyZWFkaW5nLkxvY2soKSAgIyB3b3JrcwogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEpCiAgICAgICAgc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yLCB0aHJlYWRpbmcuTG9jaywgYT0xKQogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEsIDIsIGE9MSwgYj0yKQoKICAgIGRlZiB0ZXN0X2xvY2tfbm9fc3ViY2xhc3Moc2VsZik6CiAgICAgICAgIyBJbnRlbnRpb25hbGx5IGRpc2FsbG93IHN1YmNsYXNzZXMgb2YgdGhyZWFkaW5nLkxvY2sgYmVjYXVzZSB0aGV5IGhhdmUKICAgICAgICAjIG5ldmVyIGJlZW4gYWxsb3dlZCwgc28gd2h5IHN0YXJ0IG5vdyBqdXN0IGJlY2F1c2UgdGhlIHR5cGUgaXMgcHVibGljPwogICAgICAgIHdpdGggc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yKToKICAgICAgICAgICAgY2xhc3MgTXlMb2NrKHRocmVhZGluZy5Mb2NrKTogcGFzcwoKICAgIGRlZiB0ZXN0X2xvY2tfb3Jfbm9uZShzZWxmKToKICAgICAgICBpbXBvcnQgdHlwZXMKICAgICAgICBzZWxmLmFzc2VydElzSW5zdGFuY2UodGhyZWFkaW5nLkxvY2sgfCBOb25lLCB0eXBlcy5VbmlvblR5cGUpCgogICAgIyBDcmVhdGUgYSBidW5jaCBvZiB0aHJlYWRzLCBsZXQgZWFjaCBkbyBzb21lIHdvcmssIHdhaXQgdW50aWwgYWxsIGFyZQogICAgIyBkb25lLgogICAgZGVmIHRlc3RfdmFyaW91c19vcHMoc2VsZik6CiAgICAgICAgIyBUaGlzIHRha2VzIGFib3V0IG4vMyBzZWNvbmRzIHRvIHJ1biAoYWJvdXQgbi8zIGNsdW1wcyBvZiB0YXNrcywKICAgICAgICAjIHRpbWVzIGFib3V0IDEgc2Vjb25kIHBlciBjbHVtcCkuCiAgICAgICAgTlVNVEFTS1MgPSAxMAoKICAgICAgICAjIG5vIG1vcmUgdGhhbiAzIG9mIHRoZSAxMCBjYW4gcnVuIGF0IG9uY2UKICAgICAgICBzZW1hID0gdGhyZWFkaW5nLkJvdW5kZWRTZW1hcGhvcmUodmFsdWU9MykKICAgICAgICBtdXRleCA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgbnVtcnVubmluZyA9IENvdW50ZXIoKQoKICAgICAgICB0aHJlYWRzID0gW10KCiAgICAgICAgZm9yIGkgaW4gcmFuZ2UoTlVNVEFTS1MpOgogICAgICAgICAgICB0ID0gVGVzdFRocmVhZCgiPHRocmVhZCAlZD4iJWksIHNlbGYsIHNlbWEsIG11dGV4LCBudW1ydW5uaW5nKQogICAgICAgICAgICB0aHJlYWRzLmFwcGVuZCh0KQogICAgICAgICAgICBzZWxmLmFzc2VydElzTm9uZSh0LmlkZW50KQogICAgICAgICAgICBzZWxmLmFzc2VydFJlZ2V4KHJlcHIodCksIHInXjxUZXN0VGhyZWFkXCguKiwgaW5pdGlhbFwpPiQnKQogICAgICAgICAgICB0LnN0YXJ0KCkKCiAgICAgICAgaWYgaGFzYXR0cih0aHJlYWRpbmcsICdnZXRfbmF0aXZlX2lkJyk6CiAgICAgICAgICAgIG5hdGl2ZV9pZHMgPSBzZXQodC5uYXRpdmVfaWQgZm9yIHQgaW4gdGhyZWFkcykgfCB7dGhyZWFkaW5nLmdldF9uYXRpdmVfaWQoYX0KICAgICAgICAgICAgc2VsZi5hc3NlcnROb3RJbihOb25lLCBuYXRpdmVfaWRzKQogICAgICAgICAgICBzZWxmLmFzc2VydEVxdWFsKGxlbihuYXRpdmVfaWRzKSwgTlVNVEFTS1MgKyAxKQoKICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludCgnd2FpdGluZyBmb3IgYWxsIHRhc2tzIHRvIGNvbXBsZXRlJykKICAgICAgICBmb3IgdCBpbiB0aHJlYWRzOgogICAgICAgICAgICB0LmpvaW4oKQogICAgICAgICAgICBzZWxmLmFzc2VydEZhbHNlKHQuaXNfYWxpdmUoKSkKICAgICAgICAgICAgc2VsZi5hc3NlcnROb3RFcXVhbCh0LmlkZW50LCAwKQogICAgICAgICAgICBzZWxmLmFzc2VydElzTm90Tm9uZSh0LmlkZW50KQogICAgICAgICAgICBzZWxmLmFzc2VydFJlZ2V4KHJlcHIodCksIHInXjxUZXN0VGhyZWFkXCguKiwgc3RvcHBlZCAtP1xkK1wpPiQnKQogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KCdhbGwgdGFza3MgZG9uZScpCiAgICAgICAgc2VsZi5hc3NlcnRFcXVhbChudW1ydW5uaW5nLmdldCgpLCAwKQoKICAgIGRlZiB0ZXN0X2lkZW50X29mX25vX3RocmVhZGluZ190aHJlYWRzKHNlbGYpOgogICAgICAgICMgVGhlIGlkZW50IHN0aWxsIG11c3Qgd29yayBmb3IgdGhlIG1haW4gdGhyZWFkIGFuZCBkdW1teSB0aHJlYWRzLgogICAgICAgIHNlbGYuYXNzZXJ0SXNOb3ROb25lKHRocmVhZGluZy5jdXJyZW50X3RocmVhZCgpLmlkZW50KQogICAgICAgIGRlZiBmKCk6CiAgICAgICAgICAgIGlkZW50LmFwcGVuZCh0aHJlYWRpbmcuY3VycmVudF90aHJlYWQoKS5pZGVudCkKICAgICAgICAgICAgZG9uZS5zZXQoKQogICAgICAgIGRvbmUgPSB0aHJlYWRpbmcuRXZlbnQoKQogICAgICAgIGlkZW50ID0gW10KICAgICAgICB3aXRoIHRocmVhZGluZ19oZWxwZXIud2FpdF90aHJlYWRzX2V4aXQoKToKICAgICAgICAgICAgdGlkID0gX3RocmVhZC5zdGFydF9uZXdfdGhyZWFkKGYsICgpKQogICAgICAgICAgICBkb25lLndhaXQoKQogICAgICAgICAgICBzZWxmLmFzc2VydEVxdWFsKGlkZW50WzBdLCB0aWQpCgogICAgIyBydW4gd2l0aCBhIHNtYWxsKGlzaCkgdGhyZWFkIHN0YWNrIHNpemUgKDI1NiBLaUIpCiAgICBkZWYgdGVzdF92YXJpb3VzX29wc19zbWFsbF9zdGFjayhzZWxmKToKICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludCgnd2l0aCAyNTYgS2lCIHRocmVhZCBzdGFjayBzaXplLi4uJykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDI2MjE0NCkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgIyBTYW5pdGl6ZXIgYnVpbGRzIG1heSByZXF1aXJlIG1vcmUgdGhhbiAyNTYgS2lCIChnaC0xNDEwNDQpLgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBtaW5pbXVtIHRocmVhZCBzdGFjayBzaXplIGlzIGFib3ZlIDI1NiBLaUInKQogICAgICAgIGV4Y2VwdCBfdGhyZWFkLmVycm9yOgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBkb2VzIG5vdCBzdXBwb3J0IGNoYW5naW5nIHRocmVhZCBzdGFjayBzaXplJykKICAgICAgICBzZWxmLnRlc3RfdmFyaW91c19vcHMoKQogICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDApCgogICAgIyBydW4gd2l0aCBhIGxhcmdlIHRocmVhZCBzdGFjayBzaXplICgxIE1pQikKICAgIGRlZiB0ZXN0X3ZhcmlvdXNfb3BzX2xhcmdlX3N0YWNrKHNlbGYpOgogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KCd3aXRoIDEgTWlCIHRocmVhZCBzdGFjayBzaXplLi4uJykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDB4MTAwMDAwKQogICAgICAgIGV4Y2VwdCBfdGhyZWFkLmVycm9yOgogICAgICAgICAgICByYWlzZSB1bml0dGVzdC5Ta2lwVGVzdCgKICAgICAgICAgICAgICAgICdwbGF0Zm9ybSBkb2VzIG5vdCBzdXBwb3J0IGNoYW5naW5nIHRocmVhZCBzdGFjayBzaXplJykKICAgICAgICBzZWxmLnRlc3RfdmFyaW91c19vcHMoKQogICAgICAgIHRocmVhZGluZy5zdGFja19zaXplKDApCgogICAgZGVmIHRlc3Rfc3RhY2tfc2l6ZV9ub19sZWFrKHNlbGYpOgogICAgICAgICMgZ2gtMTQxMDQ0OiBhIGN1c3RvbSB0aHJlYWQgc3RhY2sgc2l6ZSB1c2VkIHRvIGxlYWsgVGhyZWFkIG9iamVjdHMKICAgICAgICAjIHdoZW4gdGhlIHNpemUgcGFzc2VkIF90aHJlYWQuc3RhY2tfc2l6ZSgpIGJ1dCB3YXMgdG9vIHNtYWxsIGZvcgogICAgICAgICMgdGhyZWFkaW5nLlRocmVhZCBib290c3RyYXAgdW5kZXIgQWRkcmVzc1Nhbml0aXplci4gIFRoZSByZXBvcnRlZAogICAgICAgICMgcmVwcm8gdXNlZCAxMjcgS2lCIGFuZCBkaWQgbm90IGpvaW4gdGhlIHRocmVhZC4KICAgICAgICAjIFByb2JlIHdpdGggMSBNaUIgc28gdGhlIGNhcGFiaWxpdHkgY2hlY2sgc3RheXMgYWJvdmUgc2FuaXRpemVyCiAgICAgICAgIyBmbG9vcnMgKFRTYW4gbmVlZHMgMTIgbWFyZ2lucyAvIH4zODggS2lCOyBnaC0xNDEwNDQpLgogICAgICAgIHRyeToKICAgICAgICAgICAgdGhyZWFkaW5nLnN0YWNrX3NpemUoMHgxMDAwMDApCiAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6CiAgICAgICAgICAgIHNlbGYuc2tpcFRlc3QoCiAgICAgICAgICAgICAgICAncGxhdGZvcm0= +LCkKCiAgICAgICAgc3RyX2xpc3QgPSBbInN0ciJdCiAgICAgICAgc3RyX3R1cGxlID0gKCJzdHIiLCkKCiAgICAgICAgbGlzdF9pbl90dXBsZSA9IChbMV0sKQogICAgICAgIHR1cGxlX2luX2xpc3QgPSBbKDEsKV0KCiAgICAgICAgdGVzdF9jYXNlcyA9ICgKICAgICAgICAgICAgKG51bV9saXN0LCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAobnVtX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgMSkpLAogICAgICAgICAgICAoc3RyX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAic3RyIikpLAogICAgICAgICAgICAoc3RyX3R1cGxlLCBsYW1iZGEgYXJnOiBzZWxmLmFzc2VydEVxdWFsKGFyZywgInN0ciIpKSwKICAgICAgICAgICAgKGxpc3RfaW5fdHVwbGUsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCBbMV0pKSwKICAgICAgICAgICAgKHR1cGxlX2luX2xpc3QsIGxhbWJkYSBhcmc6IHNlbGYuYXNzZXJ0RXF1YWwoYXJnLCAoMSwpKSkKICAgICAgICApCgogICAgICAgIGZvciBhcmdzLCB0YXJnZXQgaW4gdGVzdF9jYXNlczoKICAgICAgICAgICAgd2l0aCBzZWxmLnN1YlRlc3QodGFyZ2V0PXRhcmdldCwgYXJncz1hcmdzKToKICAgICAgICAgICAgICAgIHQgPSB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD10YXJnZXQsIGFyZ3M9YXJncykKICAgICAgICAgICAgICAgIHQuc3RhcnQoKQogICAgICAgICAgICAgICAgdC5qb2luKCkKCiAgICBkZWYgdGVzdF9sb2NrX25vX2FyZ3Moc2VsZik6CiAgICAgICAgdGhyZWFkaW5nLkxvY2soKSAgIyB3b3JrcwogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEpCiAgICAgICAgc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yLCB0aHJlYWRpbmcuTG9jaywgYT0xKQogICAgICAgIHNlbGYuYXNzZXJ0UmFpc2VzKFR5cGVFcnJvciwgdGhyZWFkaW5nLkxvY2ssIDEsIDIsIGE9MSwgYj0yKQoKICAgIGRlZiB0ZXN0X2xvY2tfbm9fc3ViY2xhc3Moc2VsZik6CiAgICAgICAgIyBJbnRlbnRpb25hbGx5IGRpc2FsbG93IHN1YmNsYXNzZXMgb2YgdGhyZWFkaW5nLkxvY2sgYmVjYXVzZSB0aGV5IGhhdmUKICAgICAgICAjIG5ldmVyIGJlZW4gYWxsb3dlZCwgc28gd2h5IHN0YXJ0IG5vdyBqdXN0IGJlY2F1c2UgdGhlIHR5cGUgaXMgcHVibGljPwogICAgICAgIHdpdGggc2VsZi5hc3NlcnRSYWlzZXMoVHlwZUVycm9yKToKICAgICAgICAgICAgY2xhc3MgTXlMb2NrKHRocmVhZGluZy5Mb2NrKTogcGFzcwoKICAgIGRlZiB0ZXN0X2xvY2tfb3Jfbm9uZShzZWxmKToKICAgICAgICBpbXBvcnQgdHlwZXMKICAgICAgICBzZWxmLmFzc2VydElzSW5zdGFuY2UodGhyZWFkaW5nLkxvY2sgfCBOb25lLCB0eXBlcy5VbmlvblR5cGUpCgogICAgIyBDcmVhdGUgYSBidW5jaCBvZiB0aHJlYWRzLCBsZXQgZWFjaCBkbyBzb21lIHdvcmssIHdhaXQgdW50aWwgYWxsIGFyZQogICAgIyBkb25lLgogICAgZGVmIHRlc3RfdmFyaW91c19vcHMoc2VsZik6CiAgICAgICAgIyBUaGlzIHRha2VzIGFib3V0IG4vMyBzZWNvbmRzIHRvIHJ1biAoYWJvdXQgbi8zIGNsdW1wcyBvZiB0YXNrcywKICAgICAgICAjIHRpbWVzIGFib3V0IDEgc2Vjb25kIHBlciBjbHVtcCkuCiA \ No newline at end of file From de3886e74deab62ae5082f54fe4950f57b9917d9 Mon Sep 17 00:00:00 2001 From: Kailash Nelson <37966146+KingLizard1020@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:32:03 -0600 Subject: [PATCH 21/21] Fix threading tests for sanitizer stack minimum --- Lib/test/test_threading.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 563098d8f1da8c1..ae8f0faf122c68c 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -245,17 +245,19 @@ def f(): done.wait() self.assertEqual(ident[0], tid) - # run with a small(ish) thread stack size (256 KiB) + # run with a small(ish) thread stack size (512 KiB) def test_various_ops_small_stack(self): if verbose: - print('with 256 KiB thread stack size...') + print('with 512 KiB thread stack size...') try: - threading.stack_size(262144) - except _thread.error: + threading.stack_size(512 * 1024) + except (ValueError, _thread.error): raise unittest.SkipTest( - 'platform does not support changing thread stack size') - self.test_various_ops() - threading.stack_size(0) + 'platform does not support a 512 KiB thread stack') + try: + self.test_various_ops() + finally: + threading.stack_size(0) # run with a large thread stack size (1 MiB) def test_various_ops_large_stack(self): @@ -275,10 +277,10 @@ def test_stack_size_no_leak(self): # threading.Thread bootstrap under AddressSanitizer. The reported # repro used 127 KiB and did not join the thread. try: - threading.stack_size(262144) - except _thread.error: + threading.stack_size(0x100000) + except (ValueError, _thread.error): self.skipTest( - 'platform does not support changing thread stack size') + 'platform does not support a 1 MiB thread stack') threading.stack_size(0) def run_script(script):