Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ test-e2e: install


install: deps/nvim-treesitter deps/nvim-treesitter/parser/java.so deps/neotest deps/nvim-nio deps/plenary.nvim
@-$(MAKE) _install_groovy_parser 2>/dev/null || echo "Note: Groovy parser not compiled (optional for Java-only development)"

_install_groovy_parser: deps/nvim-treesitter/parser/groovy.so

deps/plenary.nvim:
mkdir -p deps
Expand All @@ -42,9 +45,17 @@ deps/nvim-treesitter/parser/java.so: deps/nvim-treesitter
mkdir -p $$(dirname $@)
cp deps/tree-sitter-java/parser.so $@

deps/nvim-treesitter/parser/groovy.so: deps/nvim-treesitter
@if [ ! -d deps/tree-sitter-groovy ]; then \
git clone https://github.com/tree-sitter/tree-sitter-groovy deps/tree-sitter-groovy; \
fi
cd deps/tree-sitter-groovy && cc -o parser.so -I./src src/parser.c src/scanner.c -Os -std=c11 -shared
mkdir -p $$(dirname $@)
cp deps/tree-sitter-groovy/parser.so $@


clean:
rm -rf deps/nvim-treesitter deps/neotest deps/tree-sitter-java
rm -rf deps/nvim-treesitter deps/neotest deps/tree-sitter-java deps/tree-sitter-groovy

validate:
stylua --check .
Expand Down
2 changes: 1 addition & 1 deletion lua/neotest-java/core/file_checker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ local FileChecker = function(dependencies)
end

for _, re in ipairs(dependencies.patterns) do
local name_without_extension = my_path:name():gsub("%.java$", "")
local name_without_extension = my_path:name():gsub("%.java$", ""):gsub("%.groovy$", "")
if name_without_extension:match(re) then
return true
end
Expand Down
65 changes: 60 additions & 5 deletions lua/neotest-java/core/positions_discoverer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ local function build_position(file_path, source, captured_nodes)
return nil
end

local name = vim.treesitter.get_node_text(name_node, source)

if name_node:type() == "string" or name_node:type() == "string_literal" then
name = name:gsub("^[\"']", ""):gsub("[\"']$", "")
end

return {
type = match_type,
path = file_path,
name = vim.treesitter.get_node_text(name_node, source),
name = name,
range = { definition_node:range() },
}
end
Expand Down Expand Up @@ -78,17 +84,15 @@ end

local PositionsDiscoverer = {}

--- @param deps neotest-java.PositionsDiscoverer.Dependencies
--- @return neotest-java.PositionsDiscoverer
local function create_positions_discoverer(deps)
local function get_java_query()
local annotations = { "Test", "ParameterizedTest", "TestFactory", "CartesianTest" }
local a = vim.iter(annotations)
:map(function(v)
return string.format([["%s"]], v)
end)
:join(" ")

