🌐 AI搜索 & 代理 主页
Skip to content

Commit c40f08e

Browse files
committed
Add thread test
1 parent 05e3c83 commit c40f08e

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

Lib/test/test_external_inspection.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,6 +2399,166 @@ def level1():
23992399
lines_no_cache = [f.lineno for f in frames_no_cache]
24002400
self.assertEqual(lines_cached, lines_no_cache)
24012401

2402+
@skip_if_not_supported
2403+
@unittest.skipIf(
2404+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2405+
"Test only runs on Linux with process_vm_readv support",
2406+
)
2407+
def test_cache_per_thread_isolation(self):
2408+
"""Test that frame cache is per-thread and cache invalidation works independently."""
2409+
script_body = """\
2410+
import threading
2411+
2412+
lock = threading.Lock()
2413+
2414+
def sync(msg):
2415+
with lock:
2416+
sock.sendall(msg + b"\\n")
2417+
sock.recv(1)
2418+
2419+
# Thread 1 functions
2420+
def baz1():
2421+
sync(b"t1:baz1")
2422+
2423+
def bar1():
2424+
baz1()
2425+
2426+
def blech1():
2427+
sync(b"t1:blech1")
2428+
2429+
def foo1():
2430+
bar1() # Goes down to baz1, syncs
2431+
blech1() # Returns up, goes down to blech1, syncs
2432+
2433+
# Thread 2 functions
2434+
def baz2():
2435+
sync(b"t2:baz2")
2436+
2437+
def bar2():
2438+
baz2()
2439+
2440+
def blech2():
2441+
sync(b"t2:blech2")
2442+
2443+
def foo2():
2444+
bar2() # Goes down to baz2, syncs
2445+
blech2() # Returns up, goes down to blech2, syncs
2446+
2447+
t1 = threading.Thread(target=foo1)
2448+
t2 = threading.Thread(target=foo2)
2449+
t1.start()
2450+
t2.start()
2451+
t1.join()
2452+
t2.join()
2453+
"""
2454+
2455+
with self._target_process(script_body) as (p, client_socket, make_unwinder):
2456+
unwinder = make_unwinder(cache_frames=True)
2457+
buffer = b""
2458+
2459+
def recv_msg():
2460+
"""Receive a single message from socket."""
2461+
nonlocal buffer
2462+
while b"\n" not in buffer:
2463+
chunk = client_socket.recv(256)
2464+
if not chunk:
2465+
return None
2466+
buffer += chunk
2467+
msg, buffer = buffer.split(b"\n", 1)
2468+
return msg
2469+
2470+
def get_thread_frames(target_funcs):
2471+
"""Get frames for thread matching target functions."""
2472+
for _ in range(3):
2473+
try:
2474+
traces = unwinder.get_stack_trace()
2475+
for interp in traces:
2476+
for thread in interp.threads:
2477+
funcs = [f.funcname for f in thread.frame_info]
2478+
if any(f in funcs for f in target_funcs):
2479+
return funcs
2480+
except RuntimeError:
2481+
pass
2482+
return None
2483+
2484+
# Track results for each sync point
2485+
results = {}
2486+
2487+
# Process 4 sync points: baz1, baz2, blech1, blech2
2488+
# With the lock, threads are serialized - handle one at a time
2489+
for _ in range(4):
2490+
msg = recv_msg()
2491+
self.assertIsNotNone(msg, "Expected message from subprocess")
2492+
2493+
# Determine which thread/function and take snapshot
2494+
if msg == b"t1:baz1":
2495+
funcs = get_thread_frames(["baz1", "bar1", "foo1"])
2496+
self.assertIsNotNone(funcs, "Thread 1 not found at baz1")
2497+
results["t1:baz1"] = funcs
2498+
elif msg == b"t2:baz2":
2499+
funcs = get_thread_frames(["baz2", "bar2", "foo2"])
2500+
self.assertIsNotNone(funcs, "Thread 2 not found at baz2")
2501+
results["t2:baz2"] = funcs
2502+
elif msg == b"t1:blech1":
2503+
funcs = get_thread_frames(["blech1", "foo1"])
2504+
self.assertIsNotNone(funcs, "Thread 1 not found at blech1")
2505+
results["t1:blech1"] = funcs
2506+
elif msg == b"t2:blech2":
2507+
funcs = get_thread_frames(["blech2", "foo2"])
2508+
self.assertIsNotNone(funcs, "Thread 2 not found at blech2")
2509+
results["t2:blech2"] = funcs
2510+
2511+
# Release thread to continue
2512+
client_socket.sendall(b"k")
2513+
2514+
# Validate Phase 1: baz snapshots
2515+
t1_baz = results.get("t1:baz1")
2516+
t2_baz = results.get("t2:baz2")
2517+
self.assertIsNotNone(t1_baz, "Missing t1:baz1 snapshot")
2518+
self.assertIsNotNone(t2_baz, "Missing t2:baz2 snapshot")
2519+
2520+
# Thread 1 at baz1: should have foo1->bar1->baz1
2521+
self.assertIn("baz1", t1_baz)
2522+
self.assertIn("bar1", t1_baz)
2523+
self.assertIn("foo1", t1_baz)
2524+
self.assertNotIn("blech1", t1_baz)
2525+
# No cross-contamination
2526+
self.assertNotIn("baz2", t1_baz)
2527+
self.assertNotIn("bar2", t1_baz)
2528+
self.assertNotIn("foo2", t1_baz)
2529+
2530+
# Thread 2 at baz2: should have foo2->bar2->baz2
2531+
self.assertIn("baz2", t2_baz)
2532+
self.assertIn("bar2", t2_baz)
2533+
self.assertIn("foo2", t2_baz)
2534+
self.assertNotIn("blech2", t2_baz)
2535+
# No cross-contamination
2536+
self.assertNotIn("baz1", t2_baz)
2537+
self.assertNotIn("bar1", t2_baz)
2538+
self.assertNotIn("foo1", t2_baz)
2539+
2540+
# Validate Phase 2: blech snapshots (cache invalidation test)
2541+
t1_blech = results.get("t1:blech1")
2542+
t2_blech = results.get("t2:blech2")
2543+
self.assertIsNotNone(t1_blech, "Missing t1:blech1 snapshot")
2544+
self.assertIsNotNone(t2_blech, "Missing t2:blech2 snapshot")
2545+
2546+
# Thread 1 at blech1: bar1/baz1 should be GONE (cache invalidated)
2547+
self.assertIn("blech1", t1_blech)
2548+
self.assertIn("foo1", t1_blech)
2549+
self.assertNotIn("bar1", t1_blech, "Cache not invalidated: bar1 still present")
2550+
self.assertNotIn("baz1", t1_blech, "Cache not invalidated: baz1 still present")
2551+
# No cross-contamination
2552+
self.assertNotIn("blech2", t1_blech)
2553+
2554+
# Thread 2 at blech2: bar2/baz2 should be GONE (cache invalidated)
2555+
self.assertIn("blech2", t2_blech)
2556+
self.assertIn("foo2", t2_blech)
2557+
self.assertNotIn("bar2", t2_blech, "Cache not invalidated: bar2 still present")
2558+
self.assertNotIn("baz2", t2_blech, "Cache not invalidated: baz2 still present")
2559+
# No cross-contamination
2560+
self.assertNotIn("blech1", t2_blech)
2561+
24022562

24032563
if __name__ == "__main__":
24042564
unittest.main()

0 commit comments

Comments
 (0)