%%% @doc
|
|
%%% Unit tests for epp-related compiler utils.
|
|
%%% Make it easier to validate internal behaviour of compiler data and
|
|
%%% handling of module parsing without having to actually set up
|
|
%%% entire projects.
|
|
%%% @end
|
|
-module(rebar_compiler_epp_SUITE).
|
|
-include_lib("common_test/include/ct.hrl").
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-compile([export_all, nowarn_export_all]).
|
|
|
|
all() ->
|
|
[{group, module}].
|
|
|
|
groups() ->
|
|
[{module, [], [
|
|
analyze, analyze_old_behaviour, analyze_old_behavior,
|
|
analyze_empty, analyze_bad_mod,
|
|
resolve_module
|
|
]}
|
|
].
|
|
|
|
init_per_group(module, Config) ->
|
|
to_file(Config, {"direct.hrl", "-direct(val). "}),
|
|
Config;
|
|
init_per_group(_, Config) ->
|
|
Config.
|
|
|
|
end_per_group(_, Config) ->
|
|
Config.
|
|
|
|
init_per_testcase(_, Config) ->
|
|
Config.
|
|
|
|
end_per_testcase(_, Config) ->
|
|
Config.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% module analysis group %%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
analyze() ->
|
|
[{docs, "Analyzing a module returns all the "
|
|
"parseable dependencies for it in a map."}].
|
|
analyze(Config) ->
|
|
?assert(check_analyze(
|
|
#{include => [
|
|
"eunit-[0-9.]+/include/eunit.hrl$",
|
|
"stdlib-[0-9.]+/include/assert.hrl$",
|
|
"/direct.hrl$"
|
|
],
|
|
%% missing includes
|
|
missing_include_file => [
|
|
"^false.hrl$"
|
|
],
|
|
missing_include_lib => [
|
|
"^some_app/include/lib.hrl$"
|
|
],
|
|
parse_transform => [
|
|
erl_id_trans,
|
|
eunit_autoexport, % added by include file!
|
|
missing_parse_trans1,
|
|
missing_parse_trans2
|
|
],
|
|
behaviour => [gen_server, gen_statem],
|
|
is_behaviour => true
|
|
},
|
|
rebar_compiler_epp:deps(
|
|
to_file(Config, fake_mod()),
|
|
[{includes, []}, {macros, []}]
|
|
)
|
|
)),
|
|
ok.
|
|
|
|
analyze_old_behaviour() ->
|
|
[{docs, "Analyzing old-style behaviour annotation"}].
|
|
analyze_old_behaviour(Config) ->
|
|
?assert(check_analyze(
|
|
#{include => [],
|
|
missing_include_file => [],
|
|
missing_include_lib => [],
|
|
parse_transform => [],
|
|
behaviour => [],
|
|
is_behaviour => true
|
|
},
|
|
rebar_compiler_epp:deps(
|
|
to_file(Config, old_behaviour_mod()),
|
|
[{includes, []}, {macros, []}]
|
|
)
|
|
)),
|
|
ok.
|
|
|
|
analyze_old_behavior() ->
|
|
[{docs, "Analyzing old-style behavior annotation"}].
|
|
analyze_old_behavior(Config) ->
|
|
?assert(check_analyze(
|
|
#{include => [],
|
|
missing_include_file => [],
|
|
missing_include_lib => [],
|
|
parse_transform => [],
|
|
behaviour => [],
|
|
is_behaviour => true
|
|
},
|
|
rebar_compiler_epp:deps(
|
|
to_file(Config, old_behavior_mod()),
|
|
[{includes, []}, {macros, []}]
|
|
)
|
|
)),
|
|
ok.
|
|
|
|
analyze_empty() ->
|
|
[{docs, "Making sure empty files are properly handled as valid but null "
|
|
"and let some other compiler phase handle this. We follow "
|
|
"what EPP handles."}].
|
|
analyze_empty(Config) ->
|
|
?assert(check_analyze(
|
|
#{include => [],
|
|
missing_include_file => [],
|
|
missing_include_lib => [],
|
|
parse_transform => [],
|
|
behaviour => [],
|
|
is_behaviour => false
|
|
},
|
|
rebar_compiler_epp:deps(
|
|
to_file(Config, empty_mod()),
|
|
[{includes, []}, {macros, []}]
|
|
)
|
|
)),
|
|
ok.
|
|
|
|
analyze_bad_mod() ->
|
|
[{docs, "Errors for bad modules that don't compile are skipped "
|
|
"by EPP and so we defer that to a later phase of the "
|
|
"compilation process"}].
|
|
analyze_bad_mod(Config) ->
|
|
?assert(check_analyze(
|
|
#{include => [],
|
|
missing_include_file => [],
|
|
missing_include_lib => [],
|
|
parse_transform => [],
|
|
behaviour => [],
|
|
is_behaviour => false
|
|
},
|
|
rebar_compiler_epp:deps(
|
|
to_file(Config, bad_mod()),
|
|
[{includes, []}, {macros, []}]
|
|
)
|
|
)),
|
|
ok.
|
|
|
|
resolve_module() ->
|
|
[{doc, "given a module name and a bunch of paths, find "
|
|
"the first path that matches the module"}].
|
|
resolve_module(Config) ->
|
|
Path1 = to_file(Config, fake_mod()),
|
|
Path2 = to_file(Config, old_behaviour_mod()),
|
|
Path3 = to_file(Config, empty_mod()),
|
|
?assertEqual(
|
|
{ok, Path2},
|
|
rebar_compiler_epp:resolve_module(
|
|
old_behaviour,
|
|
[Path1, Path2, Path3]
|
|
)
|
|
),
|
|
ok.
|
|
|
|
%%%%%%%%%%%%%%%
|
|
%%% HELPERS %%%
|
|
%%%%%%%%%%%%%%%
|
|
|
|
%% check each field of `Map' and validate them against `CheckMap'.
|
|
%% This allows to check each value in the map has a matching assertion.
|
|
%% Then check each field of `CheckMap' against `Map' to find if
|
|
%% any missing value exists.
|
|
check_analyze(CheckMap, Map) ->
|
|
ct:pal("check_analyze:~n~p~n~p", [CheckMap, Map]),
|
|
maps:fold(fun(K,V,Acc) -> check(CheckMap, K, V) and Acc end,
|
|
true, Map)
|
|
andalso
|
|
maps:fold(
|
|
fun(K,_,Acc) ->
|
|
check(CheckMap, K, maps:get(K, Map, make_ref())) and Acc
|
|
end,
|
|
true,
|
|
Map
|
|
).
|
|
|
|
check(Map, K, V) ->
|
|
case maps:is_key(K, Map) of
|
|
false -> false;
|
|
true ->
|
|
#{K := Val} = Map,
|
|
compare_val(Val, V)
|
|
end.
|
|
|
|
%% two identical values always works
|
|
compare_val(V, V) ->
|
|
true;
|
|
%% compare lists of strings; each string must be checked individually
|
|
%% because they are assumed to be regexes.
|
|
compare_val(V1, V2) when is_list(hd(V1)) ->
|
|
match_regexes(V1, V2);
|
|
compare_val(V1, _V2) when not is_integer(hd(V1)) ->
|
|
%% failing list of some sort, but not a string
|
|
false;
|
|
%% strings as regexes
|
|
compare_val(V1, V2) when is_list(V1) ->
|
|
match_regex(V1, [V2]) =/= nomatch;
|
|
%% anything else is not literally the same and is bad
|
|
compare_val(_, _) ->
|
|
false.
|
|
|
|
match_regexes([], List) ->
|
|
List == []; % no extra patterns, that would be weird
|
|
match_regexes([H|T], List) ->
|
|
case match_regex(H, List) of
|
|
nomatch ->
|
|
false;
|
|
{ok, Entry} ->
|
|
match_regexes(T, List -- [Entry])
|
|
end.
|
|
|
|
match_regex(_Pattern, []) ->
|
|
nomatch;
|
|
match_regex(Pattern, [H|T]) ->
|
|
case re:run(H, Pattern) of
|
|
nomatch -> match_regex(Pattern, T);
|
|
_ -> {ok, H}
|
|
end.
|
|
|
|
%% custom zip function that causes value failures (by using make_ref()
|
|
%% that will never match in compare_val/2) rather than crashing because
|
|
%% of lists of different lengths.
|
|
zip([], []) -> [];
|
|
zip([], [H|T]) -> [{make_ref(),H} | zip([], T)];
|
|
zip([H|T], []) -> [{H,make_ref()} | zip(T, [])];
|
|
zip([X|Xs], [Y|Ys]) -> [{X,Y} | zip(Xs, Ys)].
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%% Module specifications %%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
%% turn a module string to a file that will live in CT's scratch dir
|
|
to_file(Config, {Name,Contents}) ->
|
|
Path = filename:join([?config(priv_dir, Config), Name]),
|
|
file:write_file(Path, Contents, [sync]),
|
|
Path.
|
|
|
|
%% base module with all the interesting includes and attributes
|
|
%% we want to track
|
|
fake_mod() ->
|
|
{"somemod.erl", "
|
|
-module(somemod).
|
|
-export([f/1]).
|
|
-include(\"direct.hrl\").
|
|
-include(\"direct.hrl\").
|
|
-include_lib(\"some_app/include/lib.hrl\").
|
|
-include_lib(\"eunit/include/eunit.hrl\").
|
|
-compile({parse_transform, {erl_id_trans, []}}).
|
|
-compile({parse_transform, missing_parse_trans1}).
|
|
-compile([{parse_transform, {missing_parse_trans2, []}}]).
|
|
-behaviour(gen_server).
|
|
-behavior(gen_statem).
|
|
-callback f() -> ok.
|
|
-ifdef(OPT).
|
|
-include(\"true.hrl\").
|
|
-else.
|
|
-include(\"false.hrl\").
|
|
-endif.
|
|
f(X) -> X.
|
|
"}.
|
|
|
|
%% variations for attributes that can't be checked in the
|
|
%% same base module
|
|
old_behaviour_mod() ->
|
|
{"old_behaviour.erl", "
|
|
-module(old_behaviour).
|
|
-export([f/1, behaviour_info/1]).
|
|
f(X) -> X.
|
|
behaviour_info(callbacks) -> [{f,1}].
|
|
"}.
|
|
|
|
old_behavior_mod() ->
|
|
{"old_behaviour.erl", "
|
|
-module(old_behaviour).
|
|
-export([f/1, behaviour_info/1]).
|
|
f(X) -> X.
|
|
behavior_info(callbacks) -> [{f,1}].
|
|
"}.
|
|
|
|
empty_mod() ->
|
|
{"empty.erl", ""}.
|
|
|
|
bad_mod() ->
|
|
{"badmod.erl", "
|
|
-module(bad_mod). % wrong name!
|
|
f(x) -> X+1. % bad vars
|
|
f((x)cv) -> bad syntax.
|
|
"}.
|
|
|