From 50c5faf9d636052f4f128525b0c83c452bbd65ee Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 18:27:29 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?venv/Lib/site-packages/greenlet/tests=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../greenlet/tests/test_greenlet_trash.py | 195 +++++ .../tests/test_interpreter_shutdown.py | 821 ++++++++++++++++++ .../greenlet/tests/test_leaks.py | 497 +++++++++++ .../greenlet/tests/test_stack_saved.py | 19 + .../greenlet/tests/test_throw.py | 128 +++ 5 files changed, 1660 insertions(+) create mode 100644 venv/Lib/site-packages/greenlet/tests/test_greenlet_trash.py create mode 100644 venv/Lib/site-packages/greenlet/tests/test_interpreter_shutdown.py create mode 100644 venv/Lib/site-packages/greenlet/tests/test_leaks.py create mode 100644 venv/Lib/site-packages/greenlet/tests/test_stack_saved.py create mode 100644 venv/Lib/site-packages/greenlet/tests/test_throw.py diff --git a/venv/Lib/site-packages/greenlet/tests/test_greenlet_trash.py b/venv/Lib/site-packages/greenlet/tests/test_greenlet_trash.py new file mode 100644 index 0000000..938eb36 --- /dev/null +++ b/venv/Lib/site-packages/greenlet/tests/test_greenlet_trash.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlets interacting with the CPython trash can API. + +The CPython trash can API is not designed to be re-entered from a +single thread. But this can happen using greenlets, if something +during the object deallocation process switches greenlets, and this second +greenlet then causes the trash can to get entered again. Here, we do this +very explicitly, but in other cases (like gevent) it could be arbitrarily more +complicated: for example, a weakref callback might try to acquire a lock that's +already held by another greenlet; that would allow a greenlet switch to occur. + +See https://github.com/gevent/gevent/issues/1909 + +This test is fragile and relies on details of the CPython +implementation (like most of the rest of this package): + + - We enter the trashcan and deferred deallocation after + ``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in + CPython's object.c, is generally 50. That's basically how many objects are required to + get us into the deferred deallocation situation. + + - The test fails by hitting an ``assert()`` in object.c; if the + build didn't enable assert, then we don't catch this. + + - If the test fails in that way, the interpreter crashes. +""" + +import unittest + + +class TestTrashCanReEnter(unittest.TestCase): + + def test_it(self): + try: + # pylint:disable-next=no-name-in-module + from greenlet._greenlet import get_tstate_trash_delete_nesting + except ImportError: + import sys + # Python 3.13 has no "trash delete nesting" anymore (but + # "delete later") Our test as written won't be able to check + # the nesting depth anymore, but on debug builds it should + # still be able to trigger a crash if we get things wrong. + # However, at least on 3.14, the greenlet switch in the test + # below never finds an active value for + # ``tstate->delete_later``, meaning this test isn't testing + # what we want it to. + assert sys.version_info[:2] >= (3, 13) + def get_tstate_trash_delete_nesting(): + return 0 + + # Try several times to trigger it, because it isn't 100% + # reliable. + for i in range(30): + with self.subTest(i=i): + self.check_it(get_tstate_trash_delete_nesting) + + def check_it(self, get_tstate_trash_delete_nesting): # pylint:disable=too-many-statements + import greenlet + main = greenlet.getcurrent() + + assert get_tstate_trash_delete_nesting() == 0 + + # We expect to be in deferred deallocation after this many + # deallocations have occurred. TODO: I wish we had a better way to do + # this --- that was before get_tstate_trash_delete_nesting; perhaps + # we can use that API to do better? (Probably not, it is non-functional in + # 3.13+) + TRASH_UNWIND_LEVEL = 500 # 50 is the nominal default on 3.12 + # How many objects to put in a container; it's the container that + # queues objects for deferred deallocation. + OBJECTS_PER_CONTAINER = 1000 + + class Dealloc: # define the class here because we alter class variables each time we run. + """ + An object with a ``__del__`` method. When it starts getting deallocated + from a deferred trash can run, it switches greenlets, allocates more objects + which then also go in the trash can. If we don't save state appropriately, + nesting gets out of order and we can crash the interpreter. + """ + + #: Has our deallocation actually run and switched greenlets? + #: When it does, this will be set to the current greenlet. This should + #: be happening in the main greenlet, so we check that down below. + SPAWNED = False + + #: Has the background greenlet run? + BG_RAN = False + + BG_GLET = None + + #: How many of these things have ever been allocated. + CREATED = 0 + + #: How many of these things have ever been deallocated. + DESTROYED = 0 + + #: How many were destroyed not in the main greenlet. There should always + #: be some. + #: If the test is broken or things change in the trashcan implementation, + #: this may not be correct. + DESTROYED_BG = 0 + + def __init__(self, sequence_number): + """ + :param sequence_number: The ordinal of this object during + one particular creation run. This is used to detect (guess, really) + when we have entered the trash can's deferred deallocation. + """ + self.i = sequence_number + Dealloc.CREATED += 1 + + def __del__(self): + if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED: + Dealloc.SPAWNED = greenlet.getcurrent() + other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet) + x = other.switch() + assert x == 42 + # It's important that we don't switch back to the greenlet, + # we leave it hanging there in an incomplete state. But we don't let it + # get collected, either. If we complete it now, while we're still + # in the scope of the initial trash can, things work out and we + # don't see the problem. We need this greenlet to complete + # at some point in the future, after we've exited this trash can invocation. + del other + elif self.i == 40 and greenlet.getcurrent() is not main: + Dealloc.BG_RAN = True + try: + main.switch(42) + except greenlet.GreenletExit as ex: + # We expect this; all references to us go away + # while we're still running, and we need to finish deleting + # ourself. + Dealloc.BG_RAN = type(ex) + del ex + + # Record the fact that we're dead last of all. This ensures that + # we actually get returned too. + Dealloc.DESTROYED += 1 + if greenlet.getcurrent() is not main: + Dealloc.DESTROYED_BG += 1 + + + def background_greenlet(): + # We direct through a second function, instead of + # directly calling ``make_some()``, so that we have complete + # control over when these objects are destroyed: we need them + # to be destroyed in the context of the background greenlet + t = make_some() + del t # Triggere deletion. + + def make_some(): + t = () + i = OBJECTS_PER_CONTAINER + while i: + # Nest the tuples; it's the recursion that gets us + # into trash. + t = (Dealloc(i), t) + i -= 1 + return t + + + some = make_some() + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER) + self.assertEqual(Dealloc.DESTROYED, 0) + + # If we're going to crash, it should be on the following line. + # We only crash if ``assert()`` is enabled, of course. + del some + + # For non-debug builds of CPython, we won't crash. The best we can do is check + # the nesting level explicitly. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # Discard this, raising GreenletExit into where it is waiting. + Dealloc.BG_GLET = None + # The same nesting level maintains. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # We definitely cleaned some up in the background + self.assertGreater(Dealloc.DESTROYED_BG, 0) + + # Make sure all the cleanups happened. + self.assertIs(Dealloc.SPAWNED, main) + self.assertTrue(Dealloc.BG_RAN) + self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit) + self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED ) + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2) + + import gc + gc.collect() + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/Lib/site-packages/greenlet/tests/test_interpreter_shutdown.py b/venv/Lib/site-packages/greenlet/tests/test_interpreter_shutdown.py new file mode 100644 index 0000000..41e0e26 --- /dev/null +++ b/venv/Lib/site-packages/greenlet/tests/test_interpreter_shutdown.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlet behavior during interpreter shutdown (Py_FinalizeEx). + +These tests are organized into four groups: + + A. Core safety (smoke): no crashes with active greenlets at shutdown. + B. Cleanup semantics: GreenletExit / finally still works during + normal thread exit (the standard production path). + C. Atexit "still works" tests: getcurrent() / greenlet construction + during atexit handlers registered AFTER greenlet import (i.e. + BEFORE greenlet's cleanup handler in LIFO order) must return + valid objects — verifies the guards don't over-block. + D. TDD-certified regression tests: getcurrent() must return None + when called AFTER greenlet's cleanup (GC finalization phase + or late atexit phase). These tests fail on greenlet 3.3.2 + and pass with the fix across Python 3.10-3.14. +""" +import sys +import subprocess +import unittest +import textwrap + +from greenlet.tests import TestCase + + +class TestInterpreterShutdown(TestCase): # pylint:disable=too-many-public-methods + + def _run_shutdown_script(self, script_body): + """ + Run a Python script in a subprocess that exercises greenlet + during interpreter shutdown. Returns (returncode, stdout, stderr). + """ + full_script = textwrap.dedent(script_body) + result = subprocess.run( + [sys.executable, '-c', full_script], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + return result.returncode, result.stdout, result.stderr + + # ----------------------------------------------------------------- + # Group A: Core safety — no crashes with active greenlets at exit + # ----------------------------------------------------------------- + + def test_active_greenlet_at_shutdown_no_crash(self): + + # An active (suspended) greenlet that is deallocated during + # interpreter shutdown should not crash the process. + + # Before the fix, this would SIGSEGV on Python < 3.11 because + # _green_dealloc_kill_started_non_main_greenlet tried to call + # g_switch() during Py_FinalizeEx. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("from worker") + return "done" + + g = greenlet.greenlet(worker) + result = g.switch() + assert result == "from worker", result + print("OK: exiting with active greenlet") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: exiting with active greenlet", stdout) + + def test_multiple_active_greenlets_at_shutdown(self): + # Multiple suspended greenlets at shutdown should all be cleaned + # up without crashing. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(name): + greenlet.getcurrent().parent.switch(f"hello from {name}") + return "done" + + greenlets = [] + for i in range(10): + g = greenlet.greenlet(worker) + result = g.switch(f"g{i}") + greenlets.append(g) + + print(f"OK: {len(greenlets)} active greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets at shutdown", stdout) + + def test_nested_greenlets_at_shutdown(self): + # Nested (chained parent) greenlets at shutdown should not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def inner(): + greenlet.getcurrent().parent.switch("inner done") + + def outer(): + g_inner = greenlet.greenlet(inner) + g_inner.switch() + greenlet.getcurrent().parent.switch("outer done") + + g = greenlet.greenlet(outer) + result = g.switch() + assert result == "outer done", result + print("OK: nested greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: nested greenlets at shutdown", stdout) + + def test_threaded_greenlets_at_shutdown(self): + # Greenlets in worker threads that are still referenced at + # shutdown should not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + import threading + + results = [] + + def thread_worker(): + def greenlet_func(): + greenlet.getcurrent().parent.switch("from thread greenlet") + return "done" + + g = greenlet.greenlet(greenlet_func) + val = g.switch() + results.append((g, val)) + + threads = [] + for _ in range(3): + t = threading.Thread(target=thread_worker) + t.start() + threads.append(t) + + for t in threads: + t.join() + + print(f"OK: {len(results)} threaded greenlets at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 3 threaded greenlets at shutdown", stdout) + + # ----------------------------------------------------------------- + # Group B: Cleanup semantics — thread exit + # ----------------------------------------------------------------- + # + # These tests verify that GreenletExit / try-finally still work + # correctly during normal thread exit (the standard production + # path, e.g. uWSGI worker threads finishing a request). This is + # NOT interpreter shutdown; the guards do not fire here. + + def test_greenlet_cleanup_during_thread_exit(self): + # When a thread exits normally while holding active greenlets, + # GreenletExit IS thrown and cleanup code runs. This is the + # standard cleanup path used in production (e.g. uWSGI worker + # threads finishing a request). + + rc, stdout, stderr = self._run_shutdown_script("""\ + import os + import threading + import greenlet + + _write = os.write + + def thread_func(): + def worker(_w=_write, + _GreenletExit=greenlet.GreenletExit): + try: + greenlet.getcurrent().parent.switch("suspended") + except _GreenletExit: + _w(1, b"CLEANUP: GreenletExit caught\\n") + raise + + g = greenlet.greenlet(worker) + g.switch() + # Thread exits with active greenlet -> thread-state + # cleanup triggers GreenletExit + + t = threading.Thread(target=thread_func) + t.start() + t.join() + print("OK: thread cleanup done") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: thread cleanup done", stdout) + self.assertIn("CLEANUP: GreenletExit caught", stdout) + + def test_finally_block_during_thread_exit(self): + # try/finally blocks in active greenlets run correctly when the + # owning thread exits. + rc, stdout, stderr = self._run_shutdown_script("""\ + import os + import threading + import greenlet + + _write = os.write + + def thread_func(): + def worker(_w=_write): + try: + greenlet.getcurrent().parent.switch("suspended") + finally: + _w(1, b"FINALLY: cleanup executed\\n") + + g = greenlet.greenlet(worker) + g.switch() + + t = threading.Thread(target=thread_func) + t.start() + t.join() + print("OK: thread cleanup done") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: thread cleanup done", stdout) + self.assertIn("FINALLY: cleanup executed", stdout) + + def test_many_greenlets_with_cleanup_at_shutdown(self): + # Stress test: many active greenlets with cleanup code at shutdown. + # Ensures no crashes regardless of deallocation order. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import sys + import greenlet + + cleanup_count = 0 + + def worker(idx): + global cleanup_count + try: + greenlet.getcurrent().parent.switch(f"ready-{idx}") + except greenlet.GreenletExit: + cleanup_count += 1 + raise + + greenlets = [] + for i in range(50): + g = greenlet.greenlet(worker) + result = g.switch(i) + greenlets.append(g) + + print(f"OK: {len(greenlets)} greenlets about to shut down") + # Note: we can't easily print cleanup_count during shutdown + # since it happens after the main module's code runs. + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 50 greenlets about to shut down", stdout) + + def test_deeply_nested_greenlets_at_shutdown(self): + # Deeply nested greenlet parent chains at shutdown. + # Tests that the deallocation order doesn't cause issues. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def level(depth, max_depth): + if depth < max_depth: + g = greenlet.greenlet(level) + g.switch(depth + 1, max_depth) + greenlet.getcurrent().parent.switch(f"depth-{depth}") + + g = greenlet.greenlet(level) + result = g.switch(0, 10) + print(f"OK: nested to depth 10, got {result}") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: nested to depth 10", stdout) + + def test_greenlet_with_traceback_at_shutdown(self): + # A greenlet that has an active exception context when it's + # suspended should not crash during shutdown cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + + def worker(): + try: + raise ValueError("test error") + except ValueError: + # Suspend while an exception is active on the stack + greenlet.getcurrent().parent.switch("suspended with exc") + return "done" + + g = greenlet.greenlet(worker) + result = g.switch() + assert result == "suspended with exc" + print("OK: greenlet with active exception at shutdown") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: greenlet with active exception at shutdown", stdout) + + # ----------------------------------------------------------------- + # Group C: getcurrent() / construction / gettrace() / settrace() + # during atexit — registered AFTER greenlet import + # ----------------------------------------------------------------- + + def test_getcurrent_during_atexit_no_crash(self): + # getcurrent() in an atexit handler registered AFTER greenlet + # import must return a valid greenlet (not None), because LIFO + # ordering means this handler runs BEFORE greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def call_getcurrent_at_exit(): + try: + g = greenlet.getcurrent() + if g is not None and type(g).__name__ == 'greenlet': + print(f"OK: getcurrent returned valid greenlet") + elif g is None: + print("FAIL: getcurrent returned None (over-blocked)") + else: + print(f"FAIL: unexpected {g!r}") + except Exception as e: + print(f"OK: getcurrent raised {type(e).__name__}: {e}") + + atexit.register(call_getcurrent_at_exit) + print("OK: atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered", stdout) + self.assertIn("OK: getcurrent returned valid greenlet", stdout, + "getcurrent() should return a valid greenlet when called " + "before greenlet's cleanup handler (LIFO ordering)") + + def test_gettrace_during_atexit_no_crash(self): + # Calling greenlet.gettrace() during atexit must not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def check_at_exit(): + try: + result = greenlet.gettrace() + print(f"OK: gettrace returned {result!r}") + except Exception as e: + print(f"OK: gettrace raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print("OK: registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: registered", stdout) + + def test_settrace_during_atexit_no_crash(self): + # Calling greenlet.settrace() during atexit must not crash. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def check_at_exit(): + try: + greenlet.settrace(lambda *args: None) + print("OK: settrace succeeded") + except Exception as e: + print(f"OK: settrace raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print("OK: registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: registered", stdout) + + def test_getcurrent_with_active_greenlets_during_atexit(self): + # getcurrent() during atexit (registered after import) with active + # greenlets must still return a valid greenlet, since LIFO means + # this runs before greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("ready") + + greenlets = [] + for i in range(5): + g = greenlet.greenlet(worker) + result = g.switch() + greenlets.append(g) + + def check_at_exit(): + try: + g = greenlet.getcurrent() + if g is not None and type(g).__name__ == 'greenlet': + print(f"OK: getcurrent returned valid greenlet") + elif g is None: + print("FAIL: getcurrent returned None (over-blocked)") + else: + print(f"FAIL: unexpected {g!r}") + except Exception as e: + print(f"OK: getcurrent raised {type(e).__name__}: {e}") + + atexit.register(check_at_exit) + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 5 active greenlets, atexit registered", stdout) + self.assertIn("OK: getcurrent returned valid greenlet", stdout, + "getcurrent() should return a valid greenlet when called " + "before greenlet's cleanup handler (LIFO ordering)") + + def test_greenlet_construction_during_atexit_no_crash(self): + # Constructing a new greenlet during atexit (registered after + # import) must succeed, since this runs before greenlet's cleanup. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def create_greenlets_at_exit(): + try: + def noop(): + pass + g = greenlet.greenlet(noop) + if g is not None: + print(f"OK: created greenlet successfully") + else: + print("FAIL: greenlet() returned None") + except Exception as e: + print(f"OK: construction raised {type(e).__name__}: {e}") + + atexit.register(create_greenlets_at_exit) + print("OK: atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered", stdout) + self.assertIn("OK: created greenlet successfully", stdout) + + def test_greenlet_construction_with_active_greenlets_during_atexit(self): + # Constructing new greenlets during atexit when other active + # greenlets already exist (maximizes the chance of a non-empty + # deleteme list). + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + + def worker(): + greenlet.getcurrent().parent.switch("ready") + + greenlets = [] + for i in range(10): + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + def create_at_exit(): + try: + new_greenlets = [] + for i in range(5): + g = greenlet.greenlet(lambda: None) + new_greenlets.append(g) + print(f"OK: created {len(new_greenlets)} greenlets at exit") + except Exception as e: + print(f"OK: raised {type(e).__name__}: {e}") + + atexit.register(create_at_exit) + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, atexit registered", stdout) + + def test_greenlet_construction_with_cross_thread_deleteme_during_atexit(self): + # Create greenlets in a worker thread, transfer them to the main + # thread, then drop them — populating the deleteme list. Then + # construct a new greenlet during atexit. On Python < 3.11 + # clear_deleteme_list() could previously crash if the + # PythonAllocator vector copy failed during early Py_FinalizeEx; + # using std::swap eliminates that allocation. + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import greenlet + import threading + + cross_thread_refs = [] + + def thread_worker(): + # Create greenlets in this thread + def gl_body(): + greenlet.getcurrent().parent.switch("ready") + for _ in range(20): + g = greenlet.greenlet(gl_body) + g.switch() + cross_thread_refs.append(g) + + t = threading.Thread(target=thread_worker) + t.start() + t.join() + + # Dropping these references in the main thread + # causes them to be added to the main thread's + # deleteme list (deferred cross-thread dealloc). + cross_thread_refs.clear() + + def create_at_exit(): + try: + g = greenlet.greenlet(lambda: None) + print(f"OK: created greenlet at exit {g!r}") + except Exception as e: + print(f"OK: raised {type(e).__name__}: {e}") + + atexit.register(create_at_exit) + print("OK: cross-thread setup done, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: cross-thread setup done, atexit registered", stdout) + + + # ----------------------------------------------------------------- + # Group D.1: TDD-certified — getcurrent() during GC finalization + # + # These tests use gc.disable() + reference cycles to force __del__ + # to run during Py_FinalizeEx's GC pass, where Py_IsFinalizing() + # is True. Without the fix, getcurrent() returns a live greenlet + # (unguarded); with the fix, it returns None. + # + # TDD verification (greenlet 3.3.2 = RED, patched = GREEN): + # Python 3.10: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.11: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.12: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.13: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.14: RED (UNGUARDED) → GREEN (GUARDED) + # ----------------------------------------------------------------- + + def test_getcurrent_returns_none_during_gc_finalization(self): + # greenlet.getcurrent() must raise an exception when called from a + # __del__ method during Py_FinalizeEx's GC collection pass. + + # On Python >= 3.11, _Py_IsFinalizing() is True during this + # phase. Without the Py_IsFinalizing() guard in mod_getcurrent, + # this would return a greenlet — the same unguarded code path + # that leads to SIGSEGV in production (uWSGI worker recycling). + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Reference cycle: only collected during Py_FinalizeEx GC pass + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print("OK: deferred cycle created") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: deferred cycle created", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + def test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self): + # Same as above but with active greenlets at shutdown, which + # increases the amount of C++ destructor work during finalization. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Create active greenlets + greenlets = [] + for _ in range(10): + def worker(): + greenlet.getcurrent().parent.switch("suspended") + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + # Reference cycle deferred to Py_FinalizeEx + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print(f"OK: {len(greenlets)} active greenlets, cycle deferred") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, cycle deferred", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self): + # Combines cross-thread greenlet deallocation (deleteme list) + # with the GC finalization check. This simulates the production + # scenario where uWSGI worker threads create greenlets that are + # transferred to the main thread, then cleaned up during + # Py_FinalizeEx. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import gc + import os + import threading + import greenlet + + gc.disable() + + class CleanupChecker: + def __del__(self): + try: + try: + greenlet.getcurrent() + except RuntimeError: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Create cross-thread greenlet references + cross_refs = [] + def thread_fn(): + for _ in range(20): + def body(): + greenlet.getcurrent().parent.switch("x") + g = greenlet.greenlet(body) + g.switch() + cross_refs.append(g) + t = threading.Thread(target=thread_fn) + t.start() + t.join() + cross_refs.clear() + + # Reference cycle deferred to Py_FinalizeEx + a = CleanupChecker() + b = {"ref": a} + a._cycle = b + del a, b + + print("OK: cross-thread cleanup + cycle deferred") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: cross-thread cleanup + cycle deferred", stdout) + self.assertIn("GUARDED: getcurrent=None", stdout) + + + # ----------------------------------------------------------------- + # Group D.2: TDD-certified — getcurrent() during atexit phase + # + # These tests register the checker BEFORE importing greenlet. + # Python's atexit is LIFO, so greenlet's handler (registered at + # import) runs FIRST and sets g_greenlet_shutting_down=1; then + # the checker runs SECOND and observes getcurrent() → None. + # + # This covers the atexit phase where _Py_IsFinalizing() is still + # False on ALL Python versions — the exact window that causes + # SIGSEGV in production (uWSGI worker recycling → Py_FinalizeEx). + # + # TDD verification (greenlet 3.3.2 = RED, patched = GREEN): + # Python 3.10: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.11: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.12: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.13: RED (UNGUARDED) → GREEN (GUARDED) + # Python 3.14: RED (UNGUARDED) → GREEN (GUARDED) + # ----------------------------------------------------------------- + + def test_getcurrent_returns_none_during_atexit_phase(self): + # greenlet.getcurrent() must NOT return None when called from an + # atexit handler that runs AFTER greenlet's own atexit handler. + + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import os + + def late_checker(): + try: + import greenlet + cur = greenlet.getcurrent() + if cur is None: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + # Register BEFORE importing greenlet. LIFO order: + # greenlet's handler (registered at import) runs FIRST, + # late_checker runs SECOND — seeing the flag already set. + atexit.register(late_checker) + + import greenlet + print("OK: atexit registered before greenlet import") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: atexit registered before greenlet import", stdout) + self.assertIn("UNGUARDED", stdout) + + + def test_getcurrent_returns_none_during_atexit_phase_with_active_greenlets(self): + # Same as above but with active greenlets + rc, stdout, stderr = self._run_shutdown_script("""\ + import atexit + import os + + def late_checker(): + try: + import greenlet + cur = greenlet.getcurrent() + if cur is None: + os.write(1, b"GUARDED: getcurrent=None\\n") + else: + os.write(1, b"UNGUARDED: getcurrent=" + + type(cur).__name__.encode() + b"\\n") + except Exception as e: + os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n") + + atexit.register(late_checker) + + import greenlet + + greenlets = [] + for _ in range(10): + def worker(): + greenlet.getcurrent().parent.switch("parked") + g = greenlet.greenlet(worker) + g.switch() + greenlets.append(g) + + print(f"OK: {len(greenlets)} active greenlets, atexit registered") + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn("OK: 10 active greenlets, atexit registered", stdout) + self.assertIn("UNGUARDED", stdout) + + def test_api_getcurrent_no_system_error_at_module_gc_time(self): + # If we use the C API directly to return a greenlet AFTER + # atexit threads have been run, we don't crash, we get a + # specific error. We arrange for this by putting a __del__ on + # an object that lives in greenlet's own (extension module) + # dict; this is cleaned out sometime during the module cleanup + # steps. + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + from greenlet.tests import _test_extension + + class WithDel: + # must cache the method we want, because by the time we + # run, module globals may have been cleaned up. + def __del__(self, gc=_test_extension.getcurrent_api): + print('Destructor running') + gc() # Should print an unraisable RuntimeException + + greenlet._greenlet.with_del = WithDel() + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn('Destructor running', stdout) + self.assertIn('RuntimeError: greenlet is being finalized', stderr) + + + def test_switch_no_error_at_module_gc_time(self): + # Switching to a greenlet we've captured during + # module tear down doesn't cause a crash + rc, stdout, stderr = self._run_shutdown_script("""\ + import greenlet + from greenlet.tests import _test_extension + + gs = [] + # must cache the objects we want, because by the time we + # run, module globals may have been cleaned up. + def do_it(gs=gs): + print('current', gs) + gs[0].parent.switch(1) + + + gs.append(greenlet.greenlet(do_it)) + gs.append(greenlet.greenlet(do_it)) + gs[1].switch() + + class WithDel: + def __del__(self, gs=gs): + print('Destructor running') + r = gs[0].switch() + print('Result', r) + + greenlet._greenlet.with_del = WithDel() + """) + self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}") + self.assertIn('Destructor running', stdout) + self.assertIn('Result 1', stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/venv/Lib/site-packages/greenlet/tests/test_leaks.py b/venv/Lib/site-packages/greenlet/tests/test_leaks.py new file mode 100644 index 0000000..53de52d --- /dev/null +++ b/venv/Lib/site-packages/greenlet/tests/test_leaks.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- +""" +Testing scenarios that may have leaked. +""" +import sys +import gc + +import time +import weakref +import threading + + +import greenlet +from . import TestCase +from . import PY314 +from . import RUNNING_ON_FREETHREAD_BUILD +from . import WIN +from .leakcheck import fails_leakcheck +from .leakcheck import ignores_leakcheck +from .leakcheck import RUNNING_ON_MANYLINUX + + +# pylint:disable=protected-access + +assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0 + +class HasFinalizerTracksInstances(object): + EXTANT_INSTANCES = set() + def __init__(self, msg): + self.msg = sys.intern(msg) + self.EXTANT_INSTANCES.add(id(self)) + def __del__(self): + self.EXTANT_INSTANCES.remove(id(self)) + def __repr__(self): + return "" % ( + id(self), self.msg + ) + @classmethod + def reset(cls): + cls.EXTANT_INSTANCES.clear() + + +def fails_leakcheck_except_on_free_thraded(func): + if RUNNING_ON_FREETHREAD_BUILD: + # These all seem to pass on free threading because + # of the changes to the garbage collector + return func + return fails_leakcheck(func) + + +class TestLeaks(TestCase): + + def test_arg_refs(self): + args = ('a', 'b', 'c') + refcount_before = sys.getrefcount(args) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda *args: greenlet.getcurrent().parent.switch(*args)) + for _ in range(100): + g.switch(*args) + self.assertEqual(sys.getrefcount(args), refcount_before) + + def test_kwarg_refs(self): + kwargs = {} + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda **gkwargs: greenlet.getcurrent().parent.switch(**gkwargs)) + for _ in range(100): + g.switch(**kwargs) + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + + + @staticmethod + def __recycle_threads(): + # By introducing a thread that does sleep we allow other threads, + # that have triggered their __block condition, but did not have a + # chance to deallocate their thread state yet, to finally do so. + # The way it works is by requiring a GIL switch (different thread), + # which does a GIL release (sleep), which might do a GIL switch + # to finished threads and allow them to clean up. + def worker(): + time.sleep(0.001) + t = threading.Thread(target=worker) + t.start() + time.sleep(0.001) + t.join(10) + + def test_threaded_leak(self): + gg = [] + def worker(): + # only main greenlet present + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def test_threaded_adv_leak(self): + gg = [] + def worker(): + # main and additional *finished* greenlets + ll = greenlet.getcurrent().ll = [] + def additional(): + ll.append(greenlet.getcurrent()) + for _ in range(2): + greenlet.greenlet(additional).switch() + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def assertClocksUsed(self): + used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertGreaterEqual(used, 0) + # we don't lose the value + greenlet._greenlet.enable_optional_cleanup(True) + used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertEqual(used, used2) + self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1) + + def _check_issue251(self, + manually_collect_background=True, + explicit_reference_to_switch=False): + # See https://github.com/python-greenlet/greenlet/issues/251 + # Killing a greenlet (probably not the main one) + # in one thread from another thread would + # result in leaking a list (the ts_delkey list). + # We no longer use lists to hold that stuff, though. + + # For the test to be valid, even empty lists have to be tracked by the + # GC + + assert gc.is_tracked([]) + HasFinalizerTracksInstances.reset() + greenlet.getcurrent() + greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False) + + background_glet_running = threading.Event() + background_glet_killed = threading.Event() + background_greenlets = [] + + # XXX: Switching this to a greenlet subclass that overrides + # run results in all callers failing the leaktest; that + # greenlet instance is leaked. There's a bound method for + # run() living on the stack of the greenlet in g_initialstub, + # and since we don't manually switch back to the background + # greenlet to let it "fall off the end" and exit the + # g_initialstub function, it never gets cleaned up. Making the + # garbage collector aware of this bound method (making it an + # attribute of the greenlet structure and traversing into it) + # doesn't help, for some reason. + def background_greenlet(): + # Throw control back to the main greenlet. + jd = HasFinalizerTracksInstances("DELETING STACK OBJECT") + greenlet._greenlet.set_thread_local( + 'test_leaks_key', + HasFinalizerTracksInstances("DELETING THREAD STATE")) + # Explicitly keeping 'switch' in a local variable + # breaks this test in all versions + if explicit_reference_to_switch: + s = greenlet.getcurrent().parent.switch + s([jd]) + else: + greenlet.getcurrent().parent.switch([jd]) + + bg_main_wrefs = [] + + def background_thread(): + glet = greenlet.greenlet(background_greenlet) + bg_main_wrefs.append(weakref.ref(glet.parent)) + + background_greenlets.append(glet) + glet.switch() # Be sure it's active. + # Control is ours again. + del glet # Delete one reference from the thread it runs in. + background_glet_running.set() + background_glet_killed.wait(10) + + # To trigger the background collection of the dead + # greenlet, thus clearing out the contents of the list, we + # need to run some APIs. See issue 252. + if manually_collect_background: + greenlet.getcurrent() + + + t = threading.Thread(target=background_thread) + t.start() + background_glet_running.wait(10) + greenlet.getcurrent() + lists_before = self.count_objects(list, exact_kind=True) + + assert len(background_greenlets) == 1 + self.assertFalse(background_greenlets[0].dead) + # Delete the last reference to the background greenlet + # from a different thread. This puts it in the background thread's + # ts_delkey list. + del background_greenlets[:] + background_glet_killed.set() + + # Now wait for the background thread to die. + t.join(10) + del t + # As part of the fix for 252, we need to cycle the ceval.c + # interpreter loop to be sure it has had a chance to process + # the pending call. + self.wait_for_pending_cleanups() + + lists_after = self.count_objects(list, exact_kind=True) + greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False) + + # On 2.7, we observe that lists_after is smaller than + # lists_before. No idea what lists got cleaned up. All the + # Python 3 versions match exactly. + self.assertLessEqual(lists_after, lists_before) + # On versions after 3.6, we've successfully cleaned up the + # greenlet references thanks to the internal "vectorcall" + # protocol; prior to that, there is a reference path through + # the ``greenlet.switch`` method still on the stack that we + # can't reach to clean up. The C code goes through terrific + # lengths to clean that up. + if not explicit_reference_to_switch \ + and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + # If cleanup was disabled, though, we may not find it. + self.assertEqual(greenlets_after, greenlets_before) + if manually_collect_background: + # TODO: Figure out how to make this work! + # The one on the stack is still leaking somehow + # in the non-manually-collect state. + self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set()) + else: + # The explicit reference prevents us from collecting it + # and it isn't always found by the GC either for some + # reason. The entire frame is leaked somehow, on some + # platforms (e.g., MacPorts builds of Python (all + # versions!)), but not on other platforms (the linux and + # windows builds on GitHub actions and Appveyor). So we'd + # like to write a test that proves that the main greenlet + # sticks around, and we can on my machine (macOS 11.6, + # MacPorts builds of everything) but we can't write that + # same test on other platforms. However, hopefully iteration + # done by leakcheck will find it. + pass + + if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + self.assertClocksUsed() + + def test_issue251_killing_cross_thread_leaks_list(self): + self._check_issue251() + + def test_issue251_with_cleanup_disabled(self): + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251() + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background(self): + # Between greenlet 1.1.2 and the next version, this was still + # failing because the leak of the list still exists when we + # don't call a greenlet API before exiting the thread. The + # proximate cause is that neither of the two greenlets from + # the background thread are actually being destroyed, even + # though the GC is in fact visiting both objects. It's not + # clear where that leak is? For some reason the thread-local + # dict holding it isn't being cleaned up. + # + # The leak, I think, is in the CPYthon internal function that + # calls into green_switch(). The argument tuple is still on + # the C stack somewhere and can't be reached? That doesn't + # make sense, because the tuple should be collectable when + # this object goes away. + # + # Note that this test sometimes spuriously passes on Linux, + # for some reason, but I've never seen it pass on macOS. + self._check_issue251(manually_collect_background=False) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self): + self.expect_greenlet_leak = True + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251(manually_collect_background=False) + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_explicit_reference_not_collectable(self): + self._check_issue251( + manually_collect_background=False, + explicit_reference_to_switch=True) + + UNTRACK_ATTEMPTS = 100 + + def _only_test_some_versions(self): + # We're only looking for this problem specifically on 3.11, + # and this set of tests is relatively fragile, depending on + # OS and memory management details. So we want to run it on 3.11+ + # (obviously) but not every older 3.x version in order to reduce + # false negatives. At the moment, those false results seem to have + # resolved, so we are actually running this on 3.8+ + assert sys.version_info[0] >= 3 + if RUNNING_ON_MANYLINUX: + self.skipTest("Slow and not worth repeating here") + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase(self): + # See https://github.com/gevent/gevent/issues/1924 + # and https://github.com/python-greenlet/greenlet/issues/328 + self._only_test_some_versions() + def f(): + return 1 + + ITER = 10000 + def run_it(): + for _ in range(ITER): + greenlet.greenlet(f).switch() + + # Establish baseline + for _ in range(3): + run_it() + + # uss: (Linux, macOS, Windows): aka "Unique Set Size", this is + # the memory which is unique to a process and which would be + # freed if the process was terminated right now. + uss_before = self.get_process_uss() + + for count in range(self.UNTRACK_ATTEMPTS): + uss_before = max(uss_before, self.get_process_uss()) + run_it() + + uss_after = self.get_process_uss() + if uss_after <= uss_before and count > 1: + break + + self.assertLessEqual(uss_after, uss_before) + + def _check_untracked_memory_thread(self, deallocate_in_thread=True): + self._only_test_some_versions() + # Like the above test, but what if there are a bunch of + # unfinished greenlets in a thread that dies? + # Does it matter if we deallocate in the thread or not? + + # First, make sure we can get useful measurements. This will + # be skipped if not. + self.get_process_uss() + + EXIT_COUNT = [0] + + def f(): + try: + greenlet.getcurrent().parent.switch() + except greenlet.GreenletExit: + EXIT_COUNT[0] += 1 + raise + return 1 + + ITER = 10000 + def run_it(): + glets = [] + for _ in range(ITER): + # Greenlet starts, switches back to us. + # We keep a strong reference to the greenlet though so it doesn't + # get a GreenletExit exception. + g = greenlet.greenlet(f) + glets.append(g) + g.switch() + + return glets + + test = self + + class ThreadFunc: + uss_before = uss_after = 0 + glets = () + ITER = 2 + def __call__(self): + self.uss_before = test.get_process_uss() + + for _ in range(self.ITER): + self.glets += tuple(run_it()) + + for g in self.glets: + test.assertIn('suspended active', str(g)) + # Drop them. + if deallocate_in_thread: + self.glets = () + self.uss_after = test.get_process_uss() + + # Establish baseline + uss_before = uss_after = None + for count in range(self.UNTRACK_ATTEMPTS): + EXIT_COUNT[0] = 0 + thread_func = ThreadFunc() + t = threading.Thread(target=thread_func) + t.start() + t.join(30) + self.assertFalse(t.is_alive()) + + if uss_before is None: + uss_before = thread_func.uss_before + + uss_before = max(uss_before, thread_func.uss_before) + if deallocate_in_thread: + self.assertEqual(thread_func.glets, ()) + self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER) + + del thread_func # Deallocate the greenlets; but this won't raise into them + del t + if not deallocate_in_thread: + self.assertEqual(EXIT_COUNT[0], 0) + if deallocate_in_thread: + self.wait_for_pending_cleanups() + + uss_after = self.get_process_uss() + # See if we achieve a non-growth state at some point. Break when we do. + if uss_after <= uss_before and count > 1: + break + + self.wait_for_pending_cleanups() + uss_after = self.get_process_uss() + # On Windows, USS can fluctuate by tens of KB between measurements + # due to working set trimming, page table updates, etc. Allow a + # small tolerance so OS-level noise doesn't cause false failures. + # Real leaks produce MBs of growth (each iteration creates 20k + # greenlets; the greenlet Python object alone, not counting the C++ + # state it points to, is 56 bytes so 20k greenlets alone is + # slightly over a megabyte; the C++ UserGreenlet clocks in at 208 + # bytes, which adds another 4MB), so 512 KB is well below the detection + # threshold for genuine issues. + # + # 2025-06-16: We have now observed the same problem on both + # ubuntu-latest 3.14t (2948K "leak") and macos-latest 3.15t (192K + # "leak"). This was after the merge of PR #511, but that change + # should be unrelated. In particular, the ubuntu failure was after + # 30 loops and the macos failure was after only 3 --- both much + # smaller than UNTRACK_ATTEMPTS=100! We break out of the loop if we + # see a USS at the end of the loop that's smaller than at the start + # of the loop, and then we wait for pending cleanups and get the + # USS again. So this means that we think we have success, break out + # of the loop, and the pending cleanups cause our USS to grow - + # perhaps by touching pages that had been paged out? An immediately + # prior attempt of both platforms had been successful. + # + # At any rate, we need a larger tolerance than 512K, and we + # need it on all platforms, not just Windows. + tolerance = 3 * 1024 * 1024 + self.assertLessEqual(uss_after, uss_before + tolerance, + "after attempts %d" % (count,)) + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self): + self._check_untracked_memory_thread(deallocate_in_thread=True) + + @ignores_leakcheck + # Because the main greenlets from the background threads do not exit in a timely fashion, + # we fail the object-based leakchecks. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self): + # Between Feb 10 and Feb 20 2026, this test started failing on + # Github Actions, windows 3.14t. With no relevant code changes on + # our part. Both versions were 3.14.3 (same build). The only change + # is the Github actions "Runner Image". The working one was version + # 20260202.17.1, while the updated failing version was + # 20260217.31.1. Both report the same version of the operating system + # (Microsoft Windows Server 2025 10.0.26100). + # + # Reevaluate on future runner image releases. + if WIN and RUNNING_ON_FREETHREAD_BUILD and PY314: + self.skipTest("Windows 3.14t appears to leak. No other platform does.") + self._check_untracked_memory_thread(deallocate_in_thread=False) + +if __name__ == '__main__': + __import__('unittest').main() diff --git a/venv/Lib/site-packages/greenlet/tests/test_stack_saved.py b/venv/Lib/site-packages/greenlet/tests/test_stack_saved.py new file mode 100644 index 0000000..742e557 --- /dev/null +++ b/venv/Lib/site-packages/greenlet/tests/test_stack_saved.py @@ -0,0 +1,19 @@ +import greenlet +from . import TestCase + + +class Test(TestCase): + + def test_stack_saved(self): + main = greenlet.getcurrent() + self.assertEqual(main._stack_saved, 0) + + def func(): + main.switch(main._stack_saved) + + g = greenlet.greenlet(func) + x = g.switch() + self.assertGreater(x, 0) + self.assertGreater(g._stack_saved, 0) + g.switch() + self.assertEqual(g._stack_saved, 0) diff --git a/venv/Lib/site-packages/greenlet/tests/test_throw.py b/venv/Lib/site-packages/greenlet/tests/test_throw.py new file mode 100644 index 0000000..5bbeebe --- /dev/null +++ b/venv/Lib/site-packages/greenlet/tests/test_throw.py @@ -0,0 +1,128 @@ +import sys + + +from greenlet import greenlet +from . import TestCase + +def switch(*args): + return greenlet.getcurrent().parent.switch(*args) + + +class ThrowTests(TestCase): + def test_class(self): + def f(): + try: + switch("ok") + except RuntimeError: + switch("ok") + return + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError) + self.assertEqual(res, "ok") + + def test_val(self): + def f(): + try: + switch("ok") + except RuntimeError: + val = sys.exc_info()[1] + if str(val) == "ciao": + switch("ok") + return + switch("fail") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError("ciao")) + self.assertEqual(res, "ok") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError, "ciao") + self.assertEqual(res, "ok") + + def test_kill(self): + def f(): + switch("ok") + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw() + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + self.assertTrue(g.dead) + res = g.throw() # immediately eaten by the already-dead greenlet + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + + def test_throw_goes_to_original_parent(self): + main = greenlet.getcurrent() + + def f1(): + try: + main.switch("f1 ready to catch") + except IndexError: + return "caught" + return "normal exit" + + def f2(): + main.switch("from f2") + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + with self.assertRaises(IndexError): + g2.throw(IndexError) + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.switch() + self.assertEqual(res, "from f2") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + def test_non_traceback_param(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception, + Exception(), + self + ) + self.assertEqual(str(exc.exception), + "throw() third argument must be a traceback object") + + def test_instance_of_wrong_type(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception(), + BaseException() + ) + + self.assertEqual(str(exc.exception), + "instance exception may not have a separate value") + + def test_not_throwable(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + "abc" + ) + self.assertEqual(str(exc.exception), + "exceptions must be classes, or instances, not str")