Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Include filename and line number in error messages
?WAS_CALLED now includes the location (filename and line number) where
the criteria failed (when it does).  This is redundant when using
Erlang/OTP R15B (which provides line numbers in stack traces) or
later, but may prove useful for earlier versions.

?WAIT_CALLED now displays the location which waits for the criteria to
be fulfilled which aids troubleshooting.  The filename and line
numbers are printed in a format which the compilation mode in emacs
renders as clickable links.

Based on ideas from Magnus F and Tomas A.
  • Loading branch information
klajo committed Jan 25, 2012
1 parent b982f74 commit 8be9777
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 42 deletions.
2 changes: 1 addition & 1 deletion include/mockgyver.hrl
Expand Up @@ -31,7 +31,7 @@
?FOR_ALL_TIMEOUT, ?PER_TC_TIMEOUT)).

-define(WRAP(Type, Expr),
{'$mock', Type, Expr}).
{'$mock', Type, Expr, {?FILE, ?LINE}}).

-define(MOCK(Expr), ?WRAP(m_init, (Expr))).

Expand Down
67 changes: 45 additions & 22 deletions src/mockgyver.erl
Expand Up @@ -324,8 +324,8 @@
-export([start_link/0]).
-export([stop/0]).

-export([reg_call_and_get_action/1, get_action/1, set_action/1]).
-export([verify/2]).
-export([reg_call_and_get_action/1, get_action/1, set_action/1, set_action/2]).
-export([verify/2, verify/3]).

%% For test
-export([check_criteria/2]).
Expand All @@ -344,7 +344,7 @@
call_waiters=[], mock_mfas=[], watch_mfas=[]}).
-record(call, {m, f, a}).
-record(action, {mfa, func}).
-record(call_waiter, {from, mfa, crit}).
-record(call_waiter, {from, mfa, crit, loc}).

%-record(trace, {msg}).
-record('DOWN', {mref, type, obj, info}).
Expand Down Expand Up @@ -375,7 +375,11 @@ get_action(MFA) ->

%% @private
set_action(MFA) ->
chk(call({set_action, MFA})).
set_action(MFA, _Opts=[]).

%% @private
set_action(MFA, Opts) ->
chk(call({set_action, MFA, Opts})).

%% @private
start_link() ->
Expand All @@ -401,8 +405,12 @@ end_session() ->
%% @private
%% once | {at_least, N} | {at_most, N} | {times, N} | never
verify({M, F, A}, Criteria) ->
verify({M, F, A}, Criteria, _Opts=[]).

%% @private
verify({M, F, A}, Criteria, Opts) ->
wait_until_trace_delivered(),
chk(call({verify, {M, F, A}, Criteria})).
chk(call({verify, {M, F, A}, Criteria, Opts})).


%%%===================================================================
Expand Down Expand Up @@ -456,13 +464,13 @@ handle_call({reg_call_and_get_action, MFA}, _From, State0) ->
handle_call({get_action, MFA}, _From, State) ->
ActionFun = i_get_action(MFA, State),
{reply, ActionFun, State};
handle_call({set_action, MFA}, _From, State0) ->
{Reply, State} = i_set_action(MFA, State0),
handle_call({set_action, MFA, Opts}, _From, State0) ->
{Reply, State} = i_set_action(MFA, Opts, State0),
{reply, Reply, State};
handle_call({verify, MFA, {was_called, Criteria}}, _From, State) ->
handle_call({verify, MFA, {was_called, Criteria}, Opts}, _From, State) ->
Reply = get_and_check_matches(MFA, Criteria, State),
{reply, Reply, State};
handle_call({verify, MFA, {wait_called, Criteria}}, From, State) ->
{reply, possibly_add_location(Reply, Opts), State};
handle_call({verify, MFA, {wait_called, Criteria}, Opts}, From, State) ->
case get_and_check_matches(MFA, Criteria, State) of
{ok, _} = Reply ->
{reply, Reply, State};
Expand All @@ -471,23 +479,24 @@ handle_call({verify, MFA, {wait_called, Criteria}}, From, State) ->
%% criteria is not yet fulfilled - at least there's a
%% chance it might actually happen.
Waiters = State#state.call_waiters,
Waiter = #call_waiter{from=From, mfa=MFA, crit=Criteria},
Waiter = #call_waiter{from=From, mfa=MFA, crit=Criteria,
loc=proplists:get_value(location, Opts)},
{noreply, State#state{call_waiters = [Waiter | Waiters]}};
{error, _} = Error ->
%% Fail directly if the waiter's criteria can never be
%% fulfilled, if the criteria syntax was bad, etc.
{reply, Error, State}
{reply, possibly_add_location(Error, Opts), State}
end;
handle_call({verify, MFA, num_calls}, _From, State) ->
handle_call({verify, MFA, num_calls, _Opts}, _From, State) ->
Matches = get_matches(MFA, State),
{reply, {ok, length(Matches)}, State};
handle_call({verify, MFA, get_calls}, _From, State) ->
handle_call({verify, MFA, get_calls, _Opts}, _From, State) ->
Matches = get_matches(MFA, State),
{reply, {ok, Matches}, State};
handle_call({verify, MFA, forget_when}, _From, State0) ->
handle_call({verify, MFA, forget_when, _Opts}, _From, State0) ->
State = i_forget_action(MFA, State0),
{reply, ok, State};
handle_call({verify, MFA, forget_calls}, _From, State0) ->
handle_call({verify, MFA, forget_calls, _Opts}, _From, State0) ->
State = remove_matching_calls(MFA, State0),
{reply, ok, State};
handle_call(stop, _From, State) ->
Expand Down Expand Up @@ -557,12 +566,13 @@ possibly_print_call_waiters(Waiters, Calls) ->
"~s~n",
[[fmt_waiter_calls(Waiter, Calls) || Waiter <- Waiters]]).

