292 lines
11 KiB
Rust
292 lines
11 KiB
Rust
// Common macros and helpers for tests
|
|
//
|
|
// This file is used in two different ways, which makes it a bit of a pain to build:
|
|
// - as a module `include!`-ed from `src/test_utils.rs`
|
|
// - as a module `mod test_utils` in various integration tests
|
|
//
|
|
// the inner mod enables the #![allow(dead_code)] to
|
|
// be applied - `src/test_utils.rs` uses `include!` to pull in this file
|
|
|
|
#[allow(
|
|
dead_code,
|
|
unused_macros,
|
|
reason = "many tests do not use the complete set of functionality offered here"
|
|
)]
|
|
#[allow(missing_docs, reason = "only used in tests")]
|
|
#[macro_use]
|
|
mod inner {
|
|
|
|
#[allow(
|
|
unused_imports,
|
|
reason = "pulls in `use crate as pyo3` from src/test_utils.rs, no function in integration tests"
|
|
)]
|
|
use super::*;
|
|
|
|
use pyo3::prelude::*;
|
|
|
|
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), feature = "macros"))]
|
|
use pyo3::sync::MutexExt;
|
|
use pyo3::types::{IntoPyDict, PyList};
|
|
|
|
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), feature = "macros"))]
|
|
use std::sync::{Mutex, PoisonError};
|
|
|
|
use uuid::Uuid;
|
|
|
|
#[macro_export]
|
|
macro_rules! py_assert {
|
|
($py:expr, $($val:ident)+, $assertion:literal) => {
|
|
pyo3::py_run!($py, $($val)+, concat!("assert ", $assertion))
|
|
};
|
|
($py:expr, *$dict:expr, $assertion:literal) => {
|
|
pyo3::py_run!($py, *$dict, concat!("assert ", $assertion))
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! assert_py_eq {
|
|
($val:expr, $expected:expr) => {
|
|
assert!($val.eq($expected).unwrap());
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! py_expect_exception {
|
|
// Case1: idents & no err_msg
|
|
($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{
|
|
use pyo3::types::IntoPyDict;
|
|
use pyo3::BoundObject;
|
|
let d = [$((stringify!($val), (&$val).into_pyobject($py).unwrap().into_any().into_bound()),)+].into_py_dict($py).unwrap();
|
|
py_expect_exception!($py, *d, $code, $err)
|
|
}};
|
|
// Case2: dict & no err_msg
|
|
($py:expr, *$dict:expr, $code:expr, $err:ident) => {{
|
|
let res = $py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed()));
|
|
let err = res.expect_err(&format!("Did not raise {}", stringify!($err)));
|
|
if !err.matches($py, $py.get_type::<pyo3::exceptions::$err>()).unwrap() {
|
|
panic!("Expected {} but got {:?}", stringify!($err), err)
|
|
}
|
|
err
|
|
}};
|
|
// Case3: idents & err_msg
|
|
($py:expr, $($val:ident)+, $code:expr, $err:ident, $err_msg:literal $(,$notes:literal)*) => {{
|
|
let err = py_expect_exception!($py, $($val)+, $code, $err);
|
|
// Suppose that the error message looks like 'TypeError: ~'
|
|
assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg));
|
|
if err.value($py).hasattr("add_note").unwrap() {
|
|
let notes = err.value($py).getattr("__notes__").map(|n| n.cast_into::<pyo3::types::PyList>().unwrap()).unwrap_or_else(|_| pyo3::types::PyList::empty($py));
|
|
let mut _notes_iter = notes.iter();
|
|
$(
|
|
assert_eq!(_notes_iter.next().as_ref().map(|v| v.extract::<std::borrow::Cow<'_, str>>().unwrap()).as_deref(), Some($notes));
|
|
)*
|
|
}
|
|
err
|
|
}};
|
|
// Case4: dict & err_msg
|
|
($py:expr, *$dict:expr, $code:expr, $err:ident, $err_msg:literal) => {{
|
|
let err = py_expect_exception!($py, *$dict, $code, $err);
|
|
assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg));
|
|
err
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! py_expect_warning {
|
|
($py:expr, $($val:ident)+, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{
|
|
use pyo3::types::IntoPyDict;
|
|
let d = [$((stringify!($val), ($val.as_ref() as &Bound<'_, PyAny>).into_pyobject($py).expect("Failed to create test dict element")),)+].into_py_dict($py).expect("Failed to create test dict");
|
|
py_expect_warning!($py, *d, $code, [$(($warning_msg, $warning_category)),+])
|
|
}};
|
|
($py:expr, *$dict:expr, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{
|
|
$crate::test_utils::CatchWarnings::enter($py, |warning_record| {
|
|
$py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code");
|
|
let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object($py))),+];
|
|
|
|
assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len());
|
|
|
|
for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) {
|
|
let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string();
|
|
let actual_category = warning.getattr("category").unwrap();
|
|
|
|
assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg);
|
|
assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category);
|
|
}
|
|
|
|
Ok(())
|
|
}).expect("failed to test warnings");
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! py_expect_warning_for_fn {
|
|
($fn:ident, $($val:ident)+, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {
|
|
pyo3::Python::attach(|py| {
|
|
let f = wrap_pyfunction!($fn)(py).unwrap();
|
|
py_expect_warning!(
|
|
py,
|
|
f,
|
|
"f()",
|
|
[$(($warning_msg, $warning_category)),+]
|
|
);
|
|
});
|
|
};
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
pub struct UnraisableCapture<'py> {
|
|
hook: Bound<'py, UnraisableCaptureHook>,
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
impl<'py> UnraisableCapture<'py> {
|
|
/// Runs the closure `f` with a custom sys.unraisablehook installed.
|
|
///
|
|
/// `f`
|
|
pub fn enter<R>(py: Python<'py>, f: impl FnOnce(&Self) -> R) -> R {
|
|
// unraisablehook is a global, so only one thread can be using this struct at a time.
|
|
static UNRAISABLE_HOOK_MUTEX: Mutex<()> = Mutex::new(());
|
|
|
|
// NB this is best-effort, other tests could always modify sys.unraisablehook directly.
|
|
let mutex_guard = UNRAISABLE_HOOK_MUTEX
|
|
.lock_py_attached(py)
|
|
.unwrap_or_else(PoisonError::into_inner);
|
|
|
|
let guard = Self {
|
|
hook: UnraisableCaptureHook::install(py),
|
|
};
|
|
|
|
let result = f(&guard);
|
|
|
|
drop(guard);
|
|
drop(mutex_guard);
|
|
|
|
result
|
|
}
|
|
|
|
/// Takes the captured unraisable error, if any.
|
|
pub fn take_capture(&self) -> Option<(PyErr, Bound<'py, PyAny>)> {
|
|
let mut guard = self.hook.get().capture.lock().unwrap();
|
|
guard.take().map(|(e, o)| (e, o.into_bound(self.hook.py())))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
impl Drop for UnraisableCapture<'_> {
|
|
fn drop(&mut self) {
|
|
let py = self.hook.py();
|
|
self.hook.get().uninstall(py);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
#[pyclass(crate = "pyo3", frozen)]
|
|
struct UnraisableCaptureHook {
|
|
pub capture: Mutex<Option<(PyErr, Py<PyAny>)>>,
|
|
old_hook: Py<PyAny>,
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
#[pymethods(crate = "pyo3")]
|
|
impl UnraisableCaptureHook {
|
|
pub fn hook(&self, unraisable: Bound<'_, PyAny>) {
|
|
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
|
|
let instance = unraisable.getattr("object").unwrap();
|
|
self.capture.lock().unwrap().replace((err, instance.into()));
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "macros")]
|
|
impl UnraisableCaptureHook {
|
|
fn install(py: Python<'_>) -> Bound<'_, Self> {
|
|
let sys = py.import("sys").unwrap();
|
|
|
|
let old_hook = sys.getattr("unraisablehook").unwrap().into();
|
|
let capture = Mutex::new(None);
|
|
|
|
let capture = Bound::new(py, UnraisableCaptureHook { capture, old_hook }).unwrap();
|
|
|
|
sys.setattr("unraisablehook", capture.getattr("hook").unwrap())
|
|
.unwrap();
|
|
|
|
capture
|
|
}
|
|
|
|
fn uninstall(&self, py: Python<'_>) {
|
|
let sys = py.import("sys").unwrap();
|
|
sys.setattr("unraisablehook", &self.old_hook).unwrap();
|
|
}
|
|
}
|
|
|
|
pub struct CatchWarnings<'py> {
|
|
catch_warnings: Bound<'py, PyAny>,
|
|
}
|
|
|
|
/// catch_warnings is not thread-safe, so only one thread can be using this struct at
|
|
/// a time.
|
|
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))] // Python 3.14t has thread-safe catch_warnings
|
|
static CATCH_WARNINGS_MUTEX: Mutex<()> = Mutex::new(());
|
|
|
|
impl<'py> CatchWarnings<'py> {
|
|
pub fn enter<R>(
|
|
py: Python<'py>,
|
|
f: impl FnOnce(&Bound<'py, PyList>) -> PyResult<R>,
|
|
) -> PyResult<R> {
|
|
// NB this is best-effort, other tests could always call the warnings API directly.
|
|
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))]
|
|
let _mutex_guard = CATCH_WARNINGS_MUTEX
|
|
.lock_py_attached(py)
|
|
.unwrap_or_else(PoisonError::into_inner);
|
|
let warnings = py.import("warnings")?;
|
|
let kwargs = [("record", true)].into_py_dict(py)?;
|
|
let catch_warnings = warnings
|
|
.getattr("catch_warnings")?
|
|
.call((), Some(&kwargs))?;
|
|
let list = catch_warnings.call_method0("__enter__")?.cast_into()?;
|
|
let _guard = Self { catch_warnings };
|
|
f(&list)
|
|
}
|
|
}
|
|
|
|
impl Drop for CatchWarnings<'_> {
|
|
fn drop(&mut self) {
|
|
let py = self.catch_warnings.py();
|
|
self.catch_warnings
|
|
.call_method1("__exit__", (py.None(), py.None(), py.None()))
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
macro_rules! assert_warnings {
|
|
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
|
|
$crate::test_utils::CatchWarnings::enter($py, |w| {
|
|
use $crate::types::{PyListMethods, PyStringMethods};
|
|
$body;
|
|
let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+];
|
|
assert_eq!(w.len(), expected_warnings.len());
|
|
for (warning, (category, message)) in w.iter().zip(expected_warnings) {
|
|
|
|
assert!(warning.getattr("category").unwrap().is(&category));
|
|
assert_eq!(
|
|
warning.getattr("message").unwrap().str().unwrap().to_string_lossy(),
|
|
message
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
}};
|
|
}
|
|
|
|
#[allow(unused_imports, reason = "not all tests use this macro")]
|
|
pub(crate) use assert_warnings;
|
|
|
|
pub fn generate_unique_module_name(base: &str) -> std::ffi::CString {
|
|
let uuid = Uuid::new_v4().simple().to_string();
|
|
std::ffi::CString::new(format!("{base}_{uuid}")).unwrap()
|
|
}
|
|
}
|
|
|
|
#[allow(unused_imports, reason = "some integration tests use just the macros")]
|
|
pub use inner::*;
|