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