Browse Source

Initial tests for dependency resolving

- Reworked the helpers for existing suites and expanded them
- Created a mock git resource module to test for its dependency fetching
- Added a test suite for dependency resolving with first checks for
  common cases (https://gist.github.com/ferd/197cc5c0b85aae370436)

Left to do would include:

- Verify warnings
- Verify failures
- Verify dependency updates resolving
pull/33/head
Fred Hebert 10 years ago
parent
commit
c34e15c2f2
5 changed files with 360 additions and 86 deletions
  1. +2
    -1
      rebar.config
  2. +129
    -0
      test/mock_git_resource.erl
  3. +6
    -85
      test/rebar_compile_SUITE.erl
  4. +85
    -0
      test/rebar_deps_SUITE.erl
  5. +138
    -0
      test/rebar_test_utils.erl

+ 2
- 1
rebar.config View File

@ -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]}]}.

+ 129
- 0
test/mock_git_resource.erl View File

@ -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.

+ 6
- 85
test/rebar_compile_SUITE.erl View File

@ -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")]).

+ 85
- 0
test/rebar_deps_SUITE.erl View File

@ -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)
).

+ 138
- 0
test/rebar_test_utils.erl View File

@ -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}]}.

Loading…
Cancel
Save