Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/luarocks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ jobs:
version: ${{ env.LUAROCKS_VERSION }}
dependencies: |
neotest
plenary.nvim
nvim-nio
tree-sitter-java
nvim-dap
Expand Down
1 change: 0 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ off work to one of these:
| [`nvim-neotest/neotest`](https://github.com/nvim-neotest/neotest) | Yes | The framework this *is* an adapter for — defines `neotest.Adapter`, `RunSpec`, `Result`, `Tree`, and the `lib.files`/`lib.treesitter` helpers reused directly in `core/`. |
| [`nvim-neotest/nvim-nio`](https://github.com/nvim-neotest/nvim-nio) | Yes | Async runtime (`nio`) — lets LSP requests and process calls happen off the main thread without blocking Neovim. |
| [`nvim-treesitter`](https://github.com/nvim-treesitter/nvim-treesitter) + `tree-sitter-java` | Yes | Parses Java source so `positions_discoverer.lua` can find classes and test methods (see [flow 1](#1-test-discovery)). |
| [`nvim-lua/plenary.nvim`](https://github.com/nvim-lua/plenary.nvim) | Yes | Directory scanning fallback and job control for launching the debug JVM (`build_tool/launcher.lua`). |
| A Java language server (e.g. [`nvim-jdtls`](https://github.com/mfussenegger/nvim-jdtls)/JDT.LS) | Yes, at runtime | neotest-java doesn't bundle or configure a Java LSP itself — it just looks for an already-attached client named `jdtls` (`core/spec_builder/compiler/client_provider.lua`) to resolve the `java` binary, the classpath, and to trigger workspace compiles (see [flow 2](#2-spec-building)). No `jdtls` client attached means spec building can't proceed. |
| [`mfussenegger/nvim-dap`](https://github.com/mfussenegger/nvim-dap) | Only for debugging | Only required if you run tests with `strategy = "dap"`; both `core/spec_builder/init.lua` and `build_tool/launcher.lua` `pcall`-require it and fail gracefully/loudly if it's missing. |
| [JUnit Platform Console Standalone](https://mvnrepository.com/artifact/org.junit.platform/junit-platform-console-standalone) (a jar, not a plugin) | Yes | The actual test runner — downloaded by `install.lua` and invoked as a subprocess by the command built in [flow 2](#2-spec-building). |
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ make

This command will:

1. Clone neotest, plenary and nvim-treesitter
1. Clone neotest and nvim-treesitter
2. Install the Java parser for nvim-treesitter
3. Build the Java projects (some failing tests are expected)
4. Run the neotest-java test suite
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ test-e2e: install

install: deps/nvim-treesitter deps/nvim-treesitter/parser/java.so deps/neotest deps/nvim-nio deps/plenary.nvim

# plenary.nvim is NOT a dependency of neotest-java (see #317 — our code now
# uses vim.system/vim.uv directly). It IS a transitive dependency of
# nvim-neotest/neotest itself (neotest.lib.file/positions/subprocess require
# plenary.path), so the e2e harness — which builds a real user-like
# environment by hand instead of using a plugin manager — must provide it,
# just as lazy.nvim/packer would in a real setup.
deps/plenary.nvim:
mkdir -p deps
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim.git $@
Expand Down
42 changes: 31 additions & 11 deletions lua/neotest-java/build_tool/launcher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ local log = require("neotest-java.logger")
local nio = require("nio")
local lib = require("neotest.lib")

--- A minimal shape of the job runner this module needs. The default
--- implementation is backed by `plenary.job`, but it can be swapped out
--- (e.g. in tests, or by a future implementation that drops the
--- plenary.nvim dependency) as long as it honors this contract:
--- * `new(opts)` accepts the same callback fields plenary.Job does
--- (`command`, `cwd`, `args`, `on_stderr`, `on_stdout`, `on_exit`)
--- A minimal shape of the job runner this module needs:
--- * `new(opts)` accepts `command`, `cwd`, `args`, `on_stderr`, `on_stdout`,
--- `on_exit` callback fields
--- * the returned job exposes a `start()` method
--- @class neotest-java.JobRunner
--- @field new fun(self: neotest-java.JobRunner, opts: table): { start: fun() }

--- Default job runner, built directly on `vim.system` (see #317), matching
--- the same `vim.system` usage already established in
--- `command/command_executor.lua`. Unlike that module this doesn't need
--- `nio.wrap`, since `launch_debug_test` below already waits on its own
--- `nio.control.event()` rather than blocking on the process exiting.
--- @type neotest-java.JobRunner
local default_job_runner = {
new = function(_, opts)
return {
start = function()
vim.system(vim.list_extend({ opts.command }, opts.args), {
cwd = opts.cwd,
stdout = opts.on_stdout,
stderr = opts.on_stderr,
}, function(obj)
if opts.on_exit then
opts.on_exit(nil, obj.code)
end
end)
end,
}
end,
}

---@param deps neotest-java.BuildToolLauncherDeps | nil
---@return neotest-java.BuildToolLauncher
local function BuildToolLauncher(deps)
deps = deps or {}
deps.job_runner = deps.job_runner or require("plenary.job")
deps.job_runner = deps.job_runner or default_job_runner
deps.dap_repl = deps.dap_repl
if deps.dap_repl == nil then
local _, repl = pcall(require, "dap.repl")
deps.dap_repl = repl
local ok, repl = pcall(require, "dap.repl")
deps.dap_repl = ok and repl or nil
end

return {
Expand Down Expand Up @@ -101,8 +122,7 @@ end
local launcher = BuildToolLauncher()

-- Exposed for tests / future callers that want to inject a custom job_runner
-- (e.g. a stub, or a non-plenary implementation) without going through the
-- module-level singleton.
-- (e.g. a stub) without going through the module-level singleton.
launcher.new = BuildToolLauncher

return launcher
1 change: 0 additions & 1 deletion lua/neotest-java/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ return {
check_plugin("nvim-treesitter", "nvim-treesitter/nvim-treesitter")
check_treesitter()
check_plugin("nio", "nvim-neotest/nvim-nio")
check_plugin("plenary", "nvim-lua/plenary.nvim")
check_plugin("jdtls", "mfussenegger/nvim-jdtls")

health.start("Required plugin dependencies for debugging")
Expand Down
37 changes: 35 additions & 2 deletions lua/neotest-java/util/detect_project_type.lua
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
local scan = require("plenary.scandir")
local dir_scan = require("neotest-java.util.dir_scan")
local Path = require("neotest-java.model.path")

-- Build-marker file name patterns, reused as `dir_scan`'s search patterns
-- so we only collect entries we actually care about while walking the tree.
local MARKER_PATTERNS = {
"pom%.xml$",
"mvnw$",
"mvnw%.cmd$",
"settings%.gradle$",
"settings%.gradle%.kts$",
"build%.gradle$",
"build%.gradle%.kts$",
"gradlew$",
"gradlew%.bat$",
}

--- Default recursive scandir, built on top of `util/dir_scan.lua`'s
--- existing `vim.uv.fs_scandir`-based walk, replacing the previous
--- `plenary.scandir` default (see #317). `dir_scan.scan` already filters
--- entries by `search_patterns` and returns a flat array of matching
--- `Path`s directly (not `{path, typ}` wrapper tables) — a directory
--- happening to be named exactly like one of our marker filenames is not
--- a realistic concern, so every match is treated as a file path.
--- @param root_str string
--- @return string[]
local function default_scandir(root_str)
local matches = dir_scan(Path(root_str), { search_patterns = MARKER_PATTERNS })
local files = {}
for i, path in ipairs(matches) do
files[i] = path:to_string()
end
return files
end

--- Detect project type (maven | gradle | unknown)
--- @param root_dir neotest-java.Path
--- @param scandir? fun(path: string, opts?: table): string[]
--- @param readdir? fun(path: string): string[]
--- @return "maven"|"gradle"|"unknown"
local function detect_project_type(root_dir, scandir, readdir)
scandir = scandir or scan.scan_dir
scandir = scandir or default_scandir
readdir = readdir or vim.fn.readdir

local root_str = root_dir:to_string()
Expand Down
8 changes: 6 additions & 2 deletions lua/neotest-java/util/dir_scan.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

--- @param dir neotest-java.Path
--- @return fun(): neotest-java.DirScanResultItem | nil
local iter_dir = function(dir)
local default_iter_dir = function(dir)
local handle, _ = vim.uv.fs_scandir(dir:to_string())
if not handle then
return function()
Expand Down Expand Up @@ -46,7 +46,11 @@ end
local function scan(dir, opts, dependencies)
opts = opts or {}
dependencies = dependencies or {}
iter_dir = dependencies.iter_dir or iter_dir
-- IMPORTANT: local, not a reassignment of the module-level default —
-- otherwise an injected stub (e.g. from a test) would permanently
-- replace the real implementation for every subsequent call in the
-- same process, not just this one.
local iter_dir = dependencies.iter_dir or default_iter_dir

local stack = { dir }
local result = {}
Expand Down
4 changes: 0 additions & 4 deletions scripts/minimal_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ ensure_repo(DEPENDENCIES_DIR .. "/mini.nvim", "https://github.com/echasnovski/mi
ensure_repo(DEPENDENCIES_DIR .. "/nvim-nio", "https://github.com/nvim-neotest/nvim-nio")
ensure_repo(DEPENDENCIES_DIR .. "/neotest", "https://github.com/nvim-neotest/neotest")
ensure_repo(DEPENDENCIES_DIR .. "/nvim-treesitter", "https://github.com/nvim-treesitter/nvim-treesitter")
ensure_repo(DEPENDENCIES_DIR .. "/plenary.nvim", "https://github.com/nvim-lua/plenary.nvim")

-- ─────────────────────────────────────────────────────────────
-- Ensure the Java treesitter parser (.so) is compiled and present
Expand Down Expand Up @@ -113,14 +112,11 @@ vim.opt.runtimepath:append(DEPENDENCIES_DIR .. "/mini.nvim")
vim.opt.runtimepath:append(DEPENDENCIES_DIR .. "/nvim-nio")
vim.opt.runtimepath:append(DEPENDENCIES_DIR .. "/neotest")
vim.opt.runtimepath:append(DEPENDENCIES_DIR .. "/nvim-treesitter")
vim.opt.runtimepath:append(DEPENDENCIES_DIR .. "/plenary.nvim")

-- ─────────────────────────────────────────────────────────────
-- Enable mini.test
-- ─────────────────────────────────────────────────────────────

-- Inject luassert as global 'assert' so tests can use assert.are.same etc.
_G.assert = require("luassert")
vim.opt.rtp:append(vim.fn.getcwd())
package.path = package.path .. ";" .. vim.fn.getcwd() .. "/?.lua"

Expand Down
12 changes: 6 additions & 6 deletions tests/e2e/full_workflow_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("E2E: neotest-java full workflow", function()

it("runs tests and reports correct pass/fail results", function()
-- Make sure test file exists
assert.is_true(vim.fn.filereadable(test_file) == 1, "Test file should exist: " .. test_file)
assert(vim.fn.filereadable(test_file) == 1, "Test file should exist: " .. test_file)

-- Run the tests (this triggers async execution)
nio.run(function()
Expand All @@ -49,9 +49,9 @@ describe("E2E: neotest-java full workflow", function()
end

-- Verify we got results
assert.is_not_nil(results, "Should have test results")
assert(results ~= nil, "Should have test results")
---@diagnostic disable-next-line: param-type-mismatch
assert.is_true(next(results) ~= nil, "Results should not be empty")
assert(next(results) ~= nil, "Results should not be empty")

-- Count pass/fail
local passed = 0
Expand All @@ -72,9 +72,9 @@ describe("E2E: neotest-java full workflow", function()

-- Verify counts
-- SampleTest has 4 tests: 2 should pass, 2 should fail
assert.is_true(total >= 4, "Should have at least 4 test results, got " .. total)
assert.is_true(passed >= 2, "Should have at least 2 passing tests, got " .. passed)
assert.is_true(failed >= 2, "Should have at least 2 failing tests, got " .. failed)
assert(total >= 4, "Should have at least 4 test results, got " .. total)
assert(passed >= 2, "Should have at least 2 passing tests, got " .. passed)
assert(failed >= 2, "Should have at least 2 failing tests, got " .. failed)

print(string.format("\n✓ E2E Test Results: %d total, %d passed, %d failed", total, passed, failed))
end)
Expand Down
5 changes: 2 additions & 3 deletions tests/integration/test_detect_project_type_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ local detect_project_type = require("neotest-java.util.detect_project_type")

--- Real-filesystem integration coverage for `detect_project_type`'s
--- fallback path: a genuine multi-directory layout with no build markers
--- at the top level, scanned by the *real* `plenary.scandir` (no stubs
--- injected), exactly as it runs in production. This is the exact
--- behavior a future non-plenary implementation (see #317) must preserve.
--- at the top level, scanned by the *real* default `dir_scan`-based
--- recursive walk (no stubs injected), exactly as it runs in production.
describe("detect_project_type (real filesystem integration)", function()
local root_dir

Expand Down
7 changes: 3 additions & 4 deletions tests/integration/test_launcher_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ local launcher = BuildToolLauncher({ dap_repl = false })
--- "Listening for transport dt_socket at address: <port>" to stdout as
--- soon as the debug socket is open, then continues running normally.
---
--- This exercises the real `plenary.job` spawn + JDWP "Listening" wait +
--- process-exit logic end-to-end, which is the exact behavior a future
--- non-plenary implementation (see #317) must preserve.
--- This exercises the real default `vim.system`-backed job runner's spawn +
--- JDWP "Listening" wait + process-exit logic end-to-end.
describe("BuildToolLauncher (real JVM integration)", function()
it(
"detects the JDWP 'Listening' message from a real JVM and waits for it to exit",
Expand All @@ -41,7 +40,7 @@ describe("BuildToolLauncher (real JVM integration)", function()
local terminated_event = launcher.launch_debug_test("java", args, Path(vim.uv.cwd()))

-- `launch_debug_test` only returns once the JVM has printed the
-- "Listening" line, proving the real plenary.job stdout wiring
-- "Listening" line, proving the real vim.system-backed stdout wiring
-- correctly detects it.
assert(terminated_event ~= nil, "should return a terminated_command_event")

Expand Down
26 changes: 0 additions & 26 deletions tests/performance/path_benchmark.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@
-- Native (vim.fs + strings) : 83.88 ms (0.0008 ms/op)
-- Path Struct : 550.41 ms (0.0055 ms/op)
-- Allocation Cost (Path()) : 10.25 ms (0.0001 ms/op)
-- Plenary Path (Creation) : 199.67 ms (0.0020 ms/op)
-- Plenary Path (Parent) : 1843.74 ms (0.0184 ms/op)
--
-- === MEMORY FOOTPRINT ===
-- Path (Allocation) : 21875.00 KB total (0.2188 KB/op)
-- Plenary Path (Creation) : 21256.73 KB total (0.2126 KB/op)

local has_plenary, PlenaryPath = pcall(require, "plenary.path")

local Path = require("neotest-java.model.path")

Expand Down Expand Up @@ -61,28 +56,7 @@ benchmark("Allocation Cost (Path())", function()
local _ = Path(LONG_PATH)
end)

if has_plenary then
benchmark("Plenary Path (Creation)", function()
local _ = PlenaryPath:new(LONG_PATH)
end)

benchmark("Plenary Path (Parent)", function()
local p = PlenaryPath:new(LONG_PATH)
local _ = p:parent()
end)
else
print("Skipping Plenary benchmark (plugin not found)")
end

print("\n=== MEMORY FOOTPRINT ===")
benchmark_memory("Path (Allocation)", function()
local _ = Path(LONG_PATH)
end)

if has_plenary then
benchmark_memory("Plenary Path (Creation)", function()
local _ = PlenaryPath:new(LONG_PATH)
end)
else
print("Skipping Plenary benchmark (plugin not found)")
end
2 changes: 2 additions & 0 deletions tests/testrc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ set runtimepath+=.
set runtimepath+=./deps/neotest
set runtimepath+=./deps/nvim-treesitter
set runtimepath+=./deps/nvim-nio
" Transitive dependency of neotest itself (see Makefile note); not required
" by neotest-java directly.
set runtimepath+=./deps/plenary.nvim
Loading
Loading