fmt_waiter_calls(#call_waiter{mfa={WaitM,WaitF,WaitA0}}=Waiter, Calls) ->
fmt_waiter_calls(#call_waiter{mfa={WaitM,WaitF,WaitA0}, loc={File,Line}}=Waiter,
Calls) ->
{arity, WaitA} = erlang:fun_info(WaitA0, arity),
CandMFAs = get_sorted_candidate_mfas(Waiter),
CallMFAs = get_sorted_calls_similar_to_waiter(Waiter, Calls),
lists:flatten(
[f("Waiter: ~p:~p/~p~n~n", [WaitM, WaitF, WaitA]),
[f("~s:~p:~n Waiter: ~p:~p/~p~n~n", [File, Line, WaitM, WaitF, WaitA]),
case CandMFAs of
[] -> f(" Unfortunately there are no similar functions~n", []);
_ -> f(" Did you intend to verify one of these functions?~n"
Expand Down Expand Up @@ -844,7 +854,7 @@ i_get_action({M,F,Args}, #state{actions=Actions}) ->
false -> undefined
end.

i_set_action({M, F, ActionFun}, #state{actions=Actions0} = State) ->
i_set_action({M, F, ActionFun}, _Opts, #state{actions=Actions0} = State) ->
{arity, A} = erlang:fun_info(ActionFun, arity),
MFA = {M, F, A},
case erlang:is_builtin(M, F, A) of
Expand All @@ -866,9 +876,22 @@ wait_until_trace_delivered() ->
Ref = erlang:trace_delivered(all),
receive {trace_delivered, _, Ref} -> ok end.

chk(ok) -> ok;
chk({ok, Value}) -> Value;
chk({error, Reason}) -> erlang:error(Reason).
chk(ok) -> ok;
chk({ok, Value}) -> Value;
chk({error, Reason}) -> erlang:error(Reason);
chk({error, Reason, Location}) -> erlang:error({{reason, Reason},
{location, Location}}).

possibly_add_location({error, Reason}, Opts) ->
case proplists:get_value(location, Opts) of
undefined -> {error, Reason};
Location -> {error, Reason, Location}
end;
possibly_add_location(ok, _Opts) ->
ok;
possibly_add_location({ok, _}=OkRes, _Opts) ->
OkRes.


mock_and_load_mods(MFAs) ->
ModsFAs = group_fas_by_mod(MFAs),
Expand Down
40 changes: 22 additions & 18 deletions src/mockgyver_xform.erl
Expand Up @@ -51,9 +51,9 @@
-export([parse_transform/2]).

%% Records
-record(m_init, {exec_fun}).
-record(m_when, {m, f, a, action}).
-record(m_verify, {m, f, a, g, crit}).
-record(m_init, {exec_fun, loc}).
-record(m_when, {m, f, a, action, loc}).
-record(m_verify, {m, f, a, g, crit, loc}).

-record(env, {mock_mfas, trace_mfas}).

Expand Down Expand Up @@ -104,12 +104,13 @@ rewrite_when_stmts(Forms, Ctxt) ->

rewrite_when_stmts_2(Type, Form0, _Ctxt, Acc) ->
case is_mock_expr(Type, Form0) of
{true, #m_when{m=M, f=F, action=ActionFun}} ->
{true, #m_when{m=M, f=F, action=ActionFun, loc=Location}} ->
Befores = [],
[Form] = codegen:exprs(
fun() ->
mockgyver:set_action({{'$var', M}, {'$var', F},
{'$form', ActionFun}})
mockgyver:set_action(
{{'$var',M}, {'$var',F}, {'$form',ActionFun}},
[{location, {'$var',Location}}])
end),
Afters = [],
{Befores, Form, Afters, false, Acc};
Expand Down Expand Up @@ -139,14 +140,15 @@ rewrite_verify_stmts(Forms, Ctxt) ->

rewrite_verify_stmts_2(Type, Form0, _Ctxt, Acc) ->
case is_mock_expr(Type, Form0) of
{true, #m_verify{m=M, f=F, a=A, g=G, crit=C}} ->
{true, #m_verify{m=M, f=F, a=A, g=G, crit=C, loc=Location}} ->
Fun = mk_verify_checker_fun(A, G),
Befores = [],
[Form] = codegen:exprs(
fun() ->
mockgyver:verify(
{{'$var', M}, {'$var', F}, {'$form', Fun}},
{'$form', C})
{'$form', C},
[{location, {'$var', Location}}])
end),
Afters = [],
{Befores, Form, Afters, false, Acc};
Expand Down Expand Up @@ -184,17 +186,18 @@ is_mock_expr(tuple, Form) ->
is_mock_expr(_Type, _Form) ->
false.

analyze_mock_form([Type, Expr]) ->
analyze_mock_form([Type, Expr, Location0]) ->
Location = erl_syntax:concrete(Location0),
case erl_syntax:atom_value(Type) of
m_init -> analyze_init_expr(Expr);
m_when -> analyze_when_expr(Expr);
m_verify -> analyze_verify_expr(Expr)
m_init -> analyze_init_expr(Expr, Location);
m_when -> analyze_when_expr(Expr, Location);
m_verify -> analyze_verify_expr(Expr, Location)
end.

analyze_init_expr(Expr) ->
#m_init{exec_fun=Expr}.
analyze_init_expr(Expr, Location) ->
#m_init{exec_fun=Expr, loc=Location}.

analyze_when_expr(Expr) ->
analyze_when_expr(Expr, Location) ->
%% The sole purpose of the entire if expression is to let us write
%% ?WHEN(m:f(_) -> some_result).
%% or
Expand All @@ -211,7 +214,7 @@ analyze_when_expr(Expr) ->
end,
Clauses0),
ActionFun = erl_syntax:fun_expr(Clauses),
#m_when{m=M, f=F, a=A, action=ActionFun}.
#m_when{m=M, f=F, a=A, action=ActionFun, loc=Location}.

ensure_all_clauses_implement_the_same_function(Clauses) ->
lists:foldl(fun(Clause, undefined) ->
Expand All @@ -234,13 +237,14 @@ get_when_call_sig(Clause) ->
{M, F, A} = analyze_application(Call),
{M, F, length(A)}.

analyze_verify_expr(Form) ->
analyze_verify_expr(Form, Location) ->
[Case, Criteria] = erl_syntax:tuple_elements(Form),
[Clause | _] = erl_syntax:case_expr_clauses(Case),
[Call | _] = erl_syntax:clause_patterns(Clause),
G = erl_syntax:clause_guard(Clause),
{M, F, A} = analyze_application(Call),
#m_verify{m=M, f=F, a=A, g=G, crit=erl_syntax:revert(Criteria)}.
#m_verify{m=M, f=F, a=A, g=G, crit=erl_syntax:revert(Criteria),
loc=Location}.

mk_verify_checker_fun(Args0, Guard0) ->
%% Let's say there's a statement like this:
Expand Down
3 changes: 2 additions & 1 deletion test/mockgyver_tests.erl
Expand Up @@ -150,7 +150,8 @@ allows_variables_in_criteria_test(_) ->
returns_error_on_invalid_criteria_test(_) ->
lists:foreach(
fun(C) ->
?assertError({invalid_criteria, C},
?assertError({{reason, {invalid_criteria, C}},
{location, _}},
?WAS_CALLED(mockgyver_dummy:return_arg(_), C))
end,
[0,
Expand Down

0 comments on commit 8be9777

Please sign in to comment.