diff --git a/lua/neotest-java/build_tool/init.lua b/lua/neotest-java/build_tool/init.lua index e5bb455..4c7d0f9 100644 --- a/lua/neotest-java/build_tool/init.lua +++ b/lua/neotest-java/build_tool/init.lua @@ -5,6 +5,8 @@ local nio = require("nio") local Job = require("plenary.job") local lib = require("neotest.lib") +local _, repl = pcall(require, "dap.repl") + ---@class neotest-java.BuildTool ---@field get_build_dirname fun(): neotest-java.Path ---@field get_project_filename fun(): string @@ -25,8 +27,9 @@ end ---@param command string ---@param args string[] +---@param cwd neotest-java.Path ---@return nio.control.Event -build_tools.launch_debug_test = function(command, args) +build_tools.launch_debug_test = function(command, args, cwd) lib.notify("Running debug test", vim.log.levels.INFO) log.trace("run_debug_test function") @@ -36,14 +39,31 @@ build_tools.launch_debug_test = function(command, args) local stderr = {} local job = Job:new({ command = command, + cwd = cwd:to_string(), args = args, on_stderr = function(_, data) + if data == nil then + return + end stderr[#stderr + 1] = data + if repl then + vim.schedule(function() + repl.append(data) + end) + end end, on_stdout = function(_, data) + if data == nil then + return + end if string.find(data, "Listening") then test_command_started_listening.set() end + if repl then + vim.schedule(function() + repl.append(data) + end) + end end, on_exit = function(_, code) terminated_command_event.set() @@ -56,6 +76,9 @@ build_tools.launch_debug_test = function(command, args) end, }) log.debug("starting job with command: ", command, " ", table.concat(args, " ")) + if repl then + repl.clear() + end job:start() test_command_started_listening.wait() diff --git a/lua/neotest-java/command/junit_command_builder.lua b/lua/neotest-java/command/junit_command_builder.lua index a6f34e5..97889c0 100644 --- a/lua/neotest-java/command/junit_command_builder.lua +++ b/lua/neotest-java/command/junit_command_builder.lua @@ -80,9 +80,10 @@ CommandBuilder.build_junit = function(self, port) 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 .. "'") if v.method_name then table.insert(selectors, "--select-method='" .. v.method_name .. "'") + else + table.insert(selectors, "--select-class='" .. class_name .. "'") end end end @@ -101,6 +102,10 @@ CommandBuilder.build_junit = function(self, port) unpack(self._jvm_args), } + if self._basedir then + table.insert(jvm_args, 1, "-Duser.dir=" .. self._basedir:to_string()) + end + local junit_command = { command = self._java_bin:to_string(), args = vim.iter({ @@ -114,6 +119,7 @@ CommandBuilder.build_junit = function(self, port) "--disable-banner", "--details=testfeed", "--config=junit.platform.output.capture.stdout=true", + "--config=junit.platform.output.capture.stderr=true", }) :flatten() :totable(), diff --git a/lua/neotest-java/core/spec_builder/init.lua b/lua/neotest-java/core/spec_builder/init.lua index 1462fcc..da893bc 100644 --- a/lua/neotest-java/core/spec_builder/init.lua +++ b/lua/neotest-java/core/spec_builder/init.lua @@ -21,7 +21,7 @@ local Binaries = require("neotest-java.command.binaries") --- @field scan fun(base_dir: neotest-java.Path, opts: { search_patterns: string[] }): neotest-java.Path[] --- @field compile fun(cwd: neotest-java.Path, compile_mode: string) --- @field classpath_provider neotest-java.ClasspathProvider ---- @field report_folder_name_gen fun(build_dir: neotest-java.Path): neotest-java.Path +--- @field report_folder_name_gen fun(module_dir: neotest-java.Path, build_dir: neotest-java.Path): neotest-java.Path --- @field build_tool_getter fun(project_type: string): neotest-java.BuildTool --- @field detect_project_type fun(base_dir: neotest-java.Path): string --- @field binaries neotest-java.LspBinaries @@ -51,8 +51,9 @@ local DEFAULT_DEPENDENCIES = { }) end, classpath_provider = ClasspathProvider({ client_provider = client_provider }), - report_folder_name_gen = function(build_dir) - return build_dir:append("junit-reports"):append(nio.fn.strftime("%d%m%y%H%M%S")) + report_folder_name_gen = function(module_dir, build_dir) + local base = (module_dir and module_dir:append(build_dir:to_string())) or build_dir + return base:append("junit-reports"):append(nio.fn.strftime("%d%m%y%H%M%S")) end, build_tool_getter = function(project_type) return build_tools.get(project_type) @@ -117,6 +118,10 @@ function SpecBuilder.build_spec(args, config, deps) command:basedir(module.base_dir) + -- JUNIT REPORT DIRECTORY + local reports_dir = deps.report_folder_name_gen(module.base_dir, build_dir) + command:reports_dir(reports_dir) + command:spring_property_filepaths(build_tool.get_spring_property_filepaths(project:get_module_dirs())) -- TEST SELECTORS @@ -147,7 +152,7 @@ function SpecBuilder.build_spec(args, config, deps) -- PREPARE DEBUG TEST COMMAND local junit = command:build_junit(port) 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 terminated_command_event = build_tools.launch_debug_test(junit.command, junit.args, module.base_dir) local project_name = vim.fn.fnamemodify(root:to_string(), ":t") return { @@ -159,7 +164,7 @@ function SpecBuilder.build_spec(args, config, deps) port = port, projectName = project_name, }, - cwd = root:to_string(), + cwd = module.base_dir:to_string(), symbol = position.type == "test" and position.name or nil, context = { strategy = args.strategy, @@ -173,7 +178,7 @@ function SpecBuilder.build_spec(args, config, deps) logger.info("junit command: ", command:build_to_string()) return { command = command:build_to_string(), - cwd = root:to_string(), + cwd = module.base_dir:to_string(), symbol = position.name, context = { reports_dir = reports_dir }, } diff --git a/lua/neotest-java/model/junit_result.lua b/lua/neotest-java/model/junit_result.lua index ef39264..44a7eba 100644 --- a/lua/neotest-java/model/junit_result.lua +++ b/lua/neotest-java/model/junit_result.lua @@ -79,8 +79,7 @@ function JunitResult:classname() end ---@return neotest.ResultStatus ----@return string | nil failure_message a short output ----@return string | nil failure_output a more detailed output +---@return table an array-like table containing tables of the form {failure_message: string, failure_output: string} function JunitResult:status() local failed = self.testcase.failure or self.testcase.error -- This is not parsed correctly by the library @@ -88,10 +87,21 @@ function JunitResult:status() -- it breaks in the first '>' -- so it does not detect message attribute sometimes if failed and not failed._attr then - return FAILED, failed[1], failed[2] + local failures = {} + for i, fail in ipairs(failed) do + failures[i] = { + failure_message = fail._attr.message or fail._attr.type or "", + failure_output = fail[1], + } + end + return FAILED, failures end if failed and failed._attr then - return FAILED, failed._attr.message, failed[1] + local fail = { + failure_message = failed._attr.message or failed._attr.type or "", + failure_output = failed[1], + } + return FAILED, { fail } end return PASSED end @@ -100,24 +110,32 @@ end ---@return neotest.Error[] | nil function JunitResult:errors(with_name_prefix) with_name_prefix = with_name_prefix or false - local status, failure_message, failure_output = self:status() + local status, failures = self:status() + if status == PASSED then + return nil + end + local filename = string.match(self:classname(), "[%.]?([%a%$_][%a%d%$_]+)$") .. ".java" local line_searchpattern = string.gsub(filename, "%.", "%%.") .. ":(%d+)%)" + local errors = {} + + for i, failure in ipairs(failures) do + local line + if failure.failure_output then + line = string.match(failure.failure_output, line_searchpattern) + -- NOTE: errors array is expecting lines properties to be 0 index based + line = line and line - 1 or nil + end - local line - if failure_output then - line = string.match(failure_output, line_searchpattern) - -- NOTE: errors array is expecting lines properties to be 0 index based - line = line and line - 1 or nil - end + local failure_message = failure.failure_message + if with_name_prefix then + failure_message = self:name() .. " -> " .. failure_message + end - if status == PASSED then - return nil - end - if with_name_prefix then - failure_message = self:name() .. " -> " .. failure_message + errors[i] = { message = failure_message, line = line } end - return { { message = failure_message, line = line } } + + return errors end ---@return string[] @@ -127,9 +145,12 @@ function JunitResult:output() system_out = { system_out } end - local status, _, failure_output = self:status() + local status, failures = self:status() if status == FAILED then - system_out[#system_out + 1] = failure_output + for _, failure in ipairs(failures) do + system_out[#system_out + 1] = failure.failure_output + system_out[#system_out + 1] = NEW_LINE + end else -- PASSED system_out[#system_out + 1] = "Test passed" .. NEW_LINE end @@ -141,7 +162,25 @@ end --- Each time this function is called, it will create a temporary file with the output content ---@return neotest.Result function JunitResult:result() - local status, failure_message = self:status() + local status, failures = self:status() + + if status == PASSED then + return { + status = status, + output = create_file_with_content(self:output()), + } + end + + local failure_message = "" + if failures then + for i, failure in ipairs(failures) do + failure_message = failure_message .. failure.failure_message + if i < #failures then + failure_message = failure_message .. NEW_LINE + end + end + end + return { status = status, short = failure_message, @@ -187,7 +226,18 @@ function JunitResult.merge_results(results) return result:errors(), result:name() end) :map(function(error, name) - return name .. " -> " .. error[1].message + if #error == 1 then + return name .. " -> " .. error[1].message + end + + local errs = name .. " -> {" .. NEW_LINE + for i, err in ipairs(error) do + errs = errs .. err.message + if i < #error then + errs = errs .. NEW_LINE + end + end + return errs .. NEW_LINE .. "}" end) :fold(nil, function(a, b) if not a then diff --git a/tests/unit/result_builder_spec.lua b/tests/unit/result_builder_spec.lua index 00cbfc1..a5bf69a 100644 --- a/tests/unit/result_builder_spec.lua +++ b/tests/unit/result_builder_spec.lua @@ -451,7 +451,6 @@ describe("ResultBuilder", function() local results = result_builder.build_results(DEFAULT_SPEC, SUCCESSFUL_RESULT, tree, scan_dir, read_file) --then - print(vim.inspect({ results = results, expected = expected })) assert.are.same(expected, results) end) @@ -520,4 +519,80 @@ describe("ResultBuilder", function() --then assert.are.same(expected, results) end) + + async.it("should build results with multiple failures", function() + local file_content = [[ + package com.example; + + import org.junit.jupiter.Test; + + import static org.junit.jupiter.api.Assertions.assertFalse; + + public class TwoTests { + + @Test + void test1() { + assertFalse(true); + } + + @Test + void test2() { + assertFalse(true); + } + } + ]] + + local report_file = [[ + + + + FAILURE OUTPUT + + + + + FAILURE OUTPUT + + + FAILURE OUTPUT + + + + ]] + + local file_path = create_tempfile_with_test(file_content) + + local tree = plugin.discover_positions(file_path:to_string()) + local scan_dir = function(dir) + assert(dir == DEFAULT_SPEC.context.reports_dir, "should scan in spec.context.reports_dir") + return { file_path } + end + local read_file = function() + return report_file + end + + local expected = { + ["com.example.TwoTests#test1"] = { + errors = { { message = "test1() -> expected: but was: " } }, + short = "test1() -> expected: but was: ", + status = "failed", + output = TEMPNAME, + }, + ["com.example.TwoTests#test2"] = { + errors = { + { message = "test2() -> expected: but was: " }, + { message = "test2() -> java.lang.StackOverflowError" }, + }, + short = "test2() -> expected: but was: \njava.lang.StackOverflowError", + status = "failed", + output = TEMPNAME, + }, + } + + --when + local results = result_builder.build_results(DEFAULT_SPEC, SUCCESSFUL_RESULT, tree, scan_dir, read_file) + + --then + assert.are.same(expected, results) + end) end) diff --git a/tests/unit/spec_builder_spec.lua b/tests/unit/spec_builder_spec.lua index c1845e2..f22ad5a 100644 --- a/tests/unit/spec_builder_spec.lua +++ b/tests/unit/spec_builder_spec.lua @@ -89,6 +89,7 @@ describe("SpecBuilder", function() eq({ command = vim.iter({ "java", + "-Duser.dir=" .. Path("."):to_string(), "-Dspring.config.additional-location=" .. Path("src/main/resources/application.properties"):to_string(), "-jar", "my-junit-jar.jar", @@ -99,13 +100,13 @@ describe("SpecBuilder", function() "--disable-banner", "--details=testfeed", "--config=junit.platform.output.capture.stdout=true", - "--select-class='com.example.ExampleTest'", + "--config=junit.platform.output.capture.stderr=true", "--select-method='com.example.ExampleTest#shouldNotFail()'", }):join(" "), context = { reports_dir = Path("report_folder"), }, - cwd = ".", + cwd = Path("."):to_string(), symbol = "shouldNotFail", }, actual) end) @@ -164,6 +165,7 @@ describe("SpecBuilder", function() eq({ command = vim.iter({ "java", + "-Duser.dir=" .. Path("/user/home/root"):to_string(), "-Dspring.config.additional-location=" .. Path("src/main/resources/application.properties"):to_string(), "-myExtraJvmArg", "-jar", @@ -175,13 +177,13 @@ describe("SpecBuilder", function() "--disable-banner", "--details=testfeed", "--config=junit.platform.output.capture.stdout=true", - "--select-class='com.example.ExampleTest'", + "--config=junit.platform.output.capture.stderr=true", "--select-method='com.example.ExampleTest#shouldNotFail()'", }):join(" "), context = { reports_dir = Path("report_folder"), }, - cwd = "root", + cwd = Path("/user/home/root"):to_string(), symbol = "shouldNotFail", }, actual) end) @@ -249,6 +251,7 @@ describe("SpecBuilder", function() eq({ command = vim.iter({ "java", + "-Duser.dir=" .. Path("/user/home/root/module-2"):to_string(), "-Dspring.config.additional-location=" .. Path("src/main/resources/application.properties"):to_string(), "-jar", "my-junit-jar.jar", @@ -259,13 +262,13 @@ describe("SpecBuilder", function() "--disable-banner", "--details=testfeed", "--config=junit.platform.output.capture.stdout=true", - "--select-class='com.example.ExampleInSecondModuleTest'", + "--config=junit.platform.output.capture.stderr=true", "--select-method='com.example.ExampleInSecondModuleTest#shouldNotFail()'", }):join(" "), context = { reports_dir = Path("report_folder"), }, - cwd = "root", + cwd = Path("/user/home/root/module-2"):to_string(), symbol = "shouldNotFail", }, actual) end)