test_deps_if/
lib.rs

1//! A helper crate for `test_deps`
2
3use proc_macro::TokenStream;
4use quote;
5use std::collections::HashMap;
6use syn::{self, Attribute, Block, ItemFn};
7
8/// Defines the test dependency
9///
10/// ## Argument
11/// This macro takes GNU Make-like syntax as its argument that is described as:
12/// ```notrust
13/// target [: prereq_0 [prereq_1 ... prereq_n] ]
14/// ```
15/// *Target* is the alias of the test that the macro is applied to. *Prereq_k* are the aliases that the
16/// *target* waits for. It is guaranteed that the *target* begins only after all the *prereq_k* finish.
17/// For example, `#[deps(A)]` defines a test alias *A* that has no prerequisites, which means *A* immediately
18/// starts when you hit `cargo test`. `#[deps(A: B C)]` is another example where *A* waits until *B* and *C*
19/// complete. Note that this macro doesn't care whether the tests succeeded or failed. In the example of
20/// `#[deps(A: B C)]`, *A* will begin even if *B* and *C* failed in their tests.
21///
22/// The available characters for *target* and *prereq* are `a-zA-Z0-9` and underscore (`_`). It may not
23/// start with a digit.
24///
25/// ## Panic
26/// ### At compile time
27/// - Unsupported character is used for *target* or *prereq*
28/// - Argument is not legally formatted
29/// - Duplicated *prereq* alias is specified for a test
30/// - Same alias appears in both *target* and *prereq* for a test
31/// ### At run time
32/// - *Target* with same alias completed twice
33///
34/// ## Example
35/// You can combine other testing macros.
36/// ```notrust
37/// #[deps(A)]
38/// #[ignore]
39/// #[should_panic]
40/// #[test]
41/// fn some_test() {}
42/// ```
43#[proc_macro_attribute]
44pub fn deps(args: TokenStream, input: TokenStream) -> TokenStream {
45    let args = proc_macro2::TokenStream::from(args);
46    let input = proc_macro2::TokenStream::from(input);
47
48    // TODO: proc_macro2::TokenStream impls only into_iter(self).
49    //       Not clone but borrow it once iter(&self) implemented.
50    let arg_tokens = verify_args_text(args.clone());
51    let (target, prereqs) = verify_args_format(&arg_tokens);
52
53    let mut ast: ItemFn = syn::parse2(input).unwrap();
54
55    let body_orig = ast.block.as_ref();
56    let body_new: Block = syn::parse_quote! {{
57        struct Ticket;
58        impl Drop for Ticket {
59            fn drop(&mut self) {
60                let target = String::from(#target);
61                test_deps::target_completed(&target).unwrap();
62            }
63        }
64        let t = Ticket;
65        {
66            let prereqs: Vec<String> = vec![#(String::from(#prereqs)),*];
67            test_deps::ensure_prereqs(&prereqs).unwrap();
68        }
69        #body_orig
70    }};
71    *ast.block = body_new;
72
73    let mut gen = quote::quote! {
74        #ast
75    };
76
77    if is_ignored_test(&ast.attrs) {
78        let dummy_fn = format!("__dummy__{}", ast.sig.ident);
79        let dummy_fn_ident = proc_macro2::Ident::new(&dummy_fn, proc_macro2::Span::call_site());
80        gen = quote::quote! {
81            #[deps(#args)]
82            #[test]
83            fn #dummy_fn_ident(){}
84
85            #gen
86        };
87    }
88
89    gen.into()
90}
91
92// TODO: Support more special characters e.g. '-' and '.' for target and prereq name
93//       As of Mar 2021, Span::{start, end} are nightly and they return always 0 when called through proc_macro2.
94//       Due to the restriction, there is no way to distinguish an isolated punctuation from one consisting of a
95//       word. For example, today's TokenStream presents "abc.def" and "abc .def" in the same way. This TODO will
96//       be revisited once the functions get supported (https://doc.rust-lang.org/proc_macro/struct.Span.html).
97fn verify_args_text(args: proc_macro2::TokenStream) -> Vec<String> {
98    let mut tokens = Vec::new();
99    let mut illegal_str = None;
100    for arg in args.into_iter() {
101        tokens.push(arg.to_string());
102        match arg {
103            proc_macro2::TokenTree::Ident(_) => {}
104            proc_macro2::TokenTree::Punct(pt) => {
105                if pt.as_char() != ':' {
106                    if illegal_str.is_none() {
107                        illegal_str = Some(pt.to_string());
108                    }
109                }
110            }
111            _ => {
112                if illegal_str.is_none() {
113                    illegal_str = Some(arg.to_string());
114                }
115            }
116        }
117    }
118    if let Some(x) = illegal_str {
119        panic!("Illegal string: {}", x);
120    }
121
122    tokens
123}
124
125fn verify_args_format(tokens: &Vec<String>) -> (&String, &[String]) {
126    if tokens.len() == 0 {
127        panic!("Illegal format: Missing target name");
128    } else {
129        let mut tokiter = tokens.iter();
130        let icolon = tokiter.position(|x| x == ":");
131        if let Some(i) = icolon {
132            if tokiter.position(|x| x == ":").is_some() {
133                panic!("Illegal format: Separator ':' should appear at most once");
134            }
135            if i == 0 {
136                panic!("Illegal format: Missing target name");
137            } else if i == tokens.len() - 1 {
138                panic!("Illegal format: Missing prereq names");
139            } else if i != 1 {
140                panic!("Illegal format: Target name should appear only once");
141            }
142        } else {
143            if tokens.len() != 1 {
144                panic!("Illegal format: Target name should appear only once");
145            }
146        }
147    }
148    let mut counts = HashMap::new();
149    for i in 1..tokens.len() {
150        if counts.insert(&tokens[i], ()).is_some() {
151            panic!("Illegal format: Duplicated prereq {}", tokens[i]);
152        }
153    }
154    if counts.contains_key(&tokens[0]) {
155        panic!(
156            "Illegal format: {} appears in both target and prereq",
157            tokens[0]
158        );
159    }
160
161    let mut sepiter = tokens.split(|s| s == ":");
162    let target = &sepiter.next().unwrap()[0];
163    let prereqs = sepiter.next().unwrap_or(&[]);
164    (target, prereqs)
165}
166
167fn is_ignored_test(attrs: &Vec<Attribute>) -> bool {
168    for attr in attrs {
169        if let Ok(m) = attr.parse_meta() {
170            if m.path().is_ident("ignore") {
171                return true;
172            }
173        }
174    }
175    false
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::str::FromStr;
182
183    #[test]
184    fn valid_names() {
185        let names = [
186            "a",
187            "ab",
188            "_",
189            "__",
190            "a_",
191            "_a",
192            "_a_",
193            "a_a",
194            "a0",
195            "a0a",
196            "_0",
197            "_0_",
198            "a0_",
199            "a_0",
200            "_a0",
201            "_0a",
202            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",
203        ];
204        for name in &names {
205            assert_eq!(
206                vec![String::from(*name)],
207                verify_args_text(proc_macro2::TokenStream::from_str(name).unwrap())
208            );
209        }
210    }
211
212    #[test]
213    #[should_panic(expected = "Illegal string: 0a")]
214    fn invalid_name_starts_with_digit() {
215        verify_args_text(proc_macro2::TokenStream::from_str("0a").unwrap());
216    }
217
218    #[test]
219    #[should_panic(expected = "Illegal string: !")]
220    fn invalid_name_contains_special_char() {
221        verify_args_text(proc_macro2::TokenStream::from_str("a!").unwrap());
222    }
223
224    #[test]
225    fn valid_tokens() {
226        let one_tgt = vec![String::from("a")];
227        let one_tgt_one_prq = vec![String::from("a"), String::from(":"), String::from("b")];
228        let one_tgt_two_prqs = vec![
229            String::from("a"),
230            String::from(":"),
231            String::from("b"),
232            String::from("c"),
233        ];
234        assert_eq!(verify_args_format(&one_tgt), (&one_tgt[0], &one_tgt[1..]));
235        assert_eq!(
236            verify_args_format(&one_tgt_one_prq),
237            (&one_tgt_one_prq[0], &one_tgt_one_prq[2..])
238        );
239        assert_eq!(
240            verify_args_format(&one_tgt_two_prqs),
241            (&one_tgt_two_prqs[0], &one_tgt_two_prqs[2..])
242        );
243    }
244
245    #[test]
246    #[should_panic(expected = "Illegal format: Missing target name")]
247    fn invalid_tokens_empty() {
248        verify_args_format(&vec![]);
249    }
250
251    #[test]
252    #[should_panic(expected = "Illegal format: Missing target name")]
253    fn invalid_tokens_single_colon() {
254        verify_args_format(&vec![String::from(":")]);
255    }
256
257    #[test]
258    #[should_panic(expected = "Illegal format: Missing prereq names")]
259    fn invalid_tokens_no_prereq() {
260        verify_args_format(&vec![String::from("a"), String::from(":")]);
261    }
262
263    #[test]
264    #[should_panic(expected = "Illegal format: Missing target name")]
265    fn invalid_tokens_no_target() {
266        verify_args_format(&vec![String::from(":"), String::from("a")]);
267    }
268
269    #[test]
270    #[should_panic(expected = "Illegal format: Target name should appear only once")]
271    fn invalid_tokens_double_targets() {
272        verify_args_format(&vec![String::from("a"), String::from("b")]);
273    }
274
275    #[test]
276    #[should_panic(expected = "Illegal format: Separator ':' should appear at most once")]
277    fn invalid_tokens_double_colons() {
278        verify_args_format(&vec![String::from(":"), String::from(":")]);
279    }
280
281    #[test]
282    #[should_panic(expected = "Illegal format: Target name should appear only once")]
283    fn invalid_tokens_double_targets_with_prereq() {
284        verify_args_format(&vec![
285            String::from("a"),
286            String::from("b"),
287            String::from(":"),
288            String::from("c"),
289        ]);
290    }
291
292    #[test]
293    #[should_panic(expected = "Illegal format: Separator ':' should appear at most once")]
294    fn invalid_tokens_two_colons() {
295        verify_args_format(&vec![
296            String::from("a"),
297            String::from(":"),
298            String::from("b"),
299            String::from(":"),
300        ]);
301    }
302
303    #[test]
304    #[should_panic(expected = "Illegal format: Duplicated prereq b")]
305    fn invalid_tokens_dup_prereqs() {
306        verify_args_format(&vec![
307            String::from("a"),
308            String::from(":"),
309            String::from("b"),
310            String::from("b"),
311        ]);
312    }
313
314    #[test]
315    #[should_panic(expected = "Illegal format: a appears in both target and prereq")]
316    fn invalid_tokens_deps_loop() {
317        verify_args_format(&vec![
318            String::from("a"),
319            String::from(":"),
320            String::from("b"),
321            String::from("a"),
322        ]);
323    }
324}