Skip to content

Commit 4b9be76

Browse files
committed
add custom ExUnit formatter in debug adapter
return more test metadata do not filter returned tests by `test` and `doctest` test_type Fixes elixir-lsp/vscode-elixir-ls#396
1 parent 7b3344c commit 4b9be76

File tree

3 files changed

+276
-26
lines changed

3 files changed

+276
-26
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
defmodule ElixirLS.DebugAdapter.ExUnitFormatter do
2+
use GenServer
3+
alias ElixirLS.DebugAdapter.Output
4+
5+
@width 80
6+
7+
def start_link(args) do
8+
GenServer.start_link(__MODULE__, Keyword.delete(args, :name),
9+
name: Keyword.get(args, :name, __MODULE__)
10+
)
11+
end
12+
13+
@impl true
14+
def init(_args) do
15+
{:ok,
16+
%{
17+
failure_counter: 0
18+
}}
19+
end
20+
21+
@impl true
22+
def terminate(reason, _state) do
23+
case reason do
24+
:normal ->
25+
:ok
26+
27+
:shutdown ->
28+
:ok
29+
30+
{:shutdown, _} ->
31+
:ok
32+
33+
_other ->
34+
message = Exception.format_exit(reason)
35+
36+
Output.telemetry(
37+
"dap_server_error",
38+
%{
39+
"elixir_ls.dap_process" => inspect(__MODULE__),
40+
"elixir_ls.dap_server_error" => message
41+
},
42+
%{}
43+
)
44+
45+
Output.debugger_important("Terminating #{__MODULE__}: #{message}")
46+
end
47+
48+
:ok
49+
end
50+
51+
@impl true
52+
def handle_cast({:suite_started, _opts}, state) do
53+
# the suite has started with the specified options to the runner
54+
# we don't need to do anything
55+
{:noreply, state}
56+
end
57+
58+
def handle_cast({:suite_finished, _times_us}, state) do
59+
# the suite has finished. Returns several measurements in microseconds for running the suite
60+
# not interesting
61+
{:noreply, state}
62+
end
63+
64+
def handle_cast({:module_started, %ExUnit.TestModule{}}, state) do
65+
# a test module has started
66+
# we report on individual tests
67+
{:noreply, state}
68+
end
69+
70+
def handle_cast({:module_finished, %ExUnit.TestModule{}}, state) do
71+
# a test module has finished
72+
# we report on individual tests
73+
{:noreply, state}
74+
end
75+
76+
def handle_cast({:test_started, test = %ExUnit.Test{}}, state) do
77+
# a test has started
78+
case test.state do
79+
nil ->
80+
# initial state
81+
Output.ex_unit_event(%{
82+
"event" => "test_started",
83+
"type" => test.tags.test_type,
84+
"name" => test_name(test),
85+
"describe" => test.tags.describe,
86+
"module" => inspect(test.module),
87+
"file" => test.tags.file
88+
})
89+
90+
{:skipped, _} ->
91+
# Skipped via @tag :skip
92+
Output.ex_unit_event(%{
93+
"event" => "test_skipped",
94+
"type" => test.tags.test_type,
95+
"name" => test_name(test),
96+
"describe" => test.tags.describe,
97+
"module" => inspect(test.module),
98+
"file" => test.tags.file
99+
})
100+
101+
{:excluded, _} ->
102+
# Excluded via :exclude filters
103+
Output.ex_unit_event(%{
104+
"event" => "test_excluded",
105+
"type" => test.tags.test_type,
106+
"name" => test_name(test),
107+
"describe" => test.tags.describe,
108+
"module" => inspect(test.module),
109+
"file" => test.tags.file
110+
})
111+
112+
_ ->
113+
:ok
114+
end
115+
116+
{:noreply, state}
117+
end
118+
119+
def handle_cast({:test_finished, test = %ExUnit.Test{}}, state) do
120+
# a test has finished
121+
state =
122+
case test.state do
123+
nil ->
124+
# Passed
125+
Output.ex_unit_event(%{
126+
"event" => "test_passed",
127+
"type" => test.tags.test_type,
128+
"time" => test.time,
129+
"name" => test_name(test),
130+
"describe" => test.tags.describe,
131+
"module" => inspect(test.module),
132+
"file" => test.tags.file
133+
})
134+
135+
state
136+
137+
{:excluded, _} ->
138+
# Excluded via :exclude filters
139+
state
140+
141+
{:failed, failures} ->
142+
# Failed
143+
formatter_cb = fn _key, value -> value end
144+
145+
message =
146+
ExUnit.Formatter.format_test_failure(
147+
test,
148+
failures,
149+
state.failure_counter + 1,
150+
@width,
151+
formatter_cb
152+
)
153+
154+
Output.ex_unit_event(%{
155+
"event" => "test_failed",
156+
"type" => test.tags.test_type,
157+
"time" => test.time,
158+
"name" => test_name(test),
159+
"describe" => test.tags.describe,
160+
"module" => inspect(test.module),
161+
"file" => test.tags.file,
162+
"message" => message
163+
})
164+
165+
%{state | failure_counter: state.failure_counter + 1}
166+
167+
{:invalid, test_module = %ExUnit.TestModule{state: {:failed, failures}}} ->
168+
# Invalid (when setup_all fails)
169+
formatter_cb = fn _key, value -> value end
170+
171+
message =
172+
ExUnit.Formatter.format_test_all_failure(
173+
test_module,
174+
failures,
175+
state.failure_counter + 1,
176+
@width,
177+
formatter_cb
178+
)
179+
180+
Output.ex_unit_event(%{
181+
"event" => "test_errored",
182+
"type" => test.tags.test_type,
183+
"name" => test_name(test),
184+
"describe" => test.tags.describe,
185+
"module" => inspect(test.module),
186+
"file" => test.tags.file,
187+
"message" => message
188+
})
189+
190+
%{state | failure_counter: state.failure_counter + 1}
191+
192+
{:skipped, _} ->
193+
# Skipped via @tag :skip
194+
state
195+
end
196+
197+
{:noreply, state}
198+
end
199+
200+
def handle_cast({:sigquit, _tests}, state) do
201+
# the VM is going to shutdown. It receives the test cases (or test module in case of setup_all) still running
202+
# we probably don't need to do anything
203+
{:noreply, state}
204+
end
205+
206+
def handle_cast(:max_failures_reached, state) do
207+
# undocumented event - we probably don't need to do anything
208+
{:noreply, state}
209+
end
210+
211+
def handle_cast({:case_started, _test_case}, state) do
212+
# deprecated event, ignore
213+
# TODO remove when we require elixir 2.0
214+
{:noreply, state}
215+
end
216+
217+
def handle_cast({:case_finished, _test_case}, state) do
218+
# deprecated event, ignore
219+
# TODO remove when we require elixir 2.0
220+
{:noreply, state}
221+
end
222+
223+
# TODO extract to common module
224+
defp test_name(test = %ExUnit.Test{}) do
225+
describe = test.tags.describe
226+
# drop test prefix
227+
test_name = drop_test_prefix(test.name, test.tags.test_type)
228+
229+
if describe != nil do
230+
test_name |> String.replace_prefix(describe <> " ", "")
231+
else
232+
test_name
233+
end
234+
end
235+
236+
defp drop_test_prefix(test_name, kind),
237+
do: test_name |> Atom.to_string() |> String.replace_prefix(Atom.to_string(kind) <> " ", "")
238+
end

