Skip to content

Commit fe88c60

Browse files
committed
Merge owned exception payload tests into one file
Replace the five integration test files under crates/vm/tests with exception_payload.rs, which keeps two tests on a native ValueError subclass with a Drop witness: - an owned payload built through py_new keeps its arguments and initial exception state, and round-trips through raise/except with its native field intact; - a context self-cycle is collected and the native payload is dropped. The tests share a mutex so forced collection is not skipped while the other test's interpreter is collecting. Assisted-by: Grok:grok-4.7 Assisted-by: Claude:claude-opus-5-5
1 parent 5b7284a commit fe88c60

5 files changed

Lines changed: 57 additions & 487 deletions

File tree

‎crates/vm/tests/exception_marker_construction.rs‎

Lines changed: 0 additions & 173 deletions
This file was deleted.

crates/vm/tests/exception_payload_context_gc.rs renamed to crates/vm/tests/exception_payload.rs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
extern crate alloc;
55

6-
// Keep collection isolated from other interpreter fixtures in this process.
76
use alloc::sync::Arc;
87
use core::sync::atomic::{AtomicBool, Ordering};
98
use rustpython_vm::{
@@ -16,6 +15,8 @@ use rustpython_vm::{
1615
types::Constructor,
1716
};
1817

18+
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
19+
1920
#[pyclass(module = "exception_payload_test", name = "DropError", base = PyValueError, traverse = "manual")]
2021
#[repr(C)]
2122
#[derive(Debug)]
@@ -24,25 +25,21 @@ struct DropError {
2425
dropped: Arc<AtomicBool>,
2526
}
2627

27-
// SAFETY: repr(C) keeps the valid base first for the payload's entire lifetime;
28-
// pyclass checks the base offset and compatible payload alignment.
28+
// SAFETY: repr(C) keeps the valid base first; pyclass checks offset and alignment.
2929
unsafe impl PySubclass for DropError {
3030
type Base = PyValueError;
3131

32-
/// Exposes the embedded base without changing the native payload's lifetime.
3332
fn as_base(&self) -> &Self::Base {
3433
&self.base
3534
}
3635
}
3736

3837
// SAFETY: The base owns every Python reference; the flag owns none.
3938
unsafe impl Traverse for DropError {
40-
/// Visits the base's Python references; the destruction flag holds no Python objects.
4139
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
4240
self.base.traverse(tracer_fn);
4341
}
4442

45-
/// Delegates collection to the production wrapper's clearing implementation.
4643
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
4744
self.base.clear(out);
4845
}
@@ -54,7 +51,6 @@ impl DropError {}
5451
impl Constructor for DropError {
5552
type Args = FuncArgs;
5653

57-
/// Constructs the owned base and initializes its native destruction witness.
5854
fn py_new(cls: &Py<PyType>, args: FuncArgs, vm: &VirtualMachine) -> PyResult<Self> {
5955
Ok(Self {
6056
base: PyValueError::py_new(cls, args, vm)?,
@@ -64,15 +60,67 @@ impl Constructor for DropError {
6460
}
6561

6662
impl Drop for DropError {
67-
/// Records actual native destruction independently of Python weakref invalidation.
6863
fn drop(&mut self) {
6964
self.dropped.store(true, Ordering::SeqCst);
7065
}
7166
}
7267

73-
/// Proves a context-only cycle releases the native exception payload itself.
68+
#[test]
69+
fn owned_native_exception_round_trips_through_python() {
70+
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
71+
Interpreter::without_stdlib(Default::default()).enter(|vm| {
72+
let class = DropError::make_static_type();
73+
let argument = vm.ctx.new_str("payload");
74+
let payload = DropError::py_new(&class, vec![argument.clone().into()].into(), vm).unwrap();
75+
let dropped = payload.dropped.clone();
76+
let exception = payload.into_ref_with_type(vm, class).unwrap();
77+
let base = exception.as_base().as_base().as_base();
78+
assert!(base.__traceback__().is_none());
79+
assert!(base.__cause__().is_none());
80+
assert!(base.__context__().is_none());
81+
assert!(
82+
!exception
83+
.as_object()
84+
.get_attr("__suppress_context__", vm)
85+
.unwrap()
86+
.try_to_bool(vm)
87+
.unwrap()
88+
);
89+
assert!(!dropped.load(Ordering::SeqCst));
90+
91+
let scope = vm.new_scope_with_builtins();
92+
scope.globals.set_item("exc", exception.into(), vm).unwrap();
93+
scope.globals.set_item("arg", argument.into(), vm).unwrap();
94+
let source = r#"
95+
try:
96+
raise exc
97+
except ValueError as e:
98+
assert e.args == (arg,)
99+
assert isinstance(e, ValueError)
100+
caught = e
101+
"#;
102+
let code = vm
103+
.compile(
104+
source,
105+
rustpython_vm::compiler::Mode::Exec,
106+
"<native-exception>",
107+
)
108+
.map_err(|error| error.into_pyexception(vm, Some(source)))
109+
.unwrap();
110+
if let Err(error) = vm.run_code_obj(code, scope.clone()) {
111+
vm.print_exception(&error);
112+
panic!("owned native exception round-trip failed");
113+
}
114+
let caught = scope.globals.get_item("caught", vm).unwrap();
115+
let native = caught.downcast::<DropError>().unwrap();
116+
assert!(Arc::ptr_eq(&native.dropped, &dropped));
117+
assert!(!native.dropped.load(Ordering::SeqCst));
118+
});
119+
}
120+
74121
#[test]
75122
fn context_cycle_destroys_owned_exception_payload() {
123+
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
76124
Interpreter::without_stdlib(Default::default()).enter(|vm| {
77125
let class = DropError::make_static_type();
78126
let payload = DropError::py_new(&class, FuncArgs::default(), vm).unwrap();

‎crates/vm/tests/exception_payload_argument_gc.rs‎

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)