aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric <[email protected]>2012-09-12 16:27:41 -0500
committerEric <[email protected]>2012-09-12 16:27:41 -0500
commit2311bc5cbe06dcf2cb03d9b0a93392e1d9095bbb (patch)
tree6356c02752681f522fae2d843d0a97151e6af7bf
parent152a385e6cc6de064e2a0f1b1ef3061826598180 (diff)
downloadrelx-2311bc5cbe06dcf2cb03d9b0a93392e1d9095bbb.tar.gz
relx-2311bc5cbe06dcf2cb03d9b0a93392e1d9095bbb.tar.bz2
relx-2311bc5cbe06dcf2cb03d9b0a93392e1d9095bbb.zip
support OTP App discovery in the system
-rw-r--r--.gitignore3
-rw-r--r--src/rcl_app_info.erl134
-rw-r--r--src/rcl_prv_discover.erl175
-rw-r--r--src/rcl_state.erl120
-rw-r--r--test/rclt_discover_SUITE.erl194
5 files changed, 609 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index 61eb48f..ed62865 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@ ebin/*
relcool
# This is a generated file that should be ignored
src/rcl_goal.erl
-logs \ No newline at end of file
+logs
+test/*_data \ No newline at end of file
diff --git a/src/rcl_app_info.erl b/src/rcl_app_info.erl
new file mode 100644
index 0000000..eaf3302
--- /dev/null
+++ b/src/rcl_app_info.erl
@@ -0,0 +1,134 @@
+%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
+%%%---------------------------------------------------------------------------
+%%% @author Eric Merritt <[email protected]>
+%%% @copyright (C) 2012 Erlware, LLC.
+%%%
+%%% @doc This module represents useful, relevant information about an
+%%% application. The relevant information is.
+%%%
+%%% <ul>
+%%% <li> Name - The application name as an atom </li>
+%%% <li> Vsn - The application version as a list </li>
+%%% <li> The root directory of the application. The directory that contains the
+%%% ebin/src/priv etc </li>
+%%% <li> Active Deps - The Active or 'application' dependencies of the OTP
+%%% App. That is the things in the 'applications' property of the application
+%%% metadata </li>
+%%% <li> Library Deps - The Inactive or Library dependencies of the ATP
+%%% app. That is the things in the 'included_applications property of the
+%%% application metadata.
+%%% </ul>
+%%%
+-module(rcl_app_info).
+
+-export([new/0,
+ new/5,
+ name/1,
+ name/2,
+ vsn/1,
+ vsn/2,
+ dir/1,
+ dir/2,
+ active_deps/1,
+ active_deps/2,
+ library_deps/1,
+ library_deps/2]).
+
+-export_type([t/0]).
+
+-record(app_info_t, {name :: atom(),
+ vsn :: string(),
+ dir :: file:name(),
+ active_deps :: [atom()],
+ library_deps :: [atom()]}).
+
+%%============================================================================
+%% types
+%%============================================================================
+-opaque t() :: record(app_info_t).
+
+%%============================================================================
+%% API
+%% ============================================================================
+%% @doc Build a new, empty, app info value. This is not of a lot of use and you
+%% probably wont be doing this much.
+-spec new() -> t().
+new() ->
+ #app_info_t{}.
+
+%% @doc build a complete version of the app info with all fields set.
+-spec new(atom(), string(), file:name(), [atom()], [atom()]) -> t().
+new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps)
+ when erlang:is_atom(AppName),
+ erlang:is_list(Vsn),
+ erlang:is_list(Dir),
+ erlang:is_list(ActiveDeps),
+ erlang:is_list(LibraryDeps) ->
+ #app_info_t{name=AppName, vsn=Vsn, dir=Dir,
+ active_deps=ActiveDeps,
+ library_deps=LibraryDeps}.
+
+-spec name(t()) -> atom().
+name(#app_info_t{name=Name}) ->
+ Name.
+
+-spec name(t(), atom()) -> t().
+name(AppInfo=#app_info_t{}, AppName)
+ when erlang:is_atom(AppName) ->
+ AppInfo#app_info_t{name=AppName}.
+
+-spec vsn(t()) -> string().
+vsn(#app_info_t{vsn=Vsn}) ->
+ Vsn.
+
+-spec vsn(t(), string()) -> t().
+vsn(AppInfo=#app_info_t{}, AppVsn)
+ when erlang:is_list(AppVsn) ->
+ AppInfo#app_info_t{vsn=AppVsn}.
+
+-spec dir(t()) -> file:name().
+dir(#app_info_t{dir=Dir}) ->
+ Dir.
+-spec dir(t(), file:name()) -> t().
+dir(AppInfo=#app_info_t{}, Dir) ->
+ AppInfo#app_info_t{dir=Dir}.
+
+-spec active_deps(t()) -> [atom()].
+active_deps(#app_info_t{active_deps=Deps}) ->
+ Deps.
+-spec active_deps(t(), [atom()]) -> t().
+active_deps(AppInfo=#app_info_t{}, ActiveDeps)
+ when erlang:is_list(ActiveDeps) ->
+ AppInfo#app_info_t{active_deps=ActiveDeps}.
+
+-spec library_deps(t()) -> [atom()].
+library_deps(#app_info_t{library_deps=Deps}) ->
+ Deps.
+
+-spec library_deps(t(), [atom()]) -> t().
+library_deps(AppInfo=#app_info_t{}, LibraryDeps)
+ when erlang:is_list(LibraryDeps) ->
+ AppInfo#app_info_t{library_deps=LibraryDeps}.
+
+
+
+
+
+%%%===================================================================
+%%% Test Functions
+%%%===================================================================
diff --git a/src/rcl_prv_discover.erl b/src/rcl_prv_discover.erl
new file mode 100644
index 0000000..e9a6dea
--- /dev/null
+++ b/src/rcl_prv_discover.erl
@@ -0,0 +1,175 @@
+%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
+%%%---------------------------------------------------------------------------
+%%% @author Eric Merritt <[email protected]>
+%%% @copyright (C) 2012 Erlware, LLC.
+%%%
+%%% @doc This provider uses the lib_dir setting of the state. It searches the
+%%% Lib Dirs looking for all OTP Applications that are available. When it finds
+%%% those OTP Applications it loads the information about them and adds them to
+%%% the state of available apps. This implements the rcl_provider behaviour.
+-module(rcl_prv_discover).
+
+-behaviour(rcl_provider).
+
+-export([init/1,
+ do/1,
+ format/1]).
+
+%%============================================================================
+%% API
+%%============================================================================
+-spec init(rcl_state:t()) -> {ok, rcl_state:t()}.
+init(State) ->
+ {ok, State}.
+
+-spec do(rcl_state:t()) -> {error, Reason::term()} | {ok, rcl_state:t()}.
+do(State) ->
+ LibDirs = rcl_state:lib_dirs(State),
+ AppMeta = lists:flatten(ec_plists:map(fun discover_dir/1, LibDirs)),
+ Errors = [case El of
+ {error, Ret} -> Ret;
+ _ -> El
+ end
+ || El <- AppMeta,
+ case El of
+ {error, _} ->
+ true;
+ _ ->
+ false
+ end],
+
+ lists:filter(fun({error, _}) -> true;
+ (_) -> false
+ end, AppMeta),
+ case Errors of
+ [] ->
+ {ok, rcl_state:available_apps(State, lists:flatten(AppMeta))};
+ _ ->
+ {error, Errors}
+ end.
+
+-spec format({error, [ErrorDetail::term()]}) -> iolist().
+format({error, ErrorDetails}) ->
+ [[format_detail(ErrorDetail), "\n"] || ErrorDetail <- ErrorDetails].
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+-spec format_detail(ErrorDetail::term()) -> iolist().
+format_detail({accessing, File, eaccess}) ->
+ io_lib:format("permission denied accessing file ~s", [File]);
+format_detail({accessing, File, Type}) ->
+ io_lib:format("error (~p) accessing file ~s", [Type, File]);
+format_detail({error, {no_beam_files, EbinDir}}) ->
+ io_lib:format("no beam files found in directory ~s", [EbinDir]);
+format_detail({error, {not_a_directory, EbinDir}}) ->
+ io_lib:format("~s is not a directory when it should be a directory", [EbinDir]);
+format_detail({error, {unable_to_load_app, AppDir, _}}) ->
+ io_lib:format("Unable to load the application metadata from ~s", [AppDir]);
+format_detail({error, {invalid_app_file, File}}) ->
+ io_lib:format("Application metadata file exists but is malformed: ~s",
+ [File]);
+format_detail({error, {unversioned_app, AppDir, _AppName}}) ->
+ io_lib:format("Application metadata exists but version is not available: ~s",
+ [AppDir]).
+
+-spec discover_dir(file:name()) ->
+ [rcl_app_info:t() | {error, Reason::term()}] |
+ rcl_app_info:t() | {error, Reason::term()}.
+discover_dir(File) ->
+ case filelib:is_dir(File) of
+ true ->
+ case file:list_dir(File) of
+ {error, Reason} ->
+ {error, {accessing, File, Reason}};
+ {ok, List} ->
+ ec_plists:map(fun discover_dir/1, [filename:join([File, Dir]) || Dir <- List])
+ end;
+ false ->
+ is_valid_otp_app(File)
+ end.
+
+-spec is_valid_otp_app(file:name()) -> [rcl_app_info:t() | {error, Reason::term()} | []].
+is_valid_otp_app(File) ->
+ %% Is this an ebin dir?
+ EbinDir = filename:dirname(File),
+ case filename:basename(EbinDir) of
+ "ebin" ->
+ case lists:suffix(".app", File) of
+ true ->
+ has_at_least_one_beam(EbinDir, File);
+ false ->
+ []
+ end;
+ _ ->
+ []
+ end.
+
+-spec has_at_least_one_beam(file:name(), file:filename()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+has_at_least_one_beam(EbinDir, File) ->
+ case file:list_dir(EbinDir) of
+ {ok, List} ->
+ case lists:any(fun(NFile) -> lists:suffix(".beam", NFile) end, List) of
+ true ->
+ gather_application_info(EbinDir, File);
+ false ->
+ {error, {no_beam_files, EbinDir}}
+ end;
+ _ ->
+ {error, {not_a_directory, EbinDir}}
+ end.
+
+-spec gather_application_info(file:name(), file:filename()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+gather_application_info(EbinDir, File) ->
+ AppDir = filename:dirname(EbinDir),
+ case file:consult(File) of
+ {ok, [{application, AppName, AppDetail}]} ->
+ get_vsn(AppDir, AppName, AppDetail);
+ {error, Reason} ->
+ {error, {unable_to_load_app, AppDir, Reason}};
+ _ ->
+ {error, {invalid_app_file, File}}
+ end.
+
+-spec get_vsn(file:name(), atom(), proplists:proplist()) ->
+ rcl_app_info:t() | {error, Reason::term()}.
+get_vsn(AppDir, AppName, AppDetail) ->
+ case proplists:get_value(vsn, AppDetail) of
+ undefined ->
+ {error, {unversioned_app, AppDir, AppName}};
+ AppVsn ->
+ get_deps(AppDir, AppName, AppVsn, AppDetail)
+ end.
+
+-spec get_deps(file:name(), atom(), string(), proplists:proplist()) ->
+ rcl_app_info:t().
+get_deps(AppDir, AppName, AppVsn, AppDetail) ->
+ ActiveApps = proplists:get_value(applications, AppDetail, []),
+ LibraryApps = proplists:get_value(included_applications, AppDetail, []),
+ rcl_app_info:new(AppName, AppVsn, AppDir, ActiveApps, LibraryApps).
+
+%%%===================================================================
+%%% Test Functions
+%%%===================================================================
+
+-ifndef(NOTEST).
+-include_lib("eunit/include/eunit.hrl").
+
+-endif.
diff --git a/src/rcl_state.erl b/src/rcl_state.erl
index 1b25ce3..c399b42 100644
--- a/src/rcl_state.erl
+++ b/src/rcl_state.erl
@@ -29,24 +29,47 @@
lib_dirs/1,
goals/1,
config_files/1,
+ providers/1,
+ providers/2,
+ add_release/2,
+ get_release/3,
+ releases/1,
+ default_release/1,
+ default_release/3,
+ available_apps/1,
+ available_apps/2,
format/1,
format/2]).
+
-export_type([t/0,
+ app_descriptor/0,
+ releases/0,
cmd_args/0]).
--record(?MODULE, {log :: rcl_log:t(),
+-record(state_t, {log :: rcl_log:t(),
output_dir :: file:name(),
lib_dirs=[] :: [file:name()],
config_files=[] :: [file:filename()],
- goals=[] :: [depsolver:constraint()]}).
+ goals=[] :: [depsolver:constraint()],
+ providers = [] :: [rcl_provider:t()],
+ available_apps = [] :: [app_descriptor()],
+ default_release :: {rcl_release:name(), rcl_release:vsn()},
+ releases :: ec_dictionary:dictionary({ReleaseName::atom(),
+ ReleaseVsn::string()},
+ rcl_release:t())}).
%%============================================================================
%% types
%%============================================================================
+-type app_descriptor() :: {rcl_release:app_name(), rcl_release:app_vsn(), file:name()}.
+
+-type releases() :: ec_dictionary:dictionary({rcl_release:name(),
+ rcl_release:vsn()},
+ rcl_release:t()).
-type cmd_args() :: proplists:proplist().
--opaque t() :: record(?MODULE).
+-opaque t() :: record(state_t).
%%============================================================================
%% API
@@ -54,40 +77,84 @@
%% @doc Create a new 'log level' for the system
-spec new(proplists:proplist(), [file:filename()]) -> t().
new(PropList, Targets) when erlang:is_list(PropList) ->
- #?MODULE{log = proplists:get_value(log, PropList, rcl_log:new(error)),
- output_dir=proplists:get_value(output_dir, PropList, ""),
- lib_dirs=proplists:get_value(lib_dirs, PropList, []),
- config_files=Targets,
- goals=proplists:get_value(goals, PropList, [])}.
+ State0 =
+ #state_t{log = proplists:get_value(log, PropList, rcl_log:new(error)),
+ output_dir=proplists:get_value(output_dir, PropList, ""),
+ lib_dirs=get_lib_dirs(proplists:get_value(lib_dirs, PropList, [])),
+ config_files=Targets,
+ goals=proplists:get_value(goals, PropList, []),
+ providers = [],
+ releases=ec_dictionary:new(ec_dict)},
+ create_logic_providers(State0).
%% @doc get the current log state for the system
-spec log(t()) -> rcl_log:t().
-log(#?MODULE{log=LogState}) ->
+log(#state_t{log=LogState}) ->
LogState.
-spec output_dir(t()) -> file:name().
-output_dir(#?MODULE{output_dir=OutDir}) ->
+output_dir(#state_t{output_dir=OutDir}) ->
OutDir.
-spec lib_dirs(t()) -> [file:name()].
-lib_dirs(#?MODULE{lib_dirs=LibDir}) ->
+lib_dirs(#state_t{lib_dirs=LibDir}) ->
LibDir.
-spec goals(t()) -> [depsolver:constraints()].
-goals(#?MODULE{goals=TS}) ->
+goals(#state_t{goals=TS}) ->
TS.
-spec config_files(t()) -> [file:filename()].
-config_files(#?MODULE{config_files=ConfigFiles}) ->
+config_files(#state_t{config_files=ConfigFiles}) ->
ConfigFiles.
+-spec providers(t()) -> [rcl_provider:t()].
+providers(#state_t{providers=Providers}) ->
+ Providers.
+
+-spec providers(t(), [rcl_provider:t()]) -> t().
+providers(M, NewProviders) ->
+ M#state_t{providers=NewProviders}.
+
+-spec add_release(t(), rcl_release:t()) -> t().
+add_release(M=#state_t{releases=Releases}, Release) ->
+ M#state_t{releases=ec_dictionary:add({rcl_release:name(Release),
+ rcl_release:vsn(Release)},
+ Release,
+ Releases)}.
+
+-spec get_release(t(), rcl_release:name(), rcl_release:vsn()) -> rcl_release:t().
+get_release(#state_t{releases=Releases}, Name, Vsn) ->
+ ec_dictionary:get({Name, Vsn}, Releases).
+
+-spec releases(t()) -> releases().
+releases(#state_t{releases=Releases}) ->
+ Releases.
+
+-spec default_release(t()) -> {rcl_release:name(), rcl_release:vsn()}.
+default_release(#state_t{default_release=Def}) ->
+ Def.
+
+-spec default_release(t(), rcl_release:name(), rcl_release:vsn()) -> t().
+default_release(M, Name, Vsn) ->
+ M#state_t{default_release={Name, Vsn}}.
+
+-spec available_apps(t()) -> [app_descriptor()].
+available_apps(#state_t{available_apps=Apps}) ->
+ Apps.
+
+-spec available_apps(t(), [app_descriptor()]) -> t().
+available_apps(M, NewApps) ->
+ M#state_t{available_apps=NewApps}.
+
-spec format(t()) -> iolist().
format(Mod) ->
format(Mod, 0).
-spec format(t(), non_neg_integer()) -> iolist().
-format(#?MODULE{log=LogState, output_dir=OutDir, lib_dirs=LibDirs,
- goals=Goals, config_files=ConfigFiles},
+format(#state_t{log=LogState, output_dir=OutDir, lib_dirs=LibDirs,
+ goals=Goals, config_files=ConfigFiles,
+ providers=Providers},
Indent) ->
[rcl_util:indent(Indent),
<<"state:\n">>,
@@ -98,7 +165,28 @@ format(#?MODULE{log=LogState, output_dir=OutDir, lib_dirs=LibDirs,
[[rcl_util:indent(Indent + 2), depsolver:format_constraint(Goal), ",\n"] || Goal <- Goals],
rcl_util:indent(Indent + 1), "output_dir: ", OutDir, "\n",
rcl_util:indent(Indent + 1), "lib_dirs: \n",
- [[rcl_util:indent(Indent + 2), LibDir, ",\n"] || LibDir <- LibDirs]].
+ [[rcl_util:indent(Indent + 2), LibDir, ",\n"] || LibDir <- LibDirs],
+ rcl_util:indent(Indent + 1), "providers: \n",
+ [[rcl_util:indent(Indent + 2), rcl_provider:format(Provider), ",\n"] || Provider <- Providers]].
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+-spec get_lib_dirs([file:name()]) -> [file:name()].
+get_lib_dirs(CmdDirs) ->
+ case os:getenv("ERL_LIBS") of
+ false ->
+ CmdDirs;
+ EnvString ->
+ [Lib || Lib <- re:split(EnvString, ":|;"),
+ filelib:is_dir(Lib)] ++ CmdDirs
+ end.
+
+-spec create_logic_providers(t()) -> t().
+create_logic_providers(State0) ->
+ {ConfigProvider, {ok, State1}} = rcl_provider:new(rcl_prv_config, State0),
+ {DiscoveryProvider, {ok, State2}} = rcl_provider:new(rcl_prv_discover, State1),
+ State2#state_t{providers=[ConfigProvider, DiscoveryProvider]}.
%%%===================================================================
%%% Test Functions
diff --git a/test/rclt_discover_SUITE.erl b/test/rclt_discover_SUITE.erl
new file mode 100644
index 0000000..718fd03
--- /dev/null
+++ b/test/rclt_discover_SUITE.erl
@@ -0,0 +1,194 @@
+%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you under the Apache License,
+%%% Version 2.0 (the "License"); you may not use this file
+%%% except in compliance with the License. You may obtain
+%%% a copy of the License at
+%%%
+%%% http://www.apache.org/licenses/LICENSE-2.0
+%%%
+%%% Unless required by applicable law or agreed to in writing,
+%%% software distributed under the License is distributed on an
+%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%%% KIND, either express or implied. See the License for the
+%%% specific language governing permissions and limitations
+%%% under the License.
+%%%-------------------------------------------------------------------
+%%% @author Eric Merrit <[email protected]>
+%%% @copyright (C) 2012, Eric Merrit
+-module(rclt_discover_SUITE).
+
+-export([suite/0,
+ init_per_suite/1,
+ end_per_suite/1,
+ init_per_testcase/2,
+ all/0,
+ normal_case/1,
+ no_beam_case/1,
+ bad_ebin_case/1]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+suite() ->
+ [{timetrap,{seconds,30}}].
+
+init_per_suite(Config) ->
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_testcase(_, Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ LibDir1 = filename:join([DataDir, create_random_name("lib_dir1_")]),
+ LibDir2 = filename:join([DataDir, create_random_name("lib_dir2_")]),
+ ok = rcl_util:mkdir_p(LibDir1),
+ ok = rcl_util:mkdir_p(LibDir2),
+ State = rcl_state:new([{lib_dirs, [LibDir1, LibDir2]}], []),
+ [{lib1, LibDir1},
+ {lib2, LibDir2},
+ {state, State} | Config].
+
+
+all() ->
+ [normal_case, no_beam_case, bad_ebin_case].
+
+normal_case(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ Apps1 = [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn)
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ LibDir2 = proplists:get_value(lib2, Config),
+ Apps2 = [(fun({Name, Vsn}) ->
+ create_app(LibDir2, Name, Vsn)
+ end)(App)
+ || App <-
+ [{create_random_name("lib_app2_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+ State0 = proplists:get_value(state, Config),
+ {DiscoverProvider, {ok, State1}} = rcl_provider:new(rcl_prv_discover, State0),
+ {ok, State2} = rcl_provider:do(DiscoverProvider, State1),
+ lists:foreach(fun(App) ->
+ ?assertMatch(true, lists:member(App, rcl_state:available_apps(State2)))
+ end, Apps1),
+
+ lists:foreach(fun(App) ->
+ ?assertMatch(true, lists:member(App, rcl_state:available_apps(State2)))
+ end, Apps2),
+ Length = erlang:length(Apps2) +
+ erlang:length(Apps2),
+ ?assertMatch(Length, erlang:length(rcl_state:available_apps(State2))).
+
+no_beam_case(Config) ->
+ %% We silently ignore apps with no beams
+ LibDir1 = proplists:get_value(lib1, Config),
+ _Apps1 = [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn)
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ LibDir2 = proplists:get_value(lib2, Config),
+ _Apps2 = [(fun({Name, Vsn}) ->
+ create_app(LibDir2, Name, Vsn)
+ end)(App)
+ || App <-
+ [{create_random_name("lib_app2_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+ BadName = create_random_name("error_bad"),
+ BadVsn = create_random_vsn(),
+ AppDir = filename:join([LibDir2, BadName]),
+ write_app_file(AppDir, BadName, BadVsn),
+ State0 = proplists:get_value(state, Config),
+ {DiscoverProvider, {ok, State1}} = rcl_provider:new(rcl_prv_discover, State0),
+ EbinDir = filename:join([LibDir2, BadName, "ebin"]),
+ %% Ignore apps that do not contain any beam files
+ ?assertMatch({error, [{no_beam_files, EbinDir}]},
+ rcl_provider:do(DiscoverProvider, State1)).
+
+bad_ebin_case(Config) ->
+ LibDir1 = proplists:get_value(lib1, Config),
+ _Apps1 = [(fun({Name, Vsn}) ->
+ create_app(LibDir1, Name, Vsn)
+ end)(App)
+ ||
+ App <-
+ [{create_random_name("lib_app1_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+
+ LibDir2 = proplists:get_value(lib2, Config),
+ _Apps2 = [(fun({Name, Vsn}) ->
+ create_app(LibDir2, Name, Vsn)
+ end)(App)
+ || App <-
+ [{create_random_name("lib_app2_"), create_random_vsn()}
+ || _ <- lists:seq(1, 100)]],
+ BadName = create_random_name("error_bad"),
+ BadVsn = create_random_vsn(),
+ AppDir = filename:join([LibDir2, BadName]),
+ Filename = filename:join([AppDir, "ebin", BadName ++ ".app"]),
+ io:format("BAD -> ~p~n", [Filename]),
+ ok = filelib:ensure_dir(Filename),
+ ok = ec_file:write_term(Filename, get_bad_app_metadata(BadName, BadVsn)),
+ write_beam_file(AppDir, BadName),
+ State0 = proplists:get_value(state, Config),
+ {DiscoverProvider, {ok, State1}} = rcl_provider:new(rcl_prv_discover, State0),
+
+ %% Ignore apps that do not contain any beam files
+ ?assertMatch({error, [{invalid_app_file, Filename}]},
+ rcl_provider:do(DiscoverProvider, State1)).
+
+
+
+%%% API
+%%%===================================================================
+create_app(Dir, Name, Vsn) ->
+ AppDir = filename:join([Dir, Name]),
+ write_app_file(AppDir, Name, Vsn),
+ write_beam_file(AppDir, Name),
+ rcl_app_info:new(erlang:list_to_atom(Name), Vsn, AppDir,
+ [kernel, stdlib], []).
+
+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_app_file(Dir, Name, Version) ->
+ Filename = filename:join([Dir, "ebin", Name ++ ".app"]),
+ ok = filelib:ensure_dir(Filename),
+ ok = ec_file:write_term(Filename, get_app_metadata(Name, Version)).
+
+get_app_metadata(Name, Vsn) ->
+ {application, erlang:list_to_atom(Name),
+ [{description, ""},
+ {vsn, Vsn},
+ {modules, []},
+ {applications, [kernel, stdlib]}]}.
+
+get_bad_app_metadata(Name, Vsn) ->
+ ["{application, ", Name, ",
+ [{description, \"\"},
+ {vsn, \"", Vsn, "\"},
+ {modules, [],
+ {applications, [kernel, stdlib]}]}."].
+
+
+create_random_name(Name) ->
+ random:seed(erlang:now()),
+ Name ++ erlang:integer_to_list(random:uniform(1000000)).
+
+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))]).