apps/debug_adapter/lib/debug_adapter/output.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ defmodule ElixirLS.DebugAdapter.Output do
6161
send_event(server, "output", %{"category" => "stderr", "output" => maybe_append_newline(str)})
6262
end
6363

64+
def ex_unit_event(server \\ __MODULE__, data) when is_map(data) do
65+
send_event(server, "output", %{"category" => "ex_unit", "output" => "", "data" => data})
66+
end
67+
6468
def telemetry(server \\ __MODULE__, event, properties, measurements)
6569
when is_binary(event) and is_map(properties) and is_map(measurements) do
6670
elixir_release =

apps/language_server/lib/language_server/ex_unit_test_tracer.ex

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,10 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
113113
test_info
114114
|> Enum.group_by(fn %ExUnit.Test{tags: tags} -> {tags.describe, tags.describe_line} end)
115115
|> Enum.map(fn {{describe, describe_line}, tests} ->
116-
grouped_tests = Enum.group_by(tests, fn %ExUnit.Test{tags: tags} -> tags.test_type end)
117-
118116
tests =
119-
grouped_tests
120-
|> Map.get(:test, [])
121-
|> Enum.map(fn %ExUnit.Test{tags: tags} = test ->
117+
for %ExUnit.Test{tags: tags} = test <- tests do
122118
# drop test prefix
123-
test_name = drop_test_prefix(test.name)
119+
test_name = drop_test_prefix(test.name, tags.test_type)
124120

125121
test_name =
126122
if describe != nil do
@@ -129,26 +125,29 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
129125
test_name
130126
end
131127

128+
selected_tags =
129+
for {tag, value} <- tags, tag in [:async, :test_type, :doctest, :doctest_line] do
130+
"#{tag}:#{format_tag(tag, value)}"
131+
end
132+
133+
doctest_module_path =
134+
case tags[:doctest] do
135+
nil ->
136+
nil
137+
138+
module ->
139+
if Code.ensure_loaded?(module) do
140+
to_string(module.module_info(:compile)[:source])
141+
end
142+
end
143+
132144
%{
133145
name: test_name,
134146
type: tags.test_type,
135-
line: tags.line - 1
147+
line: tags.line - 1,
148+
doctest_module_path: doctest_module_path,
149+
tags: selected_tags
136150
}
137-
end)
138-
139-
tests =
140-
case grouped_tests do
141-
%{doctest: [doctest | _]} ->
142-
test_meta = %{
143-
name: "doctest #{inspect(doctest.tags.doctest)}",
144-
line: doctest.tags.line - 1,
145-
type: :doctest
146-
}
147-
148-
[test_meta | tests]
149-
150-
_ ->
151-
tests
152151
end
153152

154153
%{
@@ -168,9 +167,18 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
168167
:ok
169168
end
170169

171-
defp drop_test_prefix(test_name) when is_atom(test_name),
172-
do: test_name |> Atom.to_string() |> drop_test_prefix
170+
defp drop_test_prefix(test_name, kind),
171+
do: test_name |> Atom.to_string() |> String.replace_prefix(Atom.to_string(kind) <> " ", "")
172+
173+
defp format_tag(tag, value) when tag in [:doctest, :module] do
174+
inspect(value)
175+
end
173176

174-
defp drop_test_prefix("test " <> rest), do: rest
175-
defp drop_test_prefix(test_name), do: test_name
177+
defp format_tag(:doctest_line, value) do
178+
to_string(value - 1)
179+
end
180+
181+
defp format_tag(_tag, value) do
182+
to_string(value)
183+
end
176184
end

0 commit comments

Comments
 (0)