diff --git a/apps/language_server/lib/language_server/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex index 27d891552..8b18e9499 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -79,8 +79,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do runnable_functions = [{:test, 3}, {:test, 2}] for func <- runnable_functions, - {line, _col} <- calls_to(calls_list, func), - is_test_module?(lines_to_env_list, line) do + {line, _col} <- calls_to(calls_list, func) do {_line, %{scope_id: scope_id}} = Enum.find(lines_to_env_list, fn {env_line, _env} -> env_line == line end) @@ -101,8 +100,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp find_describe_blocks(lines_to_env_list, calls_list, source_lines) do lines_to_env_list_length = length(lines_to_env_list) - for {line, _col} <- calls_to(calls_list, {:describe, 2}), - is_test_module?(lines_to_env_list, line) do + for {line, _col} <- calls_to(calls_list, {:describe, 2}) do DescribeBlock.find_block_info( line, lines_to_env_list, @@ -126,23 +124,9 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp get_test_modules(lines_to_env) do lines_to_env |> Enum.group_by(fn {_line, env} -> env.module end) - |> Enum.filter(fn {_module, module_lines_to_env} -> is_test_module?(module_lines_to_env) end) |> Enum.map(fn {module, [{line, _env} | _rest]} -> {module, line} end) end - defp is_test_module?(lines_to_env), do: is_test_module?(lines_to_env, :infinity) - - defp is_test_module?(lines_to_env, line) when is_list(lines_to_env) do - lines_to_env - |> Enum.max_by(fn - {env_line, _env} when env_line < line -> env_line - _ -> -1 - end) - |> elem(1) - |> Map.get(:imports) - |> Enum.any?(fn module -> module == ExUnit.Case end) - end - defp calls_to(calls_list, {function, arity}) do for call_info <- calls_list, call_info.func == function and call_info.arity === arity do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d502d3f07..9cd99b587 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -818,13 +818,71 @@ defmodule ElixirLS.LanguageServer.Server do end defp get_test_code_lenses(state, uri, source_file) do - if state.settings["enableTestLenses"] == true do - CodeLens.test_code_lens(uri, source_file.text, state.project_dir) - else - {:ok, []} + get_test_code_lenses( + state, + uri, + source_file, + state.settings["enableTestLenses"] || false, + Mix.Project.umbrella?() + ) + end + + defp get_test_code_lenses(_state, _uri, _source_file, false, _), do: {:ok, []} + + defp get_test_code_lenses(state, uri, source_file, true = _enabled, true = _umbrella) do + file_path = SourceFile.path_from_uri(uri) + + Mix.Project.apps_paths() + |> Enum.find(fn {_app, app_path} -> String.contains?(file_path, app_path) end) + |> case do + nil -> + {:ok, []} + + {app, app_path} -> + if is_test_file?(file_path, state, app, app_path) do + CodeLens.test_code_lens(uri, source_file.text, "#{state.project_dir}/#{app_path}") + else + {:ok, []} + end + end + end + + defp get_test_code_lenses(state, uri, source_file, true = _enabled, false = _umbrella) do + try do + file_path = SourceFile.path_from_uri(uri) + + if is_test_file?(file_path) do + CodeLens.test_code_lens(uri, source_file.text, state.project_dir) + else + {:ok, []} + end + rescue + _ in ArgumentError -> {:ok, []} end end + defp is_test_file?(file_path, state, app, app_path) do + app_name = Atom.to_string(app) + + test_paths = + (get_in(state.settings, ["testPaths", app_name]) || ["test"]) + |> Enum.map(fn path -> Path.join([state.project_dir, app_path, path]) end) + + test_pattern = get_in(state.settings, ["testPattern", app_name]) || "*_test.exs" + + Mix.Utils.extract_files(test_paths, test_pattern) + |> Enum.any?(fn path -> String.ends_with?(file_path, path) end) + end + + defp is_test_file?(file_path) do + test_paths = Mix.Project.config()[:test_paths] || ["test"] + test_pattern = Mix.Project.config()[:test_pattern] || "*_test.exs" + + Mix.Utils.extract_files(test_paths, test_pattern) + |> Enum.map(&Path.absname/1) + |> Enum.any?(&(&1 == file_path)) + end + # Build defp trigger_build(state) do diff --git a/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/custom_path/fixture_custom_test.exs b/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/custom_path/fixture_custom_test.exs new file mode 100644 index 000000000..4151c980c --- /dev/null +++ b/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/custom_path/fixture_custom_test.exs @@ -0,0 +1,7 @@ +defmodule TestCodeLensCustomPathsAndPatternTest do + use ExUnit.Case + + test "fixture test" do + assert true + end +end diff --git a/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/mix.exs b/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/mix.exs new file mode 100644 index 000000000..c94020c86 --- /dev/null +++ b/apps/language_server/test/fixtures/test_code_lens_custom_paths_and_pattern/mix.exs @@ -0,0 +1,14 @@ +defmodule TestCodeLensCustomPathsAndPattern.MixProject do + use Mix.Project + + def project do + [ + app: :test_code_lens_custom_paths_and_pattern, + version: "0.1.0", + test_paths: ["custom_path"], + test_pattern: "*_custom_test.exs" + ] + end + + def application, do: [] +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/custom_path/fixture_custom_test.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/custom_path/fixture_custom_test.exs new file mode 100644 index 000000000..9380c735f --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/custom_path/fixture_custom_test.exs @@ -0,0 +1,7 @@ +defmodule UmbrellaTestCodeLensCustomPathAndPatternTest do + use ExUnit.Case + + test "fixture test" do + assert true + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/mix.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/mix.exs new file mode 100644 index 000000000..7bcab07a9 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/apps/app1/mix.exs @@ -0,0 +1,14 @@ +defmodule App1.Mixfile do + use Mix.Project + + def project do + [ + app: :app1, + version: "0.1.0" + ] + end + + def application do + [] + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/mix.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/mix.exs new file mode 100644 index 000000000..00a61ff69 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens_custom_path_and_pattern/mix.exs @@ -0,0 +1,7 @@ +defmodule UmbrellaTestCodeLensCustomPathAndPattern.Mixfile do + use Mix.Project + + def project do + [apps_path: "apps"] + end +end diff --git a/apps/language_server/test/providers/code_lens/test_test.exs b/apps/language_server/test/providers/code_lens/test_test.exs index 4938e8aa4..6912dae40 100644 --- a/apps/language_server/test/providers/code_lens/test_test.exs +++ b/apps/language_server/test/providers/code_lens/test_test.exs @@ -70,19 +70,6 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do ] end - test "does not return lenses for modules that don't import ExUnit.case" do - uri = "file:///project/file.ex" - - text = """ - defmodule MyModule do - end - """ - - {:ok, lenses} = CodeLens.Test.code_lens(uri, text, @project_dir) - - assert lenses == [] - end - test "returns lenses for all describe blocks" do uri = "file:///project/file.ex" diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index ddc0a0baa..37ca702e7 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1274,6 +1274,149 @@ defmodule ElixirLS.LanguageServer.ServerTest do end) end + @tag :fixture + test "returns code lenses for runnable tests with custom test paths and test pattern", %{ + server: server + } do + in_fixture(__DIR__, "test_code_lens_custom_paths_and_pattern", fn -> + file_path = "custom_path/fixture_custom_test.exs" + file_uri = SourceFile.path_to_uri(file_path) + file_absolute_path = SourceFile.path_from_uri(file_uri) + text = File.read!(file_path) + project_dir = SourceFile.path_from_uri(root_uri()) + + initialize(server) + + Server.receive_packet( + server, + did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) + ) + + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + wait_until_compiled(server) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + resp = assert_receive(%{"id" => 4}, 5000) + + assert response(4, [ + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "testName" => "fixture test", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run test" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 3}, + "start" => %{"character" => 0, "line" => 3} + } + }, + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "module" => "Elixir.TestCodeLensCustomPathsAndPatternTest", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run tests in module" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } + } + ]) = resp + end) + end + + @tag :fixture + test "returns code lenses for runnable tests with custom test paths and test pattern in umbrella apps", + %{ + server: server + } do + in_fixture(__DIR__, "umbrella_test_code_lens_custom_path_and_pattern", fn -> + file_path = "apps/app1/custom_path/fixture_custom_test.exs" + file_uri = SourceFile.path_to_uri(file_path) + file_absolute_path = SourceFile.path_from_uri(file_uri) + text = File.read!(file_path) + project_dir = SourceFile.path_from_uri("#{root_uri()}/apps/app1") + + initialize(server) + + Server.receive_packet( + server, + did_change_configuration(%{ + "elixirLS" => %{ + "enableTestLenses" => true, + "testPaths" => %{"app1" => ["custom_path"]}, + "testPattern" => %{"app1" => "*_custom_test.exs"} + } + }) + ) + + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + wait_until_compiled(server) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + resp = assert_receive(%{"id" => 4}, 5000) + + assert response(4, [ + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "testName" => "fixture test", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run test" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 3}, + "start" => %{"character" => 0, "line" => 3} + } + }, + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "module" => "Elixir.UmbrellaTestCodeLensCustomPathAndPatternTest", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run tests in module" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } + } + ]) = resp + end) + end + defp with_new_server(func) do server = start_supervised!({Server, nil}) packet_capture = start_supervised!({PacketCapture, self()})