diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 00000000..ff3e9dbf --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,5 @@ +indent_width = 2 +column_width = 120 +line_endings = "Unix" +indent_type = "Tabs" +quote_style = "AutoPreferDouble" diff --git a/README.md b/README.md index 3a0a7ebd..c68d1573 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -

neotest-java

@@ -36,38 +35,38 @@
lazy.nvim plugin manager example - ```lua - return { - { - "rcasia/neotest-java", - ft = "java", - dependencies = { - "mfussenegger/nvim-jdtls", - "mfussenegger/nvim-dap", -- for the debugger - "rcarriga/nvim-dap-ui", -- recommended - "theHamsta/nvim-dap-virtual-text", -- recommended - }, - }, - { - "nvim-neotest/neotest", - dependencies = { - "nvim-neotest/nvim-nio", - "nvim-lua/plenary.nvim", - "antoinemadec/FixCursorHold.nvim", - "nvim-treesitter/nvim-treesitter", +```lua +return { + { + "rcasia/neotest-java", + ft = "java", + dependencies = { + "mfussenegger/nvim-jdtls", + "mfussenegger/nvim-dap", -- for the debugger + "rcarriga/nvim-dap-ui", -- recommended + "theHamsta/nvim-dap-virtual-text", -- recommended }, - config = function() - require("neotest").setup({ - adapters = { - require("neotest-java")({ - -- config here - }), - }, - }) - end, - } - } - ``` + }, + { + "nvim-neotest/neotest", + dependencies = { + "nvim-neotest/nvim-nio", + "nvim-lua/plenary.nvim", + "antoinemadec/FixCursorHold.nvim", + "nvim-treesitter/nvim-treesitter", + }, + config = function() + require("neotest").setup({ + adapters = { + require("neotest-java")({ + -- config here + }), + }, + }) + end, +} +} +```
@@ -81,8 +80,19 @@ ```lua { - junit_jar = nil, -- default: stdpath("data") .. /nvim/neotest-java/junit-platform-console-standalone-[version].jar - incremental_build = true + junit_jar = nil, -- default: stdpath("data") .. /nvim/neotest-java/junit-platform-console-standalone-[version].jar + incremental_build = true, -- default: true, avoid doing a full rebuild by default of projects or the entire workspace + target_type = "project", -- default: project, build and compile processes will target projects not the entire workspace + java_runtimes = { + -- There are no runtimes defined by default, if you wish to have + -- neotest-java resolve them based on your environment define them here, + -- one could also define environment variables with the same key/names + -- i.e. `JAVA_HOME_8` or `JAVA_HOME_11` or `JAVA_HOME_17` etc in your + -- zshenv or equivalent. + ["JAVA_HOME_8"] = "/absolute/path/to/jdk8/home/directory", + ["JAVA_HOME_11"] = "/absolute/path/to/jdk11/home/directory", + ["JAVA_HOME_17"] = "/absolute/path/to/jdk17/home/directory", + }, } ``` diff --git a/lua/neotest-java/build_tool/gradle.lua b/lua/neotest-java/build_tool/gradle.lua index 0972a23f..2586f571 100644 --- a/lua/neotest-java/build_tool/gradle.lua +++ b/lua/neotest-java/build_tool/gradle.lua @@ -17,7 +17,8 @@ end --- @param roots string[] function gradle.get_spring_property_filepaths(roots) - local base_dirs = vim.iter(roots) + local base_dirs = vim + .iter(roots) :map(function(root) return { gradle.get_output_dir(root) .. "/main", diff --git a/lua/neotest-java/build_tool/maven.lua b/lua/neotest-java/build_tool/maven.lua index d413c521..366d288d 100644 --- a/lua/neotest-java/build_tool/maven.lua +++ b/lua/neotest-java/build_tool/maven.lua @@ -18,7 +18,8 @@ end --- @param roots string[] function maven.get_spring_property_filepaths(roots) - local base_dirs = vim.iter(roots) + local base_dirs = vim + .iter(roots) :map(function(root) return { maven.get_output_dir(root) .. "/classes", diff --git a/lua/neotest-java/command/binaries.lua b/lua/neotest-java/command/binaries.lua index a6f6e1e2..0cbf828b 100644 --- a/lua/neotest-java/command/binaries.lua +++ b/lua/neotest-java/command/binaries.lua @@ -1,28 +1,28 @@ -local jdtls = require("neotest-java.command.jdtls") local compatible_path = require("neotest-java.util.compatible_path") +local runtime = require("neotest-java.command.runtime") local logger = require("neotest-java.logger") local binaries = { - java = function() - local ok, jdtls_java_home = pcall(jdtls.get_java_home) + java = function(...) + local ok, jdtls_java_home = pcall(runtime.get_java_runtime, ...) if ok then return compatible_path(jdtls_java_home .. "/bin/java") end - logger.warn("JAVA_HOME setting not found in jdtls. Using defualt binary: java") + logger.warn("Unable to detect JAVA_HOME. Using defualt binary in path: java") return "java" end, - javac = function() - local ok, jdtls_java_home = pcall(jdtls.get_java_home) + javac = function(...) + local ok, jdtls_java_home = pcall(runtime.get_java_runtime, ...) if ok then return compatible_path(jdtls_java_home .. "/bin/javac") end - logger.warn("JAVA_HOME setting not found in jdtls. Using default: javac") + logger.warn("Unable to detect JAVA_HOME. Using defualt binary in path: javac") return "javac" end, } diff --git a/lua/neotest-java/command/classpath.lua b/lua/neotest-java/command/classpath.lua new file mode 100644 index 00000000..5e443e30 --- /dev/null +++ b/lua/neotest-java/command/classpath.lua @@ -0,0 +1,59 @@ +local nio = require("nio") +local lsp = require("neotest-java.lsp") +local logger = require("neotest-java.logger") + +local M = {} + +---@param additional_classpath_entries string[] +---@param dir string +M.get_classpaths = function(additional_classpath_entries, dir) + additional_classpath_entries = additional_classpath_entries or {} + + local bufnr = nio.api.nvim_get_current_buf() + local uri = vim.uri_from_fname(dir) + + local results = { ["runtime"] = {}, ["test"] = {} } + for scope, _ in pairs(results) do + local options = vim.json.encode({ scope = scope }) + local args = { + command = "java.project.getClasspaths", + arguments = { uri, options }, + } + local err, result = lsp.execute_command("workspace/executeCommand", args, bufnr) + assert(not err, vim.inspect(err)) + + -- Note: + -- 1) module paths are not duplicated on the classpath, + -- 2) the classpath and modulepath generated by the eclipse jdtls, do + -- not include the target/test-classes directory, for maven projects + -- using the Java Platform Module System. + + if next(result.modulepaths) ~= nil then + logger.error( + "Java Platform Module System not supported. Temporarily remove " + .. "module-info.java for the test duration. modulepath=" + .. vim.inspect(result.modulepaths) + ) + end + results[scope] = result.classpaths + end + + local runtime_classpaths = results.runtime + local test_classpaths = results.test + + local classpaths = {} + for _, v in ipairs(additional_classpath_entries) do + classpaths[#classpaths + 1] = v + end + for _, v in ipairs(runtime_classpaths) do + classpaths[#classpaths + 1] = v + end + for _, v in ipairs(test_classpaths) do + classpaths[#classpaths + 1] = v + end + + logger.debug(("classpath entries: %d"):format(#classpaths)) + return classpaths +end + +return M diff --git a/lua/neotest-java/command/jdtls.lua b/lua/neotest-java/command/jdtls.lua deleted file mode 100644 index 9b1b34a1..00000000 --- a/lua/neotest-java/command/jdtls.lua +++ /dev/null @@ -1,113 +0,0 @@ -local nio = require("nio") -local write_file = require("neotest-java.util.write_file") -local compatible_path = require("neotest-java.util.compatible_path") -local logger = require("neotest-java.logger") - -local M = {} - ---- @param dir? string ---- @return string | nil -local function find_any_java_file(dir) - return assert( - vim.iter(nio.fn.globpath(dir or ".", "**/*.java", false, true)):next(), - "No Java file found in the current directory." - ) -end - ---- @param path string ---- @return number bufnr -local function preload_file_for_lsp(path) - assert(path, "path cannot be nil") - local buf = vim.fn.bufadd(path) -- allocates buffer ID - vim.fn.bufload(path) -- preload lines - - return buf -end - -M.get_java_home = function() - local any_java_file = assert(find_any_java_file(), "No Java file found in the current directory.") - local bufnr = preload_file_for_lsp(any_java_file) - local uri = vim.uri_from_bufnr(bufnr) - local future = nio.control.future() - - local setting = "org.eclipse.jdt.ls.core.vm.location" - local cmd = { - command = "java.project.getSettings", - arguments = { uri, { setting } }, - } - require("jdtls.util").execute_command(cmd, function(err1, resp) - assert(not err1, vim.inspect(err1)) - future.set(resp) - end, bufnr) - - local java_exec = future.wait() - - return java_exec[setting] -end - ----@param additional_classpath_entries string[] ----@param dir string -M.get_classpath = function(additional_classpath_entries, dir) - additional_classpath_entries = additional_classpath_entries or {} - - local classpaths = {} - - local any_java_file = assert(find_any_java_file(dir), "No Java file found in the current directory.") - local bufnr = preload_file_for_lsp(any_java_file) - local uri = vim.uri_from_bufnr(bufnr) - local runtime_classpath_future = nio.control.future() - local test_classpath_future = nio.control.future() - - ---@param future nio.control.Future - for scope, future in pairs({ ["runtime"] = runtime_classpath_future, ["test"] = test_classpath_future }) do - local options = vim.json.encode({ scope = scope }) - local cmd = { - command = "java.project.getClasspaths", - arguments = { uri, options }, - } - -- TODO: look for a way to use vim.lsp.Client innstead of jdtls.util.execute_command - require("jdtls.util").execute_command(cmd, function(err1, resp) - assert(not err1, vim.inspect(err1)) - - -- Note: - -- 1) module paths are not duplicated on the classpath, - -- 2) the classpath and modulepath generated by the eclipse jdtls, do - -- not include the target/test-classes directory, for maven projects - -- using the Java Platform Module System. - - if next(resp.modulepaths) ~= nul then - logger.error( - "Java Platform Module System not supported. Temporarily remove " - .. "module-info.java for the test duration. modulepath=" - .. vim.inspect(resp.modulepaths) - ) - end - future.set(resp.classpaths) - end, bufnr) - end - - local runtime_classpaths = runtime_classpath_future.wait() - local test_classpaths = test_classpath_future.wait() - - for _, v in ipairs(additional_classpath_entries) do - classpaths[#classpaths + 1] = v - end - for _, v in ipairs(runtime_classpaths) do - classpaths[#classpaths + 1] = v - end - for _, v in ipairs(test_classpaths) do - classpaths[#classpaths + 1] = v - end - - logger.debug(("classpath entries: %d"):format(#classpaths)) - - return classpaths -end - -M.get_classpath_file_argument = function(report_dir, additional_classpath_entries, dir) - local classpaths = M.get_classpath(additional_classpath_entries, dir) - local classpath = table.concat(classpaths, ":") - return classpath -end - -return M diff --git a/lua/neotest-java/command/junit_command_builder.lua b/lua/neotest-java/command/junit_command_builder.lua index 73660fff..4cb9c291 100644 --- a/lua/neotest-java/command/junit_command_builder.lua +++ b/lua/neotest-java/command/junit_command_builder.lua @@ -97,12 +97,13 @@ local CommandBuilder = { assert(self._spring_property_filepaths, "_spring_property_filepaths cannot be nil") local selectors = {} + -- FIX: use concat and more performant and robust string concatenation please ! for _, v in ipairs(self._test_references) do if v.type == "test" then local class_name = v.qualified_name:match("^(.-)#") or v.qualified_name - table.insert(selectors, "--select-class='" .. class_name .. "'") + table.insert(selectors, "--select-class=" .. class_name .. "") if v.method_name then - table.insert(selectors, "--select-method='" .. v.method_name .. "'") + table.insert(selectors, "--select-method=" .. v.method_name .. "") end elseif v.type == "file" then table.insert(selectors, "-c=" .. v.qualified_name) @@ -113,7 +114,7 @@ local CommandBuilder = { assert(#selectors ~= 0, "junit command has to have a selector") local junit_command = { - command = java(), + command = java(self._basedir), args = { "-Dspring.config.additional-location=" .. table.concat(self._spring_property_filepaths, ","), "-jar", diff --git a/lua/neotest-java/command/runtime.lua b/lua/neotest-java/command/runtime.lua new file mode 100644 index 00000000..6aa9b688 --- /dev/null +++ b/lua/neotest-java/command/runtime.lua @@ -0,0 +1,175 @@ +local File = require("neotest.lib.file") + +local read_xml_tag = require("neotest-java.util.read_xml_tag") +local context_holder = require("neotest-java.context_holder") + +local log = require("neotest-java.logger") +local lsp = require("neotest-java.lsp") +local nio = require("nio") + +local COMPILER = "org.eclipse.jdt.core.compiler.source" +local LOCATION = "org.eclipse.jdt.ls.core.vm.location" +local RUNTIMES = {} + +local function has_env(var) + return nio.fn.getenv(var) ~= vim.NIL +end + +local function get_env(var) + return nio.fn.getenv(var) +end + +local function get_java_home() + return get_env("JAVA_HOME") +end + +local function input_runtime(actual_version) + local message = + string.format("Enter runtime home directory for JDK-%s (default to JAVA_HOME if empty): ", actual_version) + local runtime_path = nio.fn.input({ + default = "", + prompt = message, + completion = "dir", + cancelreturn = "__INPUT_CANCELLED__", + }) + if runtime_path == "__INPUT_CANCELLED__" then + log.info(string.format("Defaulting to JAVA_HOME due to empty user input for %s", actual_version)) + return get_java_home() + elseif + not runtime_path + or #runtime_path == 0 + or nio.fn.isdirectory(runtime_path) == 0 + or nio.fn.isdirectory(string.format("%s/bin", runtime_path)) == 0 + then + log.warn(string.format("Invalid runtime home directory %s was specified, please try again", runtime_path)) + return input_runtime(actual_version) + else + log.info(string.format("Using user input %s for runtime version %s", runtime_path, actual_version)) + return runtime_path + end +end + +local function maven_runtime() + local context = context_holder.get_context() + local plugins = read_xml_tag("pom.xml", "project.build.plugins.plugin") + + for _, plugin in ipairs(plugins or {}) do + if plugin.artifactId == "maven-compiler-plugin" and plugin.configuration then + assert( + plugin.configuration.target == plugin.configuration.source, + "Target and source mismatch detected in maven-compiler-plugin" + ) + + local target_version = vim.split(plugin.configuration.target, "%.") + local actual_version = #target_version > 0 and target_version[#target_version] + if RUNTIMES[actual_version] then + return RUNTIMES[actual_version] + end + + local runtime_name = string.format("JAVA_HOME_%d", actual_version) + if context and context.config.java_runtimes[runtime_name] then + return context.config.java_runtimes[runtime_name] + elseif has_env(runtime_name) then + return get_env(runtime_name) + elseif actual_version ~= nil then + local runtime_path = input_runtime(actual_version) + RUNTIMES[actual_version] = runtime_path + return runtime_path + else + log.warn("Detected maven-compiler-plugin, but unable to resolve runtime version") + break + end + end + end + + log.warn("Unable to resolve the runtime from maven-compiler-plugin, defaulting to JAVA_HOME") + return get_java_home() +end + +local function gradle_runtime() + -- fix: the build.gradle has to be read to obtain information about the project's configured runtime + log.warn("Unable to resolve the runtime from build.gradle, defaulting to JAVA_HOME") + return get_java_home() +end + +---@param root_directory string +local function extract_runtime(root_directory) + local bufnr = nio.api.nvim_get_current_buf() + local uri = vim.uri_from_fname(root_directory) + local err, result, settings = lsp.execute_command("workspace/executeCommand", { + command = "java.project.getSettings", + arguments = { uri, { COMPILER, LOCATION } }, + }, bufnr) + + if err ~= nil or result == nil or settings == nil then + log.warn("Unable to extract runtime from lsp client") + return + end + -- used to obtain the current language server or workspace folder configuration for the the java runtimes and their locations + local config = settings.configuration or {} + + -- location starts off being nil, we require strict matching, otherwise the runtime resolve will fallback to maven or gradle, we + -- do not want to resolve to JAVA_HOME immediately here, it is too early. + local location = nil + local runtimes = config.runtimes + local compiler = result[COMPILER] + + -- we can early exit with location here, when the location exists, however that might not always be, therefore if no location is + -- present we try to resolve which is the default runtime configured for the project based on the language server or workspace, + -- that is a last resort option, but still provides a valid way to resolve the runtime if all else fails + -- settings + if result[LOCATION] then + location = result[LOCATION] + else + -- go over available runtimes and resolve it + for _, runtime in ipairs(runtimes or {}) do + -- default runtimes get priority + if runtime.default == true then + location = runtime.path + break + end + -- match runtime against compliance version + local match = runtime.name:match(".*-(.*)") + if match and match == compiler then + location = runtime.path + break + end + end + end + + -- location has to be strictly resolved from the project `settings` or from the runtimes in client's settings, and has to point + -- to a valid directory on the filesystem, otherwise we return `nil` for runtime + if not location or #location == 0 or nio.fn.isdirectory(location) == 0 then + return nil + end + return location +end + +---@param root_directory string +---@return string | nil +local function get_java_runtime(root_directory) + local runtime = extract_runtime(root_directory) + + -- in case the runtime was not found, try to fetch one from the build + -- system which the current project is using, match against maven or gradle + -- and try to find the configured runtime, or fallback to JAVA_HOME + if not runtime or #runtime == 0 then + if File.exists("pom.xml") then + runtime = maven_runtime() + elseif File.exists("build.gradle") then + runtime = gradle_runtime() + end + end + + if runtime and #runtime > 0 then + log.info(string.format("Resolved project runtime %s", runtime)) + return runtime + else + log.warn("Unable to resolve the project's runtime") + return nil + end +end + +return { + get_java_runtime = get_java_runtime, +} diff --git a/lua/neotest-java/context_holder.lua b/lua/neotest-java/context_holder.lua index 9584a078..9e1cc3ac 100644 --- a/lua/neotest-java/context_holder.lua +++ b/lua/neotest-java/context_holder.lua @@ -8,8 +8,10 @@ local default_config = { junit_jar = compatible_path( ("%s/neotest-java/junit-platform-console-standalone-%s.jar"):format(vim.fn.stdpath("data"), DEFAULT_VERSION) ), - incremental_build = true, default_version = DEFAULT_VERSION, + incremental_build = true, + build_target = "project", + java_runtimes = {}, } ---@type neotest-java.Context @@ -37,6 +39,7 @@ return { ---@field junit_jar string ---@field incremental_build boolean ---@field default_version string +---@field java_runtimes table ---@class neotest-java.Context ---@field config neotest-java.ConfigOpts diff --git a/lua/neotest-java/core/positions_discoverer.lua b/lua/neotest-java/core/positions_discoverer.lua index e58e540d..5e12a76d 100644 --- a/lua/neotest-java/core/positions_discoverer.lua +++ b/lua/neotest-java/core/positions_discoverer.lua @@ -292,7 +292,8 @@ end ---@return neotest.Tree | nil function PositionsDiscoverer.discover_positions(file_path) local annotations = { "Test", "ParameterizedTest", "TestFactory", "CartesianTest" } - local a = vim.iter(annotations) + local a = vim + .iter(annotations) :map(function(v) return string.format([["%s"]], v) end) diff --git a/lua/neotest-java/core/result_builder.lua b/lua/neotest-java/core/result_builder.lua index fce1f623..8e79b7fa 100644 --- a/lua/neotest-java/core/result_builder.lua +++ b/lua/neotest-java/core/result_builder.lua @@ -60,7 +60,7 @@ end local function load_all_testcases(reports_dir, scan, read_file) local paths = scan(reports_dir, { search_pattern = REPORT_FILE_NAMES_PATTERN }) log.debug("Found report files: ", paths) - assert(#paths ~= 0, "no report file could be generated") + assert(#paths ~= 0, string.format("no report file where found in %s", reports_dir)) return flat_map(function(filepath) local ok, data = pcall(read_file, filepath) diff --git a/lua/neotest-java/core/spec_builder/compiler/init.lua b/lua/neotest-java/core/spec_builder/compiler/init.lua index 520b418a..ff878483 100644 --- a/lua/neotest-java/core/spec_builder/compiler/init.lua +++ b/lua/neotest-java/core/spec_builder/compiler/init.lua @@ -1,21 +1,45 @@ -local jdtls_compiler = require("neotest-java.core.spec_builder.compiler.jdtls") - ----@class NeotestJavaCompiler.Opts ----@field cwd string ----@field classpath_file_dir string ----@field compile_mode string +local compiler = require("neotest-java.core.spec_builder.compiler.jdtls") +local logger = require("neotest-java.logger") +local lib = require("neotest.lib") --- Interface for Java compilers ---@class NeotestJavaCompiler local NeotestJavaCompiler = {} ----@param opts NeotestJavaCompiler.Opts ----@return string classpath_file_arg -function NeotestJavaCompiler.compile(opts) end +---@class NeotestJavaCompiler.Opts +---@field cwd string +---@field compile_target string +---@field compile_mode string +function NeotestJavaCompiler.compile(opts) + logger.debug(("%s build started"):format(opts.compile_mode)) + local result, project = nil, nil + if opts.compile_target == "workspace" then + result, project = compiler.build_workspace(opts) + elseif opts.compile_target == "project" then + result, project = compiler.build_project(opts) + end + logger.debug("build has finished") ----@type table -local compilers = { - jdtls = jdtls_compiler, -} + local msg, level + if result == 0 then + msg = "Build %s has failed" + level = vim.log.levels.ERROR + result = false + elseif result == 1 then + msg = "Built %s successfully" + level = vim.log.levels.INFO + result = true + elseif result == 2 then + msg = "Built %s with errors" + level = vim.log.levels.WARN + result = false + else + msg = "Build %s was canceled" + level = vim.log.levels.INFO + result = false + end + lib.notify(string.format(msg, opts.compile_mode, project), level) + return result, project +end -return compilers +return NeotestJavaCompiler diff --git a/lua/neotest-java/core/spec_builder/compiler/jdtls.lua b/lua/neotest-java/core/spec_builder/compiler/jdtls.lua index fe8012f3..afe51146 100644 --- a/lua/neotest-java/core/spec_builder/compiler/jdtls.lua +++ b/lua/neotest-java/core/spec_builder/compiler/jdtls.lua @@ -1,81 +1,76 @@ local logger = require("neotest-java.logger") +local lsp = require("neotest-java.lsp") +local lib = require("neotest.lib") local nio = require("nio") -local _jdtls = require("neotest-java.command.jdtls") -local scan = require("plenary.scandir") ---- @param bufnr number | nil ---- @return vim.lsp.Client -local function get_client(bufnr) - local client_future = nio.control.future() - nio.run(function() - local clients = vim.lsp.get_clients({ name = "jdtls", bufnr = bufnr }) - client_future.set(clients and clients[1]) - end) - return client_future:wait() -end +-- --- @param bufnr number | nil +-- --- @return vim.lsp.Client +-- local function get_client(bufnr) +-- local client_future = nio.control.future() +-- nio.run(function() +-- local clients = vim.lsp.get_clients({ name = "jdtls", bufnr = bufnr }) +-- client_future.set(clients and clients[1]) +-- end) +-- return client_future:wait() +-- end ---- @param dir string ---- @return string | nil -local function find_any_java_file(dir) - return assert( - vim.iter(nio.fn.globpath(dir or ".", "**/*.java", false, true)):next(), - "No Java file found in the current directory." - ) -end +-- --- @param dir string +-- --- @return string | nil +-- local function find_any_java_file(dir) +-- return assert( +-- vim.iter(nio.fn.globpath(dir or ".", "**/*.java", false, true)):next(), +-- "No Java file found in the current directory." +-- ) +-- end + +-- --- @param path string +-- --- @return number bufnr +-- local function preload_file_for_lsp(path) +-- assert(path, "path cannot be nil") +-- local buf = vim.fn.bufadd(path) -- allocates buffer ID +-- vim.fn.bufload(path) -- preload lines ---- @param path string ---- @return number bufnr -local function preload_file_for_lsp(path) - assert(path, "path cannot be nil") - local buf = vim.fn.bufadd(path) -- allocates buffer ID - vim.fn.bufload(path) -- preload lines +local function get_current_project(working_directory) + local bufnr = nio.api.nvim_get_current_buf() + local err, result = lsp.execute_command("workspace/executeCommand", { + command = "java.project.list", + arguments = { vim.uri_from_fname(working_directory) }, + }, bufnr) + assert(not err, vim.inspect(err)) - return buf + local projects = vim.tbl_filter(function(p) + local path = vim.uri_to_fname(p.uri) + return vim.startswith(path, working_directory) + end, result) + + assert(projects and #projects == 1, vim.inspect(working_directory)) + return projects[1] end ---@type NeotestJavaCompiler -local jdtls_compiler = { - compile = function(args) - -- check there is an active java client - local client = get_client() - local bufnr - if not client then - local any_java_file = assert(find_any_java_file(args.cwd), "No Java file found in the current directory.") - bufnr = preload_file_for_lsp(any_java_file) - - assert( - vim.wait(10000, function() - client = get_client(bufnr) - return not not client and not not client.initialized - end, 1000), - "jdtls client not started in time" - ) - end +local compiler = { + build_workspace = function(args) + local bufnr = nio.api.nvim_get_current_buf() + local err, result = lsp.execute_command("java/buildWorkspace", args.compile_mode == "full", bufnr) + assert(not err, vim.inspect(err)) + logger.info(string.format("Built workspace using %s build", args.compile_mode)) - logger.debug(("compilation in %s mode"):format(args.compile_mode)) - nio.run(function(_) - nio.scheduler() - client:request( - "java/buildWorkspace", - { forceRebuild = args.compile_mode == "full" }, - function(err, result, ctx) - if err then - logger.error("compilation failed: " .. vim.inspect(err)) - end - end, - bufnr or vim.api.nvim_get_current_buf() - ) - end):wait() - logger.debug("compilation complete!") + local project = get_current_project(args.cwd) + return result, project.name + end, + build_project = function(args) + local bufnr = nio.api.nvim_get_current_buf() + local project = get_current_project(args.cwd) - logger.debug("scanning for test resources in " .. args.cwd) - local resources = scan.scan_dir(args.cwd, { - only_dirs = true, - search_pattern = "test/resources$", - }) + local err, result = lsp.execute_command("java/buildProjects", { + identifiers = { { uri = project.uri } }, + isFullBuild = args.compile_mode, + }, bufnr) + assert(not err, vim.inspect(err)) + lib.notify(string.format("Built project/s %s using %s mode", project.name, args.compile_mode), vim.log.levels.INFO) - return _jdtls.get_classpath_file_argument(args.classpath_file_dir, resources, args.cwd) + return result, project.name end, } -return jdtls_compiler +return compiler diff --git a/lua/neotest-java/core/spec_builder/init.lua b/lua/neotest-java/core/spec_builder/init.lua index f4acad03..b9e6ee4b 100644 --- a/lua/neotest-java/core/spec_builder/init.lua +++ b/lua/neotest-java/core/spec_builder/init.lua @@ -11,6 +11,8 @@ local Project = require("neotest-java.types.project") local ch = require("neotest-java.context_holder") local find_module_by_filepath = require("neotest-java.util.find_module_by_filepath") local compilers = require("neotest-java.core.spec_builder.compiler") +local classpath = require("neotest-java.command.classpath") +local scan = require("plenary.scandir") local SpecBuilder = {} @@ -28,7 +30,6 @@ function SpecBuilder.build_spec(args, project_type, config) local tree = args.tree local position = tree:data() local root = assert(ch:get_context().root) - local absolute_path = position.path local project = assert(Project.from_root_dir(root), "project not detected correctly") local modules = project:get_modules() local build_tool = build_tools.get(project_type) @@ -36,26 +37,26 @@ function SpecBuilder.build_spec(args, project_type, config) -- make sure we are in root_dir nio.fn.chdir(root) - -- make sure outputDir is created to operate in it - local output_dir = build_tool.get_output_dir() - local output_dir_parent = compatible_path(path:new(output_dir):parent().filename) - - vim.uv.fs_mkdir(output_dir_parent, 493) - vim.uv.fs_mkdir(output_dir, 493) - - -- JUNIT REPORT DIRECTORY - local reports_dir = - compatible_path(string.format("%s/junit-reports/%s", output_dir, nio.fn.strftime("%d%m%y%H%M%S"))) - command:reports_dir(compatible_path(reports_dir)) - - local module_dirs = vim.iter(modules) + local module_dirs = vim + .iter(modules) :map(function(mod) return mod.base_dir end) :totable() - local base_dir = assert(find_module_by_filepath(module_dirs, position.path), "module base_dir not found") + + local base_dir = assert(find_module_by_filepath(module_dirs, position.path), "module directory not found") command:basedir(base_dir) + -- BUILD OUTPUT DIRECTORY + local output_dir = build_tool.get_output_dir(base_dir) + local output_dir_parent = compatible_path(path:new(output_dir):parent().filename) + + -- JUNIT REPORTS DIRECTORY + local base_reports_dir = string.format("%s/junit-reports", output_dir) + local reports_dir = compatible_path(string.format("%s/%s", base_reports_dir, nio.fn.strftime("%d%m%y%H%M%S"))) + command:reports_dir(compatible_path(reports_dir)) + + -- AUXILIARY TESTRUN RESOURCES command:spring_property_filepaths(build_tool.get_spring_property_filepaths(module_dirs)) -- TEST SELECTORS @@ -70,11 +71,43 @@ function SpecBuilder.build_spec(args, project_type, config) end -- COMPILATION STEP - local compile_mode = ch.config().incremental_build and "incremental" or "full" - local classpath_file_arg = - compilers.jdtls.compile({ cwd = base_dir, classpath_file_dir = output_dir, compile_mode = compile_mode }) + config = ch.config() + local build_type = config.incremental_build + local kind = config.build_target or "project" + local mode = build_type and "incremental" or "full" + local build_result, project_name = compilers.compile({ + classpath_file_dir = output_dir, + compile_target = kind, + compile_mode = mode, + cwd = base_dir, + }) + + -- VERIFY THE BUILD WAS OKAY + if build_result == false then + return { + command = {}, + context = {}, + } + end + + logger.debug("scanning for test resources in " .. base_dir) + local resources = scan.scan_dir(base_dir, { + only_dirs = true, + search_pattern = "test/resources$", + }) + logger.debug("scanning tests completed for " .. base_dir) + + local class_paths = classpath.get_classpaths(resources, base_dir) + local classpath_file_arg = table.concat(class_paths, ":") command:classpath_file_arg(classpath_file_arg) + -- MAKE TEST RESOURCES + vim.uv.fs_mkdir(output_dir_parent, 493) + vim.uv.fs_mkdir(output_dir, 493) + vim.uv.fs_mkdir(base_reports_dir, 493) + vim.uv.fs_mkdir(reports_dir, 493) + assert(vim.uv.fs_stat(reports_dir) ~= nil, string.format("unable to stat %s", reports_dir)) + -- DAP STRATEGY if args.strategy == "dap" then local port = random_port() @@ -84,17 +117,18 @@ function SpecBuilder.build_spec(args, project_type, config) logger.debug("junit debug command: ", junit.command, " ", table.concat(junit.args, " ")) local terminated_command_event = build_tools.launch_debug_test(junit.command, junit.args) - local project_name = vim.fn.fnamemodify(root, ":t") return { strategy = { type = "java", request = "attach", name = ("neotest-java (on port %s)"):format(port), - host = "localhost", + host = "127.0.0.1", port = port, + modulePaths = {}, -- ??? + classPaths = class_paths, projectName = project_name, }, - cwd = root, + cwd = base_dir, symbol = position.type == "test" and position.name or nil, context = { strategy = args.strategy, @@ -108,7 +142,7 @@ function SpecBuilder.build_spec(args, project_type, config) logger.info("junit command: ", command:build_to_string()) return { command = command:build_to_string(), - cwd = root, + cwd = base_dir, symbol = position.name, context = { reports_dir = reports_dir }, } diff --git a/lua/neotest-java/logger.lua b/lua/neotest-java/logger.lua index ac4e109a..68b8f73e 100644 --- a/lua/neotest-java/logger.lua +++ b/lua/neotest-java/logger.lua @@ -54,8 +54,7 @@ function Logger.new(filename, opts) local log_info = vim.loop.fs_stat(logger._filename) if log_info and log_info.size > LARGE then - local warn_msg = - string.format("Neotest log is large (%d MB): %s", log_info.size / (1000 * 1000), logger._filename) + local warn_msg = string.format("Neotest log is large (%d MB): %s", log_info.size / (1000 * 1000), logger._filename) vim.notify(warn_msg, vim.log.levels.WARN) end diff --git a/lua/neotest-java/lsp/init.lua b/lua/neotest-java/lsp/init.lua new file mode 100644 index 00000000..9b87b170 --- /dev/null +++ b/lua/neotest-java/lsp/init.lua @@ -0,0 +1,52 @@ +local log = require("neotest-java.logger") +local nio = require("nio") + +local function coc_command(id, params, _) + -- fix: this can be cached to avoid fetching it, however it is specific and can be overriden on a workspace folder level, + -- therefore extra care has to be taken when caching the settings, otherwise they will end up out of sync or invalid + local settings = nio.fn["coc#util#get_config"]("java") + + if params == nil then + params = {} + end + if type(params) ~= "table" then + params = { params } + end + local ok_services, services = pcall(nio.fn.CocAction, "services") + services = ok_services + and vim.tbl_filter(function(service) + return service and service.state == "running" and service.id == "java" + end, services) + or {} + assert(#services > 0, "there is no jdtls client attached") + + local ok_request, result = pcall(nio.fn.CocRequest, "java", id, params) + if not ok_request or not result then + log.warn(string.format("Unable to run lsp request %s with payload %s", id, vim.inspect(params))) + end + local err = not ok_request and result ~= vim.NIL and { message = result } or nil + return err, result, settings +end + +local function lsp_command(id, params, bufnr) + local clients = vim.lsp.get_clients({ name = "jdtls" }) + assert(#clients > 0, "there is no jdtls client attached") + + local response, error = clients[1].request_sync(id, params, 5000, bufnr) + if error then + log.warn(string.format("Unable to run lsp command %s with payload %s", id, vim.inspect(params))) + end + return error, response and response.result, clients[1].config.settings.java +end + +local function execute_command(id, params, bufnr) + if vim.g.did_coc_loaded ~= nil then + return coc_command(id, params, bufnr) + else + return lsp_command(id, params, bufnr) + end +end + +return { + execute_command = execute_command, +} diff --git a/lua/neotest-java/types/junit_result.lua b/lua/neotest-java/types/junit_result.lua index ef392649..4d5d23a0 100644 --- a/lua/neotest-java/types/junit_result.lua +++ b/lua/neotest-java/types/junit_result.lua @@ -161,7 +161,8 @@ function JunitResult.merge_results(results) return result:status() == FAILED end) and FAILED or PASSED - local output = vim.iter(results) + local output = vim + .iter(results) :map(function(result) return result:output() end) @@ -172,14 +173,16 @@ function JunitResult.merge_results(results) return { status = status, output = create_file_with_content(output) } end - local errors = vim.iter(results) + local errors = vim + .iter(results) :map(function(result) return result:errors(true) end) :flatten() :totable() - local short = vim.iter(results) + local short = vim + .iter(results) :filter(function(result) return result:status() == FAILED end) diff --git a/lua/neotest-java/util/path.lua b/lua/neotest-java/util/path.lua index c26bee0a..46a6ac2a 100644 --- a/lua/neotest-java/util/path.lua +++ b/lua/neotest-java/util/path.lua @@ -54,8 +54,7 @@ local function Path(raw_path, opts) table.insert(slugs, 1, "") end - local has_relative_dot = raw_path:sub(1, 2) == "." .. UNIX_SEPARATOR - or raw_path:sub(1, 2) == "." .. WINDOWS_SEPARATOR + local has_relative_dot = raw_path:sub(1, 2) == "." .. UNIX_SEPARATOR or raw_path:sub(1, 2) == "." .. WINDOWS_SEPARATOR if has_relative_dot then table.insert(slugs, 1, ".") end diff --git a/tests/init_spec.lua b/tests/init_spec.lua index b61ed66e..cb8568a0 100644 --- a/tests/init_spec.lua +++ b/tests/init_spec.lua @@ -3,9 +3,7 @@ local compatible_path = require("neotest-java.util.compatible_path") describe("NeotestJava plugin", function() local default_config = { default_version = "1.10.1", - junit_jar = compatible_path( - vim.fn.stdpath("data") .. "/neotest-java/junit-platform-console-standalone-1.10.1.jar" - ), + junit_jar = compatible_path(vim.fn.stdpath("data") .. "/neotest-java/junit-platform-console-standalone-1.10.1.jar"), incremental_build = true, }