rebar3/test/rebar_compiler_epp_SUITE.erl

301 lines
7.9 KiB
Erlang

%%% @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.
"}.