Skip to content

Commit 999ee8a

Browse files
authored
ci: enable more tests on 3.14t (#5524)
* ci: enable more tests on 3.14t * simplify implementation of `UnraisableCapture` * fix imports on 3.14t * force object cleanup on 3.14t
1 parent 8f669e7 commit 999ee8a

File tree

5 files changed

+132
-83
lines changed

5 files changed

+132
-83
lines changed

tests/test_buffer_protocol.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ fn test_buffer_referenced() {
9595
}
9696

9797
#[test]
98-
#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8
98+
#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8
9999
fn test_releasebuffer_unraisable_error() {
100100
use pyo3::exceptions::PyValueError;
101101
use test_utils::UnraisableCapture;
@@ -120,20 +120,20 @@ fn test_releasebuffer_unraisable_error() {
120120
}
121121

122122
Python::attach(|py| {
123-
let capture = UnraisableCapture::install(py);
124-
125123
let instance = Py::new(py, ReleaseBufferError {}).unwrap();
126-
let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap();
127124

128-
assert!(capture.borrow(py).capture.is_none());
125+
let (err, object) = UnraisableCapture::enter(py, |capture| {
126+
let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap();
127+
128+
assert!(capture.take_capture().is_none());
129129

130-
py_assert!(py, *env, "bytes(ob) == b'hello world'");
130+
py_assert!(py, *env, "bytes(ob) == b'hello world'");
131+
132+
capture.take_capture().unwrap()
133+
});
131134

132-
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
133135
assert_eq!(err.to_string(), "ValueError: oh dear");
134136
assert!(object.is(&instance));
135-
136-
capture.borrow_mut(py).uninstall(py);
137137
});
138138
}
139139

tests/test_class_attributes.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ fn recursive_class_attributes() {
151151
}
152152

153153
#[test]
154-
#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8
154+
#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8
155155
fn test_fallible_class_attribute() {
156156
use pyo3::exceptions::PyValueError;
157157
use test_utils::UnraisableCapture;
@@ -168,29 +168,31 @@ fn test_fallible_class_attribute() {
168168
}
169169

170170
Python::attach(|py| {
171-
let capture = UnraisableCapture::install(py);
172-
assert!(std::panic::catch_unwind(|| py.get_type::<BrokenClass>()).is_err());
171+
let (err, object) = UnraisableCapture::enter(py, |capture| {
172+
// Accessing the type will attempt to initialize the class attributes
173+
assert!(std::panic::catch_unwind(|| py.get_type::<BrokenClass>()).is_err());
173174

174-
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
175-
assert!(object.is_none(py));
175+
capture.take_capture().unwrap()
176+
});
176177

178+
assert!(object.is_none());
177179
assert_eq!(
178180
err.to_string(),
179181
"RuntimeError: An error occurred while initializing class BrokenClass"
180182
);
183+
181184
let cause = err.cause(py).unwrap();
182185
assert_eq!(
183186
cause.to_string(),
184187
"RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`"
185188
);
189+
186190
let cause = cause.cause(py).unwrap();
187191
assert_eq!(
188192
cause.to_string(),
189193
"ValueError: failed to create class attribute"
190194
);
191195
assert!(cause.cause(py).is_none());
192-
193-
capture.borrow_mut(py).uninstall(py);
194196
});
195197
}
196198

tests/test_class_basics.rs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#![cfg(feature = "macros")]
22

3+
#[cfg(Py_3_8)]
4+
use pyo3::ffi::c_str;
35
use pyo3::prelude::*;
46
use pyo3::types::PyType;
57
use pyo3::{py_run, PyClass};
@@ -615,7 +617,7 @@ fn access_frozen_class_without_gil() {
615617
}
616618

617619
#[test]
618-
#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8
620+
#[cfg(Py_3_8)]
619621
#[cfg_attr(target_arch = "wasm32", ignore)]
620622
fn drop_unsendable_elsewhere() {
621623
use std::sync::{
@@ -637,35 +639,40 @@ fn drop_unsendable_elsewhere() {
637639
}
638640

639641
Python::attach(|py| {
640-
let capture = UnraisableCapture::install(py);
642+
let (err, object) = UnraisableCapture::enter(py, |capture| {
643+
let dropped = Arc::new(AtomicBool::new(false));
641644

642-
let dropped = Arc::new(AtomicBool::new(false));
643-
644-
let unsendable = Py::new(
645-
py,
646-
Unsendable {
647-
dropped: dropped.clone(),
648-
},
649-
)
650-
.unwrap();
651-
652-
py.detach(|| {
653-
spawn(move || {
654-
Python::attach(move |_py| {
655-
drop(unsendable);
656-
});
657-
})
658-
.join()
645+
let unsendable = Py::new(
646+
py,
647+
Unsendable {
648+
dropped: dropped.clone(),
649+
},
650+
)
659651
.unwrap();
660-
});
661652

662-
assert!(!dropped.load(Ordering::SeqCst));
653+
py.detach(|| {
654+
spawn(move || {
655+
Python::attach(move |py| {
656+
drop(unsendable);
657+
// On the free-threaded build, dropping an object on its non-origin thread
658+
// will not immediately drop it because the refcounts need to be merged.
659+
//
660+
// Force GC to ensure the drop happens now on the wrong thread.
661+
py.run(c_str!("import gc; gc.collect()"), None, None)
662+
.unwrap();
663+
});
664+
})
665+
.join()
666+
.unwrap();
667+
});
668+
669+
assert!(!dropped.load(Ordering::SeqCst));
670+
671+
capture.take_capture().unwrap()
672+
});
663673

664-
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
665674
assert_eq!(err.to_string(), "RuntimeError: test_class_basics::drop_unsendable_elsewhere::Unsendable is unsendable, but is being dropped on another thread");
666-
assert!(object.is_none(py));
667-
668-
capture.borrow_mut(py).uninstall(py);
675+
assert!(object.is_none());
669676
});
670677
}
671678

tests/test_exceptions.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,31 +98,28 @@ fn test_exception_nosegfault() {
9898
}
9999

100100
#[test]
101-
#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))]
101+
#[cfg(Py_3_8)]
102102
fn test_write_unraisable() {
103-
use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented};
104-
use std::ptr;
103+
use pyo3::{exceptions::PyRuntimeError, types::PyNotImplemented};
105104
use test_utils::UnraisableCapture;
106105

107106
Python::attach(|py| {
108-
let capture = UnraisableCapture::install(py);
107+
UnraisableCapture::enter(py, |capture| {
108+
let err = PyRuntimeError::new_err("foo");
109+
err.write_unraisable(py, None);
109110

110-
assert!(capture.borrow(py).capture.is_none());
111+
let (err, object) = capture.take_capture().unwrap();
111112

112-
let err = PyRuntimeError::new_err("foo");
113-
err.write_unraisable(py, None);
113+
assert_eq!(err.to_string(), "RuntimeError: foo");
114+
assert!(object.is_none());
114115

115-
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
116-
assert_eq!(err.to_string(), "RuntimeError: foo");
117-
assert!(object.is_none(py));
116+
let err = PyRuntimeError::new_err("bar");
117+
err.write_unraisable(py, Some(&PyNotImplemented::get(py)));
118118

119-
let err = PyRuntimeError::new_err("bar");
120-
err.write_unraisable(py, Some(&PyNotImplemented::get(py)));
119+
let (err, object) = capture.take_capture().unwrap();
121120

122-
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
123-
assert_eq!(err.to_string(), "RuntimeError: bar");
124-
assert!(unsafe { ptr::eq(object.as_ptr(), ffi::Py_NotImplemented()) });
125-
126-
capture.borrow_mut(py).uninstall(py);
121+
assert_eq!(err.to_string(), "RuntimeError: bar");
122+
assert!(object.is(PyNotImplemented::get(py)));
123+
});
127124
});
128125
}

tests/test_utils/mod.rs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ mod inner {
1818

1919
use pyo3::prelude::*;
2020

21+
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), all(feature = "macros", Py_3_8)))]
2122
use pyo3::sync::MutexExt;
2223
use pyo3::types::{IntoPyDict, PyList};
2324

