Skip to content

Commit 4b98088

Browse files
authored
Add custom command returning tests in file (#753)
* add custom command returning tests in file * add tests * move outside of tests to prevent mix from running tests in fixture * disable other tracers * increase timeout * improve compatibility with elixir 1.11 and 1.12 * on_module is only available since 1.13
1 parent 2d59c20 commit 4b98088

File tree

12 files changed

+264
-12
lines changed

12 files changed

+264
-12
lines changed

apps/language_server/.formatter.exs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
impossible_to_format = ["test/fixtures/token_missing_error/lib/has_error.ex"]
1+
impossible_to_format = [
2+
"test/fixtures/token_missing_error/lib/has_error.ex",
3+
"test/fixtures/project_with_tests/test/error_test.exs"
4+
]
25

36
[
47
inputs:

apps/language_server/lib/language_server.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer do
1010
{ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server},
1111
{ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc},
1212
{ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []},
13-
{ElixirLS.LanguageServer.Tracer, []}
13+
{ElixirLS.LanguageServer.Tracer, []},
14+
{ElixirLS.LanguageServer.ExUnitTestTracer, []}
1415
]
1516

1617
opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0]
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
2+
use GenServer
3+
4+
@tables ~w(tests)a
5+
6+
for table <- @tables do
7+
defp table_name(unquote(table)) do
8+
:"#{__MODULE__}:#{unquote(table)}"
9+
end
10+
end
11+
12+
def start_link(args) do
13+
GenServer.start_link(__MODULE__, args, name: __MODULE__)
14+
end
15+
16+
def get_tests(path) do
17+
GenServer.call(__MODULE__, {:get_tests, path}, :infinity)
18+
end
19+
20+
@impl true
21+
def init(_args) do
22+
for table <- @tables do
23+
table_name = table_name(table)
24+
25+
:ets.new(table_name, [
26+
:named_table,
27+
:public,
28+
read_concurrency: true,
29+
write_concurrency: true
30+
])
31+
end
32+
33+
ExUnit.start(autorun: false)
34+
35+
{:ok, %{}}
36+
end
37+
38+
@impl true
39+
def handle_call({:get_tests, path}, _from, state) do
40+
:ets.delete_all_objects(table_name(:tests))
41+
tracers = Code.compiler_options()[:tracers]
42+
# TODO build lock?
43+
Code.put_compiler_option(:tracers, [__MODULE__])
44+
45+
result =
46+
try do
47+
# TODO parallel compiler and diagnostics?
48+
_ = Code.compile_file(path)
49+
50+
result =
51+
:ets.tab2list(table_name(:tests))
52+
|> Enum.map(fn {{_file, module, line}, describes} ->
53+
%{
54+
module: inspect(module),
55+
line: line,
56+
describes: describes
57+
}
58+
end)
59+
60+
{:ok, result}
61+
rescue
62+
e ->
63+
{:error, e}
64+
after
65+
Code.put_compiler_option(:tracers, tracers)
66+
end
67+
68+
{:reply, result, state}
69+
end
70+
71+
def trace({:on_module, _, _}, %Macro.Env{} = env) do
72+
test_info = Module.get_attribute(env.module, :ex_unit_tests)
73+
74+
if test_info != nil do
75+
describe_infos =
76+
test_info
77+
|> Enum.group_by(fn %ExUnit.Test{tags: tags} -> {tags.describe, tags.describe_line} end)
78+
|> Enum.map(fn {{describe, describe_line}, tests} ->
79+
tests =
80+
tests
81+
|> Enum.map(fn %ExUnit.Test{tags: tags} = test ->
82+
# drop test prefix
83+
"test " <> test_name = Atom.to_string(test.name)
84+
85+
test_name =
86+
if describe != nil do
87+
test_name |> String.replace_prefix(describe <> " ", "")
88+
else
89+
test_name
90+
end
91+
92+
%{
93+
name: test_name,
94+
type: tags.test_type,
95+
line: tags.line - 1
96+
}
97+
end)
98+
99+
%{
100+
describe: describe,
101+
line: if(describe_line, do: describe_line - 1),
102+
tests: tests
103+
}
104+
end)
105+
106+
:ets.insert(table_name(:tests), {{env.file, env.module, env.line - 1}, describe_infos})
107+
end
108+
109+
:ok
110+
end
111+
112+
def trace(_, %Macro.Env{} = _env) do
113+
:ok
114+
end
115+
end

apps/language_server/lib/language_server/providers/execute_command.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand do
1010
"expandMacro" => ExecuteCommand.ExpandMacro,
1111
"manipulatePipes" => ExecuteCommand.ManipulatePipes,
1212
"restart" => ExecuteCommand.Restart,
13-
"mixClean" => ExecuteCommand.MixClean
13+
"mixClean" => ExecuteCommand.MixClean,
14+
"getExUnitTestsInFile" => ExecuteCommand.GetExUnitTestsInFile
1415
}
1516

