Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
test_interpreter_shutdown.py825 linesDownload Raw Back to tests
1# -*- coding: utf-8 -*-
2"""
3Tests for greenlet behavior during interpreter shutdown (Py_FinalizeEx).
4
5These tests are organized into four groups:
6
7  A. Core safety (smoke): no crashes with active greenlets at shutdown.
8  B. Cleanup semantics: GreenletExit / finally still works during
9     normal thread exit (the standard production path).
10  C. Atexit "still works" tests: getcurrent() / greenlet construction
11     during atexit handlers registered AFTER greenlet import (i.e.
12     BEFORE greenlet's cleanup handler in LIFO order) must return
13     valid objects — verifies the guards don't over-block.
14  D. TDD-certified regression tests: getcurrent() must return None
15     when called AFTER greenlet's cleanup (GC finalization phase
16     or late atexit phase).  These tests fail on greenlet 3.3.2
17     and pass with the fix across Python 3.10-3.14.
18"""
19import sys
20import subprocess
21import unittest
22import textwrap
23
24from greenlet.tests import TestCase
25
26
27class TestInterpreterShutdown(TestCase): # pylint:disable=too-many-public-methods
28
29    def _run_shutdown_script(self, script_body):
30        """
31        Run a Python script in a subprocess that exercises greenlet
32        during interpreter shutdown. Returns (returncode, stdout, stderr).
33        """
34        full_script = textwrap.dedent(script_body)
35        result = subprocess.run(
36            [sys.executable, '-c', full_script],
37            capture_output=True,
38            text=True,
39            timeout=30,
40            check=False,
41        )
42        return result.returncode, result.stdout, result.stderr
43
44    # -----------------------------------------------------------------
45    # Group A: Core safety — no crashes with active greenlets at exit
46    # -----------------------------------------------------------------
47
48    def test_active_greenlet_at_shutdown_no_crash(self):
49
50        # An active (suspended) greenlet that is deallocated during
51        # interpreter shutdown should not crash the process.
52
53        # Before the fix, this would SIGSEGV on Python < 3.11 because
54        # _green_dealloc_kill_started_non_main_greenlet tried to call
55        # g_switch() during Py_FinalizeEx.
56
57        rc, stdout, stderr = self._run_shutdown_script("""\
58            import greenlet
59
60            def worker():
61                greenlet.getcurrent().parent.switch("from worker")
62                return "done"
63
64            g = greenlet.greenlet(worker)
65            result = g.switch()
66            assert result == "from worker", result
67            print("OK: exiting with active greenlet")
68        """)
69        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
70        self.assertIn("OK: exiting with active greenlet", stdout)
71
72    def test_multiple_active_greenlets_at_shutdown(self):
73        # Multiple suspended greenlets at shutdown should all be cleaned
74        # up without crashing.
75
76        rc, stdout, stderr = self._run_shutdown_script("""\
77            import greenlet
78
79            def worker(name):
80                greenlet.getcurrent().parent.switch(f"hello from {name}")
81                return "done"
82
83            greenlets = []
84            for i in range(10):
85                g = greenlet.greenlet(worker)
86                result = g.switch(f"g{i}")
87                greenlets.append(g)
88
89            print(f"OK: {len(greenlets)} active greenlets at shutdown")
90        """)
91        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
92        self.assertIn("OK: 10 active greenlets at shutdown", stdout)
93
94    def test_nested_greenlets_at_shutdown(self):
95        # Nested (chained parent) greenlets at shutdown should not crash.
96
97        rc, stdout, stderr = self._run_shutdown_script("""\
98            import greenlet
99
100            def inner():
101                greenlet.getcurrent().parent.switch("inner done")
102
103            def outer():
104                g_inner = greenlet.greenlet(inner)
105                g_inner.switch()
106                greenlet.getcurrent().parent.switch("outer done")
107
108            g = greenlet.greenlet(outer)
109            result = g.switch()
110            assert result == "outer done", result
111            print("OK: nested greenlets at shutdown")
112        """)
113        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
114        self.assertIn("OK: nested greenlets at shutdown", stdout)
115
116    def test_threaded_greenlets_at_shutdown(self):
117        # Greenlets in worker threads that are still referenced at
118        # shutdown should not crash.
119
120        rc, stdout, stderr = self._run_shutdown_script("""\
121            import greenlet
122            import threading
123
124            results = []
125
126            def thread_worker():
127                def greenlet_func():
128                    greenlet.getcurrent().parent.switch("from thread greenlet")
129                    return "done"
130
131                g = greenlet.greenlet(greenlet_func)
132                val = g.switch()
133                results.append((g, val))
134
135            threads = []
136            for _ in range(3):
137                t = threading.Thread(target=thread_worker)
138                t.start()
139                threads.append(t)
140
141            for t in threads:
142                t.join()
143
144            print(f"OK: {len(results)} threaded greenlets at shutdown")
145        """)
146        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
147        self.assertIn("OK: 3 threaded greenlets at shutdown", stdout)
148
149    # -----------------------------------------------------------------
150    # Group B: Cleanup semantics — thread exit
151    # -----------------------------------------------------------------
152    #
153    # These tests verify that GreenletExit / try-finally still work
154    # correctly during normal thread exit (the standard production
155    # path, e.g. uWSGI worker threads finishing a request).  This is
156    # NOT interpreter shutdown; the guards do not fire here.
157
158    def test_greenlet_cleanup_during_thread_exit(self):
159        # When a thread exits normally while holding active greenlets,
160        # GreenletExit IS thrown and cleanup code runs.  This is the
161        # standard cleanup path used in production (e.g. uWSGI worker
162        # threads finishing a request).
163
164        rc, stdout, stderr = self._run_shutdown_script("""\
165            import os
166            import threading
167            import greenlet
168
169            _write = os.write
170
171            def thread_func():
172                def worker(_w=_write,
173                           _GreenletExit=greenlet.GreenletExit):
174                    try:
175                        greenlet.getcurrent().parent.switch("suspended")
176                    except _GreenletExit:
177                        _w(1, b"CLEANUP: GreenletExit caught\\n")
178                        raise
179
180                g = greenlet.greenlet(worker)
181                g.switch()
182                # Thread exits with active greenlet -> thread-state
183                # cleanup triggers GreenletExit
184
185            t = threading.Thread(target=thread_func)
186            t.start()
187            t.join()
188            print("OK: thread cleanup done")
189        """)
190        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
191        self.assertIn("OK: thread cleanup done", stdout)
192        self.assertIn("CLEANUP: GreenletExit caught", stdout)
193
194    def test_finally_block_during_thread_exit(self):
195        # try/finally blocks in active greenlets run correctly when the
196        # owning thread exits.
197        rc, stdout, stderr = self._run_shutdown_script("""\
198            import os
199            import threading
200            import greenlet
201
202            _write = os.write
203
204            def thread_func():
205                def worker(_w=_write):
206                    try:
207                        greenlet.getcurrent().parent.switch("suspended")
208                    finally:
209                        _w(1, b"FINALLY: cleanup executed\\n")
210
211                g = greenlet.greenlet(worker)
212                g.switch()
213
214            t = threading.Thread(target=thread_func)
215            t.start()
216            t.join()
217            print("OK: thread cleanup done")
218        """)
219        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
220        self.assertIn("OK: thread cleanup done", stdout)
221        self.assertIn("FINALLY: cleanup executed", stdout)
222
223    def test_many_greenlets_with_cleanup_at_shutdown(self):
224        # Stress test: many active greenlets with cleanup code at shutdown.
225        # Ensures no crashes regardless of deallocation order.
226
227        rc, stdout, stderr = self._run_shutdown_script("""\
228            import sys
229            import greenlet
230
231            cleanup_count = 0
232
233            def worker(idx):
234                global cleanup_count
235                try:
236                    greenlet.getcurrent().parent.switch(f"ready-{idx}")
237                except greenlet.GreenletExit:
238                    cleanup_count += 1
239                    raise
240
241            greenlets = []
242            for i in range(50):
243                g = greenlet.greenlet(worker)
244                result = g.switch(i)
245                greenlets.append(g)
246
247            print(f"OK: {len(greenlets)} greenlets about to shut down")
248            # Note: we can't easily print cleanup_count during shutdown
249            # since it happens after the main module's code runs.
250        """)
251        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
252        self.assertIn("OK: 50 greenlets about to shut down", stdout)
253
254    def test_deeply_nested_greenlets_at_shutdown(self):
255        # Deeply nested greenlet parent chains at shutdown.
256        # Tests that the deallocation order doesn't cause issues.
257
258        rc, stdout, stderr = self._run_shutdown_script("""\
259            import greenlet
260
261            def level(depth, max_depth):
262                if depth < max_depth:
263                    g = greenlet.greenlet(level)
264                    g.switch(depth + 1, max_depth)
265                greenlet.getcurrent().parent.switch(f"depth-{depth}")
266
267            g = greenlet.greenlet(level)
268            result = g.switch(0, 10)
269            print(f"OK: nested to depth 10, got {result}")
270        """)
271        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
272        self.assertIn("OK: nested to depth 10", stdout)
273
274    def test_greenlet_with_traceback_at_shutdown(self):
275        # A greenlet that has an active exception context when it's
276        # suspended should not crash during shutdown cleanup.
277
278        rc, stdout, stderr = self._run_shutdown_script("""\
279            import greenlet
280
281            def worker():
282                try:
283                    raise ValueError("test error")
284                except ValueError:
285                    # Suspend while an exception is active on the stack
286                    greenlet.getcurrent().parent.switch("suspended with exc")
287                return "done"
288
289            g = greenlet.greenlet(worker)
290            result = g.switch()
291            assert result == "suspended with exc"
292            print("OK: greenlet with active exception at shutdown")
293        """)
294        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
295        self.assertIn("OK: greenlet with active exception at shutdown", stdout)
296
297    # -----------------------------------------------------------------
298    # Group C: getcurrent() / construction / gettrace() / settrace()
299    # during atexit — registered AFTER greenlet import
300    # -----------------------------------------------------------------
301
302    def test_getcurrent_during_atexit_no_crash(self):
303        # getcurrent() in an atexit handler registered AFTER greenlet
304        # import must return a valid greenlet (not None), because LIFO
305        # ordering means this handler runs BEFORE greenlet's cleanup.
306
307        rc, stdout, stderr = self._run_shutdown_script("""\
308            import atexit
309            import greenlet
310
311            def call_getcurrent_at_exit():
312                try:
313                    g = greenlet.getcurrent()
314                    if g is not None and type(g).__name__ == 'greenlet':
315                        print(f"OK: getcurrent returned valid greenlet")
316                    elif g is None:
317                        print("FAIL: getcurrent returned None (over-blocked)")
318                    else:
319                        print(f"FAIL: unexpected {g!r}")
320                except Exception as e:
321                    print(f"OK: getcurrent raised {type(e).__name__}: {e}")
322
323            atexit.register(call_getcurrent_at_exit)
324            print("OK: atexit registered")
325        """)
326        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
327        self.assertIn("OK: atexit registered", stdout)
328        self.assertIn("OK: getcurrent returned valid greenlet", stdout,
329                      "getcurrent() should return a valid greenlet when called "
330                      "before greenlet's cleanup handler (LIFO ordering)")
331
332    def test_gettrace_during_atexit_no_crash(self):
333        # Calling greenlet.gettrace() during atexit must not crash.
334
335        rc, stdout, stderr = self._run_shutdown_script("""\
336            import atexit
337            import greenlet
338
339            def check_at_exit():
340                try:
341                    result = greenlet.gettrace()
342                    print(f"OK: gettrace returned {result!r}")
343                except Exception as e:
344                    print(f"OK: gettrace raised {type(e).__name__}: {e}")
345
346            atexit.register(check_at_exit)
347            print("OK: registered")
348        """)
349        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
350        self.assertIn("OK: registered", stdout)
351
352    def test_settrace_during_atexit_no_crash(self):
353        # Calling greenlet.settrace() during atexit must not crash.
354
355        rc, stdout, stderr = self._run_shutdown_script("""\
356            import atexit
357            import greenlet
358
359            def check_at_exit():
360                try:
361                    greenlet.settrace(lambda *args: None)
362                    print("OK: settrace succeeded")
363                except Exception as e:
364                    print(f"OK: settrace raised {type(e).__name__}: {e}")
365
366            atexit.register(check_at_exit)
367            print("OK: registered")
368        """)
369        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
370        self.assertIn("OK: registered", stdout)
371
372    def test_getcurrent_with_active_greenlets_during_atexit(self):
373        # getcurrent() during atexit (registered after import) with active
374        # greenlets must still return a valid greenlet, since LIFO means
375        # this runs before greenlet's cleanup.
376
377        rc, stdout, stderr = self._run_shutdown_script("""\
378            import atexit
379            import greenlet
380
381            def worker():
382                greenlet.getcurrent().parent.switch("ready")
383
384            greenlets = []
385            for i in range(5):
386                g = greenlet.greenlet(worker)
387                result = g.switch()
388                greenlets.append(g)
389
390            def check_at_exit():
391                try:
392                    g = greenlet.getcurrent()
393                    if g is not None and type(g).__name__ == 'greenlet':
394                        print(f"OK: getcurrent returned valid greenlet")
395                    elif g is None:
396                        print("FAIL: getcurrent returned None (over-blocked)")
397                    else:
398                        print(f"FAIL: unexpected {g!r}")
399                except Exception as e:
400                    print(f"OK: getcurrent raised {type(e).__name__}: {e}")
401
402            atexit.register(check_at_exit)
403            print(f"OK: {len(greenlets)} active greenlets, atexit registered")
404        """)
405        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
406        self.assertIn("OK: 5 active greenlets, atexit registered", stdout)
407        self.assertIn("OK: getcurrent returned valid greenlet", stdout,
408                      "getcurrent() should return a valid greenlet when called "
409                      "before greenlet's cleanup handler (LIFO ordering)")
410
411    def test_greenlet_construction_during_atexit_no_crash(self):
412        # Constructing a new greenlet during atexit (registered after
413        # import) must succeed, since this runs before greenlet's cleanup.
414
415        rc, stdout, stderr = self._run_shutdown_script("""\
416            import atexit
417            import greenlet
418
419            def create_greenlets_at_exit():
420                try:
421                    def noop():
422                        pass
423                    g = greenlet.greenlet(noop)
424                    if g is not None:
425                        print(f"OK: created greenlet successfully")
426                    else:
427                        print("FAIL: greenlet() returned None")
428                except Exception as e:
429                    print(f"OK: construction raised {type(e).__name__}: {e}")
430
431            atexit.register(create_greenlets_at_exit)
432            print("OK: atexit registered")
433        """)
434        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
435        self.assertIn("OK: atexit registered", stdout)
436        self.assertIn("OK: created greenlet successfully", stdout)
437
438    def test_greenlet_construction_with_active_greenlets_during_atexit(self):
439        # Constructing new greenlets during atexit when other active
440        # greenlets already exist (maximizes the chance of a non-empty
441        # deleteme list).
442
443        rc, stdout, stderr = self._run_shutdown_script("""\
444            import atexit
445            import greenlet
446
447            def worker():
448                greenlet.getcurrent().parent.switch("ready")
449
450            greenlets = []
451            for i in range(10):
452                g = greenlet.greenlet(worker)
453                g.switch()
454                greenlets.append(g)
455
456            def create_at_exit():
457                try:
458                    new_greenlets = []
459                    for i in range(5):
460                        g = greenlet.greenlet(lambda: None)
461                        new_greenlets.append(g)
462                    print(f"OK: created {len(new_greenlets)} greenlets at exit")
463                except Exception as e:
464                    print(f"OK: raised {type(e).__name__}: {e}")
465
466            atexit.register(create_at_exit)
467            print(f"OK: {len(greenlets)} active greenlets, atexit registered")
468        """)
469        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
470        self.assertIn("OK: 10 active greenlets, atexit registered", stdout)
471
472    def test_greenlet_construction_with_cross_thread_deleteme_during_atexit(self):
473        # Create greenlets in a worker thread, transfer them to the main
474        # thread, then drop them — populating the deleteme list. Then
475        # construct a new greenlet during atexit. On Python < 3.11
476        # clear_deleteme_list() could previously crash if the
477        # PythonAllocator vector copy failed during early Py_FinalizeEx;
478        # using std::swap eliminates that allocation.
479        rc, stdout, stderr = self._run_shutdown_script("""\
480            import atexit
481            import greenlet
482            import threading
483
484            cross_thread_refs = []
485
486            def thread_worker():
487                # Create greenlets in this thread
488                def gl_body():
489                    greenlet.getcurrent().parent.switch("ready")
490                for _ in range(20):
491                    g = greenlet.greenlet(gl_body)
492                    g.switch()
493                    cross_thread_refs.append(g)
494
495            t = threading.Thread(target=thread_worker)
496            t.start()
497            t.join()
498
499            # Dropping these references in the main thread
500            # causes them to be added to the main thread's
501            # deleteme list (deferred cross-thread dealloc).
502            cross_thread_refs.clear()
503
504            def create_at_exit():
505                try:
506                    g = greenlet.greenlet(lambda: None)
507                    print(f"OK: created greenlet at exit {g!r}")
508                except Exception as e:
509                    print(f"OK: raised {type(e).__name__}: {e}")
510
511            atexit.register(create_at_exit)
512            print("OK: cross-thread setup done, atexit registered")
513        """)
514        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
515        self.assertIn("OK: cross-thread setup done, atexit registered", stdout)
516
517
518    # -----------------------------------------------------------------
519    # Group D.1: TDD-certified — getcurrent() during GC finalization
520    #
521    # These tests use gc.disable() + reference cycles to force __del__
522    # to run during Py_FinalizeEx's GC pass, where Py_IsFinalizing()
523    # is True.  Without the fix, getcurrent() returns a live greenlet
524    # (unguarded); with the fix, it returns None.
525    #
526    # TDD verification (greenlet 3.3.2 = RED, patched = GREEN):
527    #   Python 3.10: RED (UNGUARDED) → GREEN (GUARDED)
528    #   Python 3.11: RED (UNGUARDED) → GREEN (GUARDED)
529    #   Python 3.12: RED (UNGUARDED) → GREEN (GUARDED)
530    #   Python 3.13: RED (UNGUARDED) → GREEN (GUARDED)
531    #   Python 3.14: RED (UNGUARDED) → GREEN (GUARDED)
532    # -----------------------------------------------------------------
533
534    def test_getcurrent_returns_none_during_gc_finalization(self):
535        # greenlet.getcurrent() must return None when called from a
536        # __del__ method during Py_FinalizeEx's GC collection pass.
537
538        # On Python >= 3.11, _Py_IsFinalizing() is True during this
539        # phase.  Without the Py_IsFinalizing() guard in mod_getcurrent,
540        # this would return a greenlet — the same unguarded code path
541        # that leads to SIGSEGV in production (uWSGI worker recycling).
542        rc, stdout, stderr = self._run_shutdown_script("""\
543            import gc
544            import os
545            import greenlet
546
547            gc.disable()
548
549            class CleanupChecker:
550                def __del__(self):
551                    try:
552                        cur = greenlet.getcurrent()
553                        if cur is None:
554                            os.write(1, b"GUARDED: getcurrent=None\\n")
555                        else:
556                            os.write(1, b"UNGUARDED: getcurrent="
557                                     + type(cur).__name__.encode() + b"\\n")
558                    except Exception as e:
559                        os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n")
560
561            # Reference cycle: only collected during Py_FinalizeEx GC pass
562            a = CleanupChecker()
563            b = {"ref": a}
564            a._cycle = b
565            del a, b
566
567            print("OK: deferred cycle created")
568        """)
569        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
570        self.assertIn("OK: deferred cycle created", stdout)
571        self.assertIn("GUARDED: getcurrent=None", stdout,
572                      "getcurrent() must return None during GC finalization; "
573                      "returned a live object instead (missing Py_IsFinalizing guard)")
574
575    def test_getcurrent_returns_none_during_gc_finalization_with_active_greenlets(self):
576        # Same as above but with active greenlets at shutdown, which
577        # increases the amount of C++ destructor work during finalization.
578
579        rc, stdout, stderr = self._run_shutdown_script("""\
580            import gc
581            import os
582            import greenlet
583
584            gc.disable()
585
586            class CleanupChecker:
587                def __del__(self):
588                    try:
589                        cur = greenlet.getcurrent()
590                        if cur is None:
591                            os.write(1, b"GUARDED: getcurrent=None\\n")
592                        else:
593                            os.write(1, b"UNGUARDED: getcurrent="
594                                     + type(cur).__name__.encode() + b"\\n")
595                    except Exception as e:
596                        os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n")
597
598            # Create active greenlets
599            greenlets = []
600            for _ in range(10):
601                def worker():
602                    greenlet.getcurrent().parent.switch("suspended")
603                g = greenlet.greenlet(worker)
604                g.switch()
605                greenlets.append(g)
606
607            # Reference cycle deferred to Py_FinalizeEx
608            a = CleanupChecker()
609            b = {"ref": a}
610            a._cycle = b
611            del a, b
612
613            print(f"OK: {len(greenlets)} active greenlets, cycle deferred")
614        """)
615        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
616        self.assertIn("OK: 10 active greenlets, cycle deferred", stdout)
617        self.assertIn("GUARDED: getcurrent=None", stdout,
618                      "getcurrent() must return None during GC finalization; "
619                      "returned a live object instead (missing Py_IsFinalizing guard)")
620
621    def test_getcurrent_returns_none_during_gc_finalization_cross_thread(self):
622        # Combines cross-thread greenlet deallocation (deleteme list)
623        # with the GC finalization check.  This simulates the production
624        # scenario where uWSGI worker threads create greenlets that are
625        # transferred to the main thread, then cleaned up during
626        # Py_FinalizeEx.
627
628        rc, stdout, stderr = self._run_shutdown_script("""\
629            import gc
630            import os
631            import threading
632            import greenlet
633
634            gc.disable()
635
636            class CleanupChecker:
637                def __del__(self):
638                    try:
639                        cur = greenlet.getcurrent()
640                        if cur is None:
641                            os.write(1, b"GUARDED: getcurrent=None\\n")
642                        else:
643                            os.write(1, b"UNGUARDED: getcurrent="
644                                     + type(cur).__name__.encode() + b"\\n")
645                    except Exception as e:
646                        os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n")
647
648            # Create cross-thread greenlet references
649            cross_refs = []
650            def thread_fn():
651                for _ in range(20):
652                    def body():
653                        greenlet.getcurrent().parent.switch("x")
654                    g = greenlet.greenlet(body)
655                    g.switch()
656                    cross_refs.append(g)
657            t = threading.Thread(target=thread_fn)
658            t.start()
659            t.join()
660            cross_refs.clear()
661
662            # Reference cycle deferred to Py_FinalizeEx
663            a = CleanupChecker()
664            b = {"ref": a}
665            a._cycle = b
666            del a, b
667
668            print("OK: cross-thread cleanup + cycle deferred")
669        """)
670        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
671        self.assertIn("OK: cross-thread cleanup + cycle deferred", stdout)
672        self.assertIn("GUARDED: getcurrent=None", stdout,
673                      "getcurrent() must return None during GC finalization; "
674                      "returned a live object instead (missing Py_IsFinalizing guard)")
675
676
677    # -----------------------------------------------------------------
678    # Group D.2: TDD-certified — getcurrent() during atexit phase
679    #
680    # These tests register the checker BEFORE importing greenlet.
681    # Python's atexit is LIFO, so greenlet's handler (registered at
682    # import) runs FIRST and sets g_greenlet_shutting_down=1; then
683    # the checker runs SECOND and observes getcurrent() → None.
684    #
685    # This covers the atexit phase where _Py_IsFinalizing() is still
686    # False on ALL Python versions — the exact window that causes
687    # SIGSEGV in production (uWSGI worker recycling → Py_FinalizeEx).
688    #
689    # TDD verification (greenlet 3.3.2 = RED, patched = GREEN):
690    #   Python 3.10: RED (UNGUARDED) → GREEN (GUARDED)
691    #   Python 3.11: RED (UNGUARDED) → GREEN (GUARDED)
692    #   Python 3.12: RED (UNGUARDED) → GREEN (GUARDED)
693    #   Python 3.13: RED (UNGUARDED) → GREEN (GUARDED)
694    #   Python 3.14: RED (UNGUARDED) → GREEN (GUARDED)
695    # -----------------------------------------------------------------
696
697    def test_getcurrent_returns_none_during_atexit_phase(self):
698        # greenlet.getcurrent() must NOT return None when called from an
699        # atexit handler that runs AFTER greenlet's own atexit handler.
700
701        rc, stdout, stderr = self._run_shutdown_script("""\
702            import atexit
703            import os
704
705            def late_checker():
706                try:
707                    import greenlet
708                    cur = greenlet.getcurrent()
709                    if cur is None:
710                        os.write(1, b"GUARDED: getcurrent=None\\n")
711                    else:
712                        os.write(1, b"UNGUARDED: getcurrent="
713                                 + type(cur).__name__.encode() + b"\\n")
714                except Exception as e:
715                    os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n")
716
717            # Register BEFORE importing greenlet.  LIFO order:
718            # greenlet's handler (registered at import) runs FIRST,
719            # late_checker runs SECOND — seeing the flag already set.
720            atexit.register(late_checker)
721
722            import greenlet
723            print("OK: atexit registered before greenlet import")
724        """)
725        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
726        self.assertIn("OK: atexit registered before greenlet import", stdout)
727        self.assertIn("UNGUARDED", stdout)
728
729
730    def test_getcurrent_returns_none_during_atexit_phase_with_active_greenlets(self):
731        # Same as above but with active greenlets
732        rc, stdout, stderr = self._run_shutdown_script("""\
733            import atexit
734            import os
735
736            def late_checker():
737                try:
738                    import greenlet
739                    cur = greenlet.getcurrent()
740                    if cur is None:
741                        os.write(1, b"GUARDED: getcurrent=None\\n")
742                    else:
743                        os.write(1, b"UNGUARDED: getcurrent="
744                                 + type(cur).__name__.encode() + b"\\n")
745                except Exception as e:
746                    os.write(1, b"EXCEPTION: " + str(e).encode() + b"\\n")
747
748            atexit.register(late_checker)
749
750            import greenlet
751
752            greenlets = []
753            for _ in range(10):
754                def worker():
755                    greenlet.getcurrent().parent.switch("parked")
756                g = greenlet.greenlet(worker)
757                g.switch()
758                greenlets.append(g)
759
760            print(f"OK: {len(greenlets)} active greenlets, atexit registered")
761        """)
762        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
763        self.assertIn("OK: 10 active greenlets, atexit registered", stdout)
764        self.assertIn("UNGUARDED", stdout)
765
766    def test_api_getcurrent_no_system_error_at_module_gc_time(self):
767        # If we use the C API directly to return a greenlet AFTER
768        # atexit threads have been run, we don't crash, we get a
769        # specific error. We arrange for this by putting a __del__ on
770        # an object that lives in greenlet's own (extension module)
771        # dict; this is cleaned out sometime during the module cleanup
772        # steps.
773        rc, stdout, stderr = self._run_shutdown_script("""\
774            import greenlet
775            from greenlet.tests import _test_extension
776
777            class WithDel:
778                # must cache the method we want, because by the time we
779                # run, module globals may have been cleaned up.
780                def __del__(self, gc=_test_extension.getcurrent_api):
781                    print('Destructor running')
782                    gc() # Should print an unraisable RuntimeException
783
784            greenlet._greenlet.with_del = WithDel()
785        """)
786        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
787        self.assertIn('Destructor running', stdout)
788        self.assertIn('RuntimeError: greenlet is being finalized', stderr)
789
790
791    def test_switch_no_error_at_module_gc_time(self):
792        # Switching to a greenlet we've captured during
793        # module tear down doesn't cause a crash
794        rc, stdout, stderr = self._run_shutdown_script("""\
795            import greenlet
796            from greenlet.tests import _test_extension
797
798            gs = []
799            # must cache the objects we want, because by the time we
800            # run, module globals may have been cleaned up.
801            def do_it(gs=gs):
802                print('current', gs)
803                gs[0].parent.switch(1)
804
805
806            gs.append(greenlet.greenlet(do_it))
807            gs.append(greenlet.greenlet(do_it))
808            gs[1].switch()
809
810            class WithDel:
811                def __del__(self, gs=gs):
812                    print('Destructor running')
813                    r = gs[0].switch()
814                    print('Result', r)
815
816            greenlet._greenlet.with_del = WithDel()
817        """)
818        self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
819        self.assertIn('Destructor running', stdout)
820        self.assertIn('Result 1', stdout)
821
822
823if __name__ == '__main__':
824    unittest.main()
825 
codekingpro/portable-devtools · Team Ai