diff --git a/lua/neotest-java/command/junit_command_builder.lua b/lua/neotest-java/command/junit_command_builder.lua index 5e95b4c4..29ddf32f 100644 --- a/lua/neotest-java/command/junit_command_builder.lua +++ b/lua/neotest-java/command/junit_command_builder.lua @@ -25,7 +25,7 @@ local CommandBuilder = { local method_name if type == "test" then - method_name = node_name + method_name = qualified_name end self._test_references[#self._test_references + 1] = { @@ -99,7 +99,7 @@ local CommandBuilder = { local selectors = {} for _, v in ipairs(self._test_references) do if v.type == "test" then - table.insert(selectors, "-m=" .. v.qualified_name .. "#" .. v.method_name) + table.insert(selectors, "--select-method='" .. v.qualified_name .. "'") elseif v.type == "file" then table.insert(selectors, "-c=" .. v.qualified_name) elseif v.type == "dir" then diff --git a/lua/neotest-java/core/positions_discoverer_dev.lua b/lua/neotest-java/core/positions_discoverer_dev.lua new file mode 100644 index 00000000..ae7b2240 --- /dev/null +++ b/lua/neotest-java/core/positions_discoverer_dev.lua @@ -0,0 +1,214 @@ +local lib = require("neotest.lib") +local Tree = require("neotest.types").Tree + +local PositionsDiscoverer = {} + +---Given a file path, parse all the tests within it. +---@async +---@param file_path string Absolute file path +---@return neotest.Tree | nil +function PositionsDiscoverer.discover_positions(file_path) + local src = lib.files.read(file_path) + + -- We only need to capture package and test-method names. + local query = vim.treesitter.query.parse( + "java", + [[ + + ;; Package declaration + (program + (package_declaration + (scoped_identifier) @package.name + )? + ) + + ;; Test class + (class_declaration + name: (identifier) @class.name + ) @class.definition + + ;; Annotated test methods + (method_declaration + (modifiers + [ + (marker_annotation + name: (identifier) @annotation + (#any-of? @annotation "Test" "ParameterizedTest" "TestFactory" "CartesianTest") + ) + (annotation + name: (identifier) @annotation + (#any-of? @annotation "Test" "ParameterizedTest" "TestFactory" "CartesianTest") + ) + ] + ) + name: (identifier) @test.name + ) @test.definition + + ]] + ) + + local ts_tree = vim.treesitter.get_string_parser(src, "java"):parse()[1] + + -- function just to take the pacakage name + --- @return string | nil + local get_package_name = function() + local result = nil + for id, node in query:iter_captures(ts_tree:root(), src, 0, -1) do + local cap = query.captures[id] + if cap == "package.name" then + result = (vim.treesitter.get_node_text(node, src) or ""):gsub("%s+", "") + break + end + end + return result + end + + local pkg = get_package_name() + + --- recursively build list of nodes from TSNode tree + --- @param node TSNode + --- @return {id: string, name: string, path: string, range: table}[] + local function build_tree(node) + local captures = vim + .iter(query:iter_captures(node, src, 0, -1)) + ---@param child TSNode + :filter(function(_, child) + return not child:parent() or child:parent() == node + end) + :map(function(id, child) + return { + id = id, + child = child, + } + end) + :totable() + + return vim + .iter(captures) + :map(function(c) + return query.captures[c.id], c.child + end) + --- @param child TSNode + :map(function(cap, child) + if cap == "package.declaration" then + return { + id = pkg or "", + name = file_path:gsub(".*/", ""), + path = file_path, + range = { node:range() }, + } + end + + if cap == "class.definition" then + local name = vim.treesitter.get_node_text(child:field("name")[1], src) or "Unknown" + + local function is_nonempty(x) + if x == nil then + return false + end + if type(x) ~= "table" then + return true + end + return next(x) ~= nil -- tabla vacía => false + end + local children = vim.iter(child:iter_children()):map(build_tree):filter(is_nonempty):totable() + local children_flattered = vim.iter(children):flatten():totable() + + local inner_classname = name + local cur = child:parent() + while cur do + if cur:type() == "class_declaration" then + inner_classname = vim.treesitter.get_node_text(cur:field("name")[1], src) + .. "$" + .. inner_classname + end + cur = cur:parent() + end + + local fqn = (pkg and (pkg .. ".") or "") .. inner_classname + + -- return { + -- { + -- id = fqn, + -- name = name, + -- path = file_path, + -- range = { child:range() }, + -- type = "namespace", + -- }, + -- unpack(children_flattered), + -- } + -- + -- + -- + -- construye el nodo de clase y pega sus hijos SIN flatten + local nodes = { + { + id = fqn, + name = name, + path = file_path, + range = { child:range() }, + type = "namespace", + }, + } + + for _, subtree in ipairs(children) do + -- subtree puede ser: + -- - una lista { namespace, ...children } + -- - un único nodo (p.ej., un método) + table.insert(nodes, subtree) + end + + print(vim.inspect({ fqn = fqn, n = nodes })) + return nodes + end + + if cap == "test.definition" then + local name = vim.treesitter.get_node_text(child:field("name")[1], src) or "Unknown" + + local parts = {} + local cur = child:parent() + while cur do + if cur:type() == "class_declaration" then + parts[#parts + 1] = vim.treesitter.get_node_text(cur:field("name")[1], src) + end + cur = cur:parent() + end + + local inner_classname = vim.iter(parts):rev():join("$") + + local fqn = (pkg and (pkg .. ".") or "") .. inner_classname .. "#" .. name + + return { + { + id = fqn, + name = name, + path = file_path, + range = { child:range() }, + type = "test", + }, + } + end + end) + :flatten() + :totable() + end + + local l = build_tree(ts_tree:root()) + + print("L: " .. vim.inspect(l)) + + return Tree.from_list({ + { + id = file_path, + name = file_path:gsub(".*/", ""), + path = file_path, + range = { ts_tree:root():range() }, + type = "file", + }, + l, + }, function(pos) + return pos.id + end) +end + +return PositionsDiscoverer diff --git a/lua/neotest-java/core/result_builder.lua b/lua/neotest-java/core/result_builder.lua index a184ef14..9cbcc7f7 100644 --- a/lua/neotest-java/core/result_builder.lua +++ b/lua/neotest-java/core/result_builder.lua @@ -110,7 +110,7 @@ function ResultBuilder.build_results(spec, result, tree, scan, read_file) -- lua local classname = jresult:classname() name = name:gsub("%(.*%)", "") - local unique_key = build_unique_key(classname, name) + local unique_key = classname .. "#" .. name testcases[unique_key] = testcase testcases_junit[unique_key] = jresult end @@ -140,7 +140,7 @@ function ResultBuilder.build_results(spec, result, tree, scan, read_file) -- lua qualified_name = qualified_name .. "::" .. inner_classes end - local unique_key = build_unique_key(qualified_name, node.name) + local unique_key = node.id if is_parameterized then local jtestcases = extract_parameterized_tests(testcases, unique_key) diff --git a/lua/neotest-java/core/spec_builder/init.lua b/lua/neotest-java/core/spec_builder/init.lua index dfef793f..6aff78ed 100644 --- a/lua/neotest-java/core/spec_builder/init.lua +++ b/lua/neotest-java/core/spec_builder/init.lua @@ -66,17 +66,16 @@ function SpecBuilder.build_spec(args, project_type, config) command:test_reference(resolve_qualfied_name(child.path), child.name, "file") end end + elseif position.type == "file" then + command:test_reference(resolve_qualfied_name(absolute_path), position.name, "file") elseif position.type == "namespace" then for _, child in tree:iter() do if child.type == "test" then - command:test_reference(resolve_qualfied_name(child.path), child.name, "test") + command:test_reference(child.id, child.name, "test") end end - elseif position.type == "file" then - command:test_reference(resolve_qualfied_name(absolute_path), position.name, "file") elseif position.type == "test" then - -- note: parameterized tests are not being discovered by the junit standalone, so we run tests per file - command:test_reference(resolve_qualfied_name(absolute_path), position.name, "file") + command:test_reference(position.id, position.name, "test") end -- COMPILATION STEP diff --git a/lua/neotest-java/init.lua b/lua/neotest-java/init.lua index 743ce05a..cf9d6306 100644 --- a/lua/neotest-java/init.lua +++ b/lua/neotest-java/init.lua @@ -3,7 +3,8 @@ local File = require("neotest.lib.file") local file_checker = require("neotest-java.core.file_checker") local root_finder = require("neotest-java.core.root_finder") local dir_filter = require("neotest-java.core.dir_filter") -local position_discoverer = require("neotest-java.core.positions_discoverer") +-- local position_discoverer = require("neotest-java.core.positions_discoverer") +local position_discoverer = require("neotest-java.core.positions_discoverer_dev") local spec_builder = require("neotest-java.core.spec_builder") local result_builder = require("neotest-java.core.result_builder") local log = require("neotest-java.logger") @@ -55,7 +56,7 @@ local NeotestJavaAdapter = { -- build spec return spec_builder.build_spec(args, project_type, ch.get_context().config) end, -}; +} -- on init (function() diff --git a/tests/core/positions_discoverer_spec.lua b/tests/core/positions_discoverer_spec.lua index da01e030..2a36b491 100644 --- a/tests/core/positions_discoverer_spec.lua +++ b/tests/core/positions_discoverer_spec.lua @@ -1,6 +1,8 @@ +---@module "luassert" local _ = require("vim.treesitter") -- NOTE: needed for loading treesitter upfront for the tests local async = require("nio").tests local plugin = require("neotest-java") +local positions_discoverer = require("neotest-java.core.positions_discoverer_dev") local eq = assert.are.same @@ -29,6 +31,59 @@ describe("PositionsDiscoverer", function() return tmp_file end + async.it("method FQN with inner classes", function() + local file_path = create_tmp_javafile([[ + package com.example; + + class Outer { + class Inner { + @Test + void simpleTestMethod() {} + } + } + ]]) + + --- @type neotest.Tree + local result = assert(plugin.discover_positions(file_path)) + + eq({ + { + id = file_path, + name = file_path:gsub(".*/", ""), + path = file_path, + range = { 0, 4, 8, 2 }, + type = "file", + }, + { + { + id = "com.example.Outer", + name = "Outer", + path = file_path, + range = { 2, 4, 7, 5 }, + type = "namespace", + }, + { + { + id = "com.example.Outer$Inner", + name = "Inner", + path = file_path, + range = { 3, 6, 6, 7 }, + type = "namespace", + }, + { + { + id = "com.example.Outer$Inner#simpleTestMethod", + name = "simpleTestMethod", + path = file_path, + range = { 4, 8, 5, 34 }, + type = "test", + }, + }, + }, + }, + }, result:to_list()) + end) + async.it("should discover simple test method", function() -- given local file_path = create_tmp_javafile([[ @@ -57,6 +112,69 @@ class Test { eq(1, #actual:children()[1]:children()) end) + async.it("should discover two simple test method", function() + -- given + local file_path = create_tmp_javafile([[ +class Test { + + @Test + public void firstTestMethod() { + assertThat(1).isEqualTo(1); + } + + @Test + public void secondTestMethod() { + assertThat(1).isEqualTo(1); + } + +} + ]]) + + -- when + local actual = assert(plugin.discover_positions(file_path)) + + -- then + local actual_list = actual:to_list() + print(vim.inspect(actual_list)) + + eq({ + { + id = file_path, + name = file_path:gsub(".*/", ""), + path = file_path, + range = { 0, 0, 13, 2 }, + type = "file", + }, + { + { + id = "Test", + name = "Test", + path = file_path, + range = { 0, 0, 12, 1 }, + type = "namespace", + }, + { + { + id = "Test#firstTestMethod", + name = "firstTestMethod", + path = file_path, + range = { 2, 2, 5, 3 }, + type = "test", + }, + }, + { + { + id = "Test#secondTestMethod", + name = "secondTestMethod", + path = file_path, + range = { 7, 2, 10, 3 }, + type = "test", + }, + }, + }, + }, actual_list) + end) + async.it("should discover ParameterizedTest", function() -- given local file_path = create_tmp_javafile([[ @@ -90,11 +208,51 @@ class Test { -- then local actual_list = actual:to_list() - eq("parameterizedTestWithValueSource", actual_list[2][2][1].name) - eq("parameterizedTestWithMethodSource", actual_list[2][3][1].name) - eq("parameterizedTestWithMethodSourceAndExplicitName", actual_list[2][4][1].name) - - eq(3, #actual:children()[1]:children()) + eq({ + { + id = file_path, + name = file_path:gsub(".*/", ""), + path = file_path, + range = { 0, 0, 22, 2 }, + type = "file", + }, + { + { + id = "Test", + name = "Test", + path = file_path, + range = { 0, 0, 20, 1 }, + type = "namespace", + }, + { + { + id = "Test#parameterizedTestWithValueSource", + name = "parameterizedTestWithValueSource", + path = file_path, + range = { 2, 2, 6, 3 }, + type = "test", + }, + { + { + id = "Test#parameterizedTestWithMethodSource", + name = "parameterizedTestWithMethodSource", + path = file_path, + range = { 8, 2, 12, 3 }, + type = "test", + }, + }, + { + { + id = "Test#parameterizedTestWithMethodSourceAndExplicitName", + name = "parameterizedTestWithMethodSourceAndExplicitName", + path = file_path, + range = { 14, 2, 18, 3 }, + type = "test", + }, + }, + }, + }, + }, actual_list) end) async.it("should discover nested tests", function() @@ -119,11 +277,64 @@ public class SomeTest { -- when local actual = assert(plugin.discover_positions(file_path)) - -- then - local test_name = actual:to_list()[2][2][2][2][1].name - eq(test_name, "someTest") + print(vim.inspect(actual:to_list())) + + eq({ + { + id = file_path, + name = file_path:gsub(".*/", ""), + path = file_path, + range = { 0, 0, 15, 2 }, + type = "file", + }, + { + { + id = "SomeTest", + name = "SomeTest", + path = file_path, + range = { 0, 0, 14, 1 }, + type = "namespace", + }, + { + { + id = "SomeTest$SomeNestedTest", + name = "SomeNestedTest", + path = file_path, + range = { 1, 4, 13, 5 }, + type = "namespace", + }, + { + { + { + id = "SomeTest$SomeNestedTest$AnotherNestedTest", + name = "AnotherNestedTest", + path = file_path, + range = { 2, 8, 7, 9 }, + type = "namespace", + }, + { + { + id = "SomeTest$SomeNestedTest$AnotherNestedTest#someTest", + name = "someTest", + path = file_path, + range = { 3, 12, 6, 13 }, + type = "test", + }, + }, + }, + }, - local another_outer_test_name = actual:to_list()[2][2][3][1].name - eq(another_outer_test_name, "oneMoreOuterTest") + { + { + id = "SomeTest$SomeNestedTest#oneMoreOuterTest", + name = "oneMoreOuterTest", + path = file_path, + range = { 9, 8, 12, 9 }, + type = "test", + }, + }, + }, + }, + }, actual:to_list()) end) end) diff --git a/tests/core/result_builder_spec.lua b/tests/core/result_builder_spec.lua index 9c021ee5..df548c22 100644 --- a/tests/core/result_builder_spec.lua +++ b/tests/core/result_builder_spec.lua @@ -81,11 +81,11 @@ describe("ResultBuilder", function() local tree = plugin.discover_positions(file_path) local expected = { - [current_dir .. "tests/fixtures/maven-demo/src/test/java/com/example/ExampleTest.java::ExampleTest::shouldFail"] = { + ["com.example.ExampleTest#shouldFail"] = { status = "skipped", output = TEMPNAME, }, - [current_dir .. "tests/fixtures/maven-demo/src/test/java/com/example/ExampleTest.java::ExampleTest::shouldNotFail"] = { + ["com.example.ExampleTest#shouldNotFail"] = { status = "skipped", output = TEMPNAME, }, @@ -139,14 +139,14 @@ describe("ResultBuilder", function() end local expected = { - [file_path .. "::ExampleTest::shouldFail"] = { + ["com.example.ExampleTest#shouldFail"] = { -- errors = { { line = 13, message = "expected: but was: " } }, errors = { { message = "expected: but was: " } }, short = "expected: but was: ", status = "failed", output = TEMPNAME, }, - [file_path .. "::ExampleTest::shouldNotFail"] = { + ["com.example.ExampleTest#shouldNotFail"] = { status = "passed", output = TEMPNAME, }, @@ -207,7 +207,7 @@ describe("ResultBuilder", function() end local expected = { - [file_path .. "::ErroneousTest::shouldFailOnError"] = { + ["com.example.ErroneousTest#shouldFailOnError"] = { errors = { { message = "Error creating bean with name 'com.example.ErroneousTest': Injection of autowired dependencies failed", @@ -258,7 +258,7 @@ describe("ResultBuilder", function() local file_path = create_tempfile_with_test(file_content) local expected = { - [file_path .. "::RepositoryIT::shouldWorkProperly"] = { + ["com.example.demo.RepositoryIT#shouldWorkProperly"] = { status = "passed", output = TEMPNAME, }, @@ -346,7 +346,7 @@ describe("ResultBuilder", function() end local expected = { - [file_path .. "::ParameterizedMethodTest::parameterizedMethodShouldFail"] = { + ["com.example.ParameterizedMethodTest#parameterizedMethodShouldFail"] = { errors = { { -- line = 27, @@ -361,7 +361,7 @@ describe("ResultBuilder", function() status = "failed", output = TEMPNAME, }, - [file_path .. "::ParameterizedMethodTest::parameterizedMethodShouldNotFail"] = { + ["com.example.ParameterizedMethodTest#parameterizedMethodShouldNotFail"] = { status = "passed", output = TEMPNAME, }, @@ -425,7 +425,7 @@ describe("ResultBuilder", function() end local expected = { - [file_path .. "::EmptySourceTest::emptySourceShouldFail"] = { + ["com.example.EmptySourceTest#emptySourceShouldFail"] = { errors = { { -- line = 22, @@ -436,7 +436,7 @@ describe("ResultBuilder", function() status = "failed", output = TEMPNAME, }, - [file_path .. "::EmptySourceTest::emptySourceShouldPass"] = { + ["com.example.EmptySourceTest#emptySourceShouldPass"] = { status = "passed", output = TEMPNAME, }, @@ -496,11 +496,11 @@ describe("ResultBuilder", function() end local expected = { - [file_path .. "::NestedTest::Level2::nestedTest"] = { + ["com.example.NestedTest$Level2#nestedTest"] = { status = "passed", output = TEMPNAME, }, - [file_path .. "::NestedTest::plainTest"] = { + ["com.example.NestedTest#plainTest"] = { status = "passed", output = TEMPNAME, },