codekingpro/portable-devtools
114k
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 