mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
GH-91048: Add utils for printing the call stack for asyncio tasks (#133284)
This commit is contained in:
parent
7363e8d24d
commit
2bc8365231
17 changed files with 1309 additions and 90 deletions
|
@ -543,6 +543,105 @@ configuration mechanisms).
|
|||
.. seealso::
|
||||
:pep:`741`.
|
||||
|
||||
.. _whatsnew314-asyncio-introspection:
|
||||
|
||||
Asyncio introspection capabilities
|
||||
----------------------------------
|
||||
|
||||
Added a new command-line interface to inspect running Python processes using
|
||||
asynchronous tasks, available via:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio ps PID
|
||||
|
||||
This tool inspects the given process ID (PID) and displays information about
|
||||
currently running asyncio tasks. It outputs a task table: a flat
|
||||
listing of all tasks, their names, their coroutine stacks, and which tasks are
|
||||
awaiting them.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio pstree PID
|
||||
|
||||
This tool fetches the same information, but renders a visual async call tree,
|
||||
showing coroutine relationships in a hierarchical format. This command is
|
||||
particularly useful for debugging long-running or stuck asynchronous programs.
|
||||
It can help developers quickly identify where a program is blocked, what tasks
|
||||
are pending, and how coroutines are chained together.
|
||||
|
||||
For example given this code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def play(track):
|
||||
await asyncio.sleep(5)
|
||||
print(f"🎵 Finished: {track}")
|
||||
|
||||
async def album(name, tracks):
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for track in tracks:
|
||||
tg.create_task(play(track), name=track)
|
||||
|
||||
async def main():
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tg.create_task(
|
||||
album("Sundowning", ["TNDNBTG", "Levitate"]), name="Sundowning")
|
||||
tg.create_task(
|
||||
album("TMBTE", ["DYWTYLM", "Aqua Regia"]), name="TMBTE")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Executing the new tool on the running process will yield a table like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio ps 12345
|
||||
|
||||
tid task id task name coroutine chain awaiter name awaiter id
|
||||
---------------------------------------------------------------------------------------------------------------------------------------
|
||||
8138752 0x564bd3d0210 Task-1 0x0
|
||||
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
|
||||
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
|
||||
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
|
||||
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
|
||||
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
|
||||
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
|
||||
|
||||
|
||||
or:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio pstree 12345
|
||||
|
||||
└── (T) Task-1
|
||||
└── main
|
||||
└── __aexit__
|
||||
└── _aexit
|
||||
├── (T) Sundowning
|
||||
│ └── album
|
||||
│ └── __aexit__
|
||||
│ └── _aexit
|
||||
│ ├── (T) TNDNBTG
|
||||
│ └── (T) Levitate
|
||||
└── (T) TMBTE
|
||||
└── album
|
||||
└── __aexit__
|
||||
└── _aexit
|
||||
├── (T) DYWTYLM
|
||||
└── (T) Aqua Regia
|
||||
|
||||
If a cycle is detected in the async await graph (which could indicate a
|
||||
programming issue), the tool raises an error and lists the cycle paths that
|
||||
prevent tree construction.
|
||||
|
||||
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
|
||||
Gomez Macias in :gh:`91048`.)
|
||||
|
||||
.. _whatsnew314-tail-call:
|
||||
|
||||
A new type of interpreter
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import argparse
|
||||
import ast
|
||||
import asyncio
|
||||
import asyncio.tools
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
import inspect
|
||||
|
@ -140,6 +142,36 @@ class REPLThread(threading.Thread):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python3 -m asyncio",
|
||||
description="Interactive asyncio shell and CLI tools",
|
||||
)
|
||||
subparsers = parser.add_subparsers(help="sub-commands", dest="command")
|
||||
ps = subparsers.add_parser(
|
||||
"ps", help="Display a table of all pending tasks in a process"
|
||||
)
|
||||
ps.add_argument("pid", type=int, help="Process ID to inspect")
|
||||
pstree = subparsers.add_parser(
|
||||
"pstree", help="Display a tree of all pending tasks in a process"
|
||||
)
|
||||
pstree.add_argument("pid", type=int, help="Process ID to inspect")
|
||||
args = parser.parse_args()
|
||||
match args.command:
|
||||
case "ps":
|
||||
asyncio.tools.display_awaited_by_tasks_table(args.pid)
|
||||
sys.exit(0)
|
||||
case "pstree":
|
||||
asyncio.tools.display_awaited_by_tasks_tree(args.pid)
|
||||
sys.exit(0)
|
||||
case None:
|
||||
pass # continue to the interactive shell
|
||||
case _:
|
||||
# shouldn't happen as an invalid command-line wouldn't parse
|
||||
# but let's keep it for the next person adding a command
|
||||
print(f"error: unhandled command {args.command}", file=sys.stderr)
|
||||
parser.print_usage(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.audit("cpython.run_stdin")
|
||||
|
||||
if os.getenv('PYTHON_BASIC_REPL'):
|
||||
|
|
212
Lib/asyncio/tools.py
Normal file
212
Lib/asyncio/tools.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
"""Tools to analyze tasks running in asyncio programs."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from itertools import count
|
||||
from enum import Enum
|
||||
import sys
|
||||
from _remotedebugging import get_all_awaited_by
|
||||
|
||||
|
||||
class NodeType(Enum):
|
||||
COROUTINE = 1
|
||||
TASK = 2
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CycleFoundException(Exception):
|
||||
"""Raised when there is a cycle when drawing the call tree."""
|
||||
cycles: list[list[int]]
|
||||
id2name: dict[int, str]
|
||||
|
||||
|
||||
# ─── indexing helpers ───────────────────────────────────────────
|
||||
def _index(result):
|
||||
id2name, awaits = {}, []
|
||||
for _thr_id, tasks in result:
|
||||
for tid, tname, awaited in tasks:
|
||||
id2name[tid] = tname
|
||||
for stack, parent_id in awaited:
|
||||
awaits.append((parent_id, stack, tid))
|
||||
return id2name, awaits
|
||||
|
||||
|
||||
def _build_tree(id2name, awaits):
|
||||
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
|
||||
children = defaultdict(list)
|
||||
cor_names = defaultdict(dict) # (parent) -> {frame: node}
|
||||
cor_id_seq = count(1)
|
||||
|
||||
def _cor_node(parent_key, frame_name):
|
||||
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
|
||||
bucket = cor_names[parent_key]
|
||||
if frame_name in bucket:
|
||||
return bucket[frame_name]
|
||||
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
|
||||
id2label[node_key] = frame_name
|
||||
children[parent_key].append(node_key)
|
||||
bucket[frame_name] = node_key
|
||||
return node_key
|
||||
|
||||
# lay down parent ➜ …frames… ➜ child paths
|
||||
for parent_id, stack, child_id in awaits:
|
||||
cur = (NodeType.TASK, parent_id)
|
||||
for frame in reversed(stack): # outer-most → inner-most
|
||||
cur = _cor_node(cur, frame)
|
||||
child_key = (NodeType.TASK, child_id)
|
||||
if child_key not in children[cur]:
|
||||
children[cur].append(child_key)
|
||||
|
||||
return id2label, children
|
||||
|
||||
|
||||
def _roots(id2label, children):
|
||||
all_children = {c for kids in children.values() for c in kids}
|
||||
return [n for n in id2label if n not in all_children]
|
||||
|
||||
# ─── detect cycles in the task-to-task graph ───────────────────────
|
||||
def _task_graph(awaits):
|
||||
"""Return {parent_task_id: {child_task_id, …}, …}."""
|
||||
g = defaultdict(set)
|
||||
for parent_id, _stack, child_id in awaits:
|
||||
g[parent_id].add(child_id)
|
||||
return g
|
||||
|
||||
|
||||
def _find_cycles(graph):
|
||||
"""
|
||||
Depth-first search for back-edges.
|
||||
|
||||
Returns a list of cycles (each cycle is a list of task-ids) or an
|
||||
empty list if the graph is acyclic.
|
||||
"""
|
||||
WHITE, GREY, BLACK = 0, 1, 2
|
||||
color = defaultdict(lambda: WHITE)
|
||||
path, cycles = [], []
|
||||
|
||||
def dfs(v):
|
||||
color[v] = GREY
|
||||
path.append(v)
|
||||
for w in graph.get(v, ()):
|
||||
if color[w] == WHITE:
|
||||
dfs(w)
|
||||
elif color[w] == GREY: # back-edge → cycle!
|
||||
i = path.index(w)
|
||||
cycles.append(path[i:] + [w]) # make a copy
|
||||
color[v] = BLACK
|
||||
path.pop()
|
||||
|
||||
for v in list(graph):
|
||||
if color[v] == WHITE:
|
||||
dfs(v)
|
||||
return cycles
|
||||
|
||||
|
||||
# ─── PRINT TREE FUNCTION ───────────────────────────────────────
|
||||
def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
|
||||
"""
|
||||
Build a list of strings for pretty-print a async call tree.
|
||||
|
||||
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
|
||||
with `task_emoji` and coroutine frames with `cor_emoji`.
|
||||
"""
|
||||
id2name, awaits = _index(result)
|
||||
g = _task_graph(awaits)
|
||||
cycles = _find_cycles(g)
|
||||
if cycles:
|
||||
raise CycleFoundException(cycles, id2name)
|
||||
labels, children = _build_tree(id2name, awaits)
|
||||
|
||||
def pretty(node):
|
||||
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
|
||||
return f"{flag} {labels[node]}"
|
||||
|
||||
def render(node, prefix="", last=True, buf=None):
|
||||
if buf is None:
|
||||
buf = []
|
||||
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
|
||||
new_pref = prefix + (" " if last else "│ ")
|
||||
kids = children.get(node, [])
|
||||
for i, kid in enumerate(kids):
|
||||
render(kid, new_pref, i == len(kids) - 1, buf)
|
||||
return buf
|
||||
|
||||
return [render(root) for root in _roots(labels, children)]
|
||||
|
||||
|
||||
def build_task_table(result):
|
||||
id2name, awaits = _index(result)
|
||||
table = []
|
||||
for tid, tasks in result:
|
||||
for task_id, task_name, awaited in tasks:
|
||||
if not awaited:
|
||||
table.append(
|
||||
[
|
||||
tid,
|
||||
hex(task_id),
|
||||
task_name,
|
||||
"",
|
||||
"",
|
||||
"0x0"
|
||||
]
|
||||
)
|
||||
for stack, awaiter_id in awaited:
|
||||
coroutine_chain = " -> ".join(stack)
|
||||
awaiter_name = id2name.get(awaiter_id, "Unknown")
|
||||
table.append(
|
||||
[
|
||||
tid,
|
||||
hex(task_id),
|
||||
task_name,
|
||||
coroutine_chain,
|
||||
awaiter_name,
|
||||
hex(awaiter_id),
|
||||
]
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
def _print_cycle_exception(exception: CycleFoundException):
|
||||
print("ERROR: await-graph contains cycles – cannot print a tree!", file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
for c in exception.cycles:
|
||||
inames = " → ".join(exception.id2name.get(tid, hex(tid)) for tid in c)
|
||||
print(f"cycle: {inames}", file=sys.stderr)
|
||||
|
||||
|
||||
def _get_awaited_by_tasks(pid: int) -> list:
|
||||
try:
|
||||
return get_all_awaited_by(pid)
|
||||
except RuntimeError as e:
|
||||
while e.__context__ is not None:
|
||||
e = e.__context__
|
||||
print(f"Error retrieving tasks: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def display_awaited_by_tasks_table(pid: int) -> None:
|
||||
"""Build and print a table of all pending tasks under `pid`."""
|
||||
|
||||
tasks = _get_awaited_by_tasks(pid)
|
||||
table = build_task_table(tasks)
|
||||
# Print the table in a simple tabular format
|
||||
print(
|
||||
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
|
||||
)
|
||||
print("-" * 135)
|
||||
for row in table:
|
||||
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
|
||||
|
||||
|
||||
def display_awaited_by_tasks_tree(pid: int) -> None:
|
||||
"""Build and print a tree of all pending tasks under `pid`."""
|
||||
|
||||
tasks = _get_awaited_by_tasks(pid)
|
||||
try:
|
||||
result = build_async_tree(tasks)
|
||||
except CycleFoundException as e:
|
||||
_print_cycle_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
for tree in result:
|
||||
print("\n".join(tree))
|
839
Lib/test/test_asyncio/test_tools.py
Normal file
839
Lib/test/test_asyncio/test_tools.py
Normal file
|
@ -0,0 +1,839 @@
|
|||
import unittest
|
||||
|
||||
from asyncio import tools
|
||||
|
||||
|
||||
# mock output of get_all_awaited_by function.
|
||||
TEST_INPUTS_TREE = [
|
||||
[
|
||||
# test case containing a task called timer being awaited in two
|
||||
# different subtasks part of a TaskGroup (root1 and root2) which call
|
||||
# awaiter functions.
|
||||
(
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"timer",
|
||||
[
|
||||
[["awaiter3", "awaiter2", "awaiter"], 4],
|
||||
[["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
|
||||
[["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
|
||||
[["awaiter3", "awaiter2", "awaiter"], 7],
|
||||
],
|
||||
),
|
||||
(
|
||||
8,
|
||||
"root1",
|
||||
[[["_aexit", "__aexit__", "main"], 2]],
|
||||
),
|
||||
(
|
||||
9,
|
||||
"root2",
|
||||
[[["_aexit", "__aexit__", "main"], 2]],
|
||||
),
|
||||
(
|
||||
4,
|
||||
"child1_1",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
8,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
6,
|
||||
"child2_1",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
8,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
7,
|
||||
"child1_2",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
9,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
5,
|
||||
"child2_2",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
9,
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
),
|
||||
(
|
||||
[
|
||||
[
|
||||
"└── (T) Task-1",
|
||||
" └── main",
|
||||
" └── __aexit__",
|
||||
" └── _aexit",
|
||||
" ├── (T) root1",
|
||||
" │ └── bloch",
|
||||
" │ └── blocho_caller",
|
||||
" │ └── __aexit__",
|
||||
" │ └── _aexit",
|
||||
" │ ├── (T) child1_1",
|
||||
" │ │ └── awaiter",
|
||||
" │ │ └── awaiter2",
|
||||
" │ │ └── awaiter3",
|
||||
" │ │ └── (T) timer",
|
||||
" │ └── (T) child2_1",
|
||||
" │ └── awaiter1",
|
||||
" │ └── awaiter1_2",
|
||||
" │ └── awaiter1_3",
|
||||
" │ └── (T) timer",
|
||||
" └── (T) root2",
|
||||
" └── bloch",
|
||||
" └── blocho_caller",
|
||||
" └── __aexit__",
|
||||
" └── _aexit",
|
||||
" ├── (T) child1_2",
|
||||
" │ └── awaiter",
|
||||
" │ └── awaiter2",
|
||||
" │ └── awaiter3",
|
||||
" │ └── (T) timer",
|
||||
" └── (T) child2_2",
|
||||
" └── awaiter1",
|
||||
" └── awaiter1_2",
|
||||
" └── awaiter1_3",
|
||||
" └── (T) timer",
|
||||
]
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
# test case containing two roots
|
||||
(
|
||||
(
|
||||
9,
|
||||
[
|
||||
(5, "Task-5", []),
|
||||
(6, "Task-6", [[["main2"], 5]]),
|
||||
(7, "Task-7", [[["main2"], 5]]),
|
||||
(8, "Task-8", [[["main2"], 5]]),
|
||||
],
|
||||
),
|
||||
(
|
||||
10,
|
||||
[
|
||||
(1, "Task-1", []),
|
||||
(2, "Task-2", [[["main"], 1]]),
|
||||
(3, "Task-3", [[["main"], 1]]),
|
||||
(4, "Task-4", [[["main"], 1]]),
|
||||
],
|
||||
),
|
||||
(11, []),
|
||||
(0, []),
|
||||
),
|
||||
(
|
||||
[
|
||||
[
|
||||
"└── (T) Task-5",
|
||||
" └── main2",
|
||||
" ├── (T) Task-6",
|
||||
" ├── (T) Task-7",
|
||||
" └── (T) Task-8",
|
||||
],
|
||||
[
|
||||
"└── (T) Task-1",
|
||||
" └── main",
|
||||
" ├── (T) Task-2",
|
||||
" ├── (T) Task-3",
|
||||
" └── (T) Task-4",
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
# test case containing two roots, one of them without subtasks
|
||||
(
|
||||
[
|
||||
(1, [(2, "Task-5", [])]),
|
||||
(
|
||||
3,
|
||||
[
|
||||
(4, "Task-1", []),
|
||||
(5, "Task-2", [[["main"], 4]]),
|
||||
(6, "Task-3", [[["main"], 4]]),
|
||||
(7, "Task-4", [[["main"], 4]]),
|
||||
],
|
||||
),
|
||||
(8, []),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
["└── (T) Task-5"],
|
||||
[
|
||||
"└── (T) Task-1",
|
||||
" └── main",
|
||||
" ├── (T) Task-2",
|
||||
" ├── (T) Task-3",
|
||||
" └── (T) Task-4",
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
TEST_INPUTS_CYCLES_TREE = [
|
||||
[
|
||||
# this test case contains a cycle: two tasks awaiting each other.
|
||||
(
|
||||
[
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"a",
|
||||
[[["awaiter2"], 4], [["main"], 2]],
|
||||
),
|
||||
(4, "b", [[["awaiter"], 3]]),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
([[4, 3, 4]]),
|
||||
],
|
||||
[
|
||||
# this test case contains two cycles
|
||||
(
|
||||
[
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"A",
|
||||
[[["nested", "nested", "task_b"], 4]],
|
||||
),
|
||||
(
|
||||
4,
|
||||
"B",
|
||||
[
|
||||
[["nested", "nested", "task_c"], 5],
|
||||
[["nested", "nested", "task_a"], 3],
|
||||
],
|
||||
),
|
||||
(5, "C", [[["nested", "nested"], 6]]),
|
||||
(
|
||||
6,
|
||||
"Task-2",
|
||||
[[["nested", "nested", "task_b"], 4]],
|
||||
),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
([[4, 3, 4], [4, 6, 5, 4]]),
|
||||
],
|
||||
]
|
||||
|
||||
TEST_INPUTS_TABLE = [
|
||||
[
|
||||
# test case containing a task called timer being awaited in two
|
||||
# different subtasks part of a TaskGroup (root1 and root2) which call
|
||||
# awaiter functions.
|
||||
(
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"timer",
|
||||
[
|
||||
[["awaiter3", "awaiter2", "awaiter"], 4],
|
||||
[["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
|
||||
[["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
|
||||
[["awaiter3", "awaiter2", "awaiter"], 7],
|
||||
],
|
||||
),
|
||||
(
|
||||
8,
|
||||
"root1",
|
||||
[[["_aexit", "__aexit__", "main"], 2]],
|
||||
),
|
||||
(
|
||||
9,
|
||||
"root2",
|
||||
[[["_aexit", "__aexit__", "main"], 2]],
|
||||
),
|
||||
(
|
||||
4,
|
||||
"child1_1",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
8,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
6,
|
||||
"child2_1",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
8,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
7,
|
||||
"child1_2",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
9,
|
||||
]
|
||||
],
|
||||
),
|
||||
(
|
||||
5,
|
||||
"child2_2",
|
||||
[
|
||||
[
|
||||
["_aexit", "__aexit__", "blocho_caller", "bloch"],
|
||||
9,
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
),
|
||||
(
|
||||
[
|
||||
[1, "0x2", "Task-1", "", "", "0x0"],
|
||||
[
|
||||
1,
|
||||
"0x3",
|
||||
"timer",
|
||||
"awaiter3 -> awaiter2 -> awaiter",
|
||||
"child1_1",
|
||||
"0x4",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x3",
|
||||
"timer",
|
||||
"awaiter1_3 -> awaiter1_2 -> awaiter1",
|
||||
"child2_2",
|
||||
"0x5",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x3",
|
||||
"timer",
|
||||
"awaiter1_3 -> awaiter1_2 -> awaiter1",
|
||||
"child2_1",
|
||||
"0x6",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x3",
|
||||
"timer",
|
||||
"awaiter3 -> awaiter2 -> awaiter",
|
||||
"child1_2",
|
||||
"0x7",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x8",
|
||||
"root1",
|
||||
"_aexit -> __aexit__ -> main",
|
||||
"Task-1",
|
||||
"0x2",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x9",
|
||||
"root2",
|
||||
"_aexit -> __aexit__ -> main",
|
||||
"Task-1",
|
||||
"0x2",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x4",
|
||||
"child1_1",
|
||||
"_aexit -> __aexit__ -> blocho_caller -> bloch",
|
||||
"root1",
|
||||
"0x8",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x6",
|
||||
"child2_1",
|
||||
"_aexit -> __aexit__ -> blocho_caller -> bloch",
|
||||
"root1",
|
||||
"0x8",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x7",
|
||||
"child1_2",
|
||||
"_aexit -> __aexit__ -> blocho_caller -> bloch",
|
||||
"root2",
|
||||
"0x9",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x5",
|
||||
"child2_2",
|
||||
"_aexit -> __aexit__ -> blocho_caller -> bloch",
|
||||
"root2",
|
||||
"0x9",
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
# test case containing two roots
|
||||
(
|
||||
(
|
||||
9,
|
||||
[
|
||||
(5, "Task-5", []),
|
||||
(6, "Task-6", [[["main2"], 5]]),
|
||||
(7, "Task-7", [[["main2"], 5]]),
|
||||
(8, "Task-8", [[["main2"], 5]]),
|
||||
],
|
||||
),
|
||||
(
|
||||
10,
|
||||
[
|
||||
(1, "Task-1", []),
|
||||
(2, "Task-2", [[["main"], 1]]),
|
||||
(3, "Task-3", [[["main"], 1]]),
|
||||
(4, "Task-4", [[["main"], 1]]),
|
||||
],
|
||||
),
|
||||
(11, []),
|
||||
(0, []),
|
||||
),
|
||||
(
|
||||
[
|
||||
[9, "0x5", "Task-5", "", "", "0x0"],
|
||||
[9, "0x6", "Task-6", "main2", "Task-5", "0x5"],
|
||||
[9, "0x7", "Task-7", "main2", "Task-5", "0x5"],
|
||||
[9, "0x8", "Task-8", "main2", "Task-5", "0x5"],
|
||||
[10, "0x1", "Task-1", "", "", "0x0"],
|
||||
[10, "0x2", "Task-2", "main", "Task-1", "0x1"],
|
||||
[10, "0x3", "Task-3", "main", "Task-1", "0x1"],
|
||||
[10, "0x4", "Task-4", "main", "Task-1", "0x1"],
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
# test case containing two roots, one of them without subtasks
|
||||
(
|
||||
[
|
||||
(1, [(2, "Task-5", [])]),
|
||||
(
|
||||
3,
|
||||
[
|
||||
(4, "Task-1", []),
|
||||
(5, "Task-2", [[["main"], 4]]),
|
||||
(6, "Task-3", [[["main"], 4]]),
|
||||
(7, "Task-4", [[["main"], 4]]),
|
||||
],
|
||||
),
|
||||
(8, []),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
[1, "0x2", "Task-5", "", "", "0x0"],
|
||||
[3, "0x4", "Task-1", "", "", "0x0"],
|
||||
[3, "0x5", "Task-2", "main", "Task-1", "0x4"],
|
||||
[3, "0x6", "Task-3", "main", "Task-1", "0x4"],
|
||||
[3, "0x7", "Task-4", "main", "Task-1", "0x4"],
|
||||
]
|
||||
),
|
||||
],
|
||||
# CASES WITH CYCLES
|
||||
[
|
||||
# this test case contains a cycle: two tasks awaiting each other.
|
||||
(
|
||||
[
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"a",
|
||||
[[["awaiter2"], 4], [["main"], 2]],
|
||||
),
|
||||
(4, "b", [[["awaiter"], 3]]),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
[1, "0x2", "Task-1", "", "", "0x0"],
|
||||
[1, "0x3", "a", "awaiter2", "b", "0x4"],
|
||||
[1, "0x3", "a", "main", "Task-1", "0x2"],
|
||||
[1, "0x4", "b", "awaiter", "a", "0x3"],
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
# this test case contains two cycles
|
||||
(
|
||||
[
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(
|
||||
3,
|
||||
"A",
|
||||
[[["nested", "nested", "task_b"], 4]],
|
||||
),
|
||||
(
|
||||
4,
|
||||
"B",
|
||||
[
|
||||
[["nested", "nested", "task_c"], 5],
|
||||
[["nested", "nested", "task_a"], 3],
|
||||
],
|
||||
),
|
||||
(5, "C", [[["nested", "nested"], 6]]),
|
||||
(
|
||||
6,
|
||||
"Task-2",
|
||||
[[["nested", "nested", "task_b"], 4]],
|
||||
),
|
||||
],
|
||||
),
|
||||
(0, []),
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
[1, "0x2", "Task-1", "", "", "0x0"],
|
||||
[
|
||||
1,
|
||||
"0x3",
|
||||
"A",
|
||||
"nested -> nested -> task_b",
|
||||
"B",
|
||||
"0x4",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x4",
|
||||
"B",
|
||||
"nested -> nested -> task_c",
|
||||
"C",
|
||||
"0x5",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x4",
|
||||
"B",
|
||||
"nested -> nested -> task_a",
|
||||
"A",
|
||||
"0x3",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x5",
|
||||
"C",
|
||||
"nested -> nested",
|
||||
"Task-2",
|
||||
"0x6",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"0x6",
|
||||
"Task-2",
|
||||
"nested -> nested -> task_b",
|
||||
"B",
|
||||
"0x4",
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
class TestAsyncioToolsTree(unittest.TestCase):
|
||||
|
||||
def test_asyncio_utils(self):
|
||||
for input_, tree in TEST_INPUTS_TREE:
|
||||
with self.subTest(input_):
|
||||
self.assertEqual(tools.build_async_tree(input_), tree)
|
||||
|
||||
def test_asyncio_utils_cycles(self):
|
||||
for input_, cycles in TEST_INPUTS_CYCLES_TREE:
|
||||
with self.subTest(input_):
|
||||
try:
|
||||
tools.build_async_tree(input_)
|
||||
except tools.CycleFoundException as e:
|
||||
self.assertEqual(e.cycles, cycles)
|
||||
|
||||
|
||||
class TestAsyncioToolsTable(unittest.TestCase):
|
||||
def test_asyncio_utils(self):
|
||||
for input_, table in TEST_INPUTS_TABLE:
|
||||
with self.subTest(input_):
|
||||
self.assertEqual(tools.build_task_table(input_), table)
|
||||
|
||||
|
||||
class TestAsyncioToolsBasic(unittest.TestCase):
|
||||
def test_empty_input_tree(self):
|
||||
"""Test build_async_tree with empty input."""
|
||||
result = []
|
||||
expected_output = []
|
||||
self.assertEqual(tools.build_async_tree(result), expected_output)
|
||||
|
||||
def test_empty_input_table(self):
|
||||
"""Test build_task_table with empty input."""
|
||||
result = []
|
||||
expected_output = []
|
||||
self.assertEqual(tools.build_task_table(result), expected_output)
|
||||
|
||||
def test_only_independent_tasks_tree(self):
|
||||
input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
|
||||
expected = [["└── (T) taskA"], ["└── (T) taskB"]]
|
||||
result = tools.build_async_tree(input_)
|
||||
self.assertEqual(sorted(result), sorted(expected))
|
||||
|
||||
def test_only_independent_tasks_table(self):
|
||||
input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
|
||||
self.assertEqual(
|
||||
tools.build_task_table(input_),
|
||||
[[1, "0xa", "taskA", "", "", "0x0"], [1, "0xb", "taskB", "", "", "0x0"]],
|
||||
)
|
||||
|
||||
def test_single_task_tree(self):
|
||||
"""Test build_async_tree with a single task and no awaits."""
|
||||
result = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
],
|
||||
)
|
||||
]
|
||||
expected_output = [
|
||||
[
|
||||
"└── (T) Task-1",
|
||||
]
|
||||
]
|
||||
self.assertEqual(tools.build_async_tree(result), expected_output)
|
||||
|
||||
def test_single_task_table(self):
|
||||
"""Test build_task_table with a single task and no awaits."""
|
||||
result = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
],
|
||||
)
|
||||
]
|
||||
expected_output = [[1, "0x2", "Task-1", "", "", "0x0"]]
|
||||
self.assertEqual(tools.build_task_table(result), expected_output)
|
||||
|
||||
def test_cycle_detection(self):
|
||||
"""Test build_async_tree raises CycleFoundException for cyclic input."""
|
||||
result = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", [[["main"], 3]]),
|
||||
(3, "Task-2", [[["main"], 2]]),
|
||||
],
|
||||
)
|
||||
]
|
||||
with self.assertRaises(tools.CycleFoundException) as context:
|
||||
tools.build_async_tree(result)
|
||||
self.assertEqual(context.exception.cycles, [[3, 2, 3]])
|
||||
|
||||
def test_complex_tree(self):
|
||||
"""Test build_async_tree with a more complex tree structure."""
|
||||
result = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(3, "Task-2", [[["main"], 2]]),
|
||||
(4, "Task-3", [[["main"], 3]]),
|
||||
],
|
||||
)
|
||||
]
|
||||
expected_output = [
|
||||
[
|
||||
"└── (T) Task-1",
|
||||
" └── main",
|
||||
" └── (T) Task-2",
|
||||
" └── main",
|
||||
" └── (T) Task-3",
|
||||
]
|
||||
]
|
||||
self.assertEqual(tools.build_async_tree(result), expected_output)
|
||||
|
||||
def test_complex_table(self):
|
||||
"""Test build_task_table with a more complex tree structure."""
|
||||
result = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(2, "Task-1", []),
|
||||
(3, "Task-2", [[["main"], 2]]),
|
||||
(4, "Task-3", [[["main"], 3]]),
|
||||
],
|
||||
)
|
||||
]
|
||||
expected_output = [
|
||||
[1, "0x2", "Task-1", "", "", "0x0"],
|
||||
[1, "0x3", "Task-2", "main", "Task-1", "0x2"],
|
||||
[1, "0x4", "Task-3", "main", "Task-2", "0x3"],
|
||||
]
|
||||
self.assertEqual(tools.build_task_table(result), expected_output)
|
||||
|
||||
def test_deep_coroutine_chain(self):
|
||||
input_ = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(10, "leaf", [[["c1", "c2", "c3", "c4", "c5"], 11]]),
|
||||
(11, "root", []),
|
||||
],
|
||||
)
|
||||
]
|
||||
expected = [
|
||||
[
|
||||
"└── (T) root",
|
||||
" └── c5",
|
||||
" └── c4",
|
||||
" └── c3",
|
||||
" └── c2",
|
||||
" └── c1",
|
||||
" └── (T) leaf",
|
||||
]
|
||||
]
|
||||
result = tools.build_async_tree(input_)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_multiple_cycles_same_node(self):
|
||||
input_ = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(1, "Task-A", [[["call1"], 2]]),
|
||||
(2, "Task-B", [[["call2"], 3]]),
|
||||
(3, "Task-C", [[["call3"], 1], [["call4"], 2]]),
|
||||
],
|
||||
)
|
||||
]
|
||||
with self.assertRaises(tools.CycleFoundException) as ctx:
|
||||
tools.build_async_tree(input_)
|
||||
cycles = ctx.exception.cycles
|
||||
self.assertTrue(any(set(c) == {1, 2, 3} for c in cycles))
|
||||
|
||||
def test_table_output_format(self):
|
||||
input_ = [(1, [(1, "Task-A", [[["foo"], 2]]), (2, "Task-B", [])])]
|
||||
table = tools.build_task_table(input_)
|
||||
for row in table:
|
||||
self.assertEqual(len(row), 6)
|
||||
self.assertIsInstance(row[0], int) # thread ID
|
||||
self.assertTrue(
|
||||
isinstance(row[1], str) and row[1].startswith("0x")
|
||||
) # hex task ID
|
||||
self.assertIsInstance(row[2], str) # task name
|
||||
self.assertIsInstance(row[3], str) # coroutine chain
|
||||
self.assertIsInstance(row[4], str) # awaiter name
|
||||
self.assertTrue(
|
||||
isinstance(row[5], str) and row[5].startswith("0x")
|
||||
) # hex awaiter ID
|
||||
|
||||
|
||||
class TestAsyncioToolsEdgeCases(unittest.TestCase):
|
||||
|
||||
def test_task_awaits_self(self):
|
||||
"""A task directly awaits itself – should raise a cycle."""
|
||||
input_ = [(1, [(1, "Self-Awaiter", [[["loopback"], 1]])])]
|
||||
with self.assertRaises(tools.CycleFoundException) as ctx:
|
||||
tools.build_async_tree(input_)
|
||||
self.assertIn([1, 1], ctx.exception.cycles)
|
||||
|
||||
def test_task_with_missing_awaiter_id(self):
|
||||
"""Awaiter ID not in task list – should not crash, just show 'Unknown'."""
|
||||
input_ = [(1, [(1, "Task-A", [[["coro"], 999]])])] # 999 not defined
|
||||
table = tools.build_task_table(input_)
|
||||
self.assertEqual(len(table), 1)
|
||||
self.assertEqual(table[0][4], "Unknown")
|
||||
|
||||
def test_duplicate_coroutine_frames(self):
|
||||
"""Same coroutine frame repeated under a parent – should deduplicate."""
|
||||
input_ = [
|
||||
(
|
||||
1,
|
||||
[
|
||||
(1, "Task-1", [[["frameA"], 2], [["frameA"], 3]]),
|
||||
(2, "Task-2", []),
|
||||
(3, "Task-3", []),
|
||||
],
|
||||
)
|
||||
]
|
||||
tree = tools.build_async_tree(input_)
|
||||
# Both children should be under the same coroutine node
|
||||
flat = "\n".join(tree[0])
|
||||
self.assertIn("frameA", flat)
|
||||
self.assertIn("Task-2", flat)
|
||||
self.assertIn("Task-1", flat)
|
||||
|
||||
flat = "\n".join(tree[1])
|
||||
self.assertIn("frameA", flat)
|
||||
self.assertIn("Task-3", flat)
|
||||
self.assertIn("Task-1", flat)
|
||||
|
||||
def test_task_with_no_name(self):
|
||||
"""Task with no name in id2name – should still render with fallback."""
|
||||
input_ = [(1, [(1, "root", [[["f1"], 2]]), (2, None, [])])]
|
||||
# If name is None, fallback to string should not crash
|
||||
tree = tools.build_async_tree(input_)
|
||||
self.assertIn("(T) None", "\n".join(tree[0]))
|
||||
|
||||
def test_tree_rendering_with_custom_emojis(self):
|
||||
"""Pass custom emojis to the tree renderer."""
|
||||
input_ = [(1, [(1, "MainTask", [[["f1", "f2"], 2]]), (2, "SubTask", [])])]
|
||||
tree = tools.build_async_tree(input_, task_emoji="🧵", cor_emoji="🔁")
|
||||
flat = "\n".join(tree[0])
|
||||
self.assertIn("🧵 MainTask", flat)
|
||||
self.assertIn("🔁 f1", flat)
|
||||
self.assertIn("🔁 f2", flat)
|
||||
self.assertIn("🧵 SubTask", flat)
|
|
@ -4,7 +4,8 @@ import textwrap
|
|||
import importlib
|
||||
import sys
|
||||
import socket
|
||||
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
|
||||
from unittest.mock import ANY
|
||||
from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled
|
||||
from test.support.script_helper import make_script
|
||||
from test.support.socket_helper import find_unused_port
|
||||
|
||||
|
@ -13,13 +14,13 @@ import subprocess
|
|||
PROCESS_VM_READV_SUPPORTED = False
|
||||
|
||||
try:
|
||||
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
|
||||
from _testexternalinspection import get_stack_trace
|
||||
from _testexternalinspection import get_async_stack_trace
|
||||
from _testexternalinspection import get_all_awaited_by
|
||||
from _remotedebugging import PROCESS_VM_READV_SUPPORTED
|
||||
from _remotedebugging import get_stack_trace
|
||||
from _remotedebugging import get_async_stack_trace
|
||||
from _remotedebugging import get_all_awaited_by
|
||||
except ImportError:
|
||||
raise unittest.SkipTest(
|
||||
"Test only runs when _testexternalinspection is available")
|
||||
"Test only runs when _remotedebuggingmodule is available")
|
||||
|
||||
def _make_test_script(script_dir, script_basename, source):
|
||||
to_return = make_script(script_dir, script_basename, source)
|
||||
|
@ -184,13 +185,13 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
|
||||
root_task = "Task-1"
|
||||
expected_stack_trace = [
|
||||
["c5", "c4", "c3", "c2"],
|
||||
"c2_root",
|
||||
['c5', 'c4', 'c3', 'c2'],
|
||||
'c2_root',
|
||||
[
|
||||
[["main"], root_task, []],
|
||||
[["c1"], "sub_main_1", [[["main"], root_task, []]]],
|
||||
[["c1"], "sub_main_2", [[["main"], root_task, []]]],
|
||||
],
|
||||
[['_aexit', '__aexit__', 'main'], root_task, []],
|
||||
[['c1'], 'sub_main_1', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
|
||||
[['c1'], 'sub_main_2', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
|
||||
]
|
||||
]
|
||||
self.assertEqual(stack_trace, expected_stack_trace)
|
||||
|
||||
|
@ -397,12 +398,15 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
# sets are unordered, so we want to sort "awaited_by"s
|
||||
stack_trace[2].sort(key=lambda x: x[1])
|
||||
|
||||
expected_stack_trace = [
|
||||
['deep', 'c1', 'run_one_coro'], 'Task-2', [[['main'], 'Task-1', []]]
|
||||
expected_stack_trace = [
|
||||
['deep', 'c1', 'run_one_coro'],
|
||||
'Task-2',
|
||||
[[['staggered_race', 'main'], 'Task-1', []]]
|
||||
]
|
||||
self.assertEqual(stack_trace, expected_stack_trace)
|
||||
|
||||
@skip_if_not_supported
|
||||
@requires_gil_enabled("gh-133359: occasionally flaky on AMD64")
|
||||
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
||||
"Test only runs on Linux with process_vm_readv support")
|
||||
def test_async_global_awaited_by(self):
|
||||
|
@ -516,19 +520,19 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
# expected: at least 1000 pending tasks
|
||||
self.assertGreaterEqual(len(entries), 1000)
|
||||
# the first three tasks stem from the code structure
|
||||
self.assertIn(('Task-1', []), entries)
|
||||
self.assertIn(('server task', [[['main'], 'Task-1', []]]), entries)
|
||||
self.assertIn(('echo client spam', [[['main'], 'Task-1', []]]), entries)
|
||||
self.assertIn((ANY, 'Task-1', []), entries)
|
||||
self.assertIn((ANY, 'server task', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
|
||||
self.assertIn((ANY, 'echo client spam', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
|
||||
|
||||
expected_stack = [[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]]
|
||||
tasks_with_stack = [task for task in entries if task[1] == expected_stack]
|
||||
expected_stack = [[['_aexit', '__aexit__', 'echo_client_spam'], ANY]]
|
||||
tasks_with_stack = [task for task in entries if task[2] == expected_stack]
|
||||
self.assertGreaterEqual(len(tasks_with_stack), 1000)
|
||||
|
||||
# the final task will have some random number, but it should for
|
||||
# sure be one of the echo client spam horde (In windows this is not true
|
||||
# for some reason)
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual([[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]], entries[-1][1])
|
||||
self.assertEqual([[['_aexit', '__aexit__', 'echo_client_spam'], ANY]], entries[-1][2])
|
||||
except PermissionError:
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace")
|
||||
|
@ -544,7 +548,6 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
"Test only runs on Linux with process_vm_readv support")
|
||||
def test_self_trace(self):
|
||||
stack_trace = get_stack_trace(os.getpid())
|
||||
print(stack_trace)
|
||||
self.assertEqual(stack_trace[0], "test_self_trace")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1960,7 +1960,7 @@ def _supports_remote_attaching():
|
|||
PROCESS_VM_READV_SUPPORTED = False
|
||||
|
||||
try:
|
||||
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
|
||||
from _remotedebuggingmodule import PROCESS_VM_READV_SUPPORTED
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
Add a new ``python -m asyncio ps PID`` command-line interface to inspect
|
||||
asyncio tasks in a running Python process. Displays a flat table of await
|
||||
relationships. A variant showing a tree view is also available as
|
||||
``python -m asyncio pstree PID``. Both are useful for debugging async
|
||||
code. Patch by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
|
||||
Gomez Macias.
|
|
@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
|
|||
#_testcapi _testcapimodule.c
|
||||
#_testimportmultiple _testimportmultiple.c
|
||||
#_testmultiphase _testmultiphase.c
|
||||
#_testexternalinspection _testexternalinspection.c
|
||||
#_remotedebugging _remotedebuggingmodule.c
|
||||
#_testsinglephase _testsinglephase.c
|
||||
|
||||
# ---
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
# Modules that should always be present (POSIX and Windows):
|
||||
@MODULE_ARRAY_TRUE@array arraymodule.c
|
||||
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
|
||||
@MODULE__REMOTEDEBUGGING_TRUE@_remotedebugging _remotedebuggingmodule.c
|
||||
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
|
||||
@MODULE__CSV_TRUE@_csv _csv.c
|
||||
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
|
||||
|
@ -186,7 +187,6 @@
|
|||
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
|
||||
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
|
||||
@MODULE__TESTSINGLEPHASE_TRUE@_testsinglephase _testsinglephase.c
|
||||
@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
|
||||
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
|
||||
|
||||
# Limited API template modules; must be built as shared modules.
|
||||
|
|
|
@ -152,9 +152,9 @@ read_char(proc_handle_t *handle, uintptr_t address, char *result)
|
|||
}
|
||||
|
||||
static int
|
||||
read_int(proc_handle_t *handle, uintptr_t address, int *result)
|
||||
read_sized_int(proc_handle_t *handle, uintptr_t address, void *result, size_t size)
|
||||
{
|
||||
int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(int), result);
|
||||
int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, size, result);
|
||||
if (res < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ parse_coro_chain(
|
|||
uintptr_t gen_type_addr;
|
||||
int err = read_ptr(
|
||||
handle,
|
||||
coro_address + sizeof(void*),
|
||||
coro_address + offsets->pyobject.ob_type,
|
||||
&gen_type_addr);
|
||||
if (err) {
|
||||
return -1;
|
||||
|
@ -376,11 +376,13 @@ parse_coro_chain(
|
|||
}
|
||||
Py_DECREF(name);
|
||||
|
||||
int gi_frame_state;
|
||||
err = read_int(
|
||||
int8_t gi_frame_state;
|
||||
err = read_sized_int(
|
||||
handle,
|
||||
coro_address + offsets->gen_object.gi_frame_state,
|
||||
&gi_frame_state);
|
||||
&gi_frame_state,
|
||||
sizeof(int8_t)
|
||||
);
|
||||
if (err) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -427,7 +429,7 @@ parse_coro_chain(
|
|||
uintptr_t gi_await_addr_type_addr;
|
||||
int err = read_ptr(
|
||||
handle,
|
||||
gi_await_addr + sizeof(void*),
|
||||
gi_await_addr + offsets->pyobject.ob_type,
|
||||
&gi_await_addr_type_addr);
|
||||
if (err) {
|
||||
return -1;
|
||||
|
@ -470,7 +472,8 @@ parse_task_awaited_by(
|
|||
struct _Py_DebugOffsets* offsets,
|
||||
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
|
||||
uintptr_t task_address,
|
||||
PyObject *awaited_by
|
||||
PyObject *awaited_by,
|
||||
int recurse_task
|
||||
);
|
||||
|
||||
|
||||
|
@ -480,7 +483,8 @@ parse_task(
|
|||
struct _Py_DebugOffsets* offsets,
|
||||
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
|
||||
uintptr_t task_address,
|
||||
PyObject *render_to
|
||||
PyObject *render_to,
|
||||
int recurse_task
|
||||
) {
|
||||
char is_task;
|
||||
int err = read_char(
|
||||
|
@ -508,8 +512,13 @@ parse_task(
|
|||
Py_DECREF(call_stack);
|
||||
|
||||
if (is_task) {
|
||||
PyObject *tn = parse_task_name(
|
||||
handle, offsets, async_offsets, task_address);
|
||||
PyObject *tn = NULL;
|
||||
if (recurse_task) {
|
||||
tn = parse_task_name(
|
||||
handle, offsets, async_offsets, task_address);
|
||||
} else {
|
||||
tn = PyLong_FromUnsignedLongLong(task_address);
|
||||
}
|
||||
if (tn == NULL) {
|
||||
goto err;
|
||||
}
|
||||
|
@ -550,21 +559,23 @@ parse_task(
|
|||
goto err;
|
||||
}
|
||||
|
||||
PyObject *awaited_by = PyList_New(0);
|
||||
if (awaited_by == NULL) {
|
||||
goto err;
|
||||
}
|
||||
if (PyList_Append(result, awaited_by)) {
|
||||
if (recurse_task) {
|
||||
PyObject *awaited_by = PyList_New(0);
|
||||
if (awaited_by == NULL) {
|
||||
goto err;
|
||||
}
|
||||
if (PyList_Append(result, awaited_by)) {
|
||||
Py_DECREF(awaited_by);
|
||||
goto err;
|
||||
}
|
||||
/* we can operate on a borrowed one to simplify cleanup */
|
||||
Py_DECREF(awaited_by);
|
||||
goto err;
|
||||
}
|
||||
/* we can operate on a borrowed one to simplify cleanup */
|
||||
Py_DECREF(awaited_by);
|
||||
|
||||
if (parse_task_awaited_by(handle, offsets, async_offsets,
|
||||
task_address, awaited_by)
|
||||
) {
|
||||
goto err;
|
||||
if (parse_task_awaited_by(handle, offsets, async_offsets,
|
||||
task_address, awaited_by, 1)
|
||||
) {
|
||||
goto err;
|
||||
}
|
||||
}
|
||||
Py_DECREF(result);
|
||||
|
||||
|
@ -581,7 +592,8 @@ parse_tasks_in_set(
|
|||
struct _Py_DebugOffsets* offsets,
|
||||
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
|
||||
uintptr_t set_addr,
|
||||
PyObject *awaited_by
|
||||
PyObject *awaited_by,
|
||||
int recurse_task
|
||||
) {
|
||||
uintptr_t set_obj;
|
||||
if (read_py_ptr(
|
||||
|
@ -642,7 +654,9 @@ parse_tasks_in_set(
|
|||
offsets,
|
||||
async_offsets,
|
||||
key_addr,
|
||||
awaited_by)
|
||||
awaited_by,
|
||||
recurse_task
|
||||
)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -666,7 +680,8 @@ parse_task_awaited_by(
|
|||
struct _Py_DebugOffsets* offsets,
|
||||
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
|
||||
uintptr_t task_address,
|
||||
PyObject *awaited_by
|
||||
PyObject *awaited_by,
|
||||
int recurse_task
|
||||
) {
|
||||
uintptr_t task_ab_addr;
|
||||
int err = read_py_ptr(
|
||||
|
@ -696,7 +711,9 @@ parse_task_awaited_by(
|
|||
offsets,
|
||||
async_offsets,
|
||||
task_address + async_offsets->asyncio_task_object.task_awaited_by,
|
||||
awaited_by)
|
||||
awaited_by,
|
||||
recurse_task
|
||||
)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -715,7 +732,9 @@ parse_task_awaited_by(
|
|||
offsets,
|
||||
async_offsets,
|
||||
sub_task,
|
||||
awaited_by)
|
||||
awaited_by,
|
||||
recurse_task
|
||||
)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -1060,15 +1079,24 @@ append_awaited_by_for_thread(
|
|||
return -1;
|
||||
}
|
||||
|
||||
PyObject *result_item = PyTuple_New(2);
|
||||
if (result_item == NULL) {
|
||||
PyObject* task_id = PyLong_FromUnsignedLongLong(task_addr);
|
||||
if (task_id == NULL) {
|
||||
Py_DECREF(tn);
|
||||
Py_DECREF(current_awaited_by);
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(result_item, 0, tn); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 1, current_awaited_by); // steals ref
|
||||
PyObject *result_item = PyTuple_New(3);
|
||||
if (result_item == NULL) {
|
||||
Py_DECREF(tn);
|
||||
Py_DECREF(current_awaited_by);
|
||||
Py_DECREF(task_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(result_item, 0, task_id); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 1, tn); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 2, current_awaited_by); // steals ref
|
||||
if (PyList_Append(result, result_item)) {
|
||||
Py_DECREF(result_item);
|
||||
return -1;
|
||||
|
@ -1076,7 +1104,7 @@ append_awaited_by_for_thread(
|
|||
Py_DECREF(result_item);
|
||||
|
||||
if (parse_task_awaited_by(handle, debug_offsets, async_offsets,
|
||||
task_addr, current_awaited_by))
|
||||
task_addr, current_awaited_by, 0))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
@ -1499,7 +1527,7 @@ get_async_stack_trace(PyObject* self, PyObject* args)
|
|||
|
||||
if (parse_task_awaited_by(
|
||||
handle, &local_debug_offsets, &local_async_debug,
|
||||
running_task_addr, awaited_by)
|
||||
running_task_addr, awaited_by, 1)
|
||||
) {
|
||||
goto result_err;
|
||||
}
|
||||
|
@ -1526,13 +1554,13 @@ static PyMethodDef methods[] = {
|
|||
|
||||
static struct PyModuleDef module = {
|
||||
.m_base = PyModuleDef_HEAD_INIT,
|
||||
.m_name = "_testexternalinspection",
|
||||
.m_name = "_remotedebugging",
|
||||
.m_size = -1,
|
||||
.m_methods = methods,
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
PyInit__testexternalinspection(void)
|
||||
PyInit__remotedebugging(void)
|
||||
{
|
||||
PyObject* mod = PyModule_Create(&module);
|
||||
if (mod == NULL) {
|
|
@ -68,7 +68,7 @@
|
|||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{4D7C112F-3083-4D9E-9754-9341C14D9B39}</ProjectGuid>
|
||||
<RootNamespace>_testexternalinspection</RootNamespace>
|
||||
<RootNamespace>_remotedebugging</RootNamespace>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<SupportPGO>false</SupportPGO>
|
||||
</PropertyGroup>
|
||||
|
@ -93,7 +93,7 @@
|
|||
<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Modules\_testexternalinspection.c" />
|
||||
<ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="..\PC\python_nt.rc" />
|
|
@ -9,7 +9,7 @@
|
|||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\Modules\_testexternalinspection.c" />
|
||||
<ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="..\PC\python_nt.rc">
|
|
@ -66,7 +66,7 @@
|
|||
<!-- pyshellext.dll -->
|
||||
<Projects Include="pyshellext.vcxproj" />
|
||||
<!-- Extension modules -->
|
||||
<ExtensionModules Include="_asyncio;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
|
||||
<ExtensionModules Include="_asyncio;_remotedebugging;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
|
||||
<ExtensionModules Include="_ctypes" Condition="$(IncludeCTypes)" />
|
||||
<!-- Extension modules that require external sources -->
|
||||
<ExternalModules Include="_bz2;_lzma;_sqlite3" />
|
||||
|
@ -79,7 +79,7 @@
|
|||
<ExtensionModules Include="@(ExternalModules->'%(Identity)')" Condition="$(IncludeExternals)" />
|
||||
<Projects Include="@(ExtensionModules->'%(Identity).vcxproj')" Condition="$(IncludeExtensions)" />
|
||||
<!-- Test modules -->
|
||||
<TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testexternalinspection;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
|
||||
<TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
|
||||
<TestModules Include="xxlimited" Condition="'$(Configuration)' == 'Release'" />
|
||||
<TestModules Include="xxlimited_35" Condition="'$(Configuration)' == 'Release'" />
|
||||
<Projects Include="@(TestModules->'%(Identity).vcxproj')" Condition="$(IncludeTests)">
|
||||
|
|
|
@ -81,7 +81,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testclinic", "_testclinic.
|
|||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testinternalcapi", "_testinternalcapi.vcxproj", "{900342D7-516A-4469-B1AD-59A66E49A25F}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testexternalinspection", "_testexternalinspection.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_remotedebugging", "_remotedebugging.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testimportmultiple", "_testimportmultiple.vcxproj", "{36D0C52C-DF4E-45D0-8BC7-E294C3ABC781}"
|
||||
EndProject
|
||||
|
|
|
@ -34,7 +34,7 @@ IGNORE = {
|
|||
'_testlimitedcapi',
|
||||
'_testmultiphase',
|
||||
'_testsinglephase',
|
||||
'_testexternalinspection',
|
||||
'_remotedebugging',
|
||||
'_xxtestfuzz',
|
||||
'idlelib.idle_test',
|
||||
'test',
|
||||
|
|
40
configure
generated
vendored
40
configure
generated
vendored
|
@ -654,8 +654,8 @@ MODULE__XXTESTFUZZ_FALSE
|
|||
MODULE__XXTESTFUZZ_TRUE
|
||||
MODULE_XXSUBTYPE_FALSE
|
||||
MODULE_XXSUBTYPE_TRUE
|
||||
MODULE__TESTEXTERNALINSPECTION_FALSE
|
||||
MODULE__TESTEXTERNALINSPECTION_TRUE
|
||||
MODULE__REMOTEDEBUGGING_FALSE
|
||||
MODULE__REMOTEDEBUGGING_TRUE
|
||||
MODULE__TESTSINGLEPHASE_FALSE
|
||||
MODULE__TESTSINGLEPHASE_TRUE
|
||||
MODULE__TESTMULTIPHASE_FALSE
|
||||
|
@ -30684,7 +30684,7 @@ case $ac_sys_system in #(
|
|||
|
||||
|
||||
py_cv_module__ctypes_test=n/a
|
||||
py_cv_module__testexternalinspection=n/a
|
||||
py_cv_module__remotedebugging=n/a
|
||||
py_cv_module__testimportmultiple=n/a
|
||||
py_cv_module__testmultiphase=n/a
|
||||
py_cv_module__testsinglephase=n/a
|
||||
|
@ -33449,44 +33449,44 @@ fi
|
|||
printf "%s\n" "$py_cv_module__testsinglephase" >&6; }
|
||||
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _testexternalinspection" >&5
|
||||
printf %s "checking for stdlib extension module _testexternalinspection... " >&6; }
|
||||
if test "$py_cv_module__testexternalinspection" != "n/a"
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _remotedebugging" >&5
|
||||
printf %s "checking for stdlib extension module _remotedebugging... " >&6; }
|
||||
if test "$py_cv_module__remotedebugging" != "n/a"
|
||||
then :
|
||||
|
||||
if test "$TEST_MODULES" = yes
|
||||
then :
|
||||
if true
|
||||
then :
|
||||
py_cv_module__testexternalinspection=yes
|
||||
py_cv_module__remotedebugging=yes
|
||||
else case e in #(
|
||||
e) py_cv_module__testexternalinspection=missing ;;
|
||||
e) py_cv_module__remotedebugging=missing ;;
|
||||
esac
|
||||
fi
|
||||
else case e in #(
|
||||
e) py_cv_module__testexternalinspection=disabled ;;
|
||||
e) py_cv_module__remotedebugging=disabled ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
fi
|
||||
as_fn_append MODULE_BLOCK "MODULE__TESTEXTERNALINSPECTION_STATE=$py_cv_module__testexternalinspection$as_nl"
|
||||
if test "x$py_cv_module__testexternalinspection" = xyes
|
||||
as_fn_append MODULE_BLOCK "MODULE__REMOTEDEBUGGING_STATE=$py_cv_module__remotedebugging$as_nl"
|
||||
if test "x$py_cv_module__remotedebugging" = xyes
|
||||
then :
|
||||
|
||||
|
||||
|
||||
|
||||
fi
|
||||
if test "$py_cv_module__testexternalinspection" = yes; then
|
||||
MODULE__TESTEXTERNALINSPECTION_TRUE=
|
||||
MODULE__TESTEXTERNALINSPECTION_FALSE='#'
|
||||
if test "$py_cv_module__remotedebugging" = yes; then
|
||||
MODULE__REMOTEDEBUGGING_TRUE=
|
||||
MODULE__REMOTEDEBUGGING_FALSE='#'
|
||||
else
|
||||
MODULE__TESTEXTERNALINSPECTION_TRUE='#'
|
||||
MODULE__TESTEXTERNALINSPECTION_FALSE=
|
||||
MODULE__REMOTEDEBUGGING_TRUE='#'
|
||||
MODULE__REMOTEDEBUGGING_FALSE=
|
||||
fi
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__testexternalinspection" >&5
|
||||
printf "%s\n" "$py_cv_module__testexternalinspection" >&6; }
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__remotedebugging" >&5
|
||||
printf "%s\n" "$py_cv_module__remotedebugging" >&6; }
|
||||
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module xxsubtype" >&5
|
||||
|
@ -34119,8 +34119,8 @@ if test -z "${MODULE__TESTSINGLEPHASE_TRUE}" && test -z "${MODULE__TESTSINGLEPHA
|
|||
as_fn_error $? "conditional \"MODULE__TESTSINGLEPHASE\" was never defined.
|
||||
Usually this means the macro was only invoked conditionally." "$LINENO" 5
|
||||
fi
|
||||
if test -z "${MODULE__TESTEXTERNALINSPECTION_TRUE}" && test -z "${MODULE__TESTEXTERNALINSPECTION_FALSE}"; then
|
||||
as_fn_error $? "conditional \"MODULE__TESTEXTERNALINSPECTION\" was never defined.
|
||||
if test -z "${MODULE__REMOTEDEBUGGING_TRUE}" && test -z "${MODULE__REMOTEDEBUGGING_FALSE}"; then
|
||||
as_fn_error $? "conditional \"MODULE__REMOTEDEBUGGING\" was never defined.
|
||||
Usually this means the macro was only invoked conditionally." "$LINENO" 5
|
||||
fi
|
||||
if test -z "${MODULE_XXSUBTYPE_TRUE}" && test -z "${MODULE_XXSUBTYPE_FALSE}"; then
|
||||
|
|
|
@ -7720,7 +7720,7 @@ AS_CASE([$ac_sys_system],
|
|||
dnl (see Modules/Setup.stdlib.in).
|
||||
PY_STDLIB_MOD_SET_NA(
|
||||
[_ctypes_test],
|
||||
[_testexternalinspection],
|
||||
[_remotedebugging],
|
||||
[_testimportmultiple],
|
||||
[_testmultiphase],
|
||||
[_testsinglephase],
|
||||
|
@ -8082,7 +8082,7 @@ PY_STDLIB_MOD([_testbuffer], [test "$TEST_MODULES" = yes])
|
|||
PY_STDLIB_MOD([_testimportmultiple], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
|
||||
PY_STDLIB_MOD([_testmultiphase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
|
||||
PY_STDLIB_MOD([_testsinglephase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
|
||||
PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes])
|
||||
PY_STDLIB_MOD([_remotedebugging], [test "$TEST_MODULES" = yes])
|
||||
PY_STDLIB_MOD([xxsubtype], [test "$TEST_MODULES" = yes])
|
||||
PY_STDLIB_MOD([_xxtestfuzz], [test "$TEST_MODULES" = yes])
|
||||
PY_STDLIB_MOD([_ctypes_test],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue