ktest_macros/lib.rs
1//! Full credit to: https://github.com/Anders429/gba_test/blob/master/gba_test_macros/src/lib.rs
2//! for the original source of this code, which has been changed for our purposes.
3//!
4//! Provides the `#[ktest]` attribute for annotating tests that should be run on the Game Boy
5//! Advance.
6//!
7//! ## Usage
8//! You can use the provided `#[ktest]` attribute to write tests in the same way you would normally
9//! [write tests in Rust](https://doc.rust-lang.org/book/ch11-01-writing-tests.html):
10//!
11//! ``` rust
12//! #![feature(custom_test_frameworks)]
13//!
14//! #[cfg(test)]
15//! mod tests {
16//! use ktest_macros::test;
17//!
18//! #[ktest]
19//! fn it_works() {
20//! let result = 2 + 2;
21//! assert_eq!(result, 4);
22//! }
23//! }
24//! ```
25//!
26//! Note that you should use the `#[ktest]` attribute provided by this crate, **not** the default
27//! `#[test]` attribute.
28//!
29//! Also note that use of this macro currently depends on the
30//! [`custom_test_frameworks`](https://doc.rust-lang.org/beta/unstable-book/language-features/custom-test-frameworks.html)
31//! unstable Rust feature. As such, you will need to enable it in any crate that writes tests using
32//! this crate.
33
34use proc_macro::TokenStream;
35use proc_macro2::Span;
36use quote::quote;
37use syn::{
38 parse, parse2, parse_str, token, Attribute, Error, ExprParen, Ident, ItemFn, Meta, ReturnType,
39 Type,
40};
41
42/// Structured representation of the configuration attributes provided for a test.
43struct Attributes {
44 ignore: Ident,
45 ignore_message: Option<ExprParen>,
46 should_panic: Ident,
47 should_panic_message: Option<ExprParen>,
48}
49
50impl Attributes {
51 /// Returns the default configuration attributes for a test.
52 fn new() -> Self {
53 Self {
54 ignore: Ident::new("No", Span::call_site()),
55 ignore_message: None,
56 should_panic: Ident::new("No", Span::call_site()),
57 should_panic_message: None,
58 }
59 }
60}
61
62impl TryFrom<&Vec<Attribute>> for Attributes {
63 type Error = Error;
64
65 fn try_from(attributes: &Vec<Attribute>) -> Result<Self, Self::Error> {
66 let mut result = Attributes::new();
67
68 for attribute in attributes {
69 if let Some(ident) = attribute.path().get_ident() {
70 match ident.to_string().as_str() {
71 "ignore" => {
72 match &attribute.meta {
73 Meta::NameValue(name_value) => {
74 result.ignore = Ident::new("YesWithMessage", Span::call_site());
75 result.ignore_message = Some(ExprParen {
76 attrs: Vec::new(),
77 paren_token: token::Paren::default(),
78 expr: Box::new(name_value.value.clone()),
79 });
80 }
81 Meta::List(_) => return Err(Error::new_spanned(attribute, "valid forms for the attribute are `#[ignore]` and `#[ignore = \"reason\"]`")),
82 Meta::Path(_) => result.ignore = Ident::new("Yes", Span::call_site()),
83 }
84 }
85 "should_panic" => {
86 match &attribute.meta {
87 Meta::List(meta_list) => {
88 if let Ok(Meta::NameValue(name_value)) =
89 parse2(meta_list.tokens.clone())
90 {
91 if name_value.path == parse_str("expected").unwrap() {
92 result.should_panic =
93 Ident::new("YesWithMessage", Span::call_site());
94 result.should_panic_message = Some(ExprParen {
95 attrs: Vec::new(),
96 paren_token: token::Paren::default(),
97 expr: Box::new(name_value.value),
98 });
99 } else {
100 return Err(Error::new_spanned(attribute, "argument must be of the form: `expected = \"error message\"`"));
101 }
102 } else {
103 return Err(Error::new_spanned(attribute, "argument must be of the form: `expected = \"error message\"`"));
104 }
105 }
106 Meta::NameValue(name_value) => {
107 result.should_panic =
108 Ident::new("YesWithMessage", Span::call_site());
109 result.should_panic_message = Some(ExprParen {
110 attrs: Vec::new(),
111 paren_token: token::Paren::default(),
112 expr: Box::new(name_value.value.clone()),
113 });
114 }
115 Meta::Path(_) => {
116 result.should_panic = Ident::new("Yes", Span::call_site());
117 }
118 }
119 }
120 _ => {
121 // Not supported.
122 }
123 }
124 }
125 }
126
127 Ok(result)
128 }
129}
130
131/// Defines a test to be executed on a Game Boy Advance.
132///
133/// # Example
134/// ```
135/// # #![feature(custom_test_frameworks)]
136/// #
137/// #[ktest_macros::test]
138/// fn foo() {
139/// assert!(true);
140/// }
141/// ```
142///
143/// The test macro supports the other testing attributes you would expect to use when writing unit
144/// tests in Rust. Specifically, the `#[ignore]` and `#[should_panic]` attributes are supported.
145///
146/// # Example
147/// ```
148/// # #![feature(custom_test_frameworks)]
149/// #
150/// #[ktest_macros::test]
151/// #[ignore]
152/// fn ignored() {
153/// assert!(false);
154/// }
155///
156/// #[ktest_macros::test]
157/// #[should_panic]
158/// fn panics() {
159/// panic!("expected panic");
160/// }
161/// ```
162#[proc_macro_attribute]
163pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream {
164 let function: ItemFn = match parse(item) {
165 Ok(function) => function,
166 Err(error) => return error.into_compile_error().into(),
167 };
168 let name = function.sig.ident.clone();
169 let return_type = match &function.sig.output {
170 ReturnType::Default => parse_str::<Type>("()").unwrap(),
171 ReturnType::Type(_, return_type) => *return_type.clone(),
172 };
173 let attributes = match Attributes::try_from(&function.attrs) {
174 Ok(attributes) => attributes,
175 Err(error) => return error.into_compile_error().into(),
176 };
177 let ignore = attributes.ignore;
178 let should_panic = attributes.should_panic;
179 if return_type != parse_str::<Type>("()").unwrap()
180 && should_panic != Ident::new("No", Span::call_site())
181 {
182 return Error::new_spanned(
183 function,
184 "functions using `#[should_panic]` must return `()`",
185 )
186 .into_compile_error()
187 .into();
188 }
189
190 TokenStream::from(quote! {
191 #[allow(dead_code)]
192 #function
193
194 #[test_case]
195 #[allow(non_upper_case_globals)]
196 const #name: ::ktest::test::Test::<#return_type> = ::ktest::test::Test::<#return_type> {
197 name: stringify!(#name),
198 modules: module_path!(),
199 test: #name,
200 ignore: ::ktest::test::Ignore::#ignore,
201 should_panic: ::ktest::test::ShouldPanic::#should_panic,
202 };
203 })
204}