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}