diff rust/pyo3-sharedref/tests/test_sharedref.rs @ 52607:a7d2529ed6dd

rust-pyo3-sharedref: converted integration tests from rust-cpython This should bring full confidence on the conversion to PyO3. It highlights also the difference between the two bindings systems: in PyO3, the struct defined by the user is the inner Rust one. In rust-cpython, it is the wrapped one exposed to CPythob We enclose some of the boilerplate in helper functions. Perhaps we should first import the rust-cpython integration test, rework it with the same helpers, then only change the helpers.
author Georges Racinet <georges.racinet@octobus.net>
date Sat, 14 Dec 2024 18:21:56 +0100
parents
children d85514a88706
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/pyo3-sharedref/tests/test_sharedref.rs	Sat Dec 14 18:21:56 2024 +0100
@@ -0,0 +1,255 @@
+use pyo3::prelude::*;
+use pyo3_sharedref::*;
+
+#[pyclass]
+struct Owner {
+    string: PySharedRefCell<String>,
+}
+
+#[pymethods]
+impl Owner {
+    #[new]
+    fn new(s: String) -> Self {
+        Self {
+            string: PySharedRefCell::new(s),
+        }
+    }
+}
+
+fn with_setup(
+    test: impl FnOnce(Python<'_>, &Bound<'_, Owner>) -> PyResult<()>,
+) -> PyResult<()> {
+    pyo3::prepare_freethreaded_python();
+    Python::with_gil(|py| {
+        let owner = Bound::new(py, Owner::new("new".to_owned()))?;
+        test(py, &owner)
+    })
+}
+
+/// "leak" in the sense of `UnsafePyLeaked` the `string` data field,
+/// taking care of all the boilerplate
+fn leak_string(owner: &Bound<'_, Owner>) -> UnsafePyLeaked<&'static String> {
+    let cell = &owner.borrow().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    shared_ref.leak_immutable()
+}
+
+fn try_leak_string(
+    owner: &Bound<'_, Owner>,
+) -> Result<UnsafePyLeaked<&'static String>, TryLeakError> {
+    let cell = &owner.borrow().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    shared_ref.try_leak_immutable()
+}
+
+/// Mutate the `string` field of `owner` as would be done from Python code
+///
+/// This is to simulate normal mutation of the owner object from
+/// the Python interpreter. This could be replaced by methods of [`Owner`]
+/// (wih closure replaced by a small fixed operations)
+/// and perhaps will, once we are done converting the original tests
+/// from rust-cpython
+fn mutate_string<'py>(
+    owner: &'py Bound<'py, Owner>,
+    f: impl FnOnce(&mut String),
+) -> () {
+    let cell = &owner.borrow_mut().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    f(&mut shared_ref.borrow_mut());
+}
+
+#[test]
+fn test_leaked_borrow() -> PyResult<()> {
+    with_setup(|py, owner| {
+        let leaked = leak_string(owner);
+        let leaked_ref = unsafe { leaked.try_borrow(py) }.unwrap();
+        assert_eq!(*leaked_ref, "new");
+        Ok(())
+    })
+}
+
+#[test]
+fn test_leaked_borrow_mut() -> PyResult<()> {
+    with_setup(|py, owner| {
+        let leaked = leak_string(owner);
+        let mut leaked_iter = unsafe { leaked.map(py, |s| s.chars()) };
+        let mut leaked_ref =
+            unsafe { leaked_iter.try_borrow_mut(py) }.unwrap();
+        assert_eq!(leaked_ref.next(), Some('n'));
+        assert_eq!(leaked_ref.next(), Some('e'));
+        assert_eq!(leaked_ref.next(), Some('w'));
+        assert_eq!(leaked_ref.next(), None);
+        Ok(())
+    })
+}
+
+#[test]
+fn test_leaked_borrow_after_mut() -> PyResult<()> {
+    with_setup(|py, owner| {
+        let leaked = leak_string(owner);
+        mutate_string(owner, String::clear);
+        assert!(unsafe { leaked.try_borrow(py) }.is_err());
+        Ok(())
+    })
+}
+
+#[test]
+fn test_leaked_borrow_mut_after_mut() -> PyResult<()> {
+    with_setup(|py, owner| {
+        let leaked = leak_string(owner);
+        let mut leaked_iter = unsafe { leaked.map(py, |s| s.chars()) };
+
+        mutate_string(owner, String::clear);
+        assert!(unsafe { leaked_iter.try_borrow_mut(py) }.is_err());
+        Ok(())
+    })
+}
+
+#[test]
+#[should_panic(expected = "map() over invalidated leaked reference")]
+fn test_leaked_map_after_mut() {
+    with_setup(|py, owner| {
+        let leaked = leak_string(owner);
+        mutate_string(owner, String::clear);
+        let _leaked_iter = unsafe { leaked.map(py, |s| s.chars()) };
+        Ok(())
+    })
+    .expect("should already have panicked")
+}
+
+/// run `try_borrow_mut` on the `string` field and assert it is not an error
+///
+/// Simply returning the `Result` is not possible, because that is
+/// returning a reference to data owned by the function
+fn assert_try_borrow_string_mut_ok(owner: &Bound<'_, Owner>) {
+    let cell = &owner.borrow().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    assert!(shared_ref.try_borrow_mut().is_ok());
+}
+
+fn assert_try_borrow_string_mut_err(owner: &Bound<'_, Owner>) {
+    let cell = &owner.borrow().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    assert!(shared_ref.try_borrow_mut().is_err());
+}
+
+fn assert_try_borrow_string_err(owner: &Bound<'_, Owner>) {
+    let cell = &owner.borrow().string;
+    let shared_ref = unsafe { cell.borrow(owner) };
+    assert!(shared_ref.try_borrow().is_err());
+}
+
+#[test]
+fn test_try_borrow_mut_while_leaked_ref() -> PyResult<()> {
+    with_setup(|py, owner| {
+        assert_try_borrow_string_mut_ok(owner);
+        let leaked = leak_string(owner);
+        {
+            let _leaked_ref = unsafe { leaked.try_borrow(py) }.unwrap();
+            assert_try_borrow_string_mut_err(owner);
+            {
+                let _leaked_ref2 = unsafe { leaked.try_borrow(py) }.unwrap();
+                assert_try_borrow_string_mut_err(owner);
+            }
+            assert_try_borrow_string_mut_err(owner);
+        }
+        assert_try_borrow_string_mut_ok(owner);
+        Ok(())
+    })
+}
+
+#[test]
+fn test_try_borrow_mut_while_leaked_ref_mut() -> PyResult<()> {
+    with_setup(|py, owner| {
+        assert_try_borrow_string_mut_ok(owner);
+        let leaked = leak_string(owner);
+        let mut leaked_iter = unsafe { leaked.map(py, |s| s.chars()) };
+        {
+            let _leaked_ref =
+                unsafe { leaked_iter.try_borrow_mut(py) }.unwrap();
+            assert_try_borrow_string_mut_err(owner);
+        }
+        assert_try_borrow_string_mut_ok(owner);
+        Ok(())
+    })
+}
+
+#[test]
+fn test_try_leak_while_borrow_mut() -> PyResult<()> {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _mut_ref = shared_ref.borrow_mut();
+
+        assert!(try_leak_string(owner).is_err());
+        Ok(())
+    })
+}
+
+#[test]
+#[should_panic(expected = "already mutably borrowed")]
+fn test_leak_while_borrow_mut() {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _mut_ref = shared_ref.borrow_mut();
+
+        leak_string(owner);
+        Ok(())
+    })
+    .expect("should already have panicked")
+}
+
+#[test]
+fn test_try_borrow_mut_while_borrow() -> PyResult<()> {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _ref = shared_ref.borrow();
+
+        assert_try_borrow_string_mut_err(owner);
+        Ok(())
+    })
+}
+
+#[test]
+#[should_panic(expected = "already borrowed")]
+fn test_borrow_mut_while_borrow() {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _ref = shared_ref.borrow();
+
+        let shared_ref2 = unsafe { cell.borrow(owner) };
+        let _mut_ref = shared_ref2.borrow_mut();
+        Ok(())
+    })
+    .expect("should already have panicked")
+}
+
+#[test]
+fn test_try_borrow_while_borrow_mut() -> PyResult<()> {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _mut_ref = shared_ref.borrow_mut();
+
+        assert_try_borrow_string_err(owner);
+        Ok(())
+    })
+}
+
+#[test]
+#[should_panic(expected = "already mutably borrowed")]
+fn test_borrow_while_borrow_mut() {
+    with_setup(|_py, owner| {
+        let cell = &owner.borrow().string;
+        let shared_ref = unsafe { cell.borrow(owner) };
+        let _mut_ref = shared_ref.borrow_mut();
+
+        let shared_ref2 = unsafe { cell.borrow(owner) };
+        let _ref = shared_ref2.borrow();
+        Ok(())
+    })
+    .expect("should already have panicked")
+}