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..8824e4a --- /dev/null +++ b/.agents/tasks/2025/08/20-0906-tracer-trait2 @@ -0,0 +1,11 @@ +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.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..d40626b 100644 --- a/codetracer-python-recorder/src/tracer.rs +++ b/codetracer-python-recorder/src/tracer.rs @@ -30,7 +30,7 @@ pub struct MonitoringEvents { pub PY_YIELD: EventId, pub RAISE: EventId, pub RERAISE: EventId, - pub STOP_ITERATION: EventId, + //pub STOP_ITERATION: EventId, //See comment in Tracer trait } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -81,7 +81,7 @@ pub fn load_monitoring_events(py: Python<'_>) -> PyResult { PY_YIELD: EventId(events.getattr("PY_YIELD")?.extract()?), RAISE: EventId(events.getattr("RAISE")?.extract()?), RERAISE: EventId(events.getattr("RERAISE")?.extract()?), - STOP_ITERATION: EventId(events.getattr("STOP_ITERATION")?.extract()?), + //STOP_ITERATION: EventId(events.getattr("STOP_ITERATION")?.extract()?), //See comment in Tracer trait }) } @@ -159,6 +159,141 @@ 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. + // Tzanko: I have been unable to write Python code that emits this event. This happens both in Python 3.12, 3.13 + // Here are some relevant discussions which might explain why, I haven't investigated the issue fully + // https://github.com/python/cpython/issues/116090, + // https://github.com/python/cpython/issues/118692 + // 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 +320,75 @@ 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))?; + } + // See comment in Tracer trait + // 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 +409,53 @@ 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)?; + } + // See comment in tracer trait + // 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 +483,198 @@ 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(()) +} + +// See comment in Tracer trait +// #[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..a1cccc0 100644 --- a/codetracer-python-recorder/tests/print_tracer.rs +++ b/codetracer-python-recorder/tests/print_tracer.rs @@ -45,3 +45,373 @@ 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); //ISSUE: We can't figure out how to triger this event + 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 + return 2 + 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)); //Issue + 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)); + }); +} +