33
44extern crate alloc;
55
6- // Keep collection isolated from other interpreter fixtures in this process.
76use alloc:: sync:: Arc ;
87use core:: sync:: atomic:: { AtomicBool , Ordering } ;
98use 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.
2929unsafe 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.
3938unsafe 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 {}
5451impl 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
6662impl 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]
75122fn 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 ( ) ;
0 commit comments