Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for eunit diagnostics #1523

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 51 additions & 23 deletions apps/els_lsp/src/els_code_reload.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,63 @@
maybe_compile_and_load(Uri) ->
Ext = filename:extension(Uri),
case els_config:get(code_reload) of
#{"node" := NodeStr} when Ext == <<".erl">> ->
Nodes =
case NodeStr of
[List | _] when is_list(List) ->
NodeStr;
List when is_list(List) ->
[NodeStr];
_ ->
not_a_list
end,
#{"node" := NodeOrNodes} when Ext == <<".erl">> ->
Nodes = get_nodes(NodeOrNodes),
Module = els_uri:module(Uri),
[
begin
Node = els_utils:compose_node_name(
N,
els_config_runtime:get_name_type()
),
case rpc:call(Node, code, is_sticky, [Module]) of
true -> ok;
_ -> handle_rpc_result(rpc:call(Node, c, c, [Module]), Module)
end
end
|| N <- Nodes
],
[rpc_code_reload(Node, Module) || Node <- Nodes],
ok;
_ ->
ok
end.

-spec rpc_code_reload(atom(), module()) -> ok.
rpc_code_reload(Node, Module) ->
case rpc:call(Node, code, is_sticky, [Module]) of
true ->
ok;
_ ->
Options = options(Node, Module),
?LOG_INFO(
"[code_reload] code_reload ~p on ~p with ~p",
[Module, Node, Options]
),
handle_rpc_result(
rpc:call(Node, c, c, [Module, Options]), Module
)
end.

-spec get_nodes([string()] | string()) -> [atom()].
get_nodes(NodeOrNodes) ->
Type = els_config_runtime:get_name_type(),
case NodeOrNodes of
[Str | _] = Nodes when is_list(Str) ->
[els_utils:compose_node_name(Name, Type) || Name <- Nodes];
Name when is_list(Name) ->
[els_utils:compose_node_name(Name, Type)];
_ ->
[]
end.

-spec options(atom(), module()) -> [any()].
options(Node, Module) ->
case rpc:call(Node, erlang, get_module_info, [Module]) of
Info when is_list(Info) ->
CompileInfo = proplists:get_value(compile, Info, []),
CompileOptions = proplists:get_value(
options, CompileInfo, []
),
case [Option || {d, 'TEST', _} = Option <- CompileOptions] of
[] ->
%% Ensure TEST define is set, this is to
%% enable eunit diagnostics to run
[{d, 'TEST', true}];
_ ->
[]
end;
_ ->
[]
end.

-spec handle_rpc_result(term() | {badrpc, term()}, atom()) -> ok.
handle_rpc_result({ok, Module}, _) ->
Msg = io_lib:format("code_reload success for: ~s", [Module]),
Expand Down
3 changes: 2 additions & 1 deletion apps/els_lsp/src/els_diagnostics.erl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ available_diagnostics() ->
<<"unused_macros">>,
<<"unused_record_fields">>,
<<"refactorerl">>,
<<"eqwalizer">>
<<"eqwalizer">>,
<<"eunit">>
].

-spec default_diagnostics() -> [diagnostic_id()].
Expand Down
177 changes: 177 additions & 0 deletions apps/els_lsp/src/els_eunit_diagnostics.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
-module(els_eunit_diagnostics).
-behaviour(els_diagnostics).

%%% els_diagnostics callbacks
-export([run/1]).
-export([is_default/0]).
-export([source/0]).
-include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").

%%% els_diagnostics callbacks
-spec is_default() -> boolean().
is_default() ->
false.

-spec source() -> binary().
source() ->
<<"EUnit">>.

-spec run(uri()) -> [els_diagnostics:diagnostic()].
run(Uri) ->
case run_eunit_on_remote_node(Uri) of
ignore ->
?LOG_INFO("No remote node to run on."),
[];
_Res ->
receive
{result, Collected} ->
lists:flatmap(
fun(Data) ->
handle_result(Uri, Data)
end,
Collected
)
after 5000 ->
?LOG_INFO("EUnit Timeout."),
[]
end
end.

