diff options
author | Fred Hebert <mononcqc@ferd.ca> | 2017-04-24 19:43:44 -0400 |
---|---|---|
committer | Fred Hebert <mononcqc@ferd.ca> | 2017-04-24 20:35:59 -0400 |
commit | 03425c788c0e1ac38a3172f6c13a42cd1ffa4b4a (patch) | |
tree | 9f54fdb69bf4c37e5a6888bab5a4d8f17e9267cd | |
parent | d284b72c615bcccaea7d2c48b801a67962ccfe17 (diff) |
Abuse error_handler to get free metacalls in r3
This uses the very risky '$handle_undefined_function'/2 export from the
r3 and rebar_agent modules to allow meta-calls that can support plugins
and all other rebar3 extensions.
This is nasty but very tempting. Currently we only support:
- r3:do(Command)
- r3:do(Namespace, Command)
There is currently no way to pass arguments to the function such that we
can, for example, run cover analysis or tests on a subset of suites.
With the new abuse of '$handle_undefined_function'/2, we can detect the
unused commands (since they are not exported) and re-route them:
- r3:Command()
- r3:Command("--args=as a string")
- r3:Command(Namespace, "--args=as a string")
Of course, in doing so, we make it impossible to use the 'do' provider
(as in 'rebar3 do ct -c, cover') since the 'do' function is already
required for things to work.
Since the previous function had very strict guards, we can, without
conflict, add manual overrides that simulate the meta-calls fine.
Sample run:
https://gist.github.com/ferd/2c06d59c7083c146d25e4ee301de0073
-rw-r--r-- | src/r3.erl | 5 | ||||
-rw-r--r-- | src/rebar_agent.erl | 31 |
2 files changed, 29 insertions, 7 deletions
@@ -2,6 +2,7 @@ %%% calls from a shell. -module(r3). -export([do/1, do/2]). +-export(['$handle_undefined_function'/2]). %% @doc alias for `rebar_agent:do/1' -spec do(atom()) -> ok | {error, term()}. @@ -10,3 +11,7 @@ do(Command) -> rebar_agent:do(Command). %% @doc alias for `rebar_agent:do/2' -spec do(atom(), atom()) -> ok | {error, term()}. do(Namespace, Command) -> rebar_agent:do(Namespace, Command). + +%% @private defer to rebar_agent +'$handle_undefined_function'(Cmd, Args) -> + rebar_agent:'$handle_undefined_function'(Cmd, Args). diff --git a/src/rebar_agent.erl b/src/rebar_agent.erl index ed9e45d..627ed96 100644 --- a/src/rebar_agent.erl +++ b/src/rebar_agent.erl @@ -2,6 +2,7 @@ %%% to statefully maintain loaded project state into a running VM. -module(rebar_agent). -export([start_link/1, do/1, do/2]). +-export(['$handle_undefined_function'/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). @@ -22,13 +23,24 @@ start_link(State) -> %% @doc runs a given command in the agent's context. -spec do(atom()) -> ok | {error, term()}. do(Command) when is_atom(Command) -> - gen_server:call(?MODULE, {cmd, Command}, infinity). + gen_server:call(?MODULE, {cmd, Command}, infinity); +do(Args) when is_list(Args) -> + gen_server:call(?MODULE, {cmd, default, do, Args}, infinity). %% @doc runs a given command in the agent's context, under a given %% namespace. -spec do(atom(), atom()) -> ok | {error, term()}. do(Namespace, Command) when is_atom(Namespace), is_atom(Command) -> - gen_server:call(?MODULE, {cmd, Namespace, Command}, infinity). + gen_server:call(?MODULE, {cmd, Namespace, Command}, infinity); +do(Namespace, Args) when is_atom(Namespace), is_list(Args) -> + gen_server:call(?MODULE, {cmd, Namespace, do, Args}, infinity). + +'$handle_undefined_function'(Cmd, [Namespace, Args]) -> + gen_server:call(?MODULE, {cmd, Namespace, Cmd, Args}, infinity); +'$handle_undefined_function'(Cmd, [Args]) -> + gen_server:call(?MODULE, {cmd, default, Cmd, Args}, infinity); +'$handle_undefined_function'(Cmd, []) -> + gen_server:call(?MODULE, {cmd, default, Cmd}, infinity). %%%%%%%%%%%%%%%%% %%% CALLBACKS %%% @@ -42,11 +54,15 @@ init(State) -> %% @private handle_call({cmd, Command}, _From, State=#state{state=RState, cwd=Cwd}) -> MidState = maybe_show_warning(State), - {Res, NewRState} = run(default, Command, RState, Cwd), + {Res, NewRState} = run(default, Command, "", RState, Cwd), {reply, Res, MidState#state{state=NewRState}, hibernate}; handle_call({cmd, Namespace, Command}, _From, State = #state{state=RState, cwd=Cwd}) -> MidState = maybe_show_warning(State), - {Res, NewRState} = run(Namespace, Command, RState, Cwd), + {Res, NewRState} = run(Namespace, Command, "", RState, Cwd), + {reply, Res, MidState#state{state=NewRState}, hibernate}; +handle_call({cmd, Namespace, Command, Args}, _From, State = #state{state=RState, cwd=Cwd}) -> + MidState = maybe_show_warning(State), + {Res, NewRState} = run(Namespace, Command, Args, RState, Cwd), {reply, Res, MidState#state{state=NewRState}, hibernate}; handle_call(_Call, _From, State) -> {noreply, State}. @@ -72,13 +88,14 @@ terminate(_Reason, _State) -> %%%%%%%%%%%%%%% %% @private runs the actual command and maintains the state changes --spec run(atom(), atom(), rebar_state:t(), file:filename()) -> +-spec run(atom(), atom(), string(), rebar_state:t(), file:filename()) -> {ok, rebar_state:t()} | {{error, term()}, rebar_state:t()}. -run(Namespace, Command, RState, Cwd) -> +run(Namespace, Command, StrArgs, RState, Cwd) -> try case rebar_dir:get_cwd() of Cwd -> - Args = [atom_to_list(Namespace), atom_to_list(Command)], + PArgs = getopt:tokenize(StrArgs), + Args = [atom_to_list(Namespace), atom_to_list(Command)] ++ PArgs, CmdState0 = refresh_state(RState, Cwd), CmdState1 = rebar_state:set(CmdState0, task, atom_to_list(Command)), CmdState = rebar_state:set(CmdState1, caller, api), |