local query = [[
return [[

;; Test class
(class_declaration
Expand All @@ -113,7 +117,55 @@ local function create_positions_discoverer(deps)
) @test.definition

]]
end

local function get_groovy_query()
local annotations = { "Test", "ParameterizedTest", "TestFactory", "CartesianTest" }
local a = vim.iter(annotations)
:map(function(v)
return string.format([["%s"]], v)
end)
:join(" ")

return [[

;; Test class (Spock specs extend Specification)
(class_declaration
name: (identifier) @namespace.name
) @namespace.definition

;; JUnit-style annotated methods in Groovy
(method_declaration
(modifiers
[
(marker_annotation
name: (identifier) @annotation
(#any-of? @annotation ]] .. a .. [[)
)
(annotation
name: (identifier) @annotation
(#any-of? @annotation ]] .. a .. [[)
)
]
)
name: [
(identifier) @test.name
(string) @test.name
]
) @test.definition

;; Spock feature methods: def "test name"()
;; String literal method names are unique to Spock-style tests
(method_declaration
name: (string) @test.name
) @test.definition

]]
end

--- @param deps neotest-java.PositionsDiscoverer.Dependencies
--- @return neotest-java.PositionsDiscoverer
local function create_positions_discoverer(deps)
--- @type neotest-java.PositionsDiscoverer
return {

Expand All @@ -122,6 +174,9 @@ local function create_positions_discoverer(deps)
---@param file_path string Absolute file path
---@return neotest.Tree | nil
discover_positions = function(file_path)
local is_groovy = file_path:match("%.groovy$")
local query = is_groovy and get_groovy_query() or get_java_query()

local tree = lib.treesitter.parse_positions(file_path, query, {
require_namespaces = true,
nested_tests = false,
Expand Down
15 changes: 15 additions & 0 deletions lua/neotest-java/model/patterns.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,24 @@ local IGNORE_PATH_PATTERNS = {
"^%.classpath$", -- Eclipse classpath file
}

local GROOVY_TEST_FILE_PATTERNS = {
"Test%.groovy$",
"Tests%.groovy$",
"Spec%.groovy$",
"IT%.groovy$",
}

local GROOVY_TEST_FILE_REGEXES = {
"^.*Tests?$",
"^.*IT$",
"^.*Spec$",
}

return {
TEST_CLASS_PATTERNS = TEST_CLASS_PATTERNS,
JAVA_TEST_FILE_PATTERNS = JAVA_TEST_FILE_PATTERNS,
GROOVY_TEST_FILE_PATTERNS = GROOVY_TEST_FILE_PATTERNS,
IGNORE_PATH_PATTERNS = IGNORE_PATH_PATTERNS,
JAVA_TEST_FILE_REGEXES = JAVA_TEST_FILE_REGEXES,
GROOVY_TEST_FILE_REGEXES = GROOVY_TEST_FILE_REGEXES,
}
60 changes: 59 additions & 1 deletion scripts/minimal_init.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-- scripts/minimal_init.lua
-- Headless testing with mini.test, no user config loaded.
---@diagnostic disable: deprecated

local DEPENDENCIES_DIR = "./.dependencies"

Expand Down Expand Up @@ -102,6 +103,61 @@ end

ensure_java_parser()

local function ensure_groovy_parser()
local parser_dir = DEPENDENCIES_DIR .. "/nvim-treesitter/parser"
local groovy_so = parser_dir .. "/groovy.so"

if vim.fn.filereadable(groovy_so) == 1 then
return
end

-- Fast path: copy from old deps/ directory if it exists
local old_so = "deps/nvim-treesitter/parser/groovy.so"
if vim.fn.filereadable(old_so) == 1 then
vim.fn.mkdir(parser_dir, "p")
vim.fn.system({ "cp", old_so, groovy_so })
if vim.fn.filereadable(groovy_so) == 1 then
return
end
end

-- Fallback: clone tree-sitter-groovy and compile with cc
print("Installing Groovy treesitter parser (one-time setup)...")
local tmp_dir = vim.fn.tempname() .. "-ts-groovy"
vim.fn.system({
"git",
"clone",
"--depth=1",
"https://github.com/tree-sitter/tree-sitter-groovy",
tmp_dir,
})
vim.fn.mkdir(parser_dir, "p")

-- Check if scanner.c exists (some grammars need it)
local scanner_path = tmp_dir .. "/src/scanner.c"
local compile_cmd = {
"cc",
"-shared",
"-fPIC",
"-O2",
"-o",
groovy_so,
tmp_dir .. "/src/parser.c",
"-I" .. tmp_dir .. "/src",
}
if vim.fn.filereadable(scanner_path) == 1 then
table.insert(compile_cmd, scanner_path)
end
vim.fn.system(compile_cmd)
vim.fn.system({ "rm", "-rf", tmp_dir })

if vim.fn.filereadable(groovy_so) ~= 1 then
print("Warning: Failed to compile Groovy treesitter parser. Groovy position discovery will be unavailable.")
end
end

ensure_groovy_parser()

-- ─────────────────────────────────────────────────────────────
-- Runtime path setup (plugin roots, NOT /lua/ subdirectories)
-- ─────────────────────────────────────────────────────────────
Expand All @@ -123,7 +179,9 @@ require("mini.test").setup({
collect = {
emulate_busted = true,
find_files = function()
return vim.fn.globpath("tests/unit", "**/*_spec.lua", true, true)
local unit_files = vim.fn.globpath("tests/unit", "**/*_spec.lua", true, true)
local e2e_files = vim.fn.globpath("tests/e2e", "**/*_spec.lua", true, true)
return vim.list_extend(unit_files, e2e_files)
end,
},
execute = {
Expand Down
139 changes: 139 additions & 0 deletions tests/e2e/groovy_support_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---@diagnostic disable: undefined-field
-- E2E test: Groovy/Spock test discovery and execution
-- This test verifies that neotest-java correctly discovers and runs Groovy test files

local nio = require("nio")

describe("E2E: neotest-java Groovy/Spock support", function()
local neotest
local groovy_fixture_dir = vim.fn.getcwd() .. "/tests/fixtures/maven-groovy"
local calculator_spec = groovy_fixture_dir .. "/src/test/groovy/com/example/CalculatorSpec.groovy"
local user_service_test = groovy_fixture_dir .. "/src/test/groovy/com/example/UserServiceTest.groovy"

before_each(function()
package.loaded["neotest"] = nil
package.loaded["neotest-java"] = nil

neotest = require("neotest")
neotest.setup({
adapters = {
require("neotest-java")({
ignore_wrapper = false,
}),
},
log_level = vim.log.levels.DEBUG,
})
end)

it("discovers Groovy test files with .groovy extension", function()
assert.is_true(vim.fn.filereadable(calculator_spec) == 1, "CalculatorSpec.groovy should exist")
assert.is_true(vim.fn.filereadable(user_service_test) == 1, "UserServiceTest.groovy should exist")

nio.run(function()
neotest.run.run(calculator_spec)

local max_wait = 30000
local start_time = vim.uv.now()
local results = nil

while vim.uv.now() - start_time < max_wait do
nio.sleep(500)
results = neotest.state.results()
if results and next(results) ~= nil then
break
end
end

assert.is_not_nil(results, "Should have test results for CalculatorSpec.groovy")
assert.is_true(next(results) ~= nil, "Results should not be empty")

local test_count = 0
for test_id, _ in pairs(results) do
if test_id:match("Spec") or test_id:match("Test") then
test_count = test_count + 1
end
end

assert.is_true(test_count >= 4, "Should discover at least 4 tests from CalculatorSpec, got " .. test_count)

print(string.format("\n✓ Groovy discovery: %d tests found in CalculatorSpec.groovy", test_count))
end)
end)

it("runs Groovy JUnit tests and reports pass/fail results", function()
nio.run(function()
neotest.run.run(user_service_test)

local max_wait = 30000
local start_time = vim.uv.now()
local results = nil

while vim.uv.now() - start_time < max_wait do
nio.sleep(500)
results = neotest.state.results()
if results and next(results) ~= nil then
break
end
end

assert.is_not_nil(results, "Should have test results for UserServiceTest.groovy")

local passed = 0
local failed = 0
local total = 0

for test_id, result in pairs(results) do
if test_id:match("Test") then
total = total + 1
if result.status == "passed" then
passed = passed + 1
elseif result.status == "failed" then
failed = failed + 1
end
end
end

assert.is_true(total >= 4, "Should have at least 4 test results, got " .. total)
assert.is_true(passed >= 3, "Should have at least 3 passing tests, got " .. passed)
assert.is_true(failed >= 1, "Should have at least 1 failing test, got " .. failed)

print(string.format("\n✓ Groovy JUnit Results: %d total, %d passed, %d failed", total, passed, failed))
end)
end)

it("discovers both Java and Groovy tests in mixed projects", function()
nio.run(function()
neotest.run.run(groovy_fixture_dir)

local max_wait = 30000
local start_time = vim.uv.now()
local results = nil

while vim.uv.now() - start_time < max_wait do
nio.sleep(500)
results = neotest.state.results()
if results and next(results) ~= nil then
break
end
end

assert.is_not_nil(results, "Should have test results")

local groovy_tests = 0
local java_tests = 0

for test_id, _ in pairs(results) do
if test_id:match("%.groovy") or test_id:match("Spec") then
groovy_tests = groovy_tests + 1
elseif test_id:match("%.java") or test_id:match("Test") then
java_tests = java_tests + 1
end
end

assert.is_true(groovy_tests > 0, "Should discover Groovy tests")
assert.is_true(java_tests > 0, "Should discover Java tests")

print(string.format("\n✓ Mixed project: %d Groovy tests, %d Java tests", groovy_tests, java_tests))
end)
end)
end)
Loading
Loading