Factor out UnraisableCapture helper type and use it to check that dropping unsendable elsewhere calls into sys.unraisablehook
This commit is contained in:
parent
501ff8a17d
commit
e85bfcc3bf
|
@ -1,5 +1,8 @@
|
||||||
//! Some common macros for tests
|
//! Some common macros for tests
|
||||||
|
|
||||||
|
#[cfg(all(feature = "macros", Py_3_8))]
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! py_assert {
|
macro_rules! py_assert {
|
||||||
($py:expr, $($val:ident)+, $assertion:literal) => {
|
($py:expr, $($val:ident)+, $assertion:literal) => {
|
||||||
|
@ -41,3 +44,50 @@ macro_rules! py_expect_exception {
|
||||||
err
|
err
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sys.unraisablehook not available until Python 3.8
|
||||||
|
#[cfg(all(feature = "macros", Py_3_8))]
|
||||||
|
#[pyclass]
|
||||||
|
pub struct UnraisableCapture {
|
||||||
|
pub capture: Option<(PyErr, PyObject)>,
|
||||||
|
old_hook: Option<PyObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "macros", Py_3_8))]
|
||||||
|
#[pymethods]
|
||||||
|
impl UnraisableCapture {
|
||||||
|
pub fn hook(&mut self, unraisable: &PyAny) {
|
||||||
|
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
|
||||||
|
let instance = unraisable.getattr("object").unwrap();
|
||||||
|
self.capture = Some((err, instance.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "macros", Py_3_8))]
|
||||||
|
impl UnraisableCapture {
|
||||||
|
pub fn install(py: Python<'_>) -> Py<Self> {
|
||||||
|
let sys = py.import("sys").unwrap();
|
||||||
|
let old_hook = sys.getattr("unraisablehook").unwrap().into();
|
||||||
|
|
||||||
|
let capture = Py::new(
|
||||||
|
py,
|
||||||
|
UnraisableCapture {
|
||||||
|
capture: None,
|
||||||
|
old_hook: Some(old_hook),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
capture
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uninstall(&mut self, py: Python<'_>) {
|
||||||
|
let old_hook = self.old_hook.take().unwrap();
|
||||||
|
|
||||||
|
let sys = py.import("sys").unwrap();
|
||||||
|
sys.setattr("unraisablehook", old_hook).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -96,6 +96,7 @@ fn test_buffer_referenced() {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8
|
#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8
|
||||||
fn test_releasebuffer_unraisable_error() {
|
fn test_releasebuffer_unraisable_error() {
|
||||||
|
use common::UnraisableCapture;
|
||||||
use pyo3::exceptions::PyValueError;
|
use pyo3::exceptions::PyValueError;
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
|
@ -117,27 +118,8 @@ fn test_releasebuffer_unraisable_error() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyclass]
|
|
||||||
struct UnraisableCapture {
|
|
||||||
capture: Option<(PyErr, PyObject)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl UnraisableCapture {
|
|
||||||
fn hook(&mut self, unraisable: &PyAny) {
|
|
||||||
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
|
|
||||||
let instance = unraisable.getattr("object").unwrap();
|
|
||||||
self.capture = Some((err, instance.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
let sys = py.import("sys").unwrap();
|
let capture = UnraisableCapture::install(py);
|
||||||
let old_hook = sys.getattr("unraisablehook").unwrap();
|
|
||||||
let capture = Py::new(py, UnraisableCapture { capture: None }).unwrap();
|
|
||||||
|
|
||||||
sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let instance = Py::new(py, ReleaseBufferError {}).unwrap();
|
let instance = Py::new(py, ReleaseBufferError {}).unwrap();
|
||||||
let env = [("ob", instance.clone())].into_py_dict(py);
|
let env = [("ob", instance.clone())].into_py_dict(py);
|
||||||
|
@ -150,7 +132,7 @@ fn test_releasebuffer_unraisable_error() {
|
||||||
assert_eq!(err.to_string(), "ValueError: oh dear");
|
assert_eq!(err.to_string(), "ValueError: oh dear");
|
||||||
assert!(object.is(&instance));
|
assert!(object.is(&instance));
|
||||||
|
|
||||||
sys.setattr("unraisablehook", old_hook).unwrap();
|
capture.borrow_mut(py).uninstall(py);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,31 +228,40 @@ impl UnsendableChild {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_unsendable<T: PyClass + 'static>() -> PyResult<()> {
|
fn test_unsendable<T: PyClass + 'static>() -> PyResult<()> {
|
||||||
let obj = std::thread::spawn(|| -> PyResult<_> {
|
let obj = Python::with_gil(|py| -> PyResult<_> {
|
||||||
|
let obj: Py<T> = PyType::new::<T>(py).call1((5,))?.extract()?;
|
||||||
|
|
||||||
|
// Accessing the value inside this thread should not panic
|
||||||
|
let caught_panic =
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> PyResult<_> {
|
||||||
|
assert_eq!(obj.as_ref(py).getattr("value")?.extract::<usize>()?, 5);
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
|
.is_err();
|
||||||
|
|
||||||
|
assert!(!caught_panic);
|
||||||
|
Ok(obj)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let keep_obj_here = obj.clone();
|
||||||
|
|
||||||
|
let caught_panic = std::thread::spawn(move || {
|
||||||
|
// This access must panic
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
let obj: Py<T> = PyType::new::<T>(py).call1((5,))?.extract()?;
|
obj.borrow(py);
|
||||||
|
});
|
||||||
// Accessing the value inside this thread should not panic
|
|
||||||
let caught_panic =
|
|
||||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> PyResult<_> {
|
|
||||||
assert_eq!(obj.as_ref(py).getattr("value")?.extract::<usize>()?, 5);
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
.is_err();
|
|
||||||
|
|
||||||
assert!(!caught_panic);
|
|
||||||
Ok(obj)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.join()
|
.join();
|
||||||
.unwrap()?;
|
|
||||||
|
|
||||||
// This access must panic
|
Python::with_gil(|_py| drop(keep_obj_here));
|
||||||
Python::with_gil(|py| {
|
|
||||||
obj.borrow(py);
|
|
||||||
});
|
|
||||||
|
|
||||||
panic!("Borrowing unsendable from receiving thread did not panic.");
|
if let Err(err) = caught_panic {
|
||||||
|
if let Some(msg) = err.downcast_ref::<String>() {
|
||||||
|
panic!("{}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If a class is marked as `unsendable`, it panics when accessed by another thread.
|
/// If a class is marked as `unsendable`, it panics when accessed by another thread.
|
||||||
|
@ -528,7 +537,10 @@ fn access_frozen_class_without_gil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8
|
||||||
|
#[cfg_attr(target_arch = "wasm32", ignore)]
|
||||||
fn drop_unsendable_elsewhere() {
|
fn drop_unsendable_elsewhere() {
|
||||||
|
use common::UnraisableCapture;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -546,21 +558,35 @@ fn drop_unsendable_elsewhere() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dropped = Arc::new(AtomicBool::new(false));
|
Python::with_gil(|py| {
|
||||||
|
let capture = UnraisableCapture::install(py);
|
||||||
|
|
||||||
let unsendable = Python::with_gil(|py| {
|
let dropped = Arc::new(AtomicBool::new(false));
|
||||||
let dropped = dropped.clone();
|
|
||||||
|
|
||||||
Py::new(py, Unsendable { dropped }).unwrap()
|
let unsendable = Py::new(
|
||||||
});
|
py,
|
||||||
|
Unsendable {
|
||||||
|
dropped: dropped.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
spawn(move || {
|
py.allow_threads(|| {
|
||||||
Python::with_gil(move |_py| {
|
spawn(move || {
|
||||||
drop(unsendable);
|
Python::with_gil(move |_py| {
|
||||||
|
drop(unsendable);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.join()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!dropped.load(Ordering::SeqCst));
|
assert!(!dropped.load(Ordering::SeqCst));
|
||||||
|
|
||||||
|
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
|
||||||
|
assert_eq!(err.to_string(), "RuntimeError: test_class_basics::drop_unsendable_elsewhere::Unsendable is unsendbale, but is dropped on another thread!");
|
||||||
|
assert!(object.is_none(py));
|
||||||
|
|
||||||
|
capture.borrow_mut(py).uninstall(py);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,29 +100,11 @@ fn test_exception_nosegfault() {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(Py_3_8)]
|
#[cfg(Py_3_8)]
|
||||||
fn test_write_unraisable() {
|
fn test_write_unraisable() {
|
||||||
|
use common::UnraisableCapture;
|
||||||
use pyo3::{exceptions::PyRuntimeError, ffi, AsPyPointer};
|
use pyo3::{exceptions::PyRuntimeError, ffi, AsPyPointer};
|
||||||
|
|
||||||
#[pyclass]
|
|
||||||
struct UnraisableCapture {
|
|
||||||
capture: Option<(PyErr, PyObject)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl UnraisableCapture {
|
|
||||||
fn hook(&mut self, unraisable: &PyAny) {
|
|
||||||
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
|
|
||||||
let instance = unraisable.getattr("object").unwrap();
|
|
||||||
self.capture = Some((err, instance.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
let sys = py.import("sys").unwrap();
|
let capture = UnraisableCapture::install(py);
|
||||||
let old_hook = sys.getattr("unraisablehook").unwrap();
|
|
||||||
let capture = Py::new(py, UnraisableCapture { capture: None }).unwrap();
|
|
||||||
|
|
||||||
sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(capture.borrow(py).capture.is_none());
|
assert!(capture.borrow(py).capture.is_none());
|
||||||
|
|
||||||
|
@ -140,6 +122,6 @@ fn test_write_unraisable() {
|
||||||
assert_eq!(err.to_string(), "RuntimeError: bar");
|
assert_eq!(err.to_string(), "RuntimeError: bar");
|
||||||
assert!(object.as_ptr() == unsafe { ffi::Py_NotImplemented() });
|
assert!(object.as_ptr() == unsafe { ffi::Py_NotImplemented() });
|
||||||
|
|
||||||
sys.setattr("unraisablehook", old_hook).unwrap();
|
capture.borrow_mut(py).uninstall(py);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue