galvanic_mock/
lib.rs

1/* Copyright 2017 Christopher Bacher
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15#![feature(proc_macro)]
16#![recursion_limit = "128"]
17
18#[macro_use] mod acquire;
19mod new_mock;
20mod given;
21mod expect;
22mod generate;
23mod data;
24
25extern crate proc_macro;
26#[macro_use] extern crate lazy_static;
27
28extern crate syn;
29#[macro_use] extern crate synom;
30#[macro_use] extern crate quote;
31
32#[cfg(test)]#[macro_use]
33extern crate galvanic_assert;
34
35use proc_macro::TokenStream;
36
37use new_mock::handle_new_mock;
38use given::handle_given;
39use expect::handle_expect_interactions;
40use generate::handle_generate_mocks;
41use data::*;
42
43use std::env;
44use std::fs::File;
45use std::io::Write;
46use std::path::Path;
47
48
49enum MockedTraitLocation {
50    TraitDef(syn::Path),
51    Referred(syn::Path)
52}
53
54named!(parse_trait_path -> MockedTraitLocation,
55    delimited!(
56        punct!("("),
57        do_parse!(
58            external: option!(alt!(keyword!("intern") | keyword!("extern"))) >> path: call!(syn::parse::path) >>
59            (match external {
60                Some(..) => MockedTraitLocation::Referred(path),
61                None => MockedTraitLocation::TraitDef(path)
62            })
63        ),
64        punct!(")")
65    )
66);
67
68#[proc_macro_attribute]
69pub fn mockable(args: TokenStream, input: TokenStream) -> TokenStream {
70    let s = input.to_string();
71    let trait_item = syn::parse_item(&s).expect("Expecting a trait definition.");
72
73    let args_str = &args.to_string();
74
75    match trait_item.node {
76        syn::ItemKind::Trait(safety, generics, bounds, items) => {
77            let mut mockable_traits = acquire!(MOCKABLE_TRAITS);
78
79            if args_str.is_empty() {
80                mockable_traits.insert(trait_item.ident.clone().into(), TraitInfo::new(safety, generics, bounds, items));
81                return input;
82            }
83
84            let trait_location = parse_trait_path(args_str)
85                                 .expect(concat!("#[mockable(..)] requires the absolute path of the trait's module.",
86                                                 "It must be preceded with `extern`/`intern` if the trait is defined in another crate/module"));
87            match trait_location {
88                MockedTraitLocation::TraitDef(mut trait_path) => {
89                    trait_path.segments.push(trait_item.ident.clone().into());
90                    mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
91                    input
92                },
93                MockedTraitLocation::Referred(mut trait_path) => {
94                    trait_path.segments.push(trait_item.ident.clone().into());
95                    mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
96                    "".parse().unwrap()
97                }
98            }
99        },
100        _ => panic!("Expecting a trait definition.")
101    }
102}
103
104
105#[proc_macro_attribute]
106pub fn use_mocks(_: TokenStream, input: TokenStream) -> TokenStream {
107    use MacroInvocationPos::*;
108
109    // to parse the macros related to mock ussage the function is converted to string form
110    let mut reassembled = String::new();
111    let parsed = syn::parse_item(&input.to_string()).unwrap();
112    let mut remainder = quote!(#parsed).to_string();
113
114    // parse one macro a time then search for the next macro in the remaining string
115    let mut absolute_pos = 0;
116    while !remainder.is_empty() {
117
118        match find_next_mock_macro_invocation(&remainder) {
119            None => {
120                reassembled.push_str(&remainder);
121                remainder = String::new();
122            },
123            Some(invocation) => {
124                let (left, new_absolute_pos, right) = match invocation {
125                    NewMock(pos) => handle_macro(&remainder, pos, absolute_pos, handle_new_mock),
126                    Given(pos) => handle_macro(&remainder, pos, absolute_pos, handle_given),
127                    ExpectInteractions(pos) => handle_macro(&remainder, pos, absolute_pos, handle_expect_interactions),
128                };
129
130                absolute_pos = new_absolute_pos;
131                reassembled.push_str(&left);
132                remainder = right;
133            }
134        }
135    }
136
137    // once all macro invocations have been removed from the string (and replaced with the actual mock code) it can be parsed back into a function item
138    let mut mock_using_item = syn::parse_item(&reassembled).expect("Reassembled function whi");
139    mock_using_item.vis = syn::Visibility::Public;
140
141    let item_ident = &mock_using_item.ident;
142    let item_vis = &mock_using_item.vis;
143    let mod_fn = syn::Ident::from(format!("mod_{}", item_ident));
144
145
146    if let syn::ItemKind::Mod(Some(ref mut mod_items)) = mock_using_item.node {
147        insert_use_generated_mocks_into_modules(mod_items);
148    }
149
150    let mocks = handle_generate_mocks();
151
152    let generated_mock = (quote! {
153        #[allow(unused_imports)]
154        #item_vis use self::#mod_fn::#item_ident;
155        mod #mod_fn {
156            #![allow(dead_code)]
157            #![allow(unused_imports)]
158            #![allow(unused_variables)]
159            use super::*;
160
161            #mock_using_item
162
163            pub(in self) mod mock {
164                use std;
165                use super::*;
166
167                #(#mocks)*
168            }
169        }
170    }).to_string();
171
172    debug(&item_ident, &generated_mock);
173    generated_mock.parse().unwrap()
174}
175
176fn insert_use_generated_mocks_into_modules(mod_items: &mut Vec<syn::Item>) {
177    for item in mod_items.iter_mut() {
178        if let syn::ItemKind::Mod(Some(ref mut sub_mod_items)) = item.node {
179            insert_use_generated_mocks_into_modules(sub_mod_items);
180        }
181    }
182    mod_items.push(syn::parse_item(quote!(pub use super::*;).as_str()).unwrap());
183}
184
185fn debug(item_ident: &syn::Ident, generated_mock: &str) {
186    if let Some((_, path)) = env::vars().find(|&(ref key, _)| key == "GA_WRITE_MOCK") {
187        if path.is_empty() {
188            println!("{}", generated_mock);
189        } else {
190            let success = File::create(Path::new(&path).join(&(item_ident.to_string())))
191                               .and_then(|mut f| f.write_all(generated_mock.as_bytes()));
192            if let Err(err) = success {
193                eprintln!("Unable to write generated mock to file '{}' because: {}", path, err);
194            }
195        }
196    }
197}
198
199fn has_balanced_quotes(source: &str)  -> bool {
200    let mut count = 0;
201    let mut skip = false;
202    for c in source.chars() {
203        if skip {
204            skip = false;
205            continue;
206        }
207
208        if c == '\\' {
209            skip = true;
210        } else if c == '\"' {
211            count += 1;
212        }
213        //TODO handle raw strings
214    }
215    count % 2 == 0
216}
217
218/// Stores position of a macro invocation with the variant naming the macro.
219enum MacroInvocationPos {
220    NewMock(usize),
221    Given(usize),
222    ExpectInteractions(usize),
223}
224
225/// Find the next galvanic-mock macro invocation in the source string.
226///
227/// Looks for `new_mock!``, `given!`, `expect_interactions!`, and `then_verify_interactions!`.
228/// The `source` string must have been reassembled from a `TokenTree`.
229/// The `source` string is expected to start in a code context, i.e., not inside
230/// a string.
231fn find_next_mock_macro_invocation(source: &str) -> Option<MacroInvocationPos> {
232    use MacroInvocationPos::*;
233    // there must be a space between the macro name and the ! as the ! is a separate token in the tree
234    let macro_names = ["new_mock !", "given !", "expect_interactions !"];
235    // not efficient but does the job
236    macro_names.into_iter()
237               .filter_map(|&mac| {
238                            source.find(mac).and_then(|pos| {
239                                if has_balanced_quotes(&source[.. pos]) {
240                                    Some((pos, mac))
241                                } else { None }
242                            })
243               })
244               .min_by_key(|&(pos, _)| pos)
245               .and_then(|(pos, mac)| Some(match mac {
246                   "new_mock !" => NewMock(pos),
247                   "given !" => Given(pos),
248                   "expect_interactions !" => ExpectInteractions(pos),
249                   _ => panic!("Unreachable. No variant for macro name: {}", mac)
250                }))
251}
252
253fn handle_macro<F>(source: &str, mac_pos_relative_to_source: usize, absolute_pos_of_source: usize, handler: F) -> (String, usize, String)
254where F: Fn(&str, usize) -> (String, String) {
255    let absolute_pos_of_mac = absolute_pos_of_source + mac_pos_relative_to_source;
256
257    let (left_of_mac, right_with_mac) = source.split_at(mac_pos_relative_to_source);
258    let (mut generated_source, unhandled_source) = handler(right_with_mac, absolute_pos_of_mac);
259    generated_source.push_str(&unhandled_source);
260
261    (left_of_mac.to_string(), absolute_pos_of_mac, generated_source)
262}
263
264
265#[cfg(test)]
266mod test_has_balanced_quotes {
267    use super::*;
268
269    #[test]
270    fn should_have_balanced_quotes_if_none_exist() {
271        let x = "df df df";
272        assert!(has_balanced_quotes(x));
273    }
274
275    #[test]
276    fn should_have_balanced_quotes_if_single_pair() {
277        let x = "df \"df\" df";
278        assert!(has_balanced_quotes(x));
279    }
280
281    #[test]
282    fn should_have_balanced_quotes_if_single_pair_with_escapes() {
283        let x = "df \"d\\\"f\" df";
284        assert!(has_balanced_quotes(x));
285    }
286
287    #[test]
288    fn should_have_balanced_quotes_if_multiple_pairs() {
289        let x = "df \"df\" \"df\" df";
290        assert!(has_balanced_quotes(x));
291    }
292
293    #[test]
294    fn should_not_have_balanced_quotes_if_single() {
295        let x = "df \"df df";
296        assert!(!has_balanced_quotes(x));
297    }
298
299    #[test]
300    fn should_not_have_balanced_quotes_if_escaped_pair() {
301        let x = "df \"d\\\" df";
302        assert!(!has_balanced_quotes(x));
303    }
304}