Skip to content

Commit 67f755f

Browse files
committed
Emit the exception from a pending call
- adds a new trace event type ("opcode") for more reliable injection of the error - moves the error injection into a C helper function so it actually happens in the frame of interest immediately after the offset of interest
1 parent b750ff0 commit 67f755f

File tree

3 files changed

+95
-34
lines changed

3 files changed

+95
-34
lines changed

Lib/test/test_with_signal_safety.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33

44
from test.support import cpython_only, verbose
5+
from _testcapi import install_error_injection_hook
56
import asyncio
67
import dis
78
import sys
@@ -11,24 +12,23 @@ class InjectedException(Exception):
1112
"""Exception injected into a running frame via a trace function"""
1213
pass
1314

14-
def raise_after_instruction(target_function, target_instruction):
15+
def raise_after_offset(target_function, target_offset):
1516
"""Sets a trace function to inject an exception into given function
1617
1718
Relies on the ability to request that a trace function be called for
1819
every executed opcode, not just every line
1920
"""
2021
target_code = target_function.__code__
21-
def inject_exception(frame, event, arg):
22-
if frame.f_code is not target_code:
23-
return
24-
frame.f_trace_opcodes = True
25-
if frame.f_lasti >= target_instruction:
26-
if frame.f_lasti > frame.f_pendingi:
27-
raise InjectedException(f"Failing after {frame.f_lasti}")
28-
return inject_exception
29-
sys.settrace(inject_exception)
30-
31-
# TODO: Add a test case that ensures raise_after_instruction is working
22+
def inject_exception():
23+
print("Raising injected exception")
24+
raise InjectedException(f"Failing after {target_offset}")
25+
# This installs a trace hook that's implemented in C, and hence won't
26+
# trigger any of the per-bytecode processing in the eval loop
27+
# This means it can register the pending call that raises the exception and
28+
# the pending call won't be processed until after the trace hook returns
29+
install_error_injection_hook(target_code, target_offset, inject_exception)
30+
31+
# TODO: Add a test case that ensures raise_after_offset is working
3232
# properly (otherwise there's a risk the tests will pass due to the
3333
# exception not being injected properly)
3434

@@ -51,11 +51,11 @@ def setUp(self):
5151
self.addCleanup(sys.settrace, old_trace)
5252
sys.settrace(None)
5353

54-
def assert_cm_exited(self, tracking_cm, target_instruction, traced_operation):
54+
def assert_cm_exited(self, tracking_cm, target_offset, traced_operation):
5555
if tracking_cm.enter_without_exit:
5656
msg = ("Context manager entered without exit due to "
57-
f"exception injected at offset {target_instruction} in:\n"
58-
f"{dis.Bytecode(traced_operation).dis()}")
57+
f"exception injected at offset {target_offset} in:\n"
58+
f"{dis.Bytecode(traced_operation).dis()}")
5959
self.fail(msg)
6060

6161
def test_synchronous_cm(self):
@@ -71,21 +71,21 @@ def traced_function():
7171
with tracking_cm:
7272
1 + 1
7373
return
74-
target_instruction = -1
75-
num_instructions = len(traced_function.__code__.co_code) - 2
76-
while target_instruction < num_instructions:
77-
target_instruction += 1
78-
raise_after_instruction(traced_function, target_instruction)
74+
target_offset = -1
75+
max_offset = len(traced_function.__code__.co_code) - 2
76+
while target_offset < max_offset:
77+
target_offset += 1
78+
raise_after_offset(traced_function, target_offset)
7979
try:
8080
traced_function()
8181
except InjectedException:
8282
# key invariant: if we entered the CM, we exited it
83-
self.assert_cm_exited(tracking_cm, target_instruction, traced_function)
83+
self.assert_cm_exited(tracking_cm, target_offset, traced_function)
8484
else:
85-
self.fail(f"Exception wasn't raised @{target_instruction}")
85+
self.fail(f"Exception wasn't raised @{target_offset}")
8686

8787

88-
def test_asynchronous_cm(self):
88+
def _test_asynchronous_cm(self):
8989
class AsyncTrackingCM():
9090
def __init__(self):
9191
self.enter_without_exit = None
@@ -98,19 +98,19 @@ async def traced_coroutine():
9898
async with tracking_cm:
9999
1 + 1
100100
return
101-
target_instruction = -1
102-
num_instructions = len(traced_coroutine.__code__.co_code) - 2
101+
target_offset = -1
102+
max_offset = len(traced_coroutine.__code__.co_code) - 2
103103
loop = asyncio.get_event_loop()
104-
while target_instruction < num_instructions:
105-
target_instruction += 1
106-
raise_after_instruction(traced_coroutine, target_instruction)
104+
while target_offset < max_offset:
105+
target_offset += 1
106+
raise_after_offset(traced_coroutine, target_offset)
107107
try:
108108
loop.run_until_complete(traced_coroutine())
109109
except InjectedException:
110110
# key invariant: if we entered the CM, we exited it
111-
self.assert_cm_exited(tracking_cm, target_instruction, traced_coroutine)
111+
self.assert_cm_exited(tracking_cm, target_offset, traced_coroutine)
112112
else:
113-
self.fail(f"Exception wasn't raised @{target_instruction}")
113+
self.fail(f"Exception wasn't raised @{target_offset}")
114114

