-module(rebar_compiler_dag_SUITE).
|
|
-compile([export_all, nowarn_export_all]).
|
|
|
|
-include_lib("common_test/include/ct.hrl").
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("kernel/include/file.hrl").
|
|
|
|
all() ->
|
|
[exists, {group, with_project}].
|
|
|
|
groups() ->
|
|
%% The tests in this group are dirty, the order is specific
|
|
%% and required across runs for tests to work.
|
|
[{with_project, [sequence], [
|
|
find_structure, app_sort,
|
|
propagate_include_app1a, propagate_include_app1b,
|
|
propagate_include_app2, propagate_behaviour,
|
|
propagate_app1_ptrans, propagate_app2_ptrans,
|
|
propagate_app2_ptrans_hrl
|
|
]}
|
|
].
|
|
|
|
init_per_suite(Config) ->
|
|
rebar_compiler_erl:module_info(), % ensure it is loaded
|
|
Config.
|
|
|
|
end_per_suite(Config) ->
|
|
Config.
|
|
|
|
init_per_group(with_project, Config) ->
|
|
NewConfig = rebar_test_utils:init_rebar_state(Config, "apps"),
|
|
AppDir = ?config(apps, NewConfig),
|
|
|
|
Name1 = rebar_test_utils:create_random_name("app1_"),
|
|
Vsn1 = rebar_test_utils:create_random_vsn(),
|
|
rebar_test_utils:create_app(filename:join([AppDir,"apps",Name1]), Name1, Vsn1, [kernel, stdlib]),
|
|
|
|
Name2 = rebar_test_utils:create_random_name("app2_"),
|
|
Vsn2 = rebar_test_utils:create_random_vsn(),
|
|
rebar_test_utils:create_app(filename:join([AppDir,"apps",Name2]), Name2, Vsn2, [kernel, stdlib]),
|
|
|
|
Name3 = rebar_test_utils:create_random_name("app3_"),
|
|
Vsn3 = rebar_test_utils:create_random_vsn(),
|
|
rebar_test_utils:create_app(filename:join([AppDir,"apps",Name3]), Name3, Vsn3, [kernel, stdlib]),
|
|
|
|
apply_project(AppDir, [{app1, Name1}, {app2, Name2}, {app3, Name3}],
|
|
project()),
|
|
|
|
[{app_names, [Name1, Name2, Name3]},
|
|
{vsns, [Vsn1, Vsn2, Vsn3]}
|
|
| NewConfig];
|
|
init_per_group(_, Config) ->
|
|
Config.
|
|
|
|
end_per_group(_, Config) ->
|
|
Config.
|
|
|
|
exists(Config) ->
|
|
%% Create a DAG
|
|
Priv = ?config(priv_dir, Config),
|
|
G = rebar_compiler_dag:init(Priv, compilermod, "label", [crit_meta]),
|
|
rebar_compiler_dag:store_artifact(G, "somefile", "someartifact", [written]),
|
|
rebar_compiler_dag:maybe_store(G, Priv, compilermod, "label", [crit_meta]),
|
|
rebar_compiler_dag:terminate(G),
|
|
|
|
?assertEqual(valid, rebar_compiler_dag:status(Priv, compilermod, "label", [crit_meta])),
|
|
?assertEqual(not_found, rebar_compiler_dag:status(Priv, compilermad, "label", [crit_meta])),
|
|
?assertEqual(not_found, rebar_compiler_dag:status(Priv, compilermod, "lobel", [crit_meta])),
|
|
?assertEqual(bad_meta, rebar_compiler_dag:status(Priv, compilermod, "label", [crit_zeta])),
|
|
ok.
|
|
|
|
project() ->
|
|
[{app1, [
|
|
{"src/app1.erl",
|
|
"-module(app1).\n"
|
|
"-include(\"app1_a.hrl\").\n"
|
|
"-include(\"app1_b.hrl\").\n"
|
|
"-include_lib(\"{{app2}}/include/app2.hrl\").\n"
|
|
"-compile({parse_transform, app1_trans}).\n"
|
|
"-compile({parse_transform, {app3, []}}).\n"
|
|
"-behaviour(app2).\n"
|
|
"-export([cb/0]).\n"
|
|
"cb() -> {?APP1A, ?APP1B, ?APP2}.\n"},
|
|
{"src/app1_trans.erl",
|
|
"-module(app1_trans).n"
|
|
"-export([parse_transform/2]).\n"
|
|
"parse_transform(Forms, _Opts) -> Forms.\n"},
|
|
{"src/app1_a.hrl",
|
|
"-define(APP1A, 1).\n"},
|
|
{"include/app1_b.hrl",
|
|
"-define(APP1B, 1).\n"}
|
|
]},
|
|
{app2, [
|
|
{"src/app2.erl",
|
|
"-module(app2).\n"
|
|
"-callback cb() -> term().\n"},
|
|
{"include/app2.hrl",
|
|
"-include(\"app2_resolve.hrl\").\n"
|
|
"-define(APP2, 1).\n"},
|
|
{"src/app2_resolve.hrl",
|
|
"this file should be found but never is"},
|
|
{"include/never_found.hrl",
|
|
"%% just comments"}
|
|
]},
|
|
{app3, [
|
|
{"src/app3.erl",
|
|
"-module(app3).\n"
|
|
"-include_lib(\"{{app2}}/include/app2.hrl\").\n"
|
|
"-include(\"app3_resolve.hrl\").\n"
|
|
"-export([parse_transform/2]).\n"
|
|
"parse_transform(Forms, _Opts) -> Forms.\n"},
|
|
{"src/app3_resolve.hrl",
|
|
"%% this file should be found"}
|
|
]}
|
|
].
|
|
|
|
find_structure() ->
|
|
[{doc, "ensure a proper digraph is built with all files"}].
|
|
find_structure(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
Edges = [{V1,V2} || E <- digraph:edges(G),
|
|
{_,V1,V2,_} <- [digraph:edge(G, E)]],
|
|
%% All timestamps are the same since we just created the thing
|
|
{_, Stamp} = hd(FileStamps),
|
|
Matches = [
|
|
{"/src/app1.erl", Stamp},
|
|
{"/src/app1_trans.erl", Stamp},
|
|
{"/src/app1_a.hrl", Stamp},
|
|
{"/include/app1_b.hrl", Stamp},
|
|
{"/src/app2.erl", Stamp},
|
|
{"/include/app2.hrl", Stamp},
|
|
{"/include/app2.hrl", Stamp},
|
|
{"/src/app3.erl", Stamp},
|
|
{"/src/app3_resolve.hrl", Stamp}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
?assertEqual(undefined, find_match(".*/never_found.hrl", FileStamps)),
|
|
?assertEqual(undefined, find_match(".*/app2_resolve.hrl", FileStamps)),
|
|
ct:pal("Edges: ~p", [Edges]),
|
|
edges([
|
|
{"/src/app1.erl", "/src/app1_a.hrl"},
|
|
{"/src/app1.erl", "/include/app1_b.hrl"},
|
|
{"/src/app1.erl", "/src/app2.erl"},
|
|
{"/src/app1.erl", "/include/app2.hrl"},
|
|
{"/src/app1.erl", "/src/app1_trans.erl"},
|
|
{"/src/app1.erl", "/src/app3.erl"},
|
|
{"/src/app3.erl", "/include/app2.hrl"},
|
|
{"/src/app3.erl", "/src/app3_resolve.hrl"}
|
|
], Edges, FileStamps),
|
|
ok.
|
|
|
|
app_sort() ->
|
|
[{doc, "once the digraph is complete, we can sort apps by dependency order"}].
|
|
app_sort(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
AppPaths = [
|
|
{AppName, filename:join([AppDir, "apps", AppName])} || AppName <- AppNames
|
|
],
|
|
?assertEqual([lists:nth(2, AppNames),
|
|
lists:nth(3, AppNames),
|
|
lists:nth(1, AppNames)],
|
|
rebar_compiler_dag:compile_order(G, AppPaths)),
|
|
ok.
|
|
|
|
propagate_include_app1a() ->
|
|
[{doc, "changing the app1a header file propagates to its dependents"}].
|
|
propagate_include_app1a(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(1, AppNames), "src/app1_a.hrl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[Stamp1, Stamp2] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", Stamp2},
|
|
{"/src/app1_trans.erl", Stamp1},
|
|
{"/src/app1_a.hrl", Stamp2},
|
|
{"/include/app1_b.hrl", Stamp1},
|
|
{"/src/app2.erl", Stamp1},
|
|
{"/include/app2.hrl", Stamp1},
|
|
{"/src/app3.erl", Stamp1},
|
|
{"/src/app3_resolve.hrl", Stamp1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_include_app1b() ->
|
|
[{doc, "changing the app1b header file propagates to its dependents"}].
|
|
propagate_include_app1b(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(1, AppNames), "include/app1_b.hrl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[Stamp1, Stamp2, Stamp3] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", Stamp3},
|
|
{"/src/app1_trans.erl", Stamp1},
|
|
{"/src/app1_a.hrl", Stamp2},
|
|
{"/include/app1_b.hrl", Stamp3},
|
|
{"/src/app2.erl", Stamp1},
|
|
{"/include/app2.hrl", Stamp1},
|
|
{"/src/app3.erl", Stamp1},
|
|
{"/src/app3_resolve.hrl", Stamp1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_include_app2() ->
|
|
[{doc, "changing the app2 header file propagates to its dependents"}].
|
|
propagate_include_app2(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(2, AppNames), "include/app2.hrl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[S1, S2, S3, S4] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", S4},
|
|
{"/src/app1_trans.erl", S1},
|
|
{"/src/app1_a.hrl", S2},
|
|
{"/include/app1_b.hrl", S3},
|
|
{"/src/app2.erl", S1},
|
|
{"/include/app2.hrl", S4},
|
|
{"/src/app3.erl", S4},
|
|
{"/src/app3_resolve.hrl", S1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_behaviour() ->
|
|
[{doc, "changing the behaviour file propagates to its dependents"}].
|
|
propagate_behaviour(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(2, AppNames), "src/app2.erl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[S1, S2, S3, S4, S5] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", S5},
|
|
{"/src/app1_trans.erl", S1},
|
|
{"/src/app1_a.hrl", S2},
|
|
{"/include/app1_b.hrl", S3},
|
|
{"/src/app2.erl", S5},
|
|
{"/include/app2.hrl", S4},
|
|
{"/src/app3.erl", S4},
|
|
{"/src/app3_resolve.hrl", S1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_app1_ptrans() ->
|
|
[{doc, "changing an app-local parse transform propagates to its dependents"}].
|
|
propagate_app1_ptrans(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(1, AppNames), "src/app1_trans.erl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[S1, S2, S3, S4, S5, S6] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", S6},
|
|
{"/src/app1_trans.erl", S6},
|
|
{"/src/app1_a.hrl", S2},
|
|
{"/include/app1_b.hrl", S3},
|
|
{"/src/app2.erl", S5},
|
|
{"/include/app2.hrl", S4},
|
|
{"/src/app3.erl", S4},
|
|
{"/src/app3_resolve.hrl", S1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_app2_ptrans() ->
|
|
[{doc, "changing an app-foreign parse transform propagates to its dependents"}].
|
|
propagate_app2_ptrans(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(3, AppNames), "src/app3.erl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
[S1, S2, S3, S4, S5, S6, S7] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", S7},
|
|
{"/src/app1_trans.erl", S6},
|
|
{"/src/app1_a.hrl", S2},
|
|
{"/include/app1_b.hrl", S3},
|
|
{"/src/app2.erl", S5},
|
|
{"/include/app2.hrl", S4},
|
|
{"/src/app3.erl", S7},
|
|
{"/src/app3_resolve.hrl", S1}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
propagate_app2_ptrans_hrl() ->
|
|
%% the app-foreign ptrans' foreign hrl dep is tested by propagate_include_app2 as well
|
|
[{doc, "changing an app-foreign parse transform's local hrl propagates to its dependents"}].
|
|
propagate_app2_ptrans_hrl(Config) ->
|
|
AppDir = ?config(apps, Config),
|
|
AppNames = ?config(app_names, Config),
|
|
%% assume an empty graph
|
|
G = digraph:new([acyclic]),
|
|
next_second(),
|
|
F = filename:join([AppDir, "apps", lists:nth(3, AppNames), "src/app3_resolve.hrl"]),
|
|
bump_file(F),
|
|
analyze_apps(G, AppNames, AppDir),
|
|
FileStamps = [digraph:vertex(G, V) || V <- digraph:vertices(G)],
|
|
%% All timestamps are the same since we just created the thing
|
|
%% S1 and S7 are gone from the propagation now
|
|
[S2, S3, S4, S5, S6, S8] = lists:usort([S || {_, S} <- FileStamps]),
|
|
Matches = [
|
|
{"/src/app1.erl", S8},
|
|
{"/src/app1_trans.erl", S6},
|
|
{"/src/app1_a.hrl", S2},
|
|
{"/include/app1_b.hrl", S3},
|
|
{"/src/app2.erl", S5},
|
|
{"/include/app2.hrl", S4},
|
|
{"/src/app3.erl", S8},
|
|
{"/src/app3_resolve.hrl", S8}
|
|
],
|
|
matches(Matches, FileStamps),
|
|
ok.
|
|
|
|
%%%%%%%%%%%%%%%
|
|
%%% HELPERS %%%
|
|
%%%%%%%%%%%%%%%
|
|
|
|
apply_project(_BaseDir, _Names, []) ->
|
|
ok;
|
|
apply_project(BaseDir, Names, [{_AppName, []}|Rest]) ->
|
|
apply_project(BaseDir, Names, Rest);
|
|
apply_project(BaseDir, Names, [{AppName, [File|Files]}|Rest]) ->
|
|
apply_file(BaseDir, Names, AppName, File),
|
|
apply_project(BaseDir, Names, [{AppName, Files}|Rest]).
|
|
|
|
apply_file(BaseDir, Names, App, {FileName, Contents}) ->
|
|
AppName = proplists:get_value(App, Names),
|
|
FilePath = filename:join([BaseDir, "apps", AppName, FileName]),
|
|
ok = filelib:ensure_dir(FilePath),
|
|
file:write_file(FilePath, apply_template(Contents, Names)).
|
|
|
|
apply_template("", _) -> "";
|
|
apply_template("{{" ++ Text, Names) ->
|
|
{Var, Rest} = parse_to_var(Text),
|
|
App = list_to_atom(Var),
|
|
proplists:get_value(App, Names) ++ apply_template(Rest, Names);
|
|
apply_template([H|T], Names) ->
|
|
[H|apply_template(T, Names)].
|
|
|
|
parse_to_var(Str) -> parse_to_var(Str, []).
|
|
|
|
parse_to_var("}}"++Rest, Acc) ->
|
|
{lists:reverse(Acc), Rest};
|
|
parse_to_var([H|T], Acc) ->
|
|
parse_to_var(T, [H|Acc]).
|
|
|
|
analyze_apps(G, AppNames, AppDir) ->
|
|
populate_app(G, lists:nth(1, AppNames), AppNames, AppDir, ["app1.erl", "app1_trans.erl"]),
|
|
populate_app(G, lists:nth(2, AppNames), AppNames, AppDir, ["app2.erl"]),
|
|
populate_app(G, lists:nth(3, AppNames), AppNames, AppDir, ["app3.erl"]),
|
|
rebar_compiler_dag:populate_deps(G, ".erl", [{".beam", "ebin/"}]),
|
|
rebar_compiler_dag:propagate_stamps(G),
|
|
%% manually clear the dirty bit for ease of validation
|
|
digraph:del_vertex(G, '$r3_dirty_bit').
|
|
|
|
populate_app(G, Name, AppNames, AppDir, Sources) ->
|
|
InDirs = [filename:join([AppDir, "apps", AppName, "src"])
|
|
|| AppName <- AppNames]
|
|
++ [filename:join([AppDir, "apps", AppName, "include"])
|
|
|| AppName <- AppNames],
|
|
AbsSources = [filename:join([AppDir, "apps", Name, "src", Src])
|
|
|| Src <- Sources],
|
|
DepOpts = [{includes,
|
|
[filename:join([AppDir, "apps", Name, "src"]),
|
|
filename:join([AppDir, "apps", Name, "include"])
|
|
]},
|
|
{include_libs, [filename:join([AppDir, "apps"])]}
|
|
],
|
|
rebar_compiler_dag:populate_sources(
|
|
G, rebar_compiler_erl,
|
|
InDirs, AbsSources, DepOpts
|
|
).
|
|
|
|
find_match(Regex, FileStamps) ->
|
|
try
|
|
[throw(F) || {F, _} <- FileStamps, re:run(F, Regex) =/= nomatch],
|
|
undefined
|
|
catch
|
|
throw:F -> {ok, F}
|
|
end.
|
|
|
|
matches([], _) ->
|
|
ok;
|
|
matches([{R, Stamp} | T], FileStamps) ->
|
|
case find_match(R, FileStamps) of
|
|
{ok, F} ->
|
|
?assertEqual(Stamp, proplists:get_value(F, FileStamps)),
|
|
matches(T, FileStamps);
|
|
undefined ->
|
|
?assertEqual({R, Stamp}, FileStamps)
|
|
end.
|
|
|
|
edges([], _, _) ->
|
|
ok;
|
|
edges([{A,B}|T], Edges, Files) ->
|
|
{ok, AbsA} = find_match(A, Files),
|
|
{ok, AbsB} = find_match(B, Files),
|
|
?assert(lists:member({AbsA, AbsB}, Edges)),
|
|
edges(T, Edges, Files).
|
|
|
|
bump_file(F) ->
|
|
{ok, Bin} = file:read_file(F),
|
|
file:write_file(F, [Bin, "\n"]).
|
|
|
|
next_second() ->
|
|
%% Sleep until the next second. Rather than just doing a
|
|
%% sleep(1000) call, sleep for the amount of time required
|
|
%% to reach the next second as seen by the OS; this can save us
|
|
%% a few hundred milliseconds per test by triggering shorter delays.
|
|
{Mega, Sec, Micro} = os:timestamp(),
|
|
Now = (Mega*1000000 + Sec)*1000 + round(Micro/1000),
|
|
Ms = (trunc(Now / 1000)*1000 + 1000) - Now,
|
|
%% add a 50ms for jitter since the exact amount sometimes causes failures
|
|
timer:sleep(max(Ms+50, 1000)).
|
|
|