%%=====================================================================
%% Program with an erroneous type declaration that caused dialyzer to
%% go into an infinite loop. There are some comments that explain the
%% symptoms and the culprit: the return of test_fun() is erroneous and
%% its type should read
%% fun((config()) -> test_rep() | [test_rep()])
%% instead. But this should not throw dialyzer into an infinite loop.
%% This concerned dialyzer in R14B02 (and probably prior).
%%=====================================================================
-module(common_eunit).
-export([expand_cases/2]).
-type test_name() :: atom() | {'group', atom()}.
-type test_rep() :: {{atom(), atom(), arity()}, fun()}
| {'setup', fun(), fun()}
| {'setup', fun(), fun(), fun()}
| {atom(), test_rep()}
| {atom(), term(), test_rep()}.
-type config() :: [proplists:property()].
-type control() :: tuple() | atom().
%% The combination of the following type and the (erroneous) spec for
%% expand_cases/2 is the reason for the infinite loop in dialyzer.
-type test_fun() :: fun((config()) -> test_rep()).
%% If one comments out this spec the infinite loop disappears.
-spec expand_cases(atom(), test_name() | [test_name()]) -> test_fun().
expand_cases(Module, Cases) ->
if is_list(Cases) ->
TestFuns = [expand_case(Module, Case) || Case <- Cases],
fun(Config) -> [F(Config) || F <- TestFuns] end;
is_atom(Cases); is_tuple(Cases) ->
expand_cases(Module, [Cases])
end.
-spec expand_case(atom(), test_name()) -> test_fun().
expand_case(Module, CaseName) when is_atom(CaseName) ->
TestFun = fun(Config) ->
{{Module, CaseName, 1},
fun() -> apply(Module, CaseName, [Config]) end}
end,
setup_wrapper(Module, TestFun, {init_per_testcase, [CaseName]},
{end_per_testcase, [CaseName]});
expand_case(Module, {group, GroupName}) ->
{Control, Cases} = group_specification(Module, GroupName),
TestFun = control_wrapper(Control, expand_cases(Module, Cases)),
setup_wrapper(Module, TestFun, {init_per_group, [GroupName]},
{end_per_group, [GroupName]}).
-spec control_wrapper([control()], test_fun()) -> test_fun().
control_wrapper([Control|T], TestFun0) ->
TestFun1 = control_wrapper(T, TestFun0),
fun(Config) ->
case Control of
parallel ->
{inparallel, TestFun1(Config)};
sequence ->
{inorder, TestFun1(Config)};
{timetrap, Time} ->
Seconds = case Time of
{hours, Hs} -> Hs * 60 * 60;
{minutes, Ms} -> Ms * 60;
{seconds, Ss} -> Ss;
MSs -> MSs / 1000
end,
{timeout, Seconds, TestFun1(Config)};
C when is_atom(C) ->
{C, TestFun1(Config)};
{C, Arg} ->
{C, Arg, TestFun1(Config)}
end
end;
control_wrapper([], TestFun) ->
TestFun.
-spec setup_wrapper(atom(), test_fun(), Callback, Callback) -> test_fun()
when Callback :: {atom(), list()}.
setup_wrapper(Module, TestFun, {Setup, SA}, {Cleanup, CA}) ->
case erlang:function_exported(Module, Setup, length(SA) + 1) of
true ->
case erlang:function_exported(Module, Cleanup, length(CA) + 1) of
true ->
fun(Config0) ->
{setup,
fun() ->
apply(Module, Setup, SA ++ [Config0])
end,
fun(Config1) ->
apply(Module, Cleanup, CA ++ [Config1])
end,
TestFun}
end;
false ->
fun(Config) ->
{setup,
fun() ->
apply(Module, Setup, SA ++ [Config])
end,
TestFun}
end
end;
false ->
TestFun
end.
-spec group_specification(atom(), atom()) -> {[control()], [test_name()]}.
group_specification(Module, GroupName) ->
case lists:keyfind(GroupName, 1, Module:groups()) of
{_, Control, Cases} when is_list(Control), is_list(Cases) ->
{Control, Cases};
{_, Cases} when is_list(Cases) ->
{[], Cases};
false ->
exit({missing_group, GroupName});
_ ->
exit({bad_group_spec, GroupName})
end.