Skip to content

Commit

Permalink
Add support for eunit diagnostics (erlang-ls#1523)
Browse files Browse the repository at this point in the history
Add support for eunit diagnostics
Disabled by default
  • Loading branch information
plux authored and chenzengjin committed Jun 25, 2024
1 parent 3cb46e1 commit 777342e
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 28 deletions.
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

0 comments on commit 777342e

Please sign in to comment.