-spec run_eunit_on_remote_node(uri()) -> ignore | any().
run_eunit_on_remote_node(Uri) ->
Ext = filename:extension(Uri),
case els_config:get(code_reload) of
#{"node" := NodeOrNodes} when Ext == <<".erl">> ->
Module = els_uri:module(Uri),
case NodeOrNodes of
[Node | _] when is_list(Node) ->
rpc_eunit(Node, Module);
Node when is_list(Node) ->
rpc_eunit(Node, Module);
_ ->
ignore
end;
_ ->
ignore
end.

-spec rpc_eunit(string(), module()) -> any().
rpc_eunit(NodeStr, Module) ->
?LOG_INFO("Running EUnit tests for ~p on ~s.", [Module, NodeStr]),
Listener = els_eunit_listener:start([{parent_pid, self()}]),
rpc:call(
node_name(NodeStr),
eunit,
test,
[
Module,
[
{report, Listener},
{exact_execution, true},
{no_tty, true}
]
]
).

-spec node_name(string()) -> atom().
node_name(N) ->
els_utils:compose_node_name(N, els_config_runtime:get_name_type()).

-spec handle_result(uri(), any()) -> [els_diagnostics:diagnostic()].
handle_result(Uri, Data) ->
Status = proplists:get_value(status, Data),
case Status of
{error, {error, {Assertion, Info0}, _Stack}} when
Assertion == assert;
Assertion == assertNot;
Assertion == assertMatch;
Assertion == assertNotMatch;
Assertion == assertEqual;
Assertion == assertNotEqual;
Assertion == assertException;
Assertion == assertNotException;
Assertion == assertError;
Assertion == assertExit;
Assertion == assertThrow;
Assertion == assertCmd;
Assertion == assertCmdOutput
->
Info1 = lists:keydelete(module, 1, Info0),
{value, {line, Line}, Info2} = lists:keytake(line, 1, Info1),
Msg =
io_lib:format("~p failed.\n", [Assertion]) ++
[format_info_value(K, V) || {K, V} <- Info2] ++
format_output(Data),
[diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)];
ok ->
Line = get_line(Uri, Data),
Msg = "Test passed." ++ format_output(Data),
[diagnostic(Line, Msg, ?DIAGNOSTIC_INFO)];
{error, {error, Error, Stack}} ->
UriM = els_uri:module(Uri),
case [X || {M, _, _, _} = X <- Stack, M == UriM] of
[] ->
%% Current module not in stacktrace
%% Error will be placed on line 0
Msg = io_lib:format("Test crashed: ~p\n~p", [Error, Stack]),
Line = 0,
[diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)];
[{M, F, A, Info0} | _] ->
Msg = io_lib:format("Test crashed: ~p", [Error]),
Line = get_line(Uri, [{source, {M, F, A}} | Info0]),
[diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)]
end;
Error ->
Line = proplists:get_value(line, Data),
Msg = io_lib:format("Test crashed: ~p", [Error]),
[diagnostic(Line, Msg, ?DIAGNOSTIC_ERROR)]
end.

