Skip to content

Commit 1b641f5

Browse files
Add tests for subinterpreters
1 parent a09b474 commit 1b641f5

File tree

1 file changed

+119
-41
lines changed

1 file changed

+119
-41
lines changed

Lib/test/test_get_gc_stats.py

Lines changed: 119 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
import unittest
88

99
from 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

1920
def 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

Comments
 (0)