1617
@callback execute([any], %ElixirLS.LanguageServer.Server{}) ::
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile do
2+
alias ElixirLS.LanguageServer.{SourceFile, ExUnitTestTracer}
3+
@behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand
4+
5+
@impl ElixirLS.LanguageServer.Providers.ExecuteCommand
6+
def execute([uri], _state) do
7+
if Version.match?(System.version(), ">= 1.13.0") do
8+
path = SourceFile.Path.from_uri(uri)
9+
10+
case ExUnitTestTracer.get_tests(path) do
11+
{:ok, tests} -> {:ok, tests}
12+
{:error, reason} -> {:error, :server_error, inspect(reason)}
13+
end
14+
else
15+
{:error, :server_error, "This feature requires elixir >= 1.13"}
16+
end
17+
end
18+
end

apps/language_server/lib/language_server/source_file.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,10 @@ defmodule ElixirLS.LanguageServer.SourceFile do
240240
try do
241241
true = Code.ensure_loaded?(Mix.Tasks.Format)
242242

243-
if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do
244-
{:ok, Mix.Tasks.Format.formatter_for_file(path)}
243+
if Version.match?(System.version(), ">= 1.13.0") do
244+
{:ok, apply(Mix.Tasks.Format, :formatter_for_file, [path])}
245245
else
246-
{:ok, {nil, Mix.Tasks.Format.formatter_opts_for_file(path)}}
246+
{:ok, {nil, apply(Mix.Tasks.Format, :formatter_opts_for_file, [path])}}
247247
end
248248
rescue
249249
e ->

apps/language_server/lib/language_server/tracer.ex

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,22 @@ defmodule ElixirLS.LanguageServer.Tracer do
206206

207207
defp build_module_info(module, file, line) do
208208
defs =
209-
for {name, arity} <- Module.definitions_in(module) do
210-
def_info = Module.get_definition(module, {name, arity})
211-
{{name, arity}, build_def_info(def_info)}
209+
if Version.match?(System.version(), ">= 1.12.0") do
210+
for {name, arity} <- Module.definitions_in(module) do
211+
def_info = apply(Module, :get_definition, [module, {name, arity}])
212+
{{name, arity}, build_def_info(def_info)}
213+
end
214+
else
215+
[]
212216
end
213217

214218
attributes =
215-
for name <- Module.attributes_in(module) do
216-
{name, Module.get_attribute(module, name)}
219+
if Version.match?(System.version(), ">= 1.13.0") do
220+
for name <- apply(Module, :attributes_in, [module]) do
221+
{name, Module.get_attribute(module, name)}
222+
end
223+
else
224+
[]
217225
end
218226

219227
%{
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileTest do
2+
alias ElixirLS.LanguageServer.{ExUnitTestTracer, SourceFile}
3+
alias ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile
4+
use ElixirLS.Utils.MixTest.Case, async: false
5+
6+
setup do
7+
{:ok, _} = start_supervised(ExUnitTestTracer)
8+
9+
{:ok, %{}}
10+
end
11+
12+
if Version.match?(System.version(), ">= 1.13.0") do
13+
@tag fixture: true
14+
test "return tests" do
15+
in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
16+
uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs"))
17+
18+
assert {:ok,
19+
[
20+
%{
21+
describes: [
22+
%{
23+
describe: nil,
24+
line: nil,
25+
tests: [
26+
%{line: 19, name: "this will be a test in future", type: :test},
27+
%{line: 6, name: "fixture test", type: :test}
28+
]
29+
},
30+
%{
31+
describe: "describe with test",
32+
line: 10,
33+
tests: [
34+
%{line: 11, name: "fixture test", type: :test}
35+
]
36+
}
37+
],
38+
line: 0,
39+
module: "FixtureTest"
40+
}
41+
]} = GetExUnitTestsInFile.execute([uri], nil)
42+
end)
43+
end
44+
45+
@tag fixture: true
46+
test "return error when file fails to compile" do
47+
in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
48+
uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs"))
49+
50+
assert {:error, :server_error, "%TokenMissingError" <> _} =
51+
GetExUnitTestsInFile.execute([uri], nil)
52+
end)
53+
end
54+
end
55+
end

apps/language_server/test/server_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do
726726
Server.receive_packet(server, did_open(uri, "elixir", 1, code))
727727
Server.receive_packet(server, completion_req(1, uri, 2, 25))
728728

729-
resp = assert_receive(%{"id" => 1}, 1000)
729+
resp = assert_receive(%{"id" => 1}, 5000)
730730

731731
assert response(1, %{
732732
"isIncomplete" => true,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule ProjectWithTests.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :project_with_tests, version: "0.1.0"]
6+
end
7+
8+
def application, do: []
9+
end

0 commit comments

Comments
 (0)