Merge pull request #3470 from davidhewitt/tests-capture-warnings
add `assert_warnings` test helper
This commit is contained in:
commit
673c45aac3
|
@ -821,6 +821,7 @@ impl_signed_integer!(isize);
|
|||
mod tests {
|
||||
use super::PyErrState;
|
||||
use crate::exceptions::{self, PyTypeError, PyValueError};
|
||||
use crate::tests::common::CatchWarnings;
|
||||
use crate::{PyErr, PyTypeInfo, Python};
|
||||
|
||||
#[test]
|
||||
|
@ -1018,11 +1019,12 @@ mod tests {
|
|||
let warnings = py.import("warnings").unwrap();
|
||||
warnings.call_method0("resetwarnings").unwrap();
|
||||
|
||||
// First, test with ignoring the warning
|
||||
warnings
|
||||
.call_method1("simplefilter", ("ignore", cls))
|
||||
.unwrap();
|
||||
PyErr::warn(py, cls, "I am warning you", 0).unwrap();
|
||||
// First, test the warning is emitted
|
||||
assert_warnings!(
|
||||
py,
|
||||
{ PyErr::warn(py, cls, "I am warning you", 0).unwrap() },
|
||||
[(exceptions::PyUserWarning, "I am warning you")]
|
||||
);
|
||||
|
||||
// Test with raising
|
||||
warnings
|
||||
|
@ -1030,17 +1032,18 @@ mod tests {
|
|||
.unwrap();
|
||||
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_method1("simplefilter", ("ignore", cls))
|
||||
.unwrap();
|
||||
warnings
|
||||
.call_method1("filterwarnings", ("error", "", cls, "pyo3test"))
|
||||
.unwrap();
|
||||
|
||||
// This has the wrong module and will not raise
|
||||
PyErr::warn(py, cls, "I am warning you", 0).unwrap();
|
||||
// This has the wrong module and will not raise, just be emitted
|
||||
assert_warnings!(
|
||||
py,
|
||||
{ PyErr::warn(py, cls, "I am warning you", 0).unwrap() },
|
||||
[(exceptions::PyUserWarning, "I am warning you")]
|
||||
);
|
||||
|
||||
let err =
|
||||
PyErr::warn_explicit(py, cls, "I am warning you", "pyo3test.py", 427, None, None)
|
||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -386,6 +386,12 @@ pub use {
|
|||
#[doc(hidden)]
|
||||
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]
|
||||
mod internal_tricks;
|
||||
|
||||
|
@ -447,11 +453,6 @@ pub use pyo3_macros::pyclass;
|
|||
#[macro_use]
|
||||
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")]
|
||||
pub mod inspect;
|
||||
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
// the inner mod enables the #![allow(dead_code)] to
|
||||
// be applied - `test_utils.rs` uses `include!` to pull in this file
|
||||
|
||||
/// Common macros and helpers for tests
|
||||
#[allow(dead_code)] // many tests do not use the complete set of functionality offered here
|
||||
#[macro_use]
|
||||
mod inner {
|
||||
|
||||
#[allow(unused_imports)] // pulls in `use crate as pyo3` in `test_utils.rs`
|
||||
use super::*;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use pyo3::types::{IntoPyDict, PyList};
|
||||
|
||||
#[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! py_expect_exception {
|
||||
// Case1: idents & no err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{
|
||||
use pyo3::types::IntoPyDict;
|
||||
let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py);
|
||||
py_expect_exception!($py, *d, $code, $err)
|
||||
}};
|
||||
// Case2: dict & no err_msg
|
||||
($py:expr, *$dict:expr, $code:expr, $err:ident) => {{
|
||||
let res = $py.run($code, None, Some($dict));
|
||||
let err = res.expect_err(&format!("Did not raise {}", stringify!($err)));
|
||||
if !err.matches($py, $py.get_type::<pyo3::exceptions::$err>()) {
|
||||
panic!("Expected {} but got {:?}", stringify!($err), err)
|
||||
}
|
||||
err
|
||||
}};
|
||||
// Case3: idents & err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident, $err_msg: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));
|
||||
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
|
||||
}};
|
||||
}
|
||||
|
||||
// sys.unraisablehook not available until Python 3.8
|
||||
#[cfg(all(feature = "macros", Py_3_8))]
|
||||
#[pyclass(crate = "pyo3")]
|
||||
pub struct UnraisableCapture {
|
||||
pub capture: Option<(PyErr, PyObject)>,
|
||||
old_hook: Option<PyObject>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "macros", Py_3_8))]
|
||||
#[pymethods(crate = "pyo3")]
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
pub(crate) mod common {
|
||||
use crate as pyo3;
|
||||
include!("./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;
|
|
@ -1,93 +0,0 @@
|
|||
//! Some common macros for tests
|
||||
|
||||
#[cfg(all(feature = "macros", Py_3_8))]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[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! py_expect_exception {
|
||||
// Case1: idents & no err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{
|
||||
use pyo3::types::IntoPyDict;
|
||||
let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py);
|
||||
py_expect_exception!($py, *d, $code, $err)
|
||||
}};
|
||||
// Case2: dict & no err_msg
|
||||
($py:expr, *$dict:expr, $code:expr, $err:ident) => {{
|
||||
let res = $py.run($code, None, Some($dict));
|
||||
let err = res.expect_err(&format!("Did not raise {}", stringify!($err)));
|
||||
if !err.matches($py, $py.get_type::<pyo3::exceptions::$err>()) {
|
||||
panic!("Expected {} but got {:?}", stringify!($err), err)
|
||||
}
|
||||
err
|
||||
}};
|
||||
// Case3: idents & err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident, $err_msg: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));
|
||||
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
|
||||
}};
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use pyo3::class::basic::CompareOp;
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -9,6 +9,7 @@ use std::{
|
|||
};
|
||||
|
||||
#[macro_use]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
enum TestGetBufferError {
|
||||
|
|
|
@ -12,6 +12,7 @@ use std::ptr;
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyBytes;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyfunction]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -4,6 +4,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::PyType;
|
||||
use pyo3::{py_run, PyClass};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -4,6 +4,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::ToPyObject;
|
||||
|
||||
#[macro_use]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
// Test default generated __repr__.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::{py_run, wrap_pyfunction};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::fmt;
|
|||
#[cfg(not(target_os = "windows"))]
|
||||
use std::fs::File;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyfunction]
|
||||
|
|
|
@ -5,6 +5,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::{PyDict, PyList, PyString, PyTuple};
|
||||
|
||||
#[macro_use]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
/// Helper function that concatenates the error message from
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::cell::Cell;
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass(freelist = 2)]
|
||||
|
|
|
@ -6,6 +6,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::py_run;
|
||||
use pyo3::types::{IntoPyDict, PyList};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -5,6 +5,7 @@ use pyo3::py_run;
|
|||
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass(subclass)]
|
||||
|
|
|
@ -4,7 +4,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::IntoPyDict;
|
||||
|
||||
#[macro_use]
|
||||
#[path = "common.rs"]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use pyo3::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
macro_rules! make_struct_using_macro {
|
||||
|
|
|
@ -10,6 +10,7 @@ use pyo3::types::PyList;
|
|||
use pyo3::types::PyMapping;
|
||||
use pyo3::types::PySequence;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass(mapping)]
|
||||
|
|
|
@ -5,6 +5,7 @@ use pyo3::py_run;
|
|||
use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType};
|
||||
use pyo3::PyCell;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -4,6 +4,8 @@ use pyo3::prelude::*;
|
|||
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::{IntoPyDict, PyDict, PyTuple};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -4,6 +4,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::PyType;
|
||||
|
||||
#[macro_use]
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -5,6 +5,7 @@ use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType};
|
|||
use pyo3::{prelude::*, py_run, PyCell};
|
||||
use std::{isize, iter};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -11,6 +11,7 @@ use pyo3::types::PyDateTime;
|
|||
use pyo3::types::PyFunction;
|
||||
use pyo3::types::{self, PyCFunction};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyfunction(signature = (arg = true))]
|
||||
|
|
|
@ -6,6 +6,7 @@ use pyo3::types::{PyBytes, PyString};
|
|||
use pyo3::PyCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
/// Assumes it's a file reader or so.
|
||||
|
|
|
@ -6,6 +6,7 @@ use pyo3::{ffi, prelude::*};
|
|||
|
||||
use pyo3::py_run;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -6,6 +6,7 @@ use pyo3::types::IntoPyDict;
|
|||
|
||||
use pyo3::py_run;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyfunction]
|
||||
|
|
|
@ -4,6 +4,7 @@ use pyo3::prelude::*;
|
|||
use pyo3::types::{PyDict, PyTuple};
|
||||
use pyo3::{types::PyType, wrap_pymodule, PyCell};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyTuple};
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
|
@ -6,6 +6,7 @@ use pyo3::{py_run, PyCell};
|
|||
|
||||
use std::fmt;
|
||||
|
||||
#[path = "../src/tests/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[pyclass]
|
||||
|
|
Loading…
Reference in New Issue