diff --git a/.agents/tasks/2025/08/20-0906-tracer-trait2 b/.agents/tasks/2025/08/20-0906-tracer-trait2 new file mode 100644 index 0000000..3a0948b --- /dev/null +++ b/.agents/tasks/2025/08/20-0906-tracer-trait2 @@ -0,0 +1,9 @@ +Write tests for the complete Tracer trait + +Currently in print_tracer.rs we have only one test for the CALL event. We want tests for all other events. The test must verify that the corresponding event was raised and handled at least once. +--- FOLLOW UP TASK --- +Write tests for the complete Tracer trait\nCurrently in print_tracer.rs we have only one test for the CALL event. We want tests for all other events. The test must verify that the corresponding event was raised and handled at least once. +--- FOLLOW UP TASK --- +For each handler in CountingTracer also print the line at which the event fired. Refer to the sys.monitoring API and specifically to the CodeObject type for details. +--- FOLLOW UP TASK --- +A test is missing for the STOP_ITERATION event \ No newline at end of file diff --git a/codetracer-python-recorder/src/tracer.rs b/codetracer-python-recorder/src/tracer.rs index 2842906..12b67a1 100644 --- a/codetracer-python-recorder/src/tracer.rs +++ b/codetracer-python-recorder/src/tracer.rs @@ -159,6 +159,137 @@ pub trait Tracer: Send { /// Called on line execution. fn on_line(&mut self, _py: Python<'_>, _code: &Bound<'_, PyAny>, _lineno: u32) {} + + /// Called when an instruction is about to be executed (by offset). + fn on_instruction(&mut self, _py: Python<'_>, _code: &Bound<'_, PyAny>, _offset: i32) {} + + /// Called when a jump in the control flow graph is made. + fn on_jump( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _destination_offset: i32, + ) { + } + + /// Called when a conditional branch is considered. + fn on_branch( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _destination_offset: i32, + ) { + } + + /// Called at start of a Python function (frame on stack). + fn on_py_start(&mut self, _py: Python<'_>, _code: &Bound<'_, PyAny>, _offset: i32) {} + + /// Called on resumption of a generator/coroutine (not via throw()). + fn on_py_resume(&mut self, _py: Python<'_>, _code: &Bound<'_, PyAny>, _offset: i32) {} + + /// Called immediately before a Python function returns. + fn on_py_return( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _retval: &Bound<'_, PyAny>, + ) { + } + + /// Called immediately before a Python function yields. + fn on_py_yield( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _retval: &Bound<'_, PyAny>, + ) { + } + + /// Called when a Python function is resumed by throw(). + fn on_py_throw( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called when exiting a Python function during exception unwinding. + fn on_py_unwind( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called when an exception is raised (excluding STOP_ITERATION). + fn on_raise( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called when an exception is re-raised. + fn on_reraise( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called when an exception is handled. + fn on_exception_handled( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called when an artificial StopIteration is raised. + fn on_stop_iteration( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _exception: &Bound<'_, PyAny>, + ) { + } + + /// Called on return from any non-Python callable. + fn on_c_return( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _callable: &Bound<'_, PyAny>, + _arg0: Option<&Bound<'_, PyAny>>, + ) { + } + + /// Called when an exception is raised from any non-Python callable. + fn on_c_raise( + &mut self, + _py: Python<'_>, + _code: &Bound<'_, PyAny>, + _offset: i32, + _callable: &Bound<'_, PyAny>, + _arg0: Option<&Bound<'_, PyAny>>, + ) { + } } struct Global { @@ -185,16 +316,74 @@ pub fn install_tracer(py: Python<'_>, tracer: Box) -> PyResult<()> { if mask.contains(&events.CALL) { let cb = wrap_pyfunction!(callback_call, &module)?; - register_callback(py, &tool, &events.CALL, Some(&cb))?; - } if mask.contains(&events.LINE) { let cb = wrap_pyfunction!(callback_line, &module)?; register_callback(py, &tool, &events.LINE, Some(&cb))?; } + if mask.contains(&events.INSTRUCTION) { + let cb = wrap_pyfunction!(callback_instruction, &module)?; + register_callback(py, &tool, &events.INSTRUCTION, Some(&cb))?; + } + if mask.contains(&events.JUMP) { + let cb = wrap_pyfunction!(callback_jump, &module)?; + register_callback(py, &tool, &events.JUMP, Some(&cb))?; + } + if mask.contains(&events.BRANCH) { + let cb = wrap_pyfunction!(callback_branch, &module)?; + register_callback(py, &tool, &events.BRANCH, Some(&cb))?; + } + if mask.contains(&events.PY_START) { + let cb = wrap_pyfunction!(callback_py_start, &module)?; + register_callback(py, &tool, &events.PY_START, Some(&cb))?; + } + if mask.contains(&events.PY_RESUME) { + let cb = wrap_pyfunction!(callback_py_resume, &module)?; + register_callback(py, &tool, &events.PY_RESUME, Some(&cb))?; + } + if mask.contains(&events.PY_RETURN) { + let cb = wrap_pyfunction!(callback_py_return, &module)?; + register_callback(py, &tool, &events.PY_RETURN, Some(&cb))?; + } + if mask.contains(&events.PY_YIELD) { + let cb = wrap_pyfunction!(callback_py_yield, &module)?; + register_callback(py, &tool, &events.PY_YIELD, Some(&cb))?; + } + if mask.contains(&events.PY_THROW) { + let cb = wrap_pyfunction!(callback_py_throw, &module)?; + register_callback(py, &tool, &events.PY_THROW, Some(&cb))?; + } + if mask.contains(&events.PY_UNWIND) { + let cb = wrap_pyfunction!(callback_py_unwind, &module)?; + register_callback(py, &tool, &events.PY_UNWIND, Some(&cb))?; + } + if mask.contains(&events.RAISE) { + let cb = wrap_pyfunction!(callback_raise, &module)?; + register_callback(py, &tool, &events.RAISE, Some(&cb))?; + } + if mask.contains(&events.RERAISE) { + let cb = wrap_pyfunction!(callback_reraise, &module)?; + register_callback(py, &tool, &events.RERAISE, Some(&cb))?; + } + if mask.contains(&events.EXCEPTION_HANDLED) { + let cb = wrap_pyfunction!(callback_exception_handled, &module)?; + register_callback(py, &tool, &events.EXCEPTION_HANDLED, Some(&cb))?; + } + if mask.contains(&events.STOP_ITERATION) { + let cb = wrap_pyfunction!(callback_stop_iteration, &module)?; + register_callback(py, &tool, &events.STOP_ITERATION, Some(&cb))?; + } + if mask.contains(&events.C_RETURN) { + let cb = wrap_pyfunction!(callback_c_return, &module)?; + register_callback(py, &tool, &events.C_RETURN, Some(&cb))?; + } + if mask.contains(&events.C_RAISE) { + let cb = wrap_pyfunction!(callback_c_raise, &module)?; + register_callback(py, &tool, &events.C_RAISE, Some(&cb))?; + } + set_events(py, &tool, mask)?; - *guard = Some(Global { tracer, @@ -215,6 +404,52 @@ pub fn uninstall_tracer(py: Python<'_>) -> PyResult<()> { if global.mask.contains(&events.LINE) { register_callback(py, &global.tool, &events.LINE, None)?; } + if global.mask.contains(&events.INSTRUCTION) { + register_callback(py, &global.tool, &events.INSTRUCTION, None)?; + } + if global.mask.contains(&events.JUMP) { + register_callback(py, &global.tool, &events.JUMP, None)?; + } + if global.mask.contains(&events.BRANCH) { + register_callback(py, &global.tool, &events.BRANCH, None)?; + } + if global.mask.contains(&events.PY_START) { + register_callback(py, &global.tool, &events.PY_START, None)?; + } + if global.mask.contains(&events.PY_RESUME) { + register_callback(py, &global.tool, &events.PY_RESUME, None)?; + } + if global.mask.contains(&events.PY_RETURN) { + register_callback(py, &global.tool, &events.PY_RETURN, None)?; + } + if global.mask.contains(&events.PY_YIELD) { + register_callback(py, &global.tool, &events.PY_YIELD, None)?; + } + if global.mask.contains(&events.PY_THROW) { + register_callback(py, &global.tool, &events.PY_THROW, None)?; + } + if global.mask.contains(&events.PY_UNWIND) { + register_callback(py, &global.tool, &events.PY_UNWIND, None)?; + } + if global.mask.contains(&events.RAISE) { + register_callback(py, &global.tool, &events.RAISE, None)?; + } + if global.mask.contains(&events.RERAISE) { + register_callback(py, &global.tool, &events.RERAISE, None)?; + } + if global.mask.contains(&events.EXCEPTION_HANDLED) { + register_callback(py, &global.tool, &events.EXCEPTION_HANDLED, None)?; + } + if global.mask.contains(&events.STOP_ITERATION) { + register_callback(py, &global.tool, &events.STOP_ITERATION, None)?; + } + if global.mask.contains(&events.C_RETURN) { + register_callback(py, &global.tool, &events.C_RETURN, None)?; + } + if global.mask.contains(&events.C_RAISE) { + register_callback(py, &global.tool, &events.C_RAISE, None)?; + } + set_events(py, &global.tool, NO_EVENTS)?; free_tool_id(py, &global.tool)?; } @@ -242,3 +477,197 @@ fn callback_line(py: Python<'_>, code: Bound<'_, PyAny>, lineno: u32) -> PyResul } Ok(()) } + +#[pyfunction] +fn callback_instruction(py: Python<'_>, code: Bound<'_, PyAny>, instruction_offset: i32) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_instruction(py, &code, instruction_offset); + } + Ok(()) +} + +#[pyfunction] +fn callback_jump( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + destination_offset: i32, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_jump(py, &code, instruction_offset, destination_offset); + } + Ok(()) +} + +#[pyfunction] +fn callback_branch( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + destination_offset: i32, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_branch(py, &code, instruction_offset, destination_offset); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_start(py: Python<'_>, code: Bound<'_, PyAny>, instruction_offset: i32) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_start(py, &code, instruction_offset); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_resume(py: Python<'_>, code: Bound<'_, PyAny>, instruction_offset: i32) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_resume(py, &code, instruction_offset); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_return( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + retval: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_return(py, &code, instruction_offset, &retval); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_yield( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + retval: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_yield(py, &code, instruction_offset, &retval); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_throw( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_throw(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_py_unwind( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_py_unwind(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_raise( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_raise(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_reraise( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global.tracer.on_reraise(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_exception_handled( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_exception_handled(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_stop_iteration( + py: Python<'_>, + code: Bound<'_, PyAny>, + instruction_offset: i32, + exception: Bound<'_, PyAny>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_stop_iteration(py, &code, instruction_offset, &exception); + } + Ok(()) +} + +#[pyfunction] +fn callback_c_return( + py: Python<'_>, + code: Bound<'_, PyAny>, + offset: i32, + callable: Bound<'_, PyAny>, + arg0: Option>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_c_return(py, &code, offset, &callable, arg0.as_ref()); + } + Ok(()) +} + +#[pyfunction] +fn callback_c_raise( + py: Python<'_>, + code: Bound<'_, PyAny>, + offset: i32, + callable: Bound<'_, PyAny>, + arg0: Option>, +) -> PyResult<()> { + if let Some(global) = GLOBAL.lock().unwrap().as_mut() { + global + .tracer + .on_c_raise(py, &code, offset, &callable, arg0.as_ref()); + } + Ok(()) +} diff --git a/codetracer-python-recorder/tests/print_tracer.rs b/codetracer-python-recorder/tests/print_tracer.rs index 5972af7..6169b5c 100644 --- a/codetracer-python-recorder/tests/print_tracer.rs +++ b/codetracer-python-recorder/tests/print_tracer.rs @@ -45,3 +45,372 @@ fn tracer_prints_on_call() { }); } +static LINE_COUNT: AtomicUsize = AtomicUsize::new(0); +static INSTRUCTION_COUNT: AtomicUsize = AtomicUsize::new(0); +static JUMP_COUNT: AtomicUsize = AtomicUsize::new(0); +static BRANCH_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_START_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_RESUME_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_RETURN_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_YIELD_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_THROW_COUNT: AtomicUsize = AtomicUsize::new(0); +static PY_UNWIND_COUNT: AtomicUsize = AtomicUsize::new(0); +static RAISE_COUNT: AtomicUsize = AtomicUsize::new(0); +static RERAISE_COUNT: AtomicUsize = AtomicUsize::new(0); +static EXCEPTION_HANDLED_COUNT: AtomicUsize = AtomicUsize::new(0); +static STOP_ITERATION_COUNT: AtomicUsize = AtomicUsize::new(0); +static C_RETURN_COUNT: AtomicUsize = AtomicUsize::new(0); +static C_RAISE_COUNT: AtomicUsize = AtomicUsize::new(0); + +struct CountingTracer; + +fn offset_to_line(code: &pyo3::Bound<'_, pyo3::types::PyAny>, offset: i32) -> Option { + if offset < 0 { + return None; + } + let lines_iter = code.call_method0("co_lines").ok()?; + let iter = lines_iter.try_iter().ok()?; + for line_info in iter { + let line_info = line_info.ok()?; + let (start, end, line): (i32, i32, i32) = line_info.extract().ok()?; + if offset >= start && offset < end { + return Some(line); + } + } + None +} + +impl Tracer for CountingTracer { + fn interest(&self, events: &MonitoringEvents) -> EventSet { + events_union(&[ + events.CALL, + events.LINE, + events.INSTRUCTION, + events.JUMP, + events.BRANCH, + events.PY_START, + events.PY_RESUME, + events.PY_RETURN, + events.PY_YIELD, + events.PY_THROW, + events.PY_UNWIND, + events.STOP_ITERATION, + events.RAISE, + events.RERAISE, + events.EXCEPTION_HANDLED, + events.C_RETURN, + events.C_RAISE, + ]) + } + + fn on_line( + &mut self, + _py: Python<'_>, + _code: &pyo3::Bound<'_, pyo3::types::PyAny>, + lineno: u32, + ) { + LINE_COUNT.fetch_add(1, Ordering::SeqCst); + println!("LINE at {}", lineno); + } + + fn on_instruction( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + ) { + INSTRUCTION_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("INSTRUCTION at {}", line); + } + } + + fn on_jump( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _destination_offset: i32, + ) { + JUMP_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("JUMP at {}", line); + } + } + + fn on_branch( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _destination_offset: i32, + ) { + BRANCH_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("BRANCH at {}", line); + } + } + + fn on_py_start( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + ) { + PY_START_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_START at {}", line); + } + } + + fn on_py_resume( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + ) { + PY_RESUME_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_RESUME at {}", line); + } + } + + fn on_py_return( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _retval: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + PY_RETURN_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_RETURN at {}", line); + } + } + + fn on_py_yield( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _retval: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + PY_YIELD_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_YIELD at {}", line); + } + } + + fn on_py_throw( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + PY_THROW_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_THROW at {}", line); + } + } + + fn on_py_unwind( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + PY_UNWIND_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("PY_UNWIND at {}", line); + } + } + + fn on_raise( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + RAISE_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("RAISE at {}", line); + } + } + + fn on_reraise( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + RERAISE_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("RERAISE at {}", line); + } + } + + fn on_exception_handled( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + EXCEPTION_HANDLED_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("EXCEPTION_HANDLED at {}", line); + } + } + + fn on_stop_iteration( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _exception: &pyo3::Bound<'_, pyo3::types::PyAny>, + ) { + STOP_ITERATION_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("STOP_ITERATION at {}", line); + } + } + + fn on_c_return( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _callable: &pyo3::Bound<'_, pyo3::types::PyAny>, + _arg0: Option<&pyo3::Bound<'_, pyo3::types::PyAny>>, + ) { + C_RETURN_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("C_RETURN at {}", line); + } + } + + fn on_c_raise( + &mut self, + _py: Python<'_>, + code: &pyo3::Bound<'_, pyo3::types::PyAny>, + offset: i32, + _callable: &pyo3::Bound<'_, pyo3::types::PyAny>, + _arg0: Option<&pyo3::Bound<'_, pyo3::types::PyAny>>, + ) { + C_RAISE_COUNT.fetch_add(1, Ordering::SeqCst); + if let Some(line) = offset_to_line(code, offset) { + println!("C_RAISE at {}", line); + } + } +} + +#[test] +fn tracer_handles_all_events() { + Python::with_gil(|py| { + LINE_COUNT.store(0, Ordering::SeqCst); + INSTRUCTION_COUNT.store(0, Ordering::SeqCst); + JUMP_COUNT.store(0, Ordering::SeqCst); + BRANCH_COUNT.store(0, Ordering::SeqCst); + PY_START_COUNT.store(0, Ordering::SeqCst); + PY_RESUME_COUNT.store(0, Ordering::SeqCst); + PY_RETURN_COUNT.store(0, Ordering::SeqCst); + PY_YIELD_COUNT.store(0, Ordering::SeqCst); + PY_THROW_COUNT.store(0, Ordering::SeqCst); + PY_UNWIND_COUNT.store(0, Ordering::SeqCst); + RAISE_COUNT.store(0, Ordering::SeqCst); + RERAISE_COUNT.store(0, Ordering::SeqCst); + EXCEPTION_HANDLED_COUNT.store(0, Ordering::SeqCst); + STOP_ITERATION_COUNT.store(0, Ordering::SeqCst); + C_RETURN_COUNT.store(0, Ordering::SeqCst); + C_RAISE_COUNT.store(0, Ordering::SeqCst); + if let Err(e) = install_tracer(py, Box::new(CountingTracer)) { + e.print(py); + panic!("Install Tracer failed"); + } + let code = CString::new(r#" +def test_all(): + x = 0 + if x == 0: + x += 1 + for i in range(1): + x += i + def foo(): + return 1 + foo() + try: + raise ValueError("err") + except ValueError: + pass + def gen(): + try: + yield 1 + yield 2 + except ValueError: + pass + g = gen() + next(g) + next(g) + try: + g.throw(ValueError()) + except StopIteration: + pass + for _ in []: + pass + def gen2(): + yield 1 + for _ in gen2(): + pass + len("abc") + try: + int("a") + except ValueError: + pass + def f_unwind(): + raise KeyError + try: + f_unwind() + except KeyError: + pass + try: + try: + raise OSError() + except OSError: + raise + except OSError: + pass +test_all() +def only_stop_iter(): + if False: + yield +for _ in only_stop_iter(): + pass +"#).expect("CString::new failed"); + if let Err(e) = py.run(code.as_c_str(), None, None) { + e.print(py); + uninstall_tracer(py).ok(); + panic!("Python raised an exception"); + } + uninstall_tracer(py).unwrap(); + assert!(LINE_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one LINE event, got {}", LINE_COUNT.load(Ordering::SeqCst)); + assert!(INSTRUCTION_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one INSTRUCTION event, got {}", INSTRUCTION_COUNT.load(Ordering::SeqCst)); + assert!(JUMP_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one JUMP event, got {}", JUMP_COUNT.load(Ordering::SeqCst)); + assert!(BRANCH_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one BRANCH event, got {}", BRANCH_COUNT.load(Ordering::SeqCst)); + assert!(PY_START_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_START event, got {}", PY_START_COUNT.load(Ordering::SeqCst)); + assert!(PY_RESUME_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_RESUME event, got {}", PY_RESUME_COUNT.load(Ordering::SeqCst)); + assert!(PY_RETURN_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_RETURN event, got {}", PY_RETURN_COUNT.load(Ordering::SeqCst)); + assert!(PY_YIELD_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_YIELD event, got {}", PY_YIELD_COUNT.load(Ordering::SeqCst)); + assert!(PY_THROW_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_THROW event, got {}", PY_THROW_COUNT.load(Ordering::SeqCst)); + assert!(PY_UNWIND_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one PY_UNWIND event, got {}", PY_UNWIND_COUNT.load(Ordering::SeqCst)); + assert!(RAISE_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one RAISE event, got {}", RAISE_COUNT.load(Ordering::SeqCst)); + assert!(RERAISE_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one RERAISE event, got {}", RERAISE_COUNT.load(Ordering::SeqCst)); + assert!(EXCEPTION_HANDLED_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one EXCEPTION_HANDLED event, got {}", EXCEPTION_HANDLED_COUNT.load(Ordering::SeqCst)); + assert!(STOP_ITERATION_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one STOP_ITERATION event, got {}", STOP_ITERATION_COUNT.load(Ordering::SeqCst)); + assert!(C_RETURN_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one C_RETURN event, got {}", C_RETURN_COUNT.load(Ordering::SeqCst)); + assert!(C_RAISE_COUNT.load(Ordering::SeqCst) >= 1, "expected at least one C_RAISE event, got {}", C_RAISE_COUNT.load(Ordering::SeqCst)); + }); +} +