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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ metals.sbt

# Other
.DS_Store
.nvim-test-state
.nvimlog
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ Support levels below describe **test execution + result reporting** in neotest.
| Library | Test type | Build tool | Support | Notes |
|---------|-----------|------------|---------|-------|
| ScalaTest | `AnyFunSuite`, `AnyFreeSpec`, `AnyFlatSpec` | `sbt` | **Full** | Stable path via JUnit XML reports. |
| ScalaTest | `AnyFunSuite`, `AnyFreeSpec`, `AnyFlatSpec` | `bloop` | **Limited** | Can run, but report timing can lag (results may appear from previous run). |
| ScalaTest | `AnyFunSuite`, `AnyFreeSpec`, `AnyFlatSpec` | `bloop` | **Limited** | Uses stdout parsing for results (with additional JUnit report flags passed to runner); matching is best-effort vs XML. |
| munit | `FunSuite` | `sbt` | **Full** | Stable path via JUnit XML reports. |
| munit | `FunSuite` | `bloop` | **Limited** | Uses stdout parsing; works for common output, but parser-based matching is inherently less stable than XML. |
| specs2 | `mutable.Specification` | `sbt` | **Limited** | General execution works, but single-test selection can still run a larger scope/spec. |
| specs2 | `mutable.Specification` | `bloop` | **Limited** | Uses stdout parsing; supports fail/crash markers, but matching remains best-effort. |
| specs2 | text spec (`s2""" ... """`) | `sbt` | **Limited** | Execution works, but fine-grained single-test runs are limited. |
| specs2 | text spec (`s2""" ... """`) | `bloop` | **Limited** | Same single-test limits plus stdout parsing constraints. |
| zio-test | `ZIOSpecDefault` | `sbt` | **Full** | Stable path via JUnit XML reports. |
| zio-test | `ZIOSpecDefault` | `bloop` | **Limited** | Parallel suite output can interleave and break reliable parsing; `@@ TestAspect.sequential` is the practical workaround. |
| zio-test | `ZIOSpecDefault` | `bloop` | **Not supported** | Automatically forced to `sbt` (`bloop` execution is disabled for this framework). |
| uTest | `TestSuite` | `sbt` | **Full** | Works for run/result flow; debug single-test remains constrained by uTest selector limitations. |
| uTest | `TestSuite` | `bloop` | **Not supported** | Known issue: bloop may not discover/run uTest suites (`No test suites were run`). |
| uTest | `TestSuite` | `bloop` | **Not supported** | Known issue: uTest suites can't be discovered by bloop; tests will be run by sbt. |

> Recommendation: prefer `sbt` for stability. Use `bloop` when speed matters and current framework limitations are acceptable.

Expand Down
15 changes: 9 additions & 6 deletions lua/neotest-scala/build.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@ function M.get_tool_from_build_target_info(build_target_info)

for _, value in ipairs(values) do
local value_l = string.lower(tostring(value))
if value_l:find("/.bloop/", 1, true)
if
value_l:find("/.bloop/", 1, true)
or value_l:find("\\.bloop\\", 1, true)
or value_l:find("bloop%-bsp%-clients%-classes") then
or value_l:find("bloop%-bsp%-clients%-classes")
then
return true
end
end
Expand All @@ -122,13 +124,14 @@ function M.get_tool_from_build_target_info(build_target_info)