115115

116116
if __name__ == '__main__':

Modules/_testcapimodule.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "structmember.h"
1313
#include "datetime.h"
1414
#include "marshal.h"
15+
#include "frameobject.h"
1516
#include <signal.h>
1617

1718
#ifdef MS_WINDOWS
@@ -2345,6 +2346,58 @@ PyObject *pending_threadfunc(PyObject *self, PyObject *arg)
23452346
Py_RETURN_TRUE;
23462347
}
23472348

2349+
/* Helper for test_with_signal_safety that injects errors into the eval loop's
2350+
* pending call handling at a designated bytecode offset
2351+
*
2352+
* The hook args indicate the code object where the pending call should be
2353+
* injected, the offset where it should be registered, and the callback itself
2354+
*/
2355+
static int
2356+
error_injection_trace(PyObject *hook_args, PyFrameObject *frame,
2357+
int what, PyObject *event_arg)
2358+
{
2359+
PyObject *target_code, *callback;
2360+
int target_offset;
2361+
2362+
if (!PyArg_ParseTuple(hook_args, "OiO:error_injection_trace",
2363+
&target_code, &target_offset, &callback)) {
2364+
PyEval_SetTrace(NULL, NULL);
2365+
return -1;
2366+
}
2367+
2368+
if (((PyObject *) frame->f_code) == target_code) {
2369+
printf("Tracing frame of interest\n");
2370+
frame->f_trace_opcodes = 1;
2371+
if (what == PyTrace_OPCODE && frame->f_lasti > target_offset) {
2372+
printf("Adding pending call after %d\n", frame->f_lasti);
2373+
Py_INCREF(callback);
2374+
if (Py_AddPendingCall(&_pending_callback, callback) < 0) {
2375+
printf("Failed to add pending call\n");
2376+
Py_DECREF(callback);
2377+
PyEval_SetTrace(NULL, NULL);
2378+
return -1;
2379+
}
2380+
PyEval_SetTrace(NULL, NULL);
2381+
}
2382+
}
2383+
return 0;
2384+
}
2385+
2386+
PyObject *install_error_injection_hook(PyObject *self, PyObject *args)
2387+
{
2388+
PyObject *target_code, *target_offset, *callback;
2389+
2390+
/* Check the args are as expected */
2391+
if (!PyArg_UnpackTuple(args, "install_error_injection_hook", 3, 3,
2392+
&target_code, &target_offset, &callback)) {
2393+
return NULL;
2394+
}
2395+
printf("Registering trace hook\n");
2396+
2397+
PyEval_SetTrace(error_injection_trace, args);
2398+
Py_RETURN_NONE;
2399+
}
2400+
23482401
/* Some tests of PyUnicode_FromFormat(). This needs more tests. */
23492402
static PyObject *
23502403
test_string_from_format(PyObject *self, PyObject *args)
@@ -4415,6 +4468,7 @@ static PyMethodDef TestMethods[] = {
44154468
{"unicode_legacy_string", unicode_legacy_string, METH_VARARGS},
44164469
{"_test_thread_state", test_thread_state, METH_VARARGS},
44174470
{"_pending_threadfunc", pending_threadfunc, METH_VARARGS},
4471+
{"install_error_injection_hook", install_error_injection_hook, METH_VARARGS},
44184472
#ifdef HAVE_GETTIMEOFDAY
44194473
{"profile_int", profile_int, METH_NOARGS},
44204474
#endif
@@ -4933,5 +4987,6 @@ PyInit__testcapi(void)
49334987
TestError = PyErr_NewException("_testcapi.error", NULL, NULL);
49344988
Py_INCREF(TestError);
49354989
PyModule_AddObject(m, "error", TestError);
4990+
49364991
return m;
49374992
}

Python/ceval.c

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -981,12 +981,18 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
981981
*/
982982
goto fast_next_opcode;
983983
}
984-
if ((next_instr > handle_pending_after) &&
985-
_Py_atomic_load_relaxed(&pendingcalls_to_do)) {
986-
if (Py_MakePendingCalls() < 0)
987-
goto error;
984+
if (handle_pending_after > first_instr) {
985+
printf("Pending calls deferred until %lu\n", DEFER_OFFSET());
986+
}
987+
if (next_instr >= handle_pending_after) {
988+
printf("Checking for pending calls: %lu > %lu?\n", INSTR_OFFSET(), DEFER_OFFSET());
988989
/* Allow for subsequent jumps backwards in the bytecode */
989990
handle_pending_after = first_instr;
991+
if (_Py_atomic_load_relaxed(&pendingcalls_to_do)) {
992+
printf(" Processing pending calls\n");
993+
if (Py_MakePendingCalls() < 0)
994+
goto error;
995+
}
990996
}
991997
if (_Py_atomic_load_relaxed(&gil_drop_request)) {
992998
/* Give another thread a chance */

0 commit comments

Comments
 (0)