galvanic-mock 0.1.3

A behaviour-driven mocking framework for generic traits. Create mocks for (multiple) traits in a behaviour-driven development mocking framework. Define the behaviour of mocks and expected method calls using argument patterns. Supports mocking of generic traits and generic methods. Requires: nightly
Documentation
/* Copyright 2017 Christopher Bacher
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
use syn;
use quote;

use super::*;
use data::GivenStatement;

pub fn implement_given_behaviour() -> Vec<quote::Tokens> {
    let behaviour_item = quote! {
        pub(crate) struct GivenBehaviour {
            stmt_id: usize,
            num_matches: std::cell::Cell<usize>,
            expected_matches: Option<usize>,
            bound: std::rc::Rc<std::any::Any>,
            stmt_repr: String
        }
    };

    let behaviour_impl = quote! {
        #[allow(dead_code)]
        impl GivenBehaviour {
            pub fn with(stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_matches: None,
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn with_times(times: usize, stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_matches: Some(times),
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn matched(&self) {
                self.num_matches.set(self.num_matches.get() + 1);
            }

            pub fn is_saturated(&self) -> bool {
                match self.expected_matches {
                    Some(limit) => self.num_matches.get() >= limit,
                    None => false
                }
            }

            pub fn describe(&self) -> &str {
                &self.stmt_repr
            }
        }
    };

    vec![behaviour_item, behaviour_impl]
}

pub fn implement_given_behaviour_matcher(statement: &GivenStatement) -> quote::Tokens {
    let return_expr = match &statement.return_stmt {
        &Return::FromValue(ref expr) => quote!{ #expr },
        &Return::FromCall(ref expr) => quote!{ (#expr)(&curried_args) },
        &Return::FromSpy => panic!("return_from_spy is not implemented yet."),
        &Return::Panic => quote!{ panic!("Panic by behaviour. Don't forget the towel.") }
    };

    let match_expr = match statement.matcher {
        BehaviourMatcher::Void => quote!{ true },
        BehaviourMatcher::Explicit(ref expr) => quote!{ (#expr).match_args(&curried_args) },
        BehaviourMatcher::PerArgument(ref exprs) => {
            let mut arg_tokens = quote::Tokens::new();
            arg_tokens.append("(");
            for idx in 0..exprs.len() {
                if idx >= 1 {
                    arg_tokens.append("&&");
                }
                let expr = exprs.get(idx).unwrap();
                arg_tokens.append(quote!( (#expr) ));
                arg_tokens.append(format!(".match_args(&curried_args.{})", idx));
            }
            arg_tokens.append(")");
            arg_tokens
        }
    };

    let stmt_id = statement.stmt_id;
    let return_value = syn::Ident::from("return_value");
    let behaviour_idx = syn::Ident::from("idx");
    let maybe_remove_idx = syn::Ident::from("maybe_remove_idx");
    let binding_type = binding_name_for(statement.block_id);
    quote! {
        if behaviour.stmt_id == #stmt_id {
            let bound = behaviour.bound.downcast_ref::<#binding_type>()
                                       .expect("galvanic_mock internal error: unable to downcast binding type");
            use std::convert::Into;
            if (#match_expr).into() {
                behaviour.matched();
                #return_value = Some(#return_expr);
                if behaviour.is_saturated() {
                    #maybe_remove_idx = Some(#behaviour_idx);
                }
                break;
            }
        }
    }
}


pub fn implement_expect_behaviour() -> Vec<quote::Tokens> {
    let behaviour_item = quote! {
        pub(crate) struct ExpectBehaviour {
            stmt_id: usize,
            num_matches: std::cell::Cell<usize>,
            expected_min_matches: Option<usize>,
            expected_max_matches: Option<usize>,
            in_order: Option<bool>,
            bound: std::rc::Rc<std::any::Any>,
            stmt_repr: String
        }
    };

    let behaviour_impl = quote! {
        #[allow(dead_code)]
        impl ExpectBehaviour {
            pub fn with_times(times: usize, stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_min_matches: Some(times),
                    expected_max_matches: Some(times),
                    in_order: None,
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn with_at_least(at_least_times: usize, stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_min_matches: Some(at_least_times),
                    expected_max_matches: None,
                    in_order: None,
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn with_at_most(at_most_times: usize, stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_min_matches: None,
                    expected_max_matches: Some(at_most_times),
                    in_order: None,
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn with_between(at_least_times: usize, at_most_times: usize, stmt_id: usize, bound: std::rc::Rc<std::any::Any>, stmt_repr: &str) -> Self {
                Self {
                    stmt_id: stmt_id,
                    num_matches: std::cell::Cell::new(0),
                    expected_min_matches: Some(at_least_times),
                    expected_max_matches: Some(at_most_times),
                    in_order: None,
                    bound: bound,
                    stmt_repr: stmt_repr.to_string()
                }
            }

            pub fn matched(&self) {
                self.num_matches.set(self.num_matches.get() + 1);
            }

            pub fn is_saturated(&self) -> bool {
                self.expected_min_matches.unwrap_or(0) <= self.num_matches.get()
                    && self.num_matches.get() <= self.expected_max_matches.unwrap_or(std::usize::MAX)
            }

            pub fn describe(&self) -> &str {
                &self.stmt_repr
            }
        }
    };

    vec![behaviour_item, behaviour_impl]
}

pub fn implement_expect_behaviour_matcher(statement: &ExpectStatement) -> quote::Tokens {
    let match_expr = match statement.matcher {
        BehaviourMatcher::Void => quote!{ true },
        BehaviourMatcher::Explicit(ref expr) => quote!{ (#expr).match_args(&curried_args) },
        BehaviourMatcher::PerArgument(ref exprs) => {
            let mut arg_tokens = quote::Tokens::new();
            arg_tokens.append("(");
            for idx in 0..exprs.len() {
                if idx >= 1 {
                    arg_tokens.append("&&");
                }
                let expr = exprs.get(idx).unwrap();
                arg_tokens.append(quote!( (#expr) ));
                arg_tokens.append(format!(".match_args(&curried_args.{})", idx));
            }
            arg_tokens.append(")");
            arg_tokens
        }
    };

    let stmt_id = statement.stmt_id;
    let binding_type = binding_name_for(statement.block_id);
    quote! {
        if behaviour.stmt_id == #stmt_id {
            let bound = behaviour.bound.downcast_ref::<#binding_type>()
                                       .expect("galvanic_mock internal error: unable to downcast binding type");
            use std::convert::Into;
            if (#match_expr).into() {
                behaviour.matched();
                break;
            }
        }
    }
}