summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTristan Sloughter <tristan.sloughter@gmail.com>2014-12-04 16:41:07 -0600
committerTristan Sloughter <tristan.sloughter@gmail.com>2014-12-04 16:41:07 -0600
commit1a88cc285375e5558b12de502b515ef2ce503028 (patch)
treed5472594390cdd551c33384d060acfe5bc64d71f
parentda91aa3073050644d104455266f72c22fa57c73f (diff)
parentc34e15c2f20d6fa90d254c19357a70dcda0ef23e (diff)
Merge pull request #33 from rebar/test-dep-choice
Initial tests for dependency resolving
-rw-r--r--rebar.config3
-rw-r--r--test/mock_git_resource.erl129
-rw-r--r--test/rebar_compile_SUITE.erl91
-rw-r--r--test/rebar_deps_SUITE.erl85
-rw-r--r--test/rebar_test_utils.erl138
5 files changed, 360 insertions, 86 deletions
diff --git a/rebar.config b/rebar.config
index 3eb1958..3a035f1 100644
--- a/rebar.config
+++ b/rebar.config
@@ -30,7 +30,8 @@
{relx, "",
{git, "https://github.com/tsloughter/relx.git",
{branch, "format_error1"}}},
- {getopt, "", {git, "https://github.com/jcomellas/getopt.git", {branch, "master"}}}]}.
+ {getopt, "", {git, "https://github.com/jcomellas/getopt.git", {branch, "master"}}},
+ {meck, "", {git, "https://github.com/eproxus/meck.git", {tag, "0.8.2"}}}]}.
{erlydtl_opts, [{doc_root, "priv/templates"},
{compiler_options, [report, return, debug_info]}]}.
diff --git a/test/mock_git_resource.erl b/test/mock_git_resource.erl
new file mode 100644
index 0000000..8e50ec4
--- /dev/null
+++ b/test/mock_git_resource.erl
@@ -0,0 +1,129 @@
+%%% Mock a git resource and create an app magically for each URL submitted.
+-module(mock_git_resource).
+-export([mock/0, mock/1, unmock/0]).
+-define(MOD, rebar_git_resource).
+
+%%%%%%%%%%%%%%%%%
+%%% Interface %%%
+%%%%%%%%%%%%%%%%%
+
+%% @doc same as `mock([])'.
+mock() -> mock([]).
+
+%% @doc Mocks a fake version of the git resource fetcher that creates
+%% empty applications magically, rather than trying to download them.
+%% Specific config options are explained in each of the private functions.
+-spec mock(Opts) -> ok when
+ Opts :: [Option],
+ Option :: {update, [App]}
+ | {default_vsn, Vsn}
+ | {override_vsn, [{App, Vsn}]}
+ | {deps, [{App, [Dep]}]},
+ App :: string(),
+ Dep :: {App, string(), {git, string()} | {git, string(), term()}},
+ Vsn :: string().
+mock(Opts) ->
+ meck:new(?MOD, [no_link]),
+ mock_lock(Opts),
+ mock_update(Opts),
+ mock_vsn(Opts),
+ mock_download(Opts),
+ ok.
+
+unmock() ->
+ meck:unload(?MOD).
+
+%%%%%%%%%%%%%%%
+%%% Private %%%
+%%%%%%%%%%%%%%%
+
+%% @doc creates values for a lock file. The refs are fake, but
+%% tags and existing refs declared for a dependency are preserved.
+mock_lock(_) ->
+ meck:expect(
+ ?MOD, lock,
+ fun(_AppDir, Git) ->
+ case Git of
+ {git, Url, {tag, Ref}} -> {git, Url, {ref, Ref}};
+ {git, Url, {ref, Ref}} -> {git, Url, {ref, Ref}};
+ {git, Url} -> {git, Url, {ref, "fake-ref"}};
+ {git, Url, _} -> {git, Url, {ref, "fake-ref"}}
+ end
+ end).
+
+%% @doc The config passed to the `mock/2' function can specify which apps
+%% should be updated on a per-name basis: `{update, ["App1", "App3"]}'.
+mock_update(Opts) ->
+ ToUpdate = proplists:get_value(update, Opts, []),
+ meck:expect(
+ ?MOD, needs_update,
+ fun(_Dir, {git, Url, _Ref}) ->
+ App = app(Url),
+ lists:member(App, ToUpdate)
+ end).
+
+%% @doc Tries to fetch a version from the `*.app.src' file or otherwise
+%% just returns random stuff, avoiding to check for the presence of git.
+%% This probably breaks the assumption that stable references are returned.
+%%
+%% This function can't respect the `override_vsn' option because if the
+%% .app.src file isn't there, we can't find the app name either.
+mock_vsn(Opts) ->
+ Default = proplists:get_value(default_vsn, Opts, "0.0.0"),
+ meck:expect(
+ ?MOD, make_vsn,
+ fun(Dir) ->
+ case filelib:wildcard("*.app.src", filename:join([Dir,"src"])) of
+ [AppSrc] ->
+ {ok, App} = file:consult(AppSrc),
+ Vsn = proplists:get_value(vsn, App),
+ {plain, Vsn};
+ _ ->
+ {plain, Default}
+ end
+ end).
+
+%% @doc For each app to download, create a dummy app on disk instead.
+%% The configuration for this one (passed in from `mock/1') includes:
+%%
+%% - Specify a version, branch, ref, or tag via the `{git, URL, {_, Vsn}'
+%% format to specify a path.
+%% - If there is no version submitted (`{git, URL}'), the function instead
+%% reads from the `override_vsn' proplist (`{override_vsn, {"App1","1.2.3"}'),
+%% and otherwise uses the value associated with `default_vsn'.
+%% - Dependencies for each application must be passed of the form:
+%% `{deps, [{"app1", [{app2, ".*", {git, ...}}]}]}' -- basically
+%% the `deps' option takes a key/value list of terms to output directly
+%% into a `rebar.config' file to describe dependencies.
+mock_download(Opts) ->
+ Deps = proplists:get_value(deps, Opts, []),
+ Default = proplists:get_value(default_vsn, Opts, "0.0.0"),
+ Overrides = proplists:get_value(override_vsn, Opts, []),
+ meck:expect(
+ ?MOD, download,
+ fun (Dir, Git) ->
+ {git, Url, {_, Vsn}} = normalize_git(Git, Overrides, Default),
+ filelib:ensure_dir(Dir),
+ App = app(Url),
+ AppDeps = proplists:get_value(App, Deps, []),
+ rebar_test_utils:create_app(
+ Dir, App, Vsn,
+ [element(1,D) || D <- AppDeps]
+ ),
+ rebar_test_utils:create_config(Dir, [{deps, AppDeps}]),
+ {ok, 'WHATEVER'}
+ end).
+
+%%%%%%%%%%%%%%%
+%%% Helpers %%%
+%%%%%%%%%%%%%%%
+app(Path) ->
+ filename:basename(Path, ".git").
+
+normalize_git({git, Url}, Overrides, Default) ->
+ Vsn = proplists:get_value(app(Url), Overrides, Default),
+ {git, Url, {tag, Vsn}};
+normalize_git({git, Url, Branch}, _, _) when is_list(Branch) ->
+ {git, Url, {branch, Branch}};
+normalize_git(Git, _, _) ->
+ Git.
diff --git a/test/rebar_compile_SUITE.erl b/test/rebar_compile_SUITE.erl
index 2acc64a..6c5cd1e 100644
--- a/test/rebar_compile_SUITE.erl
+++ b/test/rebar_compile_SUITE.erl
@@ -10,7 +10,6 @@
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("kernel/include/file.hrl").
--include_lib("kernel/include/file.hrl").
suite() ->
[].
@@ -22,95 +21,17 @@ end_per_suite(_Config) ->
ok.
init_per_testcase(_, Config) ->
- DataDir = proplists:get_value(data_dir, Config),
- AppsDir = filename:join([DataDir, create_random_name("apps_dir1_")]),
- ok = ec_file:mkdir_p(AppsDir),
- Verbosity = rebar3:log_level(),
- rebar_log:init(command_line, Verbosity),
- State = rebar_state:new(),
- [{apps, AppsDir}, {state, State} | Config].
+ rebar_test_utils:init_rebar_state(Config).
all() ->
[build_basic_app].
build_basic_app(Config) ->
- AppDir = proplists:get_value(apps, Config),
-
- Name = create_random_name("app1_"),
- Vsn = create_random_vsn(),
- create_app(AppDir, Name, Vsn, [kernel, stdlib]),
-
- run_and_check(Config, [], "compile", [{app, Name}]).
-
-%%%===================================================================
-%%% Helper Functions
-%%%===================================================================
-
-run_and_check(Config, RebarConfig, Command, Expect) ->
- AppDir = proplists:get_value(apps, Config),
- State = proplists:get_value(state, Config),
-
- rebar3:run(rebar_state:new(State, RebarConfig, AppDir), Command),
-
- lists:foreach(fun({app, Name}) ->
- [App] = rebar_app_discover:find_apps([AppDir]),
- ?assertEqual(Name, ec_cnv:to_list(rebar_app_info:name(App)))
- end, Expect).
-
-create_app(AppDir, Name, Vsn, Deps) ->
- write_src_file(AppDir, Name),
- write_app_src_file(AppDir, Name, Vsn, Deps),
- rebar_app_info:new(Name, Vsn, AppDir, Deps).
-
-create_empty_app(AppDir, Name, Vsn, Deps) ->
- write_app_file(AppDir, Name, Vsn, Deps),
- rebar_app_info:new(Name, Vsn, AppDir, Deps).
-
-write_beam_file(Dir, Name) ->
- Beam = filename:join([Dir, "ebin", "not_a_real_beam" ++ Name ++ ".beam"]),
- ok = filelib:ensure_dir(Beam),
- ok = ec_file:write_term(Beam, testing_purposes_only).
-
-write_src_file(Dir, Name) ->
- Erl = filename:join([Dir, "src", "not_a_real_src" ++ Name ++ ".erl"]),
- ok = filelib:ensure_dir(Erl),
- ok = ec_file:write(Erl, erl_src_file("not_a_real_src" ++ Name ++ ".erl")).
-
-write_app_file(Dir, Name, Version, Deps) ->
- Filename = filename:join([Dir, "ebin", Name ++ ".app"]),
- ok = filelib:ensure_dir(Filename),
- ok = ec_file:write_term(Filename, get_app_metadata(ec_cnv:to_list(Name), Version, Deps)).
-
-write_app_src_file(Dir, Name, Version, Deps) ->
- Filename = filename:join([Dir, "src", Name ++ ".app.src"]),
- ok = filelib:ensure_dir(Filename),
- ok = ec_file:write_term(Filename, get_app_metadata(ec_cnv:to_list(Name), Version, Deps)).
-
-get_app_metadata(Name, Vsn, Deps) ->
- {application, erlang:list_to_atom(Name),
- [{description, ""},
- {vsn, Vsn},
- {modules, []},
- {included_applications, []},
- {registered, []},
- {applications, Deps}]}.
-
-create_random_name(Name) ->
- random:seed(erlang:now()),
- Name ++ erlang:integer_to_list(random:uniform(1000000)).
+ AppDir = ?config(apps, Config),
-create_random_vsn() ->
- random:seed(erlang:now()),
- lists:flatten([erlang:integer_to_list(random:uniform(100)),
- ".", erlang:integer_to_list(random:uniform(100)),
- ".", erlang:integer_to_list(random:uniform(100))]).
+ Name = rebar_test_utils:create_random_name("app1_"),
+ Vsn = rebar_test_utils:create_random_vsn(),
+ rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
-write_config(Filename, Values) ->
- ok = filelib:ensure_dir(Filename),
- ok = ec_file:write(Filename,
- [io_lib:format("~p.\n", [Val]) || Val <- Values]).
+ rebar_test_utils:run_and_check(Config, [], "compile", [{app, Name}]).
-erl_src_file(Name) ->
- io_lib:format("-module(~s).\n"
- "-export([main/0]).\n"
- "main() -> ok.\n", [filename:basename(Name, ".erl")]).
diff --git a/test/rebar_deps_SUITE.erl b/test/rebar_deps_SUITE.erl
new file mode 100644
index 0000000..b6ee8b8
--- /dev/null
+++ b/test/rebar_deps_SUITE.erl
@@ -0,0 +1,85 @@
+%%% TODO: check that warnings are appearing
+-module(rebar_deps_SUITE).
+-compile(export_all).
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+all() -> [flat, pick_highest_left, pick_highest_right, pick_earliest].
+
+init_per_suite(Config) ->
+ application:start(meck),
+ Config.
+
+end_per_suite(_Config) ->
+ application:stop(meck).
+
+init_per_testcase(Case, Config) ->
+ {Deps, Expect} = deps(Case),
+ [{expect,
+ [case Dep of
+ {N,V} -> {dep, N, V};
+ N -> {dep, N}
+ end || Dep <- Expect]}
+ | setup_project(Case, Config, expand_deps(Deps))].
+
+deps(flat) ->
+ {[{"B", []},
+ {"C", []}],
+ ["B", "C"]};
+deps(pick_highest_left) ->
+ {[{"B", [{"C", "2", []}]},
+ {"C", "1", []}],
+ ["B", {"C", "1"}]}; % Warn C2
+deps(pick_highest_right) ->
+ {[{"B", "1", []},
+ {"C", [{"B", "2", []}]}],
+ [{"B","1"}, "C"]}; % Warn B2
+deps(pick_earliest) ->
+ {[{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}],
+ ["B","C",{"D","1"}]}. % Warn D2
+
+end_per_testcase(_, Config) ->
+ mock_git_resource:unmock(),
+ meck:unload(),
+ Config.
+
+expand_deps([]) -> [];
+expand_deps([{Name, Deps} | Rest]) ->
+ Dep = {Name, ".*", {git, "https://example.org/user/"++Name++".git", "master"}},
+ [{Dep, expand_deps(Deps)} | expand_deps(Rest)];
+expand_deps([{Name, Vsn, Deps} | Rest]) ->
+ Dep = {Name, Vsn, {git, "https://example.org/user/"++Name++".git", {tag, Vsn}}},
+ [{Dep, expand_deps(Deps)} | expand_deps(Rest)].
+
+setup_project(Case, Config0, Deps) ->
+ Config = rebar_test_utils:init_rebar_state(Config0, atom_to_list(Case)),
+ AppDir = ?config(apps, Config),
+ TopDeps = top_level_deps(Deps),
+ RebarConf = rebar_test_utils:create_config(AppDir, [{deps, TopDeps}]),
+ mock_git_resource:mock([{deps, flat_deps(Deps)}]),
+ [{rebarconfig, RebarConf} | Config].
+
+
+flat_deps([]) -> [];
+flat_deps([{{Name,_Vsn,_Ref}, Deps} | Rest]) ->
+ [{Name, top_level_deps(Deps)}]
+ ++
+ flat_deps(Deps)
+ ++
+ flat_deps(Rest).
+
+top_level_deps(Deps) -> [{list_to_atom(Name),Vsn,Ref} || {{Name,Vsn,Ref},_} <- Deps].
+
+%%% TESTS %%%
+flat(Config) -> run(Config).
+pick_highest_left(Config) -> run(Config).
+pick_highest_right(Config) -> run(Config).
+pick_earliest(Config) -> run(Config).
+
+run(Config) ->
+ {ok, RebarConfig} = file:consult(?config(rebarconfig, Config)),
+ rebar_test_utils:run_and_check(
+ Config, RebarConfig, "install_deps", ?config(expect, Config)
+ ).
+
diff --git a/test/rebar_test_utils.erl b/test/rebar_test_utils.erl
new file mode 100644
index 0000000..08834f7
--- /dev/null
+++ b/test/rebar_test_utils.erl
@@ -0,0 +1,138 @@
+-module(rebar_test_utils).
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-export([init_rebar_state/1, init_rebar_state/2, run_and_check/4]).
+-export([create_app/4, create_empty_app/4, create_config/2]).
+-export([create_random_name/1, create_random_vsn/0]).
+
+%%%%%%%%%%%%%%
+%%% Public %%%
+%%%%%%%%%%%%%%
+
+%% @doc {@see init_rebar_state/2}
+init_rebar_state(Config) -> init_rebar_state(Config, "apps_dir1_").
+
+%% @doc Takes a common test config and a name (string) and sets up
+%% a basic OTP app directory with a pre-configured rebar state to
+%% run tests with.
+init_rebar_state(Config, Name) ->
+ application:load(rebar),
+ DataDir = ?config(priv_dir, Config),
+ AppsDir = filename:join([DataDir, create_random_name(Name)]),
+ ok = ec_file:mkdir_p(AppsDir),
+ Verbosity = rebar3:log_level(),
+ rebar_log:init(command_line, Verbosity),
+ State = rebar_state:new([{base_dir, filename:join([AppsDir, "_build"])}]),
+ [{apps, AppsDir}, {state, State} | Config].
+
+%% @doc Takes common test config, a rebar config ([] if empty), a command to
+%% run ("install_deps", "compile", etc.), and a list of expected applications
+%% and/or dependencies to be present, and verifies whether they are all in
+%% place.
+%%
+%% The expectation list takes elements of the form:
+%% - `{app, Name :: string()}': checks that the app is properly built.
+%% - `{dep, Name :: string()}': checks that the dependency has been fetched.
+%% Ignores the build status of the dependency.
+%% - `{dep, Name :: string(), Vsn :: string()}': checks that the dependency
+%% has been fetched, and that a given version has been chosen. Useful to
+%% test for conflict resolution. Also ignores the build status of the
+%% dependency.
+%%
+%% This function assumes `init_rebar_state/1-2' has run before, in order to
+%% fetch the `apps' and `state' values from the CT config.
+run_and_check(Config, RebarConfig, Command, Expect) ->
+ %% Assumes init_rebar_state has run first
+ AppDir = ?config(apps, Config),
+ State = ?config(state, Config),
+ {ok,_} = rebar3:run(rebar_state:new(State, RebarConfig, AppDir), Command),
+ BuildDir = filename:join([AppDir, "_build", "default", "lib"]),
+ Deps = rebar_app_discover:find_apps([BuildDir], all),
+ DepsNames = [{ec_cnv:to_list(rebar_app_info:name(App)), App} || App <- Deps],
+ lists:foreach(
+ fun({app, Name}) ->
+ [App] = rebar_app_discover:find_apps([AppDir]),
+ ct:pal("Name: ~p", [Name]),
+ ?assertEqual(Name, ec_cnv:to_list(rebar_app_info:name(App)))
+ ; ({dep, Name}) ->
+ ct:pal("Name: ~p", [Name]),
+ ?assertNotEqual(false, lists:keyfind(Name, 1, DepsNames))
+ ; ({dep, Name, Vsn}) ->
+ ct:pal("Name: ~p, Vsn: ~p", [Name, Vsn]),
+ case lists:keyfind(Name, 1, DepsNames) of
+ false ->
+ error({app_not_found, Name});
+ {Name, App} ->
+ ?assertEqual(Vsn, rebar_app_info:original_vsn(App))
+ end
+ end, Expect).
+
+%% @doc Creates a dummy application including:
+%% - src/<file>.erl
+%% - src/<file>.app.src
+%% And returns a `rebar_app_info' object.
+create_app(AppDir, Name, Vsn, Deps) ->
+ write_src_file(AppDir, Name),
+ write_app_src_file(AppDir, Name, Vsn, Deps),
+ rebar_app_info:new(Name, Vsn, AppDir, Deps).
+
+%% @doc Creates a dummy application including:
+%% - ebin/<file>.app
+%% And returns a `rebar_app_info' object.
+create_empty_app(AppDir, Name, Vsn, Deps) ->
+ write_app_file(AppDir, Name, Vsn, Deps),
+ rebar_app_info:new(Name, Vsn, AppDir, Deps).
+
+%% @doc Creates a rebar.config file. The function accepts a list of terms,
+%% each of which will be dumped as a consult file. For example, the list
+%% `[a, b, c]' will return the consult file `a. b. c.'.
+create_config(AppDir, Contents) ->
+ Conf = filename:join([AppDir, "rebar.config"]),
+ ok = filelib:ensure_dir(Conf),
+ Config = lists:flatten([io_lib:fwrite("~p.~n", [Term]) || Term <- Contents]),
+ ok = ec_file:write(Conf, Config),
+ Conf.
+
+%% @doc Util to create a random variation of a given name.
+create_random_name(Name) ->
+ random:seed(erlang:now()),
+ Name ++ erlang:integer_to_list(random:uniform(1000000)).
+
+%% @doc Util to create a random variation of a given version.
+create_random_vsn() ->
+ random:seed(erlang:now()),
+ lists:flatten([erlang:integer_to_list(random:uniform(100)),
+ ".", erlang:integer_to_list(random:uniform(100)),
+ ".", erlang:integer_to_list(random:uniform(100))]).
+
+%%%%%%%%%%%%%%%
+%%% Helpers %%%
+%%%%%%%%%%%%%%%
+write_src_file(Dir, Name) ->
+ Erl = filename:join([Dir, "src", "not_a_real_src" ++ Name ++ ".erl"]),
+ ok = filelib:ensure_dir(Erl),
+ ok = ec_file:write(Erl, erl_src_file("not_a_real_src" ++ Name ++ ".erl")).
+
+write_app_file(Dir, Name, Version, Deps) ->
+ Filename = filename:join([Dir, "ebin", Name ++ ".app"]),
+ ok = filelib:ensure_dir(Filename),
+ ok = ec_file:write_term(Filename, get_app_metadata(ec_cnv:to_list(Name), Version, Deps)).
+
+write_app_src_file(Dir, Name, Version, Deps) ->
+ Filename = filename:join([Dir, "src", Name ++ ".app.src"]),
+ ok = filelib:ensure_dir(Filename),
+ ok = ec_file:write_term(Filename, get_app_metadata(ec_cnv:to_list(Name), Version, Deps)).
+
+erl_src_file(Name) ->
+ io_lib:format("-module(~s).\n"
+ "-export([main/0]).\n"
+ "main() -> ok.\n", [filename:basename(Name, ".erl")]).
+
+get_app_metadata(Name, Vsn, Deps) ->
+ {application, erlang:list_to_atom(Name),
+ [{description, ""},
+ {vsn, Vsn},
+ {modules, []},
+ {included_applications, []},
+ {registered, []},
+ {applications, Deps}]}.