Skip to content

Commit 165e904

Browse files
committed
feat: Add support for '#[should_panic]' macro
1 parent 1cefabf commit 165e904

2 files changed

Lines changed: 105 additions & 5 deletions

File tree

crates/libtest2/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub mod _private {
6161

6262
pub use crate::_main_parse as main_parse;
6363
pub use crate::_parse_ignore as parse_ignore;
64+
pub use crate::_run_test as run_test;
6465
pub use crate::_test_parse as test_parse;
6566
pub use crate::case::DynCase;
6667
}

crates/libtest2/src/macros.rs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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]
94116
macro_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

Comments
 (0)