From 9aab4d7a16fba5dfcf4d60b58a05cc765eca3335 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 8 Sep 2012 11:15:49 -0500 Subject: full argument parsing and management capability --- .gitignore | 1 + Makefile | 38 ++++++++++---- README.md | 4 +- rebar.config | 8 +-- src/rcl_cmd_args.erl | 125 ++++++++++++++++++++++++++++++++++++++++++++ src/rcl_goal_utils.erl | 4 +- src/rcl_log.erl | 87 ++++++++++++++++++++---------- src/rcl_state.erl | 56 +++++++++++++++++--- src/rcl_util.erl | 9 +++- src/relcool.erl | 102 ++++++++++++++++++++++++++++++++++-- test/rclt_command_SUITE.erl | 100 +++++++++++++++++++++++++++++++++++ 11 files changed, 476 insertions(+), 58 deletions(-) create mode 100644 src/rcl_cmd_args.erl create mode 100644 test/rclt_command_SUITE.erl diff --git a/.gitignore b/.gitignore index 33a87e2..61eb48f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ebin/* relcool # This is a generated file that should be ignored src/rcl_goal.erl +logs \ No newline at end of file diff --git a/Makefile b/Makefile index db7d373..ef6fbe5 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,20 @@ # specific language governing permissions and limitations # under the License. # + + +ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/deps/*/ebin + +RELCOOL_PLT=$(CURDIR)/.relcool_plt + +# ============================================================================= +# Verify that the programs we need to run are installed on this system +# ============================================================================= ERL = $(shell which erl) -ERLFLAGS= -pa $(CURDIR)/.eunit -pa $(CURDIR)/ebin -pa $(CURDIR)/deps/**/ebin +ifeq ($(ERL),) +$(error "Erlang not available on this system") +endif REBAR=$(shell which rebar) @@ -24,11 +35,12 @@ ifeq ($(REBAR),) $(error "Rebar not available on this system") endif -RELCOOL_PLT=$(CURDIR)/.relcool_plt - -.PHONY: all compile doc clean eunit dialyzer typer shell distclean pdf get-deps escript +# ============================================================================= +# Rules to build the system +# ============================================================================= +.PHONY: all compile doc clean test dialyzer typer shell distclean pdf get-deps escript -all: compile eunit dialyzer +all: compile escript test dialyzer get-deps: $(REBAR) get-deps @@ -46,6 +58,11 @@ doc: eunit: compile $(REBAR) skip_deps=true eunit +ct: compile + $(REBAR) skip_deps=true ct + +test: compile eunit ct + $(RELCOOL_PLT): @echo Building local plt at $(RELCOOL_PLT) @echo @@ -53,7 +70,7 @@ $(RELCOOL_PLT): --apps erts kernel stdlib -r deps dialyzer: $(RELCOOL_PLT) - dialyzer --plt $(RELCOOL_PLT) -Wrace_conditions --src src + dialyzer --plt $(RELCOOL_PLT) -Wrace_conditions -I include -pa $(CURDIR)/ebin --src src typer: typer --plt $(RELCOOL_PLT) -r ./src @@ -71,8 +88,11 @@ pdf: pandoc README.md -o README.pdf clean: - $(REBAR) clean + - rm -rf $(CURDIR)/test/*.beam + - rm -rf $(CURDIR)/test/*_SUITE_data + - rm -rf $(CURDIR)/logs + $(REBAR) skip_deps=true clean distclean: clean - rm -rf $(RELCOOL_PLT) - rm -rvf $(CURDIR)/deps/* + - rm -rf $(RELCOOL_PLT) + - rm -rvf $(CURDIR)/deps/* diff --git a/README.md b/README.md index f2d3899..b38b860 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ additional specification information for releases. -v *STRING*, \--relvsn=*STRING* : Specify the version for the release --t *STRING*, \--target-spec *STRING* -: Specify a target constraint on the system. These are usually the OTP +-g *STRING*, \--goals *STRING* +: Specify a goal to the system. These are usually the OTP Apps that are part of the release -o *STRING*, \--output-dir *STRING* diff --git a/rebar.config b/rebar.config index e31e544..289db92 100644 --- a/rebar.config +++ b/rebar.config @@ -1,15 +1,15 @@ %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- {deps, [{neotoma, "", - {git, "https://github.com/seancribbs/neotoma.git", {tag, "1.5"}}}, + {git, "https://github.com/ericbmerritt/neotoma.git", {tag, "1.5.1"}}}, {erlware_commons, "", - {git, "https://github.com/ericbmerritt/erlware_commons.git", {tag, "v0.7.0"}}}, + {git, "https://github.com/ericbmerritt/erlware_commons.git", {tag, "v0.8.0"}}}, {getopt, "", {git, "https://github.com/jcomellas/getopt.git", {tag, "v0.5.1"}}}, {depsolver, "", - {git, "https://github.com/ericbmerritt/depsolver.git", {tag, "v0.0.2"}}}]}. + {git, "https://github.com/ericbmerritt/depsolver.git", {tag, "v0.1.0"}}}]}. {escript_incl_apps, - [getopt, depsolver]}. + [getopt, depsolver, erlware_commons]}. {erl_opts, [debug_info, warnings_as_errors, inline]}. diff --git a/src/rcl_cmd_args.erl b/src/rcl_cmd_args.erl new file mode 100644 index 0000000..454e762 --- /dev/null +++ b/src/rcl_cmd_args.erl @@ -0,0 +1,125 @@ +%% -*- 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 +%%% @copyright (C) 2012 Erlware, LLC. +%%% +%%% @doc Trivial utility file to help handle common tasks +-module(rcl_cmd_args). + +-export([args2state/1]). + +%%============================================================================ +%% types +%%============================================================================ + + +%%============================================================================ +%% API +%%============================================================================ +-spec args2state({error, Reason::term()} | {[getopt:option()], [string()]}) -> + {error, Reason::term()} | + {ok, {rcl_state:t(), [string()]}}. +args2state(Error={error, _}) -> + Error; +args2state({ok, {Opts, Targets}}) -> + RelName = proplists:get_value(relname, Opts, undefined), + RelVsn = proplists:get_value(relvsn, Opts, undefined), + case create_log(Opts, + [{relname, RelName}, + {relvsn, RelVsn}]) of + Error = {error, _} -> + Error; + {ok, CommandLineConfig} -> + {ok, {rcl_state:new(CommandLineConfig), Targets}} + end. + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== + +-spec create_log([getopt:option()], rcl_state:cmd_args()) -> + {ok, rcl_state:cmd_args()} | {error, Reason::term()}. +create_log(Opts, Acc) -> + LogLevel = proplists:get_value(log_level, Opts, 0), + if + LogLevel >= 0, LogLevel =< 2 -> + create_goals(Opts, [{log, rcl_log:new(LogLevel)} | Acc]); + true -> + {error, {invalid_log_level, LogLevel}} + end. + +-spec create_goals([getopt:option()], rcl_state:cmd_args()) -> + {ok, rcl_state:cmd_args()} | {error, Reason::term()}. +create_goals(Opts, Acc) -> + case convert_goals(proplists:get_all_values(goals, Opts), []) of + Error={error, {failed_to_parse, _Spec}} -> + Error; + {ok, Specs} -> + create_output_dir(Opts, [{goals, Specs} | Acc]) + end. + +-spec convert_goals([string()], [depsolver:constraint()]) -> + {error,{failed_to_parse, string()}} | + {ok,[depsolver:constraint()]}. +convert_goals([], Specs) -> + %% Reverse the specs because order matters to depsolver + {ok, lists:reverse(Specs)}; +convert_goals([RawSpec | Rest], Acc) -> + case rcl_goal:parse(RawSpec) of + {ok, Spec} -> + convert_goals(Rest, [Spec | Acc]); + {fail, _} -> + {error, {failed_to_parse, RawSpec}} + end. +-spec create_output_dir([getopt:option()], rcl_state:cmd_args()) -> + {ok, rcl_state:cmd_args()} | {error, Reason::term()}. +create_output_dir(Opts, Acc) -> + OutputDir = proplists:get_value(output_dir, Opts, "./relcool_output"), + case filelib:is_dir(OutputDir) of + false -> + case rcl_util:mkdir_p(OutputDir) of + ok -> + create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc]); + {error, _} -> + {error, {unable_to_create_output_dir, OutputDir}} + end; + true -> + create_lib_dirs(Opts, [{output_dir, OutputDir} | Acc]) + end. + +-spec create_lib_dirs([getopt:option()], rcl_state:cmd_args()) -> + {ok, rcl_state:cmd_args()} | {error, Reason::term()}. +create_lib_dirs(Opts, Acc) -> + Dirs = proplists:get_all_values(lib_dir, Opts), + case check_lib_dirs(Dirs) of + Error = {error, _} -> + Error; + ok -> + {ok, [{lib_dirs, Dirs} | Acc]} + end. + +-spec check_lib_dirs([string()]) -> ok | {error, {Reason::atom(), Dir::string()}}. +check_lib_dirs([]) -> + ok; +check_lib_dirs([Dir | Rest]) -> + case filelib:is_dir(Dir) of + false -> + {error, {not_directory, Dir}}; + true -> + check_lib_dirs(Rest) + end. diff --git a/src/rcl_goal_utils.erl b/src/rcl_goal_utils.erl index 75fcda4..213a87d 100644 --- a/src/rcl_goal_utils.erl +++ b/src/rcl_goal_utils.erl @@ -3,7 +3,7 @@ %%% %%% 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 +%%% except in compliance with the License. You may obtain %%% a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 @@ -29,7 +29,6 @@ %% types %%============================================================================ - %%============================================================================ %% API %%============================================================================ @@ -68,7 +67,6 @@ to_op(<<":btwn:">>) -> to_op(<<":between:">>) -> 'between'. - to_vsn(Version) when erlang:is_list(Version) -> to_vsn(erlang:iolist_to_binary(Version)); to_vsn(Vsn) -> diff --git a/src/rcl_log.erl b/src/rcl_log.erl index 63eee14..a5fb43a 100644 --- a/src/rcl_log.erl +++ b/src/rcl_log.erl @@ -32,11 +32,13 @@ error/2, error/3, log_level/1, - atom_log_level/1]). + atom_log_level/1, + format/1]). -export_type([int_log_level/0, log_level/0, - state/0]). + log_fun/0, + t/0]). -include_lib("relcool/include/relcool.hrl"). @@ -48,13 +50,15 @@ %% Why no warn? because for our purposes there is no difference between error %% and warn -type log_level() :: error | info | debug. --opaque state() :: {?MODULE, log_level()}. +-opaque t() :: {?MODULE, int_log_level()}. + +-type log_fun() :: fun(() -> iolist()). %%============================================================================ %% API %%============================================================================ %% @doc Create a new 'log level' for the system --spec new(int_log_level() | log_level()) -> state(). +-spec new(int_log_level() | log_level()) -> t(). new(LogLevel) when LogLevel >= 0, LogLevel =< 2 -> {?MODULE, LogLevel}; new(AtomLogLevel) @@ -68,44 +72,66 @@ new(AtomLogLevel) end, new(LogLevel). -%% @doc log at the debug level given the current log state with a string --spec debug(state(), string()) -> none(). + +%% @doc log at the debug level given the current log state with a string or +%% function that returns a string +-spec debug(t(), string() | log_fun()) -> ok. +debug(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?RCL_DEBUG, Fun); debug(LogState, String) -> debug(LogState, "~s~n", [String]). %% @doc log at the debug level given the current log state with a format string %% and argements @see io:format/2 --spec debug(state(), string(), [any()]) -> none(). +-spec debug(t(), string(), [any()]) -> ok. debug(LogState, FormatString, Args) -> log(LogState, ?RCL_DEBUG, FormatString, Args). -%% @doc log at the info level given the current log state with a string --spec info(state(), string()) -> none(). +%% @doc log at the info level given the current log state with a string or +%% function that returns a string +-spec info(t(), string() | log_fun()) -> ok. +info(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?RCL_INFO, Fun); info(LogState, String) -> info(LogState, "~s~n", [String]). %% @doc log at the info level given the current log state with a format string %% and argements @see io:format/2 --spec info(state(), string(), [any()]) -> none(). +-spec info(t(), string(), [any()]) -> ok. info(LogState, FormatString, Args) -> log(LogState, ?RCL_INFO, FormatString, Args). -%% @doc log at the error level given the current log state with a string --spec error(state(), string()) -> none(). +%% @doc log at the error level given the current log state with a string or +%% format string that returns a function +-spec error(t(), string() | log_fun()) -> ok. +error(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?RCL_ERROR, Fun); error(LogState, String) -> error(LogState, "~s~n", [String]). %% @doc log at the error level given the current log state with a format string %% and argements @see io:format/2 --spec error(state(), string(), [any()]) -> none(). +-spec error(t(), string(), [any()]) -> ok. error(LogState, FormatString, Args) -> log(LogState, ?RCL_ERROR, FormatString, Args). +%% @doc Execute the fun passed in if log level is as expected. +-spec log(t(), int_log_level(), log_fun()) -> ok. +log({?MODULE, DetailLogLevel}, LogLevel, Fun) + when DetailLogLevel >= LogLevel -> + io:format("~s~n", [Fun()]); +log(_, _, _) -> + ok. + + %% @doc when the module log level is less then or equal to the log level for the %% call then write the log info out. When its not then ignore the call. --spec log(state(), int_log_level(), string(), [any()]) -> ok. +-spec log(t(), int_log_level(), string(), [any()]) -> ok. log({?MODULE, DetailLogLevel}, LogLevel, FormatString, Args) - when DetailLogLevel =< LogLevel, + when DetailLogLevel >= LogLevel, erlang:is_list(Args) -> io:format(FormatString, Args); log(_, _, _, _) -> @@ -113,7 +139,7 @@ log(_, _, _, _) -> %% @doc return a boolean indicating if the system should log for the specified %% levelg --spec should(state(), int_log_level()) -> ok. +-spec should(t(), int_log_level() | any()) -> boolean(). should({?MODULE, DetailLogLevel}, LogLevel) when DetailLogLevel >= LogLevel -> true; @@ -121,12 +147,12 @@ should(_, _) -> false. %% @doc get the current log level as an integer --spec log_level(state()) -> int_log_level(). +-spec log_level(t()) -> int_log_level(). log_level({?MODULE, DetailLogLevel}) -> DetailLogLevel. %% @doc get the current log level as an atom --spec atom_log_level(state()) -> log_level(). +-spec atom_log_level(t()) -> log_level(). atom_log_level({?MODULE, ?RCL_ERROR}) -> error; atom_log_level({?MODULE, ?RCL_INFO}) -> @@ -134,6 +160,13 @@ atom_log_level({?MODULE, ?RCL_INFO}) -> atom_log_level({?MODULE, ?RCL_DEBUG}) -> debug. +-spec format(t()) -> iolist(). +format(Log) -> + [<<"(">>, + erlang:integer_to_list(log_level(Log)), <<":">>, + erlang:atom_to_list(atom_log_level(Log)), + <<")">>]. + %%%=================================================================== %%% Test Functions %%%=================================================================== @@ -143,23 +176,23 @@ atom_log_level({?MODULE, ?RCL_DEBUG}) -> should_test() -> ErrorLogState = new(error), - ?assert(should(ErrorLogState, ?RCL_ERROR)), - ?assert(not should(ErrorLogState, ?RCL_INFO)), - ?assert(not should(ErrorLogState, ?RCL_DEBUG)), + ?assertMatch(true, should(ErrorLogState, ?RCL_ERROR)), + ?assertMatch(true, not should(ErrorLogState, ?RCL_INFO)), + ?assertMatch(true, not should(ErrorLogState, ?RCL_DEBUG)), ?assertEqual(?RCL_ERROR, log_level(ErrorLogState)), ?assertEqual(error, atom_log_level(ErrorLogState)), InfoLogState = new(info), - ?assert(should(InfoLogState, ?RCL_ERROR)), - ?assert(should(InfoLogState, ?RCL_INFO)), - ?assert(not should(InfoLogState, ?RCL_DEBUG)), + ?assertMatch(true, should(InfoLogState, ?RCL_ERROR)), + ?assertMatch(true, should(InfoLogState, ?RCL_INFO)), + ?assertMatch(true, not should(InfoLogState, ?RCL_DEBUG)), ?assertEqual(?RCL_INFO, log_level(InfoLogState)), ?assertEqual(info, atom_log_level(InfoLogState)), DebugLogState = new(debug), - ?assert(should(DebugLogState, ?RCL_ERROR)), - ?assert(should(DebugLogState, ?RCL_INFO)), - ?assert(should(DebugLogState, ?RCL_DEBUG)), + ?assertMatch(true, should(DebugLogState, ?RCL_ERROR)), + ?assertMatch(true, should(DebugLogState, ?RCL_INFO)), + ?assertMatch(true, should(DebugLogState, ?RCL_DEBUG)), ?assertEqual(?RCL_DEBUG, log_level(DebugLogState)), ?assertEqual(debug, atom_log_level(DebugLogState)). diff --git a/src/rcl_state.erl b/src/rcl_state.erl index 7175d1f..3a61e58 100644 --- a/src/rcl_state.erl +++ b/src/rcl_state.erl @@ -24,30 +24,72 @@ -module(rcl_state). -export([new/1, - log/1]). + log/1, + output_dir/1, + lib_dirs/1, + goals/1, + format/1, + format/2]). --export_type([state/0]). +-export_type([t/0, + cmd_args/0]). --record(?MODULE, {log :: rcl_log:state()}). +-record(?MODULE, {log :: rcl_log:t(), + output_dir :: file:name(), + lib_dirs=[] :: [file:name()], + goals=[] :: [depsolver:constraint()]}). %%============================================================================ %% types %%============================================================================ --opaque state() :: record(?MODULE). + +-type cmd_args() :: proplists:proplist(). +-opaque t() :: record(?MODULE). %%============================================================================ %% API %%============================================================================ %% @doc Create a new 'log level' for the system --spec new(proplists:proplist()) -> state(). +-spec new(proplists:proplist()) -> t(). new(PropList) when erlang:is_list(PropList) -> - #?MODULE{log = proplists:get_value(log, PropList, rcl_log:new(error))}. + #?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, []), + goals=proplists:get_value(goals, PropList, [])}. %% @doc get the current log state for the system --spec log(state()) -> rc_log:state(). +-spec log(t()) -> rcl_log:t(). log(#?MODULE{log=LogState}) -> LogState. +-spec output_dir(t()) -> file:name(). +output_dir(#?MODULE{output_dir=OutDir}) -> + OutDir. + +-spec lib_dirs(t()) -> [file:name()]. +lib_dirs(#?MODULE{lib_dirs=LibDir}) -> + LibDir. + +-spec goals(t()) -> [depsolver:constraints()]. +goals(#?MODULE{goals=TS}) -> + TS. + +-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}, + Indent) -> + [rcl_util:indent(Indent), + <<"state:\n">>, + rcl_util:indent(Indent + 1), <<"log: ">>, rcl_log:format(LogState), <<",\n">>, + rcl_util:indent(Indent + 1), "goals: \n", + [[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]]. + %%%=================================================================== %%% Test Functions %%%=================================================================== diff --git a/src/rcl_util.erl b/src/rcl_util.erl index c0b5c5d..be20302 100644 --- a/src/rcl_util.erl +++ b/src/rcl_util.erl @@ -21,8 +21,10 @@ %%% @doc Trivial utility file to help handle common tasks -module(rcl_util). --export([mkdir_p/1]). +-export([mkdir_p/1, + indent/1]). +-define(ONE_LEVEL_INDENT, " "). %%============================================================================ %% types %%============================================================================ @@ -41,6 +43,11 @@ mkdir_p(Path) -> DirName = filename:join([filename:absname(Path), "tmp"]), filelib:ensure_dir(DirName). +%% @doc ident to the level specified +-spec indent(non_neg_integer()) -> iolist(). +indent(Amount) when erlang:is_integer(Amount) -> + [?ONE_LEVEL_INDENT || _ <- lists:seq(1, Amount)]. + %%%=================================================================== %%% Test Functions %%%=================================================================== diff --git a/src/relcool.erl b/src/relcool.erl index 7702207..f8a9bbd 100644 --- a/src/relcool.erl +++ b/src/relcool.erl @@ -1,12 +1,104 @@ -%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +%%% -*- 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 +%%% @copyright (C) 2012 Erlware, LLC. %%% @doc -%%% @copyright (C) 2007-2012 Erlware, Inc. -%%%--------------------------------------------------------------------------- -module(relcool). --export([main/1]). +-export([main/1, opt_spec_list/0]). + +%%============================================================================ +%% types +%%============================================================================ + +%%============================================================================ +%% API +%%============================================================================ +-spec main([string()]) -> ok. +main(Args) -> + io:format("~p~n", [Args]), + OptSpecList = opt_spec_list(), + case rcl_cmd_args:args2state(getopt:parse(OptSpecList, Args)) of + {ok, {State, _Target}} -> + run_relcool_process(State); + Error={error, _} -> + report_error(Error) + end. + +-spec opt_spec_list() -> [getopt:option_spec()]. +opt_spec_list() -> + [ + {relname, $n, "relname", string, "Specify the name for the release that will be generated"}, + {relvsn, $v, "relvsn", string, "Specify the version for the release"}, + {goals, $g, "goal", string, "Specify a target constraint on the system. These are " + "usually the OTP"}, + {output_dir, $o, "output-dir", string, "The output directory for the release. This is `./` by default."}, + {lib_dir, $l, "lib-dir", string, "Additional dirs that should be searched for OTP Apps"}, + {log_level, $V, "verbose", {integer, 2}, "Verbosity level, maybe between 0 and 2"} + ]. -main(_Args) -> +%%============================================================================ +%% internal api +%%============================================================================ +run_relcool_process(State) -> + rcl_log:info(rcl_state:log(State), "Starting relcool build process ..."), + rcl_log:debug(rcl_state:log(State), + fun() -> + rcl_state:format(State) + end), ok. + +-spec usage() -> ok. +usage() -> + getopt:usage(opt_spec_list(), "relcool", "[*release-specification-file*]"). + + +-spec report_error(term()) -> none(). +report_error(Error) -> + io:format("~s~n", [to_error(Error)]), + usage(), + erlang:halt(127). + +-spec to_error({error, Reason::term()}) -> string(). +to_error({error,{invalid_option_arg, Arg}}) -> + case Arg of + {goals, Goal} -> + io_lib:format("Invalid Goal argument -g ~p~n", [Goal]); + {relname, RelName} -> + io_lib:format("Invalid Release Name argument -n ~p~n", [RelName]); + {relvsn, RelVsn} -> + io_lib:format("Invalid Release Version argument -n ~p~n", [RelVsn]); + {output_dir, Outdir} -> + io_lib:format("Invalid Output Directory argument -n ~p~n", [Outdir]); + {lib_dir, LibDir} -> + io_lib:format("Invalid Library Directory argument -n ~p~n", [LibDir]); + {log_level, LogLevel} -> + io_lib:format("Invalid Library Directory argument -n ~p~n", [LogLevel]) + end; +to_error({error, {failed_to_parse, Spec}}) -> + io_lib:format("Unable to parse spec ~s", [Spec]); +to_error({error, {unable_to_create_output_dir, OutputDir}}) -> + io_lib:format("Unable to create output directory (possible permissions issue): ~s", + [OutputDir]); +to_error({error, {not_directory, Dir}}) -> + io_lib:format("Library directory does not exist: ~s", [Dir]); +to_error({error, {invalid_log_level, LogLevel}}) -> + io_lib:format("Invalid log level specified -V ~p, log level must be in the" + " range 0..2", [LogLevel]); +to_error({error, Error}) -> + io_lib:format("~p~n", [Error]). diff --git a/test/rclt_command_SUITE.erl b/test/rclt_command_SUITE.erl new file mode 100644 index 0000000..5967cc5 --- /dev/null +++ b/test/rclt_command_SUITE.erl @@ -0,0 +1,100 @@ +%%% -*- 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 +%%% @copyright (C) 2012, Eric Merrit +-module(rclt_command_SUITE). + +-export([suite/0, + init_per_suite/1, + end_per_suite/1, + all/0, + normal_passing_case/1, + lib_fail_case/1, + output_fail_case/1, + spec_parse_fail_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. + +all() -> + [normal_passing_case, lib_fail_case, output_fail_case]. + +normal_passing_case(Config) -> + DataDir = proplists:get_value(data_dir, Config), + Lib1 = filename:join([DataDir, "lib1"]), + Lib2 = filename:join([DataDir, "lib2"]), + Outdir = filename:join([DataDir, "outdir"]), + ok = rcl_util:mkdir_p(Lib1), + ok = rcl_util:mkdir_p(Lib2), + Goal1 = "app1<=33.33+build4", + Goal2 = "app2:btwn:33.22,45.22+build.21", + + LogLevel = "2", + RelName = "foo-release", + RelVsn = "33.222", + CmdLine = ["-V", LogLevel, "-g",Goal1,"-g",Goal2, "-l", Lib1, "-l", Lib2, + "-n", RelName, "-v", RelVsn, "-o", Outdir], + {ok, {State, Target}} = rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine)), + ?assertMatch([], Target), + ?assertMatch([Lib1, Lib2], + rcl_state:lib_dirs(State)), + ?assertMatch(Outdir, rcl_state:output_dir(State)), + + ?assertMatch([{<<"app1">>,{{33,33},{[],[<<"build4">>]}},lte}, + {<<"app2">>, + {{33,22},{[],[]}}, + {{45,22},{[],[<<"build">>,21]}}, + between}], rcl_state:goals(State)). + + +lib_fail_case(Config) -> + DataDir = proplists:get_value(data_dir, Config), + Lib1 = filename:join([DataDir, "lib1"]), + Lib2 = filename:join([DataDir, "lib3333"]), + ok = rcl_util:mkdir_p(Lib1), + + CmdLine = ["-l", Lib1, "-l", Lib2], + ?assertMatch({error, {not_directory, Lib2}}, + rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))). + + +output_fail_case(Config) -> + DataDir = proplists:get_value(data_dir, Config), + UnwritableDir = filename:join([DataDir, "unwritable"]), + ok = rcl_util:mkdir_p(UnwritableDir), + ok = file:change_mode(UnwritableDir, 8#555), + CanNotCreate = filename:join([UnwritableDir, "out-dir-should-not-create"]), + + CmdLine = ["-o", CanNotCreate], + ?assertMatch({error, {unable_to_create_output_dir, CanNotCreate}}, + rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))). + +spec_parse_fail_case(_Config) -> + Spec = "aaeu:3333:33.22a44", + CmdLine = ["-g", Spec], + ?assertMatch({error, {failed_to_parse, _Spec}}, + rcl_cmd_args:args2state(getopt:parse(relcool:opt_spec_list(), CmdLine))). -- cgit v1.2.3