summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFred Hebert <mononcqc@ferd.ca>2015-02-12 10:59:07 -0500
committerFred Hebert <mononcqc@ferd.ca>2015-02-12 10:59:07 -0500
commit3001618a4aadd9e3c8bae8019a298847780b9107 (patch)
treef7f43fdb9a6af969aa01e445b60428c40cf93ffc
parent2fd6f43c020555435c758ffe84ffe56a632387d8 (diff)
parent2e34e91dfb54218597f72faac3a39aedd57bcf97 (diff)
Merge pull request #126 from ferd/upgrade-redo
Upgrade procedure, from scratch (WIP)
-rw-r--r--src/rebar_prv_install_deps.erl159
-rw-r--r--src/rebar_prv_lock.erl2
-rw-r--r--src/rebar_prv_upgrade.erl109
-rw-r--r--src/rebar_state.erl8
-rw-r--r--test/mock_git_resource.erl4
-rw-r--r--test/rebar_deps_SUITE.erl1
-rw-r--r--test/rebar_test_utils.erl18
-rw-r--r--test/rebar_upgrade_SUITE.erl475
8 files changed, 690 insertions, 86 deletions
diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl
index 025d32a..dab4c24 100644
--- a/src/rebar_prv_install_deps.erl
+++ b/src/rebar_prv_install_deps.erl
@@ -44,6 +44,8 @@
-define(PROVIDER, install_deps).
-define(DEPS, [app_discovery]).
+-define(APP_NAME_INDEX, 2).
+
-type src_dep() :: {atom(), {atom(), string(), string()}}
| {atom(), string(), {atom(), string(), string()}}.
-type pkg_dep() :: {atom(), binary()} | atom().
@@ -132,16 +134,17 @@ handle_deps(Profile, State, Deps) ->
-spec handle_deps(atom(), rebar_state:t(), list(), list() | boolean()) ->
{ok, [rebar_app_info:t()], rebar_state:t()} | {error, string()}.
-handle_deps(Profile, State, Deps, Update) when is_boolean(Update) ->
- handle_deps(Profile, State, Deps, Update, []);
+handle_deps(Profile, State, Deps, Upgrade) when is_boolean(Upgrade) ->
+ handle_deps(Profile, State, Deps, Upgrade, []);
handle_deps(Profile, State, Deps, Locks) when is_list(Locks) ->
- handle_deps(Profile, State, Deps, false, Locks).
+ Upgrade = rebar_state:get(State, upgrade, false),
+ handle_deps(Profile, State, Deps, Upgrade, Locks).
-spec handle_deps(atom(), rebar_state:t(), list(), boolean() | {true, binary(), integer()}, list())
-> {ok, [rebar_app_info:t()], rebar_state:t()} | {error, string()}.
handle_deps(_Profile, State, [], _, _) ->
{ok, [], State};
-handle_deps(Profile, State, Deps, Update, Locks) ->
+handle_deps(Profile, State, Deps, Upgrade, Locks) ->
%% Read in package index and dep graph
{Packages, Graph} = rebar_packages:get_packages(State),
%% Split source deps from pkg deps, needed to keep backwards compatibility
@@ -150,7 +153,7 @@ handle_deps(Profile, State, Deps, Update, Locks) ->
%% Fetch transitive src deps
{State1, SrcApps, PkgDeps1, Seen} =
- update_src_deps(Profile, 0, SrcDeps, PkgDeps, [], State, Update, sets:new(), Locks),
+ update_src_deps(Profile, 0, SrcDeps, PkgDeps, [], State, Upgrade, sets:new(), Locks),
{Solved, State2} = case PkgDeps1 of
[] -> %% No pkg deps
@@ -166,7 +169,7 @@ handle_deps(Profile, State, Deps, Update, Locks) ->
[warn_skip_pkg(Pkg) || Pkg <- Discarded],
Solution
end,
- update_pkg_deps(Profile, S, Packages, Update, Seen, State1)
+ update_pkg_deps(Profile, S, Packages, Upgrade, Seen, State1)
end,
AllDeps = lists:ukeymerge(2
@@ -181,7 +184,7 @@ handle_deps(Profile, State, Deps, Update, Locks) ->
%% Internal functions
%% ===================================================================
-update_pkg_deps(Profile, Pkgs, Packages, Update, Seen, State) ->
+update_pkg_deps(Profile, Pkgs, Packages, Upgrade, Seen, State) ->
%% Create app_info record for each pkg dep
DepsDir = rebar_dir:deps_dir(State),
{Solved, _, State1}
@@ -190,7 +193,7 @@ update_pkg_deps(Profile, Pkgs, Packages, Update, Seen, State) ->
,Packages
,Pkg),
{SeenAcc1, StateAcc1} = maybe_lock(Profile, AppInfo, SeenAcc, StateAcc, 0),
- case maybe_fetch(AppInfo, Update, SeenAcc1) of
+ case maybe_fetch(AppInfo, Upgrade, SeenAcc1) of
true ->
{[AppInfo | Acc], SeenAcc1, StateAcc1};
false ->
@@ -236,7 +239,7 @@ package_to_app(DepsDir, Packages, {Name, Vsn}) ->
end.
-spec update_src_deps(atom(), non_neg_integer(), list(), list(), list(), rebar_state:t(), boolean(), sets:set(binary()), list()) -> {rebar_state:t(), list(), list(), sets:set(binary())}.
-update_src_deps(Profile, Level, SrcDeps, PkgDeps, SrcApps, State, Update, Seen, Locks) ->
+update_src_deps(Profile, Level, SrcDeps, PkgDeps, SrcApps, State, Upgrade, Seen, Locks) ->
case lists:foldl(fun(AppInfo, {SrcDepsAcc, PkgDepsAcc, SrcAppsAcc, StateAcc, SeenAcc, LocksAcc}) ->
%% If not seen, add to list of locks to write out
Name = rebar_app_info:name(AppInfo),
@@ -249,15 +252,29 @@ update_src_deps(Profile, Level, SrcDeps, PkgDeps, SrcApps, State, Update, Seen,
true ->
ok
end,
- {SrcDepsAcc, PkgDepsAcc, SrcAppsAcc, StateAcc, SeenAcc, LocksAcc};
+ %% scan for app children here if upgrading
+ case Upgrade of
+ false ->
+ {SrcDepsAcc, PkgDepsAcc, SrcAppsAcc, StateAcc, SeenAcc, LocksAcc};
+ true ->
+ {SrcDepsAcc1, PkgDepsAcc1, SrcAppsAcc1, StateAcc2, LocksAcc1} =
+ handle_dep(AppInfo
+ ,SrcDepsAcc
+ ,PkgDepsAcc
+ ,SrcAppsAcc
+ ,Level
+ ,StateAcc
+ ,LocksAcc),
+
+ {SrcDepsAcc1, PkgDepsAcc1, SrcAppsAcc1, StateAcc2, SeenAcc, LocksAcc1}
+ end;
false ->
{SeenAcc1, StateAcc1} = maybe_lock(Profile, AppInfo, SeenAcc, StateAcc, Level),
{SrcDepsAcc1, PkgDepsAcc1, SrcAppsAcc1, StateAcc2, LocksAcc1} =
- case Update of
- {true, UpdateName, UpdateLevel} ->
- handle_update(AppInfo
- ,UpdateName
- ,UpdateLevel
+ case Upgrade of
+ true ->
+ %{true, UpgradeName, UpgradeLevel} ->
+ handle_upgrade(AppInfo
,SrcDepsAcc
,PkgDepsAcc
,SrcAppsAcc
@@ -278,20 +295,18 @@ update_src_deps(Profile, Level, SrcDeps, PkgDeps, SrcApps, State, Update, Seen,
end
end,
{[], PkgDeps, SrcApps, State, Seen, Locks},
- lists:sort(SrcDeps)) of
+ sort_deps(SrcDeps)) of
{[], NewPkgDeps, NewSrcApps, State1, Seen1, _NewLocks} ->
{State1, NewSrcApps, NewPkgDeps, Seen1};
{NewSrcDeps, NewPkgDeps, NewSrcApps, State1, Seen1, NewLocks} ->
- update_src_deps(Profile, Level+1, NewSrcDeps, NewPkgDeps, NewSrcApps, State1, Update, Seen1, NewLocks)
+ update_src_deps(Profile, Level+1, NewSrcDeps, NewPkgDeps, NewSrcApps, State1, Upgrade, Seen1, NewLocks)
end.
-handle_update(AppInfo, UpdateName, UpdateLevel, SrcDeps, PkgDeps, SrcApps, Level, State, Locks) ->
+handle_upgrade(AppInfo, SrcDeps, PkgDeps, SrcApps, Level, State, Locks) ->
Name = rebar_app_info:name(AppInfo),
- {_, _, _, DepLevel} = lists:keyfind(Name, 1, Locks),
- case UpdateLevel < DepLevel
- orelse Name =:= UpdateName of
- true ->
- case maybe_fetch(AppInfo, true, []) of
+ case lists:keyfind(Name, 1, Locks) of
+ false ->
+ case maybe_fetch(AppInfo, true, sets:new()) of
true ->
handle_dep(AppInfo
,SrcDeps
@@ -302,10 +317,10 @@ handle_update(AppInfo, UpdateName, UpdateLevel, SrcDeps, PkgDeps, SrcApps, Level
,Locks);
false ->
- {SrcDeps, PkgDeps, SrcApps, State, Locks}
+ {[AppInfo|SrcDeps], PkgDeps, SrcApps, State, Locks}
end;
- false ->
- {SrcDeps, PkgDeps, SrcApps, State, Locks}
+ _StillLocked ->
+ {[AppInfo|SrcDeps], PkgDeps, SrcApps, State, Locks}
end.
handle_dep(AppInfo, SrcDeps, PkgDeps, SrcApps, Level, State, Locks) ->
@@ -334,7 +349,7 @@ handle_dep(State, DepsDir, AppInfo, Locks, Level) ->
AppInfo1 = rebar_app_info:state(AppInfo, S3),
Deps = rebar_state:get(S3, deps, []),
- %% Update lock level to be the level the dep will have in this dep tree
+ %% Upgrade lock level to be the level the dep will have in this dep tree
NewLocks = [{DepName, Source, LockLevel+Level} ||
{DepName, Source, LockLevel} <- rebar_state:get(S3, {locks, default}, [])],
AppInfo2 = rebar_app_info:deps(AppInfo1, rebar_state:deps_names(Deps)),
@@ -343,7 +358,7 @@ handle_dep(State, DepsDir, AppInfo, Locks, Level) ->
-spec maybe_fetch(rebar_app_info:t(), boolean() | {true, binary(), integer()},
sets:set(binary())) -> boolean().
-maybe_fetch(AppInfo, Update, Seen) ->
+maybe_fetch(AppInfo, Upgrade, Seen) ->
AppDir = ec_cnv:to_list(rebar_app_info:dir(AppInfo)),
Apps = rebar_app_discover:find_apps(["_checkouts"], all),
case rebar_app_utils:find(rebar_app_info:name(AppInfo), Apps) of
@@ -351,46 +366,15 @@ maybe_fetch(AppInfo, Update, Seen) ->
%% Don't fetch dep if it exists in the _checkouts dir
false;
error ->
- Exists = case rebar_app_utils:is_app_dir(filename:absname(AppDir)++"-*") of
- {true, _} ->
- true;
- _ ->
- case rebar_app_utils:is_app_dir(filename:absname(AppDir)) of
- {true, _} ->
- true;
- _ ->
- false
- end
- end,
-
- case not Exists orelse Update of
+ case not app_exists(AppDir) of
true ->
- ?INFO("Fetching ~s", [rebar_app_info:name(AppInfo)]),
- Source = rebar_app_info:source(AppInfo),
- case rebar_fetch:download_source(AppDir, Source) of
- {error, Reason} ->
- throw(Reason);
- Result ->
- Result
- end;
- _ ->
+ fetch_app(AppInfo, AppDir);
+ false ->
case sets:is_element(rebar_app_info:name(AppInfo), Seen) of
true ->
false;
false ->
- Source = rebar_app_info:source(AppInfo),
- case rebar_fetch:needs_update(AppDir, Source) of
- true ->
- ?INFO("Updating ~s", [rebar_app_info:name(AppInfo)]),
- case rebar_fetch:download_source(AppDir, Source) of
- {error, Reason} ->
- throw(Reason);
- Result ->
- Result
- end;
- false ->
- false
- end
+ maybe_upgrade(AppInfo, AppDir, Upgrade)
end
end
end.
@@ -460,6 +444,55 @@ new_dep(DepsDir, Name, Vsn, Source, State) ->
rebar_state:overrides(S, ParentOverrides++Overrides)),
rebar_app_info:source(Dep1, Source).
+app_exists(AppDir) ->
+ case rebar_app_utils:is_app_dir(filename:absname(AppDir)++"-*") of
+ {true, _} ->
+ true;
+ _ ->
+ case rebar_app_utils:is_app_dir(filename:absname(AppDir)) of
+ {true, _} ->
+ true;
+ _ ->
+ false
+ end
+ end.
+
+fetch_app(AppInfo, AppDir) ->
+ ?INFO("Fetching ~s (~p)", [rebar_app_info:name(AppInfo), rebar_app_info:source(AppInfo)]),
+ Source = rebar_app_info:source(AppInfo),
+ case rebar_fetch:download_source(AppDir, Source) of
+ {error, Reason} ->
+ throw(Reason);
+ Result ->
+ Result
+ end.
+
+maybe_upgrade(AppInfo, AppDir, false) ->
+ Source = rebar_app_info:source(AppInfo),
+ rebar_fetch:needs_update(AppDir, Source);
+maybe_upgrade(AppInfo, AppDir, true) ->
+ Source = rebar_app_info:source(AppInfo),
+ case rebar_fetch:needs_update(AppDir, Source) of
+ true ->
+ ?INFO("Updating ~s", [rebar_app_info:name(AppInfo)]),
+ case rebar_fetch:download_source(AppDir, Source) of
+ {error, Reason} ->
+ throw(Reason);
+ Result ->
+ Result
+ end;
+ false ->
+ false
+ end.
+
+sort_deps(Deps) ->
+ %% We need a sort stable, based on the name. So that for multiple deps on
+ %% the same level with the same name, we keep the order the parents had.
+ %% `lists:keysort/2' is documented as stable in the stdlib.
+ %% The list of deps is revered when we get it. For the proper stable
+ %% result, re-reverse it.
+ lists:keysort(?APP_NAME_INDEX, lists:reverse(Deps)).
+
-spec parse_goal(binary(), binary()) -> pkg_dep().
parse_goal(Name, Constraint) ->
case re:run(Constraint, "([^\\d]*)(\\d.*)", [{capture, [1,2], binary}]) of
diff --git a/src/rebar_prv_lock.erl b/src/rebar_prv_lock.erl
index 2f26a7f..8a69434 100644
--- a/src/rebar_prv_lock.erl
+++ b/src/rebar_prv_lock.erl
@@ -41,7 +41,7 @@ do(State) ->
,rebar_app_info:dep_level(Dep)}
end, AllDeps),
Dir = rebar_state:dir(State),
- file:write_file(filename:join(Dir, "rebar.lock"), io_lib:format("~p.~n", [Locks])),
+ file:write_file(filename:join(Dir, ?LOCK_FILE), io_lib:format("~p.~n", [Locks])),
{ok, State}.
-spec format_error(any()) -> iolist().
diff --git a/src/rebar_prv_upgrade.erl b/src/rebar_prv_upgrade.erl
index c0a56c3..1cae5aa 100644
--- a/src/rebar_prv_upgrade.erl
+++ b/src/rebar_prv_upgrade.erl
@@ -13,6 +13,9 @@
-define(PROVIDER, upgrade).
-define(DEPS, []).
+%% Also only upgrade top-level (0) deps. Transitive deps shouldn't be
+%% upgradable -- if the user wants this, they should declare it at the
+%% top level and then upgrade.
%% ===================================================================
%% Public API
@@ -26,33 +29,105 @@ init(State) ->
{module, ?MODULE},
{bare, false},
{deps, ?DEPS},
- {example, "rebar upgrade cowboy"},
- {short_desc, "Upgrade dependency."},
- {desc, ""},
+ {example, "rebar3 upgrade [cowboy[,ranch]]"},
+ {short_desc, "Upgrade dependencies."},
+ {desc, "Upgrade project dependecies. Mentioning no application "
+ "will upgrade all dependencies. To upgrade specific dependencies, "
+ "their names can be listed in the command."},
{opts, [
- {package, undefined, undefined, string, "Package to upgrade."}
+ {package, undefined, undefined, string,
+ "List of packages to upgrade. If not specified, all dependencies are upgraded."}
]}])),
{ok, State1}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
{Args, _} = rebar_state:command_parsed_args(State),
- Name = ec_cnv:to_binary(proplists:get_value(package, Args)),
- Locks = rebar_state:get(State, locks, []),
+ Locks = rebar_state:get(State, {locks, default}, []),
+ Deps = rebar_state:get(State, deps),
+ Names = parse_names(ec_cnv:to_binary(proplists:get_value(package, Args, <<"">>)), Locks),
+ case prepare_locks(Names, Deps, Locks, []) of
+ {error, Reason} ->
+ {error, Reason};
+ {Locks0, _Unlocks0} ->
+ Deps0 = top_level_deps(Deps, Locks),
+ State1 = rebar_state:set(State, {deps, default}, Deps0),
+ State2 = rebar_state:set(State1, {locks, default}, Locks0),
+ State3 = rebar_state:set(State2, upgrade, true),
+ Res = rebar_prv_install_deps:do(State3),
+ case Res of
+ {ok, State4} ->
+ info_useless(
+ [element(1,Lock) || Lock <- Locks],
+ [rebar_app_info:name(App) || App <- rebar_state:lock(State4)]
+ ),
+ rebar_prv_lock:do(State4);
+ _ ->
+ Res
+ end
+ end.
+
+-spec format_error(any()) -> iolist().
+format_error({unknown_dependency, Name}) ->
+ io_lib:format("Dependency ~ts not found", [Name]);
+format_error({transitive_dependency, Name}) ->
+ io_lib:format("Dependency ~ts is transient and cannot be safely upgraded. "
+ "Promote it to your top-level rebar.config file to upgrade it.",
+ [Name]);
+format_error(Reason) ->
+ io_lib:format("~p", [Reason]).
+
+
+parse_names(Bin, Locks) ->
+ case lists:usort(re:split(Bin, <<" *, *">>, [trim])) of
+ %% Nothing submitted, use *all* apps
+ [<<"">>] -> [Name || {Name, _, 0} <- Locks];
+ [] -> [Name || {Name, _, 0} <- Locks];
+ %% Regular options
+ Other -> Other
+ end.
+
+prepare_locks([], _, Locks, Unlocks) ->
+ {Locks, Unlocks};
+prepare_locks([Name|Names], Deps, Locks, Unlocks) ->
case lists:keyfind(Name, 1, Locks) of
- {_, _, _, Level} ->
- Deps = rebar_state:get(State, deps),
- case lists:keyfind(binary_to_atom(Name, utf8), 1, Deps) of
+ {_, _, 0} = Lock ->
+ AtomName = binary_to_atom(Name, utf8),
+ case lists:keyfind(AtomName, 1, Deps) of
false ->
- {error, io_lib:format("No such dependency ~s~n", [Name])};
+ {error, {?MODULE, {unknown_dependency, Name}}};
Dep ->
- rebar_prv_install_deps:handle_deps(State, [Dep], {true, Name, Level}),
- {ok, State}
+ Source = case Dep of
+ {_, Src} -> Src;
+ {_, _, Src} -> Src
+ end,
+ {NewLocks, NewUnlocks} = unlock_higher_than(0, Locks -- [Lock]),
+ prepare_locks(Names,
+ Deps,
+ NewLocks,
+ [{Name, Source, 0} | NewUnlocks ++ Unlocks])
end;
- _ ->
- {error, io_lib:format("No such dependency ~s~n", [Name])}
+ {_, _, Level} when Level > 0 ->
+ {error, {?MODULE, {transitive_dependency,Name}}};
+ false ->
+ {error, {?MODULE, {unknown_dependency,Name}}}
end.
--spec format_error(any()) -> iolist().
-format_error(Reason) ->
- io_lib:format("~p", [Reason]).
+top_level_deps(Deps, Locks) ->
+ [Dep || Dep <- Deps, lists:keymember(0, 3, Locks)].
+
+unlock_higher_than(Level, Locks) -> unlock_higher_than(Level, Locks, [], []).
+
+unlock_higher_than(_, [], Locks, Unlocks) ->
+ {Locks, Unlocks};
+unlock_higher_than(Level, [App = {_,_,AppLevel} | Apps], Locks, Unlocks) ->
+ if AppLevel > Level -> unlock_higher_than(Level, Apps, Locks, [App | Unlocks]);
+ AppLevel =< Level -> unlock_higher_than(Level, Apps, [App | Locks], Unlocks)
+ end.
+
+info_useless(Old, New) ->
+ [?INFO("App ~ts is no longer needed and can be deleted.", [Name])
+ || Name <- Old,
+ not lists:member(Name, New)],
+ ok.
+
diff --git a/src/rebar_state.erl b/src/rebar_state.erl
index 29b7c3f..70aba51 100644
--- a/src/rebar_state.erl
+++ b/src/rebar_state.erl
@@ -89,8 +89,10 @@ new(ParentState, Config, Dir) ->
Opts = ParentState#state_t.opts,
LocalOpts = case rebar_config:consult_file(filename:join(Dir, ?LOCK_FILE)) of
[D] ->
- LockedDeps = [X || X <- D, element(3, X) =:= 0],
- dict:from_list([{{locks, default}, LockedDeps}, {{deps, default}, D} | Config]);
+ %% We want the top level deps only from the lock file.
+ %% This ensures deterministic overrides for configs.
+ Deps = [X || X <- D, element(3, X) =:= 0],
+ dict:from_list([{{locks, default}, D}, {{deps, default}, Deps} | Config]);
_ ->
D = proplists:get_value(deps, Config, []),
dict:from_list([{{deps, default}, D} | Config])
@@ -133,6 +135,8 @@ current_profiles(#state_t{current_profiles=Profiles}) ->
lock(#state_t{lock=Lock}) ->
Lock.
+lock(State=#state_t{}, Apps) when is_list(Apps) ->
+ State#state_t{lock=Apps};
lock(State=#state_t{lock=Lock}, App) ->
State#state_t{lock=[App | Lock]}.
diff --git a/test/mock_git_resource.erl b/test/mock_git_resource.erl
index 00f0a03..d2f0207 100644
--- a/test/mock_git_resource.erl
+++ b/test/mock_git_resource.erl
@@ -54,11 +54,13 @@ mock_lock(_) ->
%% @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, []),
+ ToUpdate = proplists:get_value(upgrade, Opts, []),
+ ct:pal("TOUp: ~p", [ToUpdate]),
meck:expect(
?MOD, needs_update,
fun(_Dir, {git, Url, _Ref}) ->
App = app(Url),
+% ct:pal("Needed update? ~p (~p) -> ~p", [App, {Url,_Ref}, lists:member(App, ToUpdate)]),
lists:member(App, ToUpdate)
end).
diff --git a/test/rebar_deps_SUITE.erl b/test/rebar_deps_SUITE.erl
index d67efe4..8407b55 100644
--- a/test/rebar_deps_SUITE.erl
+++ b/test/rebar_deps_SUITE.erl
@@ -1,4 +1,3 @@
-%%% TODO: check that warnings are appearing
-module(rebar_deps_SUITE).
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
diff --git a/test/rebar_test_utils.erl b/test/rebar_test_utils.erl
index 6095d6d..96200a6 100644
--- a/test/rebar_test_utils.erl
+++ b/test/rebar_test_utils.erl
@@ -53,7 +53,9 @@ run_and_check(Config, RebarConfig, Command, Expect) ->
?assertEqual({error, Reason}, Res);
{ok, Expected} ->
{ok, _} = Res,
- check_results(AppDir, Expected)
+ check_results(AppDir, Expected);
+ return ->
+ Res
end.
%% @doc Creates a dummy application including:
@@ -101,6 +103,8 @@ create_random_vsn() ->
check_results(AppDir, Expected) ->
BuildDir = filename:join([AppDir, "_build", "lib"]),
CheckoutsDir = filename:join([AppDir, "_checkouts"]),
+ LockFile = filename:join([AppDir, "rebar.lock"]),
+ Locks = lists:flatten(rebar_config:consult_file(LockFile)),
Apps = rebar_app_discover:find_apps([AppDir]),
InvalidApps = rebar_app_discover:find_apps([AppDir], invalid),
ValidApps = rebar_app_discover:find_apps([AppDir], valid),
@@ -151,6 +155,18 @@ check_results(AppDir, Expected) ->
?assertEqual(iolist_to_binary(Vsn),
iolist_to_binary(rebar_app_info:original_vsn(App)))
end
+ ; ({lock, Name}) ->
+ ct:pal("Name: ~p", [Name]),
+ ?assertNotEqual(false, lists:keyfind(iolist_to_binary(Name), 1, Locks))
+ ; ({lock, Name, Vsn}) ->
+ ct:pal("Name: ~p, Vsn: ~p", [Name, Vsn]),
+ case lists:keyfind(iolist_to_binary(Name), 1, Locks) of
+ false ->
+ error({lock_not_found, Name});
+ {_LockName, {_, _, {ref, LockVsn}}, _} ->
+ ?assertEqual(iolist_to_binary(Vsn),
+ iolist_to_binary(LockVsn))
+ end
end, Expected).
write_src_file(Dir, Name) ->
diff --git a/test/rebar_upgrade_SUITE.erl b/test/rebar_upgrade_SUITE.erl
new file mode 100644
index 0000000..39b9687
--- /dev/null
+++ b/test/rebar_upgrade_SUITE.erl
@@ -0,0 +1,475 @@
+-module(rebar_upgrade_SUITE).
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-compile(export_all).
+
+all() -> [{group, git}].%, {group, pkg}].
+
+groups() ->
+ [{all, [], [top_a, top_b, top_c, top_d1, top_d2, top_e,
+ pair_a, pair_b, pair_ab, pair_c, pair_all,
+ triplet_a, triplet_b, triplet_c,
+ tree_a, tree_b, tree_c, tree_c2, tree_ac, tree_all,
+ delete_d]},
+ {git, [], [{group, all}]},
+ {pkg, [], [{group, all}]}].
+
+init_per_suite(Config) ->
+ application:start(meck),
+ Config.
+
+end_per_suite(_Config) ->
+ application:stop(meck).
+
+init_per_group(git, Config) ->
+ [{deps_type, git} | Config];
+init_per_group(pkg, Config) ->
+ [{deps_type, pkg} | Config];
+init_per_group(_, Config) ->
+ Config.
+
+end_per_group(_, Config) ->
+ Config.
+
+init_per_testcase(Case, Config) ->
+ DepsType = ?config(deps_type, Config),
+ {Deps, UpDeps, ToUp, Expectations} = upgrades(Case),
+ Expanded = expand_deps(DepsType, Deps),
+ UpExpanded = expand_deps(DepsType, UpDeps),
+ [{expected, normalize_unlocks(Expectations)},
+ {mock, fun() -> mock_deps(DepsType, Expanded, []) end},
+ {mock_update, fun() -> mock_deps(DepsType, UpExpanded, ToUp) end}
+ | setup_project(Case, Config, Expanded, UpExpanded)].
+
+end_per_testcase(_, Config) ->
+ meck:unload(),
+ Config.
+
+setup_project(Case, Config0, Deps, UpDeps) ->
+ DepsType = ?config(deps_type, Config0),
+ Config = rebar_test_utils:init_rebar_state(
+ Config0,
+ atom_to_list(Case)++"_"++atom_to_list(DepsType)++"_"
+ ),
+ AppDir = ?config(apps, Config),
+ rebar_test_utils:create_app(AppDir, "Root", "0.0.0", [kernel, stdlib]),
+ TopDeps = top_level_deps(Deps),
+ RebarConf = rebar_test_utils:create_config(AppDir, [{deps, TopDeps}]),
+ [{rebarconfig, RebarConf},
+ {next_top_deps, top_level_deps(UpDeps)} | Config].
+
+
+upgrades(top_a) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"A", [{"A","1"}, "B", "C", {"D","3"}]}};
+upgrades(top_b) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"B", {error, {rebar_prv_upgrade, {transitive_dependency, <<"B">>}}}}};
+upgrades(top_c) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"C", {error, {rebar_prv_upgrade, {transitive_dependency, <<"C">>}}}}};
+upgrades(top_d1) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"D", {error, {rebar_prv_upgrade, {transitive_dependency, <<"D">>}}}}};
+upgrades(top_d2) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"D", {error, {rebar_prv_upgrade, {transitive_dependency, <<"D">>}}}}};
+upgrades(top_e) ->
+ %% Original tree
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Updated tree
+ [{"A", "1", [{"B", [{"D", "3", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ %% Modified apps, gobally
+ ["A","B","D"],
+ %% upgrade vs. new tree
+ {"E", {error, {rebar_prv_upgrade, {unknown_dependency, <<"E">>}}}}};
+upgrades(pair_a) ->
+ {[{"A", "1", [{"C", "1", []}]},
+ {"B", "1", [{"D", "1", []}]}
+ ],
+ [{"A", "2", [{"C", "2", []}]},
+ {"B", "2", [{"D", "2", []}]}
+ ],
+ ["A","B","C","D"],
+ {"A", [{"A","2"},{"C","2"},{"B","1"},{"D","1"}]}};
+upgrades(pair_b) ->
+ {[{"A", "1", [{"C", "1", []}]},
+ {"B", "1", [{"D", "1", []}]}
+ ],
+ [{"A", "2", [{"C", "2", []}]},
+ {"B", "2", [{"D", "2", []}]}
+ ],
+ ["A","B","C","D"],
+ {"B", [{"A","1"},{"C","1"},{"B","2"},{"D","2"}]}};
+upgrades(pair_ab) ->
+ {[{"A", "1", [{"C", "1", []}]},
+ {"B", "1", [{"D", "1", []}]}
+ ],
+ [{"A", "2", [{"C", "2", []}]},
+ {"B", "2", [{"D", "2", []}]}
+ ],
+ ["A","B","C","D"],
+ {"A,B", [{"A","2"},{"C","2"},{"B","2"},{"D","2"}]}};
+upgrades(pair_c) ->
+ {[{"A", "1", [{"C", "1", []}]},
+ {"B", "1", [{"D", "1", []}]}
+ ],
+ [{"A", "2", [{"C", "2", []}]},
+ {"B", "2", [{"D", "2", []}]}
+ ],
+ ["A","B","C","D"],
+ {"C", {error, {rebar_prv_upgrade, {transitive_dependency, <<"C">>}}}}};
+upgrades(pair_all) ->
+ {[{"A", "1", [{"C", "1", []}]},
+ {"B", "1", [{"D", "1", []}]}
+ ],
+ [{"A", "2", [{"C", "2", []}]},
+ {"B", "2", [{"D", "2", []}]}
+ ],
+ ["A","B","C","D"],
+ {"", [{"A","2"},{"C","2"},{"B","2"},{"D","2"}]}};
+upgrades(triplet_a) ->
+ {[{"A", "1", [{"D",[]},
+ {"E","3",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "0", [{"H","3",[]},
+ {"I",[]}]}],
+ [{"A", "1", [{"D",[]},
+ {"E","2",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H","4",[]},
+ {"I",[]}]}],
+ ["A","C","E","H"],
+ {"A", [{"A","1"}, "D", {"E","2"},
+ {"B","1"}, {"F","1"}, "G",
+ {"C","0"}, {"H","3"}, "I"]}};
+upgrades(triplet_b) ->
+ {[{"A", "1", [{"D",[]},
+ {"E","3",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "0", [{"H","3",[]},
+ {"I",[]}]}],
+ [{"A", "1", [{"D",[]},
+ {"E","2",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H","4",[]},
+ {"I",[]}]}],
+ ["A","C","E","H"],
+ {"B", [{"A","1"}, "D", {"E","3"},
+ {"B","1"}, {"F","1"}, "G",
+ {"C","0"}, {"H","3"}, "I"]}};
+upgrades(triplet_c) ->
+ {[{"A", "1", [{"D",[]},
+ {"E","3",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "0", [{"H","3",[]},
+ {"I",[]}]}],
+ [{"A", "1", [{"D",[]},
+ {"E","2",[]}]},
+ {"B", "1", [{"F","1",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H","4",[]},
+ {"I",[]}]}],
+ ["A","C","E","H"],
+ {"C", [{"A","1"}, "D", {"E","3"},
+ {"B","1"}, {"F","1"}, "G",
+ {"C","1"}, {"H","4"}, "I"]}};
+upgrades(tree_a) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]}]}
+ ],
+ ["C"],
+ {"A", [{"A","1"}, "D", "J", "E",
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H", {"I","2"}]}};
+upgrades(tree_b) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]}]}
+ ],
+ ["C"],
+ {"B", [{"A","1"}, "D", "J", "E",
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H", {"I","2"}]}};
+upgrades(tree_c) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]}]}
+ ],
+ ["C","I"],
+ {"C", [{"A","1"}, "D", "J", "E", {"I","1"},
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H"]}};
+upgrades(tree_c2) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[{"K",[]}]},
+ {"I","2",[]}]}
+ ],
+ ["C", "H"],
+ {"C", [{"A","1"}, "D", "J", "E",
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H", {"I", "2"}, "K"]}};
+upgrades(tree_ac) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]}]}
+ ],
+ ["C","I"],
+ {"C, A", [{"A","1"}, "D", "J", "E", {"I","1"},
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H"]}};
+upgrades(tree_all) ->
+ {[{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]},
+ {"I","2",[]}]}
+ ],
+ [{"A", "1", [{"D",[{"J",[]}]},
+ {"E",[{"I","1",[]}]}]},
+ {"B", "1", [{"F",[]},
+ {"G",[]}]},
+ {"C", "1", [{"H",[]}]}
+ ],
+ ["C","I"],
+ {"", [{"A","1"}, "D", "J", "E", {"I","1"},
+ {"B","1"}, "F", "G",
+ {"C","1"}, "H"]}};
+upgrades(delete_d) ->
+ {[{"A", "1", [{"B", [{"D", "1", []}]},
+ {"C", [{"D", "2", []}]}]}
+ ],
+ [{"A", "2", [{"B", []},
+ {"C", []}]}
+ ],
+ ["A","B", "C"],
+ %% upgrade vs. new tree
+ {"", [{"A","2"}, "B", "C"]}}.
+
+%% TODO: add a test that verifies that unlocking files and then
+%% running the upgrade code is enough to properly upgrade things.
+
+top_level_deps([]) -> [];
+top_level_deps([{{Name, Vsn, Ref}, _} | Deps]) ->
+ [{list_to_atom(Name), Vsn, Ref} | top_level_deps(Deps)];
+top_level_deps([{{pkg, Name, Vsn, _URL}, _} | Deps]) ->
+ [{list_to_atom(Name), Vsn} | top_level_deps(Deps)].
+
+mock_deps(git, Deps, Upgrades) ->
+ catch mock_git_resource:unmock(),
+ mock_git_resource:mock([{deps, flat_deps(Deps)}, {upgrade, Upgrades}]);
+mock_deps(pkg, Deps, Upgrades) ->
+ catch mock_pkg_resource:unmock(),
+ mock_pkg_resource:mock([{pkgdeps, flat_pkgdeps(Deps)}, {upgrade, Upgrades}]).
+
+flat_deps([]) -> [];
+flat_deps([{{Name,_Vsn,Ref}, Deps} | Rest]) ->
+ [{{Name,vsn_from_ref(Ref)}, top_level_deps(Deps)}]
+ ++
+ flat_deps(Deps)
+ ++
+ flat_deps(Rest).
+
+vsn_from_ref({git, _, {_, Vsn}}) -> Vsn;
+vsn_from_ref({git, _, Vsn}) -> Vsn.
+
+flat_pkgdeps([]) -> [];
+flat_pkgdeps([{{pkg, Name, Vsn, _Url}, Deps} | Rest]) ->
+ [{{iolist_to_binary(Name),iolist_to_binary(Vsn)}, top_level_deps(Deps)}]
+ ++
+ flat_pkgdeps(Deps)
+ ++
+ flat_pkgdeps(Rest).
+
+expand_deps(_, []) -> [];
+expand_deps(git, [{Name, Deps} | Rest]) ->
+ Dep = {Name, ".*", {git, "https://example.org/user/"++Name++".git", "master"}},
+ [{Dep, expand_deps(git, Deps)} | expand_deps(git, Rest)];
+expand_deps(git, [{Name, Vsn, Deps} | Rest]) ->
+ Dep = {Name, Vsn, {git, "https://example.org/user/"++Name++".git", {tag, Vsn}}},
+ [{Dep, expand_deps(git, Deps)} | expand_deps(git, Rest)];
+expand_deps(pkg, [{Name, Deps} | Rest]) ->
+ Dep = {pkg, Name, "0.0.0", "https://example.org/user/"++Name++".tar.gz"},
+ [{Dep, expand_deps(pkg, Deps)} | expand_deps(pkg, Rest)];
+expand_deps(pkg, [{Name, Vsn, Deps} | Rest]) ->
+ Dep = {pkg, Name, Vsn, "https://example.org/user/"++Name++".tar.gz"},
+ [{Dep, expand_deps(pkg, Deps)} | expand_deps(pkg, Rest)].
+
+normalize_unlocks({App, Locks}) ->
+ {iolist_to_binary(App),
+ normalize_unlocks_expect(Locks)};
+normalize_unlocks({App, Vsn, Locks}) ->
+ {iolist_to_binary(App), iolist_to_binary(Vsn),
+ normalize_unlocks_expect(Locks)}.
+
+normalize_unlocks_expect({error, Reason}) ->
+ {error, Reason};
+normalize_unlocks_expect([]) ->
+ [];
+normalize_unlocks_expect([{App,Vsn} | Rest]) ->
+ [{dep, App, Vsn},
+ {lock, App, Vsn}
+ | normalize_unlocks_expect(Rest)];
+normalize_unlocks_expect([App | Rest]) ->
+ [{dep, App},
+ {lock, App} | normalize_unlocks_expect(Rest)].
+
+top_a(Config) -> run(Config).
+top_b(Config) -> run(Config).
+top_c(Config) -> run(Config).
+top_d1(Config) -> run(Config).
+top_d2(Config) -> run(Config).
+top_e(Config) -> run(Config).
+
+pair_a(Config) -> run(Config).
+pair_b(Config) -> run(Config).
+pair_ab(Config) -> run(Config).
+pair_c(Config) -> run(Config).
+pair_all(Config) -> run(Config).
+
+triplet_a(Config) -> run(Config).
+triplet_b(Config) -> run(Config).
+triplet_c(Config) -> run(Config).
+
+tree_a(Config) -> run(Config).
+tree_b(Config) -> run(Config).
+tree_c(Config) -> run(Config).
+tree_c2(Config) -> run(Config).
+tree_ac(Config) -> run(Config).
+tree_all(Config) -> run(Config).
+
+delete_d(Config) ->
+ meck:new(rebar_log, [no_link, passthrough]),
+ run(Config),
+ Infos = [{Str, Args}
+ || {_, {rebar_log, log, [info, Str, Args]}, _} <- meck:history(rebar_log)],
+ meck:unload(rebar_log),
+ ?assertNotEqual([],
+ [1 || {"App ~ts is no longer needed and can be deleted.",
+ [<<"D">>]} <- Infos]).
+run(Config) ->
+ apply(?config(mock, Config), []),
+ {ok, RebarConfig} = file:consult(?config(rebarconfig, Config)),
+ %% Install dependencies before re-mocking for an upgrade
+ rebar_test_utils:run_and_check(Config, RebarConfig, ["lock"], {ok, []}),
+ {App, Unlocks} = ?config(expected, Config),
+ ct:pal("Upgrades: ~p -> ~p", [App, Unlocks]),
+ Expectation = case Unlocks of
+ {error, Term} -> {error, Term};
+ _ -> {ok, Unlocks}
+ end,
+ apply(?config(mock_update, Config), []),
+ NewRebarConf = rebar_test_utils:create_config(?config(apps, Config),
+ [{deps, ?config(next_top_deps, Config)}]),
+ {ok, NewRebarConfig} = file:consult(NewRebarConf),
+ rebar_test_utils:run_and_check(
+ Config, NewRebarConfig, ["upgrade", App], Expectation
+ ).
+