- 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 resolvingpull/33/head
@ -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. |
@ -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) | |||
). | |||
@ -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}]}. |