Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-115773: Add tests to exercise the _Py_DebugOffsets structure #115774

Merged
merged 8 commits into from
Feb 28, 2024
77 changes: 42 additions & 35 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,74 +55,81 @@ typedef struct _Py_DebugOffsets {
uint64_t version;
// Runtime state offset;
struct _runtime_state {
off_t finalizing;
off_t interpreters_head;
uint64_t finalizing;
uint64_t interpreters_head;
} runtime_state;

// Interpreter state offset;
struct _interpreter_state {
off_t next;
off_t threads_head;
off_t gc;
off_t imports_modules;
off_t sysdict;
off_t builtins;
off_t ceval_gil;
off_t gil_runtime_state_locked;
off_t gil_runtime_state_holder;
uint64_t next;
uint64_t threads_head;
uint64_t gc;
uint64_t imports_modules;
uint64_t sysdict;
uint64_t builtins;
uint64_t ceval_gil;
uint64_t gil_runtime_state_locked;
uint64_t gil_runtime_state_holder;
} interpreter_state;

// Thread state offset;
struct _thread_state{
off_t prev;
off_t next;
off_t interp;
off_t current_frame;
off_t thread_id;
off_t native_thread_id;
uint64_t prev;
uint64_t next;
uint64_t interp;
uint64_t current_frame;
uint64_t thread_id;
uint64_t native_thread_id;
} thread_state;

// InterpreterFrame offset;
struct _interpreter_frame {
off_t previous;
off_t executable;
off_t instr_ptr;
off_t localsplus;
off_t owner;
uint64_t previous;
uint64_t executable;
uint64_t instr_ptr;
uint64_t localsplus;
uint64_t owner;
} interpreter_frame;

// CFrame offset;
struct _cframe {
off_t current_frame;
off_t previous;
uint64_t current_frame;
uint64_t previous;
} cframe;

// Code object offset;
struct _code_object {
off_t filename;
off_t name;
off_t linetable;
off_t firstlineno;
off_t argcount;
off_t localsplusnames;
off_t localspluskinds;
off_t co_code_adaptive;
uint64_t filename;
uint64_t name;
uint64_t linetable;
uint64_t firstlineno;
uint64_t argcount;
uint64_t localsplusnames;
uint64_t localspluskinds;
uint64_t co_code_adaptive;
} code_object;

// PyObject offset;
struct _pyobject {
off_t ob_type;
uint64_t ob_type;
} pyobject;

// PyTypeObject object offset;
struct _type_object {
off_t tp_name;
uint64_t tp_name;
} type_object;

// PyTuple object offset;
struct _tuple_object {
off_t ob_item;
uint64_t ob_item;
} tuple_object;

// Unicode object offset;
struct _unicode_object {
uint64_t state;
uint64_t length;
size_t asciiobject_size;
} unicode_object;
} _Py_DebugOffsets;

/* Full Python runtime state */
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ extern PyTypeObject _PyExc_MemoryError;
.tuple_object = { \
.ob_item = offsetof(PyTupleObject, ob_item), \
}, \
.unicode_object = { \
.state = offsetof(PyUnicodeObject, _base._base.state), \
.length = offsetof(PyUnicodeObject, _base._base.length), \
.asciiobject_size = sizeof(PyASCIIObject), \
}, \
}, \
.allocators = { \
.standard = _pymem_allocators_standard_INIT(runtime), \
Expand Down
84 changes: 84 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import unittest
import os
import textwrap
import importlib
import sys
from test.support import os_helper, SHORT_TIMEOUT
from test.support.script_helper import make_script

import subprocess

PROCESS_VM_READV_SUPPORTED = False

try:
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
from _testexternalinspection import get_stack_trace
except ImportError:
unittest.skip("Test only runs when _testexternalinspection is available")

def _make_test_script(script_dir, script_basename, source):
to_return = make_script(script_dir, script_basename, source)
importlib.invalidate_caches()
return to_return

class TestGetStackTrace(unittest.TestCase):

@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
def test_remote_stack_trace(self):
# Spawn a process with some realistic Python code
script = textwrap.dedent("""\
import time, sys, os
def bar():
for x in range(100):
if x == 50:
baz()
def baz():
foo()

def foo():
fifo = sys.argv[1]
with open(sys.argv[1], "w") as fifo:
fifo.write("ready")
time.sleep(1000)

bar()
""")
stack_trace = None
with os_helper.temp_dir() as work_dir:
script_dir = os.path.join(work_dir, "script_pkg")
os.mkdir(script_dir)
fifo = f"{work_dir}/the_fifo"
os.mkfifo(fifo)
script_name = _make_test_script(script_dir, 'script', script)
try:
p = subprocess.Popen([sys.executable, script_name, str(fifo)])
with open(fifo, "r") as fifo_file:
response = fifo_file.read()
self.assertEqual(response, "ready")
stack_trace = get_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
finally:
os.remove(fifo)
p.kill()
p.terminate()
p.wait(timeout=SHORT_TIMEOUT)


expected_stack_trace = [
'foo',
'baz',
'bar',
'<module>'
]
self.assertEqual(stack_trace, expected_stack_trace)

@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
def test_self_trace(self):
stack_trace = get_stack_trace(os.getpid())
self.assertEqual(stack_trace[0], "test_self_trace")

if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Modules/Setup
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ PYTHONPATH=$(COREPYTHONPATH)
#_testcapi _testcapimodule.c
#_testimportmultiple _testimportmultiple.c
#_testmultiphase _testmultiphase.c
#_testexternalinspection _testexternalinspection.c
#_testsinglephase _testsinglephase.c

# ---
Expand Down
1 change: 1 addition & 0 deletions Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
@MODULE__TESTMULTIPHASE_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.
Expand Down
Loading
Loading