-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", "-behaviour(app2).\n" "-export([cb/0]).\n" "cb() -> {}.\n" "%% 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, ".erl", ".beam")), 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", S5}, {"/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", S5}, {"/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)).