local scala_classpath = build_target_info["Scala Classpath"]
local scala_classes_directory = build_target_info["Scala Classes Directory"]
if (type(scala_classpath) == "table" and #scala_classpath > 0)
or (type(scala_classes_directory) == "table" and #scala_classes_directory > 0) then
if
(type(scala_classpath) == "table" and #scala_classpath > 0)
or (type(scala_classes_directory) == "table" and #scala_classes_directory > 0)
then
return "sbt"
end

if has_bloop_path(build_target_info["Classes Directory"])
or has_bloop_path(build_target_info["Classpath"]) then
if has_bloop_path(build_target_info["Classes Directory"]) or has_bloop_path(build_target_info["Classpath"]) then
return "bloop"
end

Expand Down
8 changes: 6 additions & 2 deletions lua/neotest-scala/framework.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ local FRAMEWORK_MARKERS = {
specs2 = {
"org%.specs2",
"extends%s+Specification",
"s2\"\"\"",
's2"""',
},
utest = {
"import%s+utest",
Expand Down Expand Up @@ -155,7 +155,11 @@ function M.select_framework_tree(opts)
local is_better = best_score == nil
or score > best_score
or (score == best_score and test_count > best_test_count)
or (score == best_score and test_count == best_test_count and namespace_count > best_namespace_count)
or (
score == best_score
and test_count == best_test_count
and namespace_count > best_namespace_count
)

if is_better then
best_score = score
Expand Down
12 changes: 6 additions & 6 deletions lua/neotest-scala/framework/scalatest/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function M.discover_positions(opts)
arguments: (arguments (string) @test.name))
)) @test.definition
]]
elseif style == "freespec" then
elseif style == "freespec" then
-- FreeSpec: "name" - { } and "name" in { }
query = [[
(object_definition
Expand All @@ -75,11 +75,11 @@ function M.discover_positions(opts)
right: (_)
) @test.definition
]]
else
-- FlatSpec:
-- "A Stack" should "pop values" in { }
-- it should "throw..." in { }
query = [[
else
-- FlatSpec:
-- "A Stack" should "pop values" in { }
-- it should "throw..." in { }
query = [[
(object_definition
name: (identifier) @namespace.name
) @namespace.definition
Expand Down
3 changes: 1 addition & 2 deletions lua/neotest-scala/framework/specs2/textspec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ function M.discover_positions(opts)
local package_name = utils.get_package_name(path) or ""

-- Find the class/object name
local class_name = content:match("class%s+([%w_]+)%s*extends")
or content:match("object%s+([%w_]+)%s*extends")
local class_name = content:match("class%s+([%w_]+)%s*extends") or content:match("object%s+([%w_]+)%s*extends")
if not class_name then
class_name = "Unknown"
end
Expand Down
5 changes: 4 additions & 1 deletion lua/neotest-scala/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ local adapter = { name = "neotest-scala" }

adapter.root = lib.files.match_root_pattern("build.sbt")

---This is a placeholder for the args function,
---it will be overridden by passing a function or a table to the adapter setup opts.
---The function receives an object with the path of the test file, build target info, project name and framework,
---and should return an array of strings with extra arguments to pass to the test command.
---@param _ neotest-scala.AdapterArgsContext
---@return string[]
local function get_args(_)
Expand All @@ -30,7 +34,6 @@ end

local cache_build_info = true


---@async
---@param file_path string
---@return boolean
Expand Down
4 changes: 3 additions & 1 deletion lua/neotest-scala/metals.lua
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ end
function M.cleanup()
for key, task in pairs(running_tasks) do
if task and task.cancel then
pcall(function() task:cancel() end)
pcall(function()
task:cancel()
end)
end
end
running_tasks = {}
Expand Down
8 changes: 7 additions & 1 deletion lua/neotest-scala/results.lua
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,13 @@ function M.collect(spec, result, node)
})

if not test_result then
vim.print("[neotest-scala] Framework '" .. framework.name .. "' returned no result for position '" .. position.id .. "'")
vim.print(
"[neotest-scala] Framework '"
.. framework.name
.. "' returned no result for position '"
.. position.id
.. "'"
)
test_result = { status = TEST_FAILED }
end

Expand Down
1 change: 0 additions & 1 deletion lua/neotest-scala/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ function M.has_nested_tests(test)
return #test:children() > 0
end


---Extract the highest line number for the given file from stacktrace
---ScalaTest stacktraces have multiple file references (class def, test method, etc.)
---We want the highest line number which corresponds to the actual test assertion
Expand Down
25 changes: 20 additions & 5 deletions tests/build_tool_switch_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,26 @@ describe("build tool switch behavior", function()
end)

describe("metals buildTargetChanged handler", function()
local original_get_client_by_id
local original_schedule
local original_handler

before_each(function()
original_get_client_by_id = vim.lsp.get_client_by_id
original_schedule = vim.schedule
original_handler = vim.lsp.handlers["metals/buildTargetChanged"]
end)

after_each(function()
vim.lsp.get_client_by_id = original_get_client_by_id
vim.schedule = original_schedule
vim.lsp.handlers["metals/buildTargetChanged"] = original_handler
local ok, metals = pcall(require, "neotest-scala.metals")
if ok and metals.cleanup then
metals.cleanup()
end
end)

it("chains previous handler and invalidates cache for the metals root", function()
local metals = require("neotest-scala.metals")
metals.cleanup()
Expand All @@ -357,8 +377,6 @@ describe("build tool switch behavior", function()
invalidated_root = root_path
end)

local original_get_client_by_id = vim.lsp.get_client_by_id
local original_schedule = vim.schedule
vim.lsp.get_client_by_id = function(_)
return { config = { root_dir = "/tmp/project" } }
end
Expand All @@ -381,9 +399,6 @@ describe("build tool switch behavior", function()

metals.cleanup()
assert.are.equal(previous, vim.lsp.handlers["metals/buildTargetChanged"])

vim.lsp.get_client_by_id = original_get_client_by_id
vim.schedule = original_schedule
end)
end)
end)
4 changes: 4 additions & 0 deletions tests/minimal_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
--- -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal_init.lua'}"

local root = vim.fn.getcwd()
local state_home = root .. "/.nvim-test-state"

vim.env.XDG_STATE_HOME = state_home
vim.fn.mkdir(state_home, "p")

-- Add the plugin itself to runtimepath
vim.opt.runtimepath:prepend(root)
Expand Down