forked from erlang-ls/erlang_ls
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for eunit diagnostics (erlang-ls#1523)
Add support for eunit diagnostics Disabled by default
- Loading branch information
Showing
6 changed files
with
302 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters