diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index f8e52430f4e..197145c0936 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -4,46 +4,66 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::IntoPyDict; +#[path = "../src/internal/macros.rs"] +#[macro_use] +mod macros; // for cfg_select polyfill on MSRV < 1.95 + mod test_utils; /// Macro to generate refcount leak tests for types. /// Ensures that creating and destroying instances doesn't leak references to the type. /// Regression test for issues #1363 and #6223. -#[cfg(not(Py_GIL_DISABLED))] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] macro_rules! assert_type_refcount_stable { // Simple case: type with parameterless constructor ($type_name:ty) => { - assert_type_refcount_stable!($type_name, stringify!($type_name), "Type()"); + assert_type_refcount_stable!($type_name, stringify!($type_name), |py| Py::new( + py, + <$type_name>::new() + ) + .unwrap()); }; // With custom constructor ($type_name:ty, $test_name:expr, $ctor:expr) => {{ - Python::attach(|py| { - #[expect(non_snake_case)] - let Type = py.get_type::<$type_name>(); - let ctor_code = $ctor; - py_run!( - py, - Type, - &format!( - r#" - import gc - import sys - - gc.collect() - count = sys.getrefcount(Type) - - for i in range(1000): - obj = {} - del obj - - gc.collect() - after = sys.getrefcount(Type) - assert after == count, f"Type ref count leaked: {{after}} vs {{count}}" - "#, - ctor_code - ) - ); - }); + let ty = Python::attach(|py| py.get_type::<$type_name>().unbind()); + + // SAFETY: ty is known to be a valid object + let before = Python::attach(|_| unsafe { pyo3::ffi::Py_REFCNT(ty.as_ptr()) }); + + let drive_refcounts = || { + Python::attach(|py| { + for _ in 0..1000 { + let _ = ($ctor)(py); + } + }) + }; + + // Using a separate thread works around tricks in free-threaded Python to + // make type object reference counting fast. We spawn a new thread and + // using a temporary thread state for the loop. When that thread is destructed, + // free-threaded Python merges the temporary thread state refcounts + // back onto the main object. + cfg_select! { + Py_GIL_DISABLED => { + std::thread::scope(|s| { + s.spawn(|| { + drive_refcounts(); + }); + }); + } + not(Py_GIL_DISABLED) => { + drive_refcounts(); + } + } + + // SAFETY: ty is known to be a valid object + let after = Python::attach(|_| unsafe { pyo3::ffi::Py_REFCNT(ty.as_ptr()) }); + + assert_eq!( + before, after, + "Type ref count leaked: {} vs {}", + after, before + ); }}; } @@ -459,83 +479,60 @@ mod inheriting_native_type { ); }); } +} - // Refcount tests for native type classes - #[cfg(not(any(PyPy, GraalPy, Py_GIL_DISABLED)))] - #[test] - fn test_setwitname_ref_counts() { - assert_type_refcount_stable!(SetWithName); - } - - #[cfg(not(any(GraalPy, Py_GIL_DISABLED)))] - #[test] - fn test_dictwithname_ref_counts() { - assert_type_refcount_stable!(DictWithName); - } - - #[cfg(not(Py_GIL_DISABLED))] - #[test] - fn test_customexception_ref_counts() { - assert_type_refcount_stable!(CustomException, "custom_exception", r#"Type('test')"#); - } - - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_tzinfowithname_ref_counts() { - assert_type_refcount_stable!(TzInfoWithName); - } +#[test] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] +fn test_inherit_object_refcount() { + #[pyclass] // no extends is equivalent to inheriting from `object` + struct InheritObject {} - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_listwithname_ref_counts() { - assert_type_refcount_stable!(ListWithName); + #[pymethods] + impl InheritObject { + #[new] + fn new() -> Self { + Self {} + } } - #[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] - #[test] - fn test_sublistwithname_ref_counts() { - assert_type_refcount_stable!(SubListWithName); - } + assert_type_refcount_stable!(InheritObject); } -#[pyclass(subclass)] -struct SimpleClass {} +#[test] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] +fn test_inherit_pyclass_refcount() { + #[pyclass(subclass)] + struct Base {} -#[pymethods] -impl SimpleClass { - #[new] - fn new() -> Self { - Self {} - } -} + #[pyclass(extends=Base)] + struct InheritPyClass {} -// Generate refcount tests for all top-level types -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_baseclass_ref_counts() { - assert_type_refcount_stable!(BaseClass); -} + #[pymethods] + impl InheritPyClass { + #[new] + fn new() -> PyClassInitializer { + PyClassInitializer::from(Base {}).add_subclass(Self {}) + } + } -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_subclass_ref_counts() { - assert_type_refcount_stable!(SubClass); + assert_type_refcount_stable!(InheritPyClass); } -#[cfg(not(Py_GIL_DISABLED))] +#[cfg(any(Py_3_12, not(Py_LIMITED_API)))] +#[cfg(not(all(target_arch = "wasm32", Py_GIL_DISABLED)))] +#[cfg(not(GraalPy))] // FIXME: it should be possible to use variable layout to inherit dict on graalpy #[test] -fn test_base_class_with_result_ref_counts() { - assert_type_refcount_stable!(BaseClassWithResult, "base_class_with_result", "Type(10)"); -} +fn test_inherit_native_type_refcount() { + #[pyclass(extends=pyo3::types::PyDict)] + struct InheritDict {} -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_subclass2_ref_counts() { - assert_type_refcount_stable!(SubClass2, "subclass2", "Type(10)"); -} + #[pymethods] + impl InheritDict { + #[new] + fn new() -> PyClassInitializer { + PyClassInitializer::from(Self {}) + } + } -#[cfg(not(Py_GIL_DISABLED))] -#[test] -fn test_simpleclass_ref_counts() { - assert_type_refcount_stable!(SimpleClass); + assert_type_refcount_stable!(InheritDict); }