77import unittest
88
99from test .support import (
10+ import_helper ,
1011 SHORT_TIMEOUT ,
1112 requires_remote_subprocess_debugging ,
1213)
1314
1415
15- def get_interpreter_identifiers (gc_stats : tuple [dict [str , str | int | float ]]) -> list [str ]:
16- return [ s ["iid" ] for s in gc_stats ]
16+ def get_interpreter_identifiers (gc_stats : tuple [dict [str , str | int | float ]]) -> tuple [str ,... ]:
17+ return tuple ( sorted ({ s ["iid" ] for s in gc_stats }))
1718
1819
1920def get_generations (gc_stats : tuple [dict [str , str | int | float ]]) -> tuple [int ,int ,int ]:
@@ -24,31 +25,31 @@ def get_generations(gc_stats: tuple[dict[str, str|int|float]]) -> tuple[int,int,
2425 return tuple (sorted (generations ))
2526
2627
27- def get_last_item_for_generation (gc_stats : tuple [dict [str , str | int | float ]],
28- generation :int ) -> dict [str , str | int | float ] | None :
28+ def get_last_item (gc_stats : tuple [dict [str , str | int | float ]],
29+ generation :int ,
30+ iid :int ) -> dict [str , str | int | float ] | None :
2931 item = None
3032 for s in gc_stats :
31- if s ["gen" ] == generation :
33+ if s ["gen" ] == generation and s [ "iid" ] == iid :
3234 if item is None or item ["ts_start" ] < s ["ts_start" ]:
3335 item = s
3436
3537 return item
3638
3739
38-
3940@requires_remote_subprocess_debugging ()
40- class TestGetStackTrace (unittest .TestCase ):
41+ class TestGetGCStats (unittest .TestCase ):
4142
42- def run_child_process (self ):
43+ def _run_child_process (self , all_interpreters ):
4344 # Run the test in a subprocess to avoid side effects
44- script = textwrap .dedent ("""\
45+ script = textwrap .dedent (f """\
4546 import json
4647 import os
4748 import sys
4849 import _remote_debugging
4950
5051 pid = int(sys.argv[1])
51- gc_stats = _remote_debugging.get_gc_stats(pid, all_interpreters=False )
52+ gc_stats = _remote_debugging.get_gc_stats(pid, all_interpreters={ all_interpreters } )
5253 print(json.dumps(gc_stats, indent=1))
5354 """ )
5455
@@ -68,56 +69,133 @@ def run_child_process(self):
6869 )
6970 return result
7071
71- def test_get_gc_stats_for_main_interpreter (self ):
72- """Test that RemoteUnwinder works on the same process after _ctypes import.
72+ def _run_in_interpreter (self , interp ):
73+ source = f"""if True:
74+ import gc
7375
74- When _ctypes is imported, it may call dlopen on the libpython shared
75- library, creating a duplicate mapping in the process address space.
76- The remote debugging code must skip these uninitialized duplicate
77- mappings and find the real PyRuntime. See gh-144563.
76+ gc.collect(0)
77+ gc.collect(1)
78+ gc.collect(2)
7879 """
80+ interp .exec (source )
7981
80- # Skip the test if the _ctypes module is missing.
82+ def _check_gc_state (self , before , after ):
83+ self .assertIsNotNone (before )
84+ self .assertIsNotNone (after )
8185
82- before_stats = json .loads (self .run_child_process ().stdout )
83- after_stats = json .loads (self .run_child_process ().stdout )
86+ self .assertGreater (after ["collections" ], before ["collections" ], (before , after ))
87+ self .assertGreater (after ["ts_start" ], before ["ts_start" ], (before , after ))
88+ self .assertGreater (after ["ts_stop" ], before ["ts_stop" ], (before , after ))
89+ self .assertGreater (after ["duration" ], before ["duration" ], (before , after ))
90+
91+ self .assertGreater (after ["object_visits" ], before ["object_visits" ], (before , after ))
92+ self .assertGreater (after ["candidates" ], before ["candidates" ], (before , after ))
93+
94+ # may not grow
95+ self .assertGreaterEqual (after ["collected" ], before ["collected" ], (before , after ))
96+ self .assertGreaterEqual (after ["uncollectable" ], before ["uncollectable" ], (before , after ))
97+
98+ if before ["gen" ] == 1 :
99+ self .assertGreaterEqual (after ["objects_transitively_reachable" ],
100+ before ["objects_transitively_reachable" ],
101+ (before , after ))
102+ self .assertGreaterEqual (after ["objects_not_transitively_reachable" ],
103+ before ["objects_not_transitively_reachable" ],
104+ (before , after ))
105+
106+ def test_get_gc_stats_for_main_interpreter (self ):
107+ before_stats = json .loads (self ._run_child_process (False ).stdout )
108+ after_stats = json .loads (self ._run_child_process (False ).stdout )
84109
85110 before_iids = get_interpreter_identifiers (before_stats )
86111 after_iids = get_interpreter_identifiers (after_stats )
87112
88- self .assertTrue ( all ([ 0 == iid for iid in before_iids ] ))
89- self .assertTrue ( all ([ 0 == iid for iid in after_iids ] ))
113+ self .assertEqual ( before_iids , ( 0 , ))
114+ self .assertEqual ( after_iids , ( 0 , ))
90115
91116 before_gens = get_generations (before_stats )
92117 after_gens = get_generations (after_stats )
93118
94119 self .assertEqual (before_gens , (0 , 1 , 2 ))
95120 self .assertEqual (after_gens , (0 , 1 , 2 ))
96121
97- before_last_items = (get_last_item_for_generation (before_stats , 0 ),
98- get_last_item_for_generation (before_stats , 1 ),
99- get_last_item_for_generation (before_stats , 2 ))
122+ iid = 0 # main interpreter ID
123+ before_last_items = (get_last_item (before_stats , 0 , iid ),
124+ get_last_item (before_stats , 1 , iid ),
125+ get_last_item (before_stats , 2 , iid ))
100126
101- after_last_items = (get_last_item_for_generation (after_stats , 0 ),
102- get_last_item_for_generation (after_stats , 1 ),
103- get_last_item_for_generation (after_stats , 2 ))
127+ after_last_items = (get_last_item (after_stats , 0 , iid ),
128+ get_last_item (after_stats , 1 , iid ),
129+ get_last_item (after_stats , 2 , iid ))
104130
105131 for before , after in zip (before_last_items , after_last_items ):
106- self .assertIsNotNone (before )
107- self .assertIsNotNone (after )
132+ self ._check_gc_state (before , after )
108133
109- self .assertGreater (after ["collections" ], before ["collections" ], (before , after ))
110- self .assertGreater (after ["ts_start" ], before ["ts_start" ], (before , after ))
111- self .assertGreater (after ["ts_stop" ], before ["ts_stop" ], (before , after ))
112- self .assertGreater (after ["duration" ], before ["duration" ], (before , after ))
134+ def test_get_gc_stats_for_all_interpreters (self ):
135+ interpreters = import_helper .import_module ("concurrent.interpreters" )
136+ interp = interpreters .create ()
113137
114- self .assertGreater (after ["object_visits" ], before ["object_visits" ], (before , after ))
115- self .assertGreater (after ["candidates" ], before ["candidates" ], (before , after ))
138+ self ._run_in_interpreter (interp ) # ensure that subinterpeter have GC stats
139+ before_stats = json .loads (self ._run_child_process (True ).stdout )
140+ self ._run_in_interpreter (interp ) # ensure that GC stats in subinterpreter changed
141+ after_stats = json .loads (self ._run_child_process (True ).stdout )
142+ interp .close ()
116143
117- # may not grow
118- self .assertGreaterEqual (after ["collected" ], before ["collected" ], (before , after ))
119- self .assertGreaterEqual (after ["uncollectable" ], before ["uncollectable" ], (before , after ))
144+ before_iids = get_interpreter_identifiers (before_stats )
145+ after_iids = get_interpreter_identifiers (after_stats )
146+
147+ self .assertEqual (before_iids , (0 , interp .id ))
148+ self .assertEqual (after_iids , (0 , interp .id ))
149+
150+ before_gens = get_generations (before_stats )
151+ after_gens = get_generations (after_stats )
152+
153+ self .assertEqual (before_gens , (0 , 1 , 2 ))
154+ self .assertEqual (after_gens , (0 , 1 , 2 ))
120155
121- if before ["gen" ] == 1 :
122- self .assertGreaterEqual (after ["objects_transitively_reachable" ], before ["objects_transitively_reachable" ], (before , after ))
123- self .assertGreaterEqual (after ["objects_not_transitively_reachable" ], before ["objects_not_transitively_reachable" ], (before , after ))
156+ for iid in after_iids :
157+ with self .subTest (f"iid={ iid } " ):
158+ before_last_items = (get_last_item (before_stats , 0 , iid ),
159+ get_last_item (before_stats , 1 , iid ),
160+ get_last_item (before_stats , 2 , iid ))
161+
162+ after_last_items = (get_last_item (after_stats , 0 , iid ),
163+ get_last_item (after_stats , 1 , iid ),
164+ get_last_item (after_stats , 2 , iid ))
165+
166+ for before , after in zip (before_last_items , after_last_items ):
167+ self ._check_gc_state (before , after )
168+
169+ def test_get_gc_stats_for_main_interpreter_if_subinterpreter_exists (self ):
170+ interpreters = import_helper .import_module ("concurrent.interpreters" )
171+ interp = interpreters .create ()
172+
173+ self ._run_in_interpreter (interp ) # ensure that subinterpeter have GC stats
174+ before_stats = json .loads (self ._run_child_process (False ).stdout )
175+ self ._run_in_interpreter (interp ) # ensure that GC stats in subinterpreter changed
176+ after_stats = json .loads (self ._run_child_process (False ).stdout )
177+ interp .close ()
178+
179+ before_iids = get_interpreter_identifiers (before_stats )
180+ after_iids = get_interpreter_identifiers (after_stats )
181+
182+ self .assertEqual (before_iids , (0 , ))
183+ self .assertEqual (after_iids , (0 , ))
184+
185+ before_gens = get_generations (before_stats )
186+ after_gens = get_generations (after_stats )
187+
188+ self .assertEqual (before_gens , (0 , 1 , 2 ))
189+ self .assertEqual (after_gens , (0 , 1 , 2 ))
190+
191+ iid = 0 # main interpreter ID
192+ before_last_items = (get_last_item (before_stats , 0 , iid ),
193+ get_last_item (before_stats , 1 , iid ),
194+ get_last_item (before_stats , 2 , iid ))
195+
196+ after_last_items = (get_last_item (after_stats , 0 , iid ),
197+ get_last_item (after_stats , 1 , iid ),
198+ get_last_item (after_stats , 2 , iid ))
199+
200+ for before , after in zip (before_last_items , after_last_items ):
201+ self ._check_gc_state (before , after )
0 commit comments