25+
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), all(feature = "macros", Py_3_8)))]
2426
use std::sync::{Mutex, PoisonError};
2527

2628
use uuid::Uuid;
@@ -118,49 +120,88 @@ mod inner {
118120
}
119121

120122
// sys.unraisablehook not available until Python 3.8
121-
#[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))]
122-
#[pyclass(crate = "pyo3")]
123-
pub struct UnraisableCapture {
124-
pub capture: Option<(PyErr, Py<PyAny>)>,
125-
old_hook: Option<Py<PyAny>>,
123+
#[cfg(all(feature = "macros", Py_3_8))]
124+
pub struct UnraisableCapture<'py> {
125+
hook: Bound<'py, UnraisableCaptureHook>,
126126
}
127127

128-
#[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))]
128+
#[cfg(all(feature = "macros", Py_3_8))]
129+
impl<'py> UnraisableCapture<'py> {
130+
/// Runs the closure `f` with a custom sys.unraisablehook installed.
131+
///
132+
/// `f`
133+
pub fn enter<R>(py: Python<'py>, f: impl FnOnce(&Self) -> R) -> R {
134+
// unraisablehook is a global, so only one thread can be using this struct at a time.
135+
static UNRAISABLE_HOOK_MUTEX: Mutex<()> = Mutex::new(());
136+
137+
// NB this is best-effort, other tests could always modify sys.unraisablehook directly.
138+
let mutex_guard = UNRAISABLE_HOOK_MUTEX
139+
.lock_py_attached(py)
140+
.unwrap_or_else(PoisonError::into_inner);
141+
142+
let guard = Self {
143+
hook: UnraisableCaptureHook::install(py),
144+
};
145+
146+
let result = f(&guard);
147+
148+
drop(guard);
149+
drop(mutex_guard);
150+
151+
result
152+
}
153+
154+
/// Takes the captured unraisable error, if any.
155+
pub fn take_capture(&self) -> Option<(PyErr, Bound<'py, PyAny>)> {
156+
let mut guard = self.hook.get().capture.lock().unwrap();
157+
guard.take().map(|(e, o)| (e, o.into_bound(self.hook.py())))
158+
}
159+
}
160+
161+
#[cfg(all(feature = "macros", Py_3_8))]
162+
impl Drop for UnraisableCapture<'_> {
163+
fn drop(&mut self) {
164+
let py = self.hook.py();
165+
self.hook.get().uninstall(py);
166+
}
167+
}
168+
169+
#[cfg(all(feature = "macros", Py_3_8))]
170+
#[pyclass(crate = "pyo3", frozen)]
171+
struct UnraisableCaptureHook {
172+
pub capture: Mutex<Option<(PyErr, Py<PyAny>)>>,
173+
old_hook: Py<PyAny>,
174+
}
175+
176+
#[cfg(all(feature = "macros", Py_3_8))]
129177
#[pymethods(crate = "pyo3")]
130-
impl UnraisableCapture {
131-
pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) {
178+
impl UnraisableCaptureHook {
179+
pub fn hook(&self, unraisable: Bound<'_, PyAny>) {
132180
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
133181
let instance = unraisable.getattr("object").unwrap();
134-
self.capture = Some((err, instance.into()));
182+
self.capture.lock().unwrap().replace((err, instance.into()));
135183
}
136184
}
137185

138-
#[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))]
139-
impl UnraisableCapture {
140-
pub fn install(py: Python<'_>) -> Py<Self> {
186+
#[cfg(all(feature = "macros", Py_3_8))]
187+
impl UnraisableCaptureHook {
188+
fn install(py: Python<'_>) -> Bound<'_, Self> {
141189
let sys = py.import("sys").unwrap();
190+
142191
let old_hook = sys.getattr("unraisablehook").unwrap().into();
192+
let capture = Mutex::new(None);
143193

144-
let capture = Py::new(
145-
py,
146-
UnraisableCapture {
147-
capture: None,
148-
old_hook: Some(old_hook),
149-
},
150-
)
151-
.unwrap();
194+
let capture = Bound::new(py, UnraisableCaptureHook { capture, old_hook }).unwrap();
152195

153-
sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap())
196+
sys.setattr("unraisablehook", capture.getattr("hook").unwrap())
154197
.unwrap();
155198

156199
capture
157200
}
158201

159-
pub fn uninstall(&mut self, py: Python<'_>) {
160-
let old_hook = self.old_hook.take().unwrap();
161-
202+
fn uninstall(&self, py: Python<'_>) {
162203
let sys = py.import("sys").unwrap();
163-
sys.setattr("unraisablehook", old_hook).unwrap();
204+
sys.setattr("unraisablehook", &self.old_hook).unwrap();
164205
}
165206
}
166207

@@ -170,6 +211,7 @@ mod inner {
170211

171212
/// catch_warnings is not thread-safe, so only one thread can be using this struct at
172213
/// a time.
214+
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))] // Python 3.14t has thread-safe catch_warnings
173215
static CATCH_WARNINGS_MUTEX: Mutex<()> = Mutex::new(());
174216

175217
impl<'py> CatchWarnings<'py> {
@@ -178,6 +220,7 @@ mod inner {
178220
f: impl FnOnce(&Bound<'py, PyList>) -> PyResult<R>,
179221
) -> PyResult<R> {
180222
// NB this is best-effort, other tests could always call the warnings API directly.
223+
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))]
181224
let _mutex_guard = CATCH_WARNINGS_MUTEX
182225
.lock_py_attached(py)
183226
.unwrap_or_else(PoisonError::into_inner);

0 commit comments

Comments
 (0)