From c34e15c2f20d6fa90d254c19357a70dcda0ef23e Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Thu, 4 Dec 2014 15:02:13 +0000 Subject: 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 --- rebar.config | 3 +- test/mock_git_resource.erl | 129 ++++++++++++++++++++++++++++++++++++++++ test/rebar_compile_SUITE.erl | 91 ++-------------------------- test/rebar_deps_SUITE.erl | 85 ++++++++++++++++++++++++++ test/rebar_test_utils.erl | 138 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 360 insertions(+), 86 deletions(-) create mode 100644 test/mock_git_resource.erl create mode 100644 test/rebar_deps_SUITE.erl create mode 100644 test/rebar_test_utils.erl 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/.erl +%% - src/.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/.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}]}. -- cgit v1.1