-spec get_line(uri(), any()) -> non_neg_integer().
get_line(Uri, Data) ->
case proplists:get_value(line, Data) of
0 ->
{M, F, A} = proplists:get_value(source, Data),
{ok, Document} = els_utils:lookup_document(Uri),
UriM = els_uri:module(Uri),
case UriM == M of
true ->
POIs = els_dt_document:pois(Document, [function]),
case [R || #{id := Id, range := R} <- POIs, Id == {F, A}] of
[] ->
0;
[#{from := {Line, _}} | _] ->
Line
end;
false ->
0
end;
Line ->
Line
end.

-spec format_output(any()) -> iolist().
format_output(Data) ->
case proplists:get_value(output, Data) of
[<<>>] ->
[];
[Output] ->
io_lib:format("\noutput:\n~s", [Output])
end.

-spec diagnostic(non_neg_integer(), iolist(), els_diagnostics:severity()) ->
els_diagnostics:diagnostic().
diagnostic(Line, Msg, Severity) ->
#{
range => els_protocol:range(#{from => {Line, 1}, to => {Line + 1, 1}}),
severity => Severity,
source => source(),
message => list_to_binary(Msg)
}.

-spec format_info_value(atom(), any()) -> iolist().
format_info_value(K, V) when is_list(V) ->
io_lib:format("~p: ~s\n", [K, V]);
format_info_value(K, V) ->
io_lib:format("~p: ~p\n", [K, V]).
61 changes: 61 additions & 0 deletions apps/els_lsp/src/els_eunit_listener.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
%% @doc Simple EUnit listener that collects the results of the tests and
%% sends them back to the parent process.

-module(els_eunit_listener).

-behaviour(eunit_listener).

-export([start/0, start/1]).
-export([init/1, handle_begin/3, handle_end/3, handle_cancel/3, terminate/2]).

-record(state, {
result = [] :: list(),
parent_pid :: pid()
}).

-type state() :: #state{}.

-spec start() -> pid().
start() ->
start([]).

-spec start(list()) -> pid().
start(Options) ->
eunit_listener:start(?MODULE, Options).

-spec init(list()) -> state().
init(Options) ->
Pid = proplists:get_value(parent_pid, Options),
receive
{start, _Reference} ->
#state{parent_pid = Pid}
end.

-spec terminate(any(), state()) -> any().
terminate({ok, _Data}, #state{parent_pid = Pid, result = Result}) ->
Pid ! {result, Result},
ok;
terminate({error, _Reason}, _State) ->
sync_end(error).

-spec sync_end(any()) -> ok.
sync_end(Result) ->
receive
{stop, Reference, ReplyTo} ->
ReplyTo ! {result, Reference, Result},
ok
end.

-spec handle_begin(atom(), any(), state()) -> state().
handle_begin(_Kind, _Data, State) ->
State.

-spec handle_end(atom(), any(), state()) -> state().
handle_end(group, _Data, State) ->
State;
handle_end(test, Data, State) ->
State#state{result = [Data | State#state.result]}.

-spec handle_cancel(atom(), any(), state()) -> state().
handle_cancel(_Kind, _Data, State) ->
State.
6 changes: 3 additions & 3 deletions apps/els_lsp/test/els_code_reload_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ code_reload(Config) ->
ok = els_code_reload:maybe_compile_and_load(Uri),
{ok, HostName} = inet:gethostname(),
NodeName = list_to_atom("fakenode@" ++ HostName),
?assert(meck:called(rpc, call, [NodeName, c, c, [Module]])),
?assert(meck:called(rpc, call, [NodeName, c, c, [Module, []]])),
ok.

-spec code_reload_sticky_mod(config()) -> ok.
Expand All @@ -90,7 +90,7 @@ code_reload_sticky_mod(Config) ->
),
ok = els_code_reload:maybe_compile_and_load(Uri),
?assert(meck:called(rpc, call, [NodeName, code, is_sticky, [Module]])),
?assertNot(meck:called(rpc, call, [NodeName, c, c, [Module]])),
?assertNot(meck:called(rpc, call, [NodeName, c, c, [Module, []]])),
ok.

%%==============================================================================
Expand All @@ -105,7 +105,7 @@ mock_rpc() ->
rpc,
call,
fun
(PNode, c, c, [Module]) when PNode =:= NodeName ->
(PNode, c, c, [Module, '_']) when PNode =:= NodeName ->
{ok, Module};
(Node, Mod, Fun, Args) ->
meck:passthrough([Node, Mod, Fun, Args])
Expand Down
9 changes: 8 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@
{plt_apps, all_deps},
%% Depending on the OTP version, erl_types (used by
%% els_typer), is either part of hipe or dialyzer.
{plt_extra_apps, [dialyzer, hipe, mnesia, common_test, debugger]}
{plt_extra_apps, [
common_test,
debugger,
dialyzer,
eunit,
hipe,
mnesia
]}
]}.

{edoc_opts, [{preprocess, true}]}.
Expand Down
Loading