@@ -28,29 +28,52 @@ macro_rules! _test_parse {
2828 // Recursively handle attributes:
2929
3030 // Edge condition (no more attributes to parse)
31- ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ ] $( ignore=$ignore: tt) ?) => {
31+ ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ ] $( ignore=$ignore: tt) ? $ ( should_panic=$should_panic : tt ) ? ) => {
3232 $crate:: _private:: test_parse!( break :
3333 name=$name
3434 body=[ $( $item) * ]
3535 $( ignore=$ignore) ?
36+ $( should_panic=$should_panic) ?
3637 ) ;
3738 } ;
3839 // Process `#[ignore]` macro (NOTE: This will only match if an `#[ignore]` macro has not already been parsed)
39- ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ ignore $( = $reason: literal) ?] $( #[ $( $attr: tt) +] ) * ] ) => {
40+ ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ ignore $( = $reason: literal) ?] $( #[ $( $attr: tt) +] ) * ] $ ( should_panic=$should_panic : tt ) ? ) => {
4041 $crate:: _private:: test_parse!( continue :
4142 name=$name
4243 body=[ $( $item) * ]
4344 attrs=[ $( #[ $( $attr) * ] ) * ]
4445 ignore=[ $( $reason) ?]
46+ $( should_panic=$should_panic) ?
47+ ) ;
48+ } ;
49+ // Process `#[should_panic]` macro (NOTE: This will only match if `#[should_panic]` macro has not already been parsed)
50+ ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ should_panic $( = $expected: literal) ?] $( #[ $( $attr: tt) +] ) * ] $( ignore=$ignore: tt) ?) => {
51+ $crate:: _private:: test_parse!( continue :
52+ name=$name
53+ body=[ $( $item) * ]
54+ attrs=[ $( #[ $( $attr) * ] ) * ]
55+ $( ignore=$ignore) ?
56+ should_panic=[ $( $expected) ?]
57+ ) ;
58+ } ;
59+ // Process `#[should_panic(expected = "..")]` macro (NOTE: Same as branch above)
60+ ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ should_panic( expected = $expected: literal) ] $( #[ $( $attr: tt) +] ) * ] $( ignore=$ignore: tt) ?) => {
61+ $crate:: _private:: test_parse!( continue :
62+ name=$name
63+ body=[ $( $item) * ]
64+ attrs=[ $( #[ $( $attr) * ] ) * ]
65+ $( ignore=$ignore) ?
66+ should_panic=[ $expected]
4567 ) ;
4668 } ;
4769 // Discard unknown attributes
48- ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ $( $unknown_attr: tt) +] $( #[ $( $attr: tt) +] ) * ] $( ignore=$ignore: tt) ?) => {
70+ ( continue : name=$name: ident body=[ $( $item: tt) * ] attrs=[ #[ $( $unknown_attr: tt) +] $( #[ $( $attr: tt) +] ) * ] $( ignore=$ignore: tt) ? $ ( should_panic=$should_panic : tt ) ? ) => {
4971 $crate:: _private:: test_parse!( continue :
5072 name=$name
5173 body=[ $( $item) * ]
5274 attrs=[ $( #[ $( $attr) * ] ) * ]
5375 $( ignore=$ignore) ?
76+ $( should_panic=$should_panic) ?
5477 ) ;
5578 } ;
5679
@@ -83,13 +106,12 @@ macro_rules! _test_parse {
83106 ) ?
84107
85108 use $crate:: IntoRunResult ;
86- let result = run ( context) ;
109+ let result = $crate :: _private :: run_test! ( context, $ ( $should_panic ) ? ) ;
87110 IntoRunResult :: into_run_result( result)
88111 }
89112 }
90113 } ;
91114}
92-
93115#[ macro_export]
94116macro_rules! _parse_ignore {
95117 ( $context: expr, [ $reason: literal] ) => {
@@ -99,3 +121,80 @@ macro_rules! _parse_ignore {
99121 $context. ignore( )
100122 } ;
101123}
124+
125+ #[ macro_export]
126+ macro_rules! _run_test {
127+ ( $context: expr, [ $( $expected: literal) ?] ) => {
128+ $crate:: assert_panic!( run( $context) , $( $expected) ?)
129+ } ;
130+ ( $context: expr $( , ) ?) => { {
131+ run( $context)
132+ } } ;
133+ }
134+
135+ #[ macro_export]
136+ macro_rules! assert_panic {
137+ ( $f: expr, $expected: literal $( , ) ?) => {
138+ match :: std:: panic:: catch_unwind( :: std:: panic:: AssertUnwindSafe ( || $f) ) {
139+ // The test should have panicked, but didn't.
140+ :: std:: result:: Result :: Ok ( _) => {
141+ // TODO: Rust includes the source file location here, consider doing the same?
142+ :: std:: result:: Result :: Err ( $crate:: RunError :: fail( "test did not panic as expected" ) )
143+ }
144+
145+ // The test panicked, as expected.
146+ :: std:: result:: Result :: Err ( payload) => {
147+ // The `panic` information is just an `Any` object representing the
148+ // value the panic was invoked with. For most panics (which use
149+ // `panic!` like `println!`), this is either `&str` or `String`.
150+ let maybe_panic_str = payload
151+ . downcast_ref:: <:: std:: string:: String >( )
152+ . map( |s| s. as_str( ) )
153+ . or_else( || payload. downcast_ref:: <& str >( ) . copied( ) ) ;
154+
155+ // Enforce `$expected` to be a string literal
156+ let expected: & ' static str = $expected;
157+
158+ // Check the panic message against the expected message.
159+ match maybe_panic_str {
160+ :: std:: option:: Option :: Some ( panic_str) if panic_str. contains( expected) => {
161+ :: std:: result:: Result :: Ok ( ( ) )
162+ }
163+
164+ :: std:: option:: Option :: Some ( panic_str) => {
165+ let error_msg = :: std:: format!(
166+ r#"panic did not contain expected string
167+ panic message: {panic_str:?}
168+ expected substring: {expected:?}"#
169+ ) ;
170+
171+ :: std:: result:: Result :: Err ( $crate:: RunError :: fail( error_msg) )
172+ }
173+
174+ :: std:: option:: Option :: None => {
175+ let type_id = ( * payload) . type_id( ) ;
176+ let error_msg = :: std:: format!(
177+ r#"expected panic with string value,
178+ found non-string value: `{type_id:?}`
179+ expected substring: {expected:?}"# ,
180+ ) ;
181+
182+ :: std:: result:: Result :: Err ( $crate:: RunError :: fail( error_msg) )
183+ }
184+ }
185+ }
186+ }
187+ } ;
188+ ( $f: expr $( , ) ?) => {
189+ match :: std:: panic:: catch_unwind( :: std:: panic:: AssertUnwindSafe ( || $f) ) {
190+ // The test should have panicked, but didn't.
191+ :: std:: result:: Result :: Ok ( _) => {
192+ // TODO: Rust includes the source file location here, consider doing the same?
193+ :: std:: result:: Result :: Err ( $crate:: RunError :: fail( "test did not panic as expected" ) )
194+ }
195+
196+ // The test panicked, as expected.
197+ :: std:: result:: Result :: Err ( _) => Ok ( ( ) ) ,
198+ }
199+ } ;
200+ }
0 commit comments