add assert_warnings test helper

This commit is contained in:
David Hewitt 2023-09-23 13:14:18 +01:00
parent aeb7a958dc
commit c8f82be32c
10 changed files with 166 additions and 93 deletions

View File

@ -822,6 +822,7 @@ impl_signed_integer!(isize);
mod tests { mod tests {
use super::PyErrState; use super::PyErrState;
use crate::exceptions::{self, PyTypeError, PyValueError}; use crate::exceptions::{self, PyTypeError, PyValueError};
use crate::tests::common::CatchWarnings;
use crate::{PyErr, PyTypeInfo, Python}; use crate::{PyErr, PyTypeInfo, Python};
#[test] #[test]
@ -1019,11 +1020,12 @@ mod tests {
let warnings = py.import("warnings").unwrap(); let warnings = py.import("warnings").unwrap();
warnings.call_method0("resetwarnings").unwrap(); warnings.call_method0("resetwarnings").unwrap();
// First, test with ignoring the warning // First, test the warning is emitted
warnings assert_warnings!(
.call_method1("simplefilter", ("ignore", cls)) py,
.unwrap(); { PyErr::warn(py, cls, "I am warning you", 0).unwrap() },
PyErr::warn(py, cls, "I am warning you", 0).unwrap(); [(exceptions::PyUserWarning, "I am warning you")]
);
// Test with raising // Test with raising
warnings warnings
@ -1031,17 +1033,18 @@ mod tests {
.unwrap(); .unwrap();
PyErr::warn(py, cls, "I am warning you", 0).unwrap_err(); PyErr::warn(py, cls, "I am warning you", 0).unwrap_err();
// Test with explicit module and specific filter // Test with error for an explicit module
warnings.call_method0("resetwarnings").unwrap(); warnings.call_method0("resetwarnings").unwrap();
warnings
.call_method1("simplefilter", ("ignore", cls))
.unwrap();
warnings warnings
.call_method1("filterwarnings", ("error", "", cls, "pyo3test")) .call_method1("filterwarnings", ("error", "", cls, "pyo3test"))
.unwrap(); .unwrap();
// This has the wrong module and will not raise // This has the wrong module and will not raise, just be emitted
PyErr::warn(py, cls, "I am warning you", 0).unwrap(); assert_warnings!(
py,
{ PyErr::warn(py, cls, "I am warning you", 0).unwrap() },
[(exceptions::PyUserWarning, "I am warning you")]
);
let err = let err =
PyErr::warn_explicit(py, cls, "I am warning you", "pyo3test.py", 427, None, None) PyErr::warn_explicit(py, cls, "I am warning you", "pyo3test.py", 427, None, None)

View File

@ -386,6 +386,12 @@ pub use {
#[doc(hidden)] #[doc(hidden)]
pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `multiple-pymethods`. pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `multiple-pymethods`.
/// Tests and helpers which reside inside PyO3's main library. Declared first so that macros
/// are available in unit tests.
#[cfg(test)]
#[macro_use]
mod tests;
#[macro_use] #[macro_use]
mod internal_tricks; mod internal_tricks;
@ -447,11 +453,6 @@ pub use pyo3_macros::pyclass;
#[macro_use] #[macro_use]
mod macros; mod macros;
/// Test macro hygiene - this is in the crate since we won't have
/// `pyo3` available in the crate root.
#[cfg(all(test, feature = "macros"))]
mod test_hygiene;
#[cfg(feature = "experimental-inspect")] #[cfg(feature = "experimental-inspect")]
pub mod inspect; pub mod inspect;

10
src/tests/mod.rs Normal file
View File

@ -0,0 +1,10 @@
#[macro_use]
pub(crate) mod common {
use crate as pyo3;
include!("../../tests/common.rs");
}
/// Test macro hygiene - this is in the crate since we won't have
/// `pyo3` available in the crate root.
#[cfg(all(test, feature = "macros"))]
mod hygiene;

View File

@ -1,20 +1,30 @@
//! Some common macros for tests // the inner mod enables the #![allow(dead_code)] to
// be applied - `test_utils.rs` uses `include!` to pull in this file
#[cfg(all(feature = "macros", Py_3_8))] /// Common macros and helpers for tests
use pyo3::prelude::*; #[allow(dead_code)] // many tests do not use the complete set of functionality offered here
#[macro_use]
mod inner {
#[macro_export] #[allow(unused_imports)] // pulls in `use crate as pyo3` in `test_utils.rs`
macro_rules! py_assert { use super::*;
use pyo3::prelude::*;
use pyo3::types::{IntoPyDict, PyList};
#[macro_export]
macro_rules! py_assert {
($py:expr, $($val:ident)+, $assertion:literal) => { ($py:expr, $($val:ident)+, $assertion:literal) => {
pyo3::py_run!($py, $($val)+, concat!("assert ", $assertion)) pyo3::py_run!($py, $($val)+, concat!("assert ", $assertion))
}; };
($py:expr, *$dict:expr, $assertion:literal) => { ($py:expr, *$dict:expr, $assertion:literal) => {
pyo3::py_run!($py, *$dict, concat!("assert ", $assertion)) pyo3::py_run!($py, *$dict, concat!("assert ", $assertion))
}; };
} }
#[macro_export] #[macro_export]
macro_rules! py_expect_exception { macro_rules! py_expect_exception {
// Case1: idents & no err_msg // Case1: idents & no err_msg
($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{ ($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{
use pyo3::types::IntoPyDict; use pyo3::types::IntoPyDict;
@ -43,28 +53,28 @@ macro_rules! py_expect_exception {
assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg)); assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg));
err err
}}; }};
} }
// sys.unraisablehook not available until Python 3.8 // sys.unraisablehook not available until Python 3.8
#[cfg(all(feature = "macros", Py_3_8))] #[cfg(all(feature = "macros", Py_3_8))]
#[pyclass] #[pyclass(crate = "pyo3")]
pub struct UnraisableCapture { pub struct UnraisableCapture {
pub capture: Option<(PyErr, PyObject)>, pub capture: Option<(PyErr, PyObject)>,
old_hook: Option<PyObject>, old_hook: Option<PyObject>,
} }
#[cfg(all(feature = "macros", Py_3_8))] #[cfg(all(feature = "macros", Py_3_8))]
#[pymethods] #[pymethods(crate = "pyo3")]
impl UnraisableCapture { impl UnraisableCapture {
pub fn hook(&mut self, unraisable: &PyAny) { pub fn hook(&mut self, unraisable: &PyAny) {
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap()); let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
let instance = unraisable.getattr("object").unwrap(); let instance = unraisable.getattr("object").unwrap();
self.capture = Some((err, instance.into())); self.capture = Some((err, instance.into()));
} }
} }
#[cfg(all(feature = "macros", Py_3_8))] #[cfg(all(feature = "macros", Py_3_8))]
impl UnraisableCapture { impl UnraisableCapture {
pub fn install(py: Python<'_>) -> Py<Self> { pub fn install(py: Python<'_>) -> Py<Self> {
let sys = py.import("sys").unwrap(); let sys = py.import("sys").unwrap();
let old_hook = sys.getattr("unraisablehook").unwrap().into(); let old_hook = sys.getattr("unraisablehook").unwrap().into();
@ -90,4 +100,53 @@ impl UnraisableCapture {
let sys = py.import("sys").unwrap(); let sys = py.import("sys").unwrap();
sys.setattr("unraisablehook", old_hook).unwrap(); sys.setattr("unraisablehook", old_hook).unwrap();
} }
}
pub struct CatchWarnings<'py> {
catch_warnings: &'py PyAny,
}
impl<'py> CatchWarnings<'py> {
pub fn enter<R>(py: Python<'py>, f: impl FnOnce(&PyList) -> PyResult<R>) -> PyResult<R> {
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__")?.extract()?;
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_export]
macro_rules! assert_warnings {
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
CatchWarnings::enter($py, |w| {
$body;
let expected_warnings = [$((<$category>::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();
}};
}
} }
pub use inner::*;