Skip to content

Commit

Permalink
Debugger support (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
MisanthropicBit authored Jun 29, 2024
1 parent de9e3f2 commit e4c872f
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 67 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ neovim as the lua interpreter.
- [Defining tests](#defining-tests)
- [Luarocks and Busted](#luarocks-and-busted)
- [Running from the command line](#running-from-the-command-line)
- [Debugging tests](#debugging-tests)
- [FAQ](#faq)

## Requirements
Expand Down Expand Up @@ -156,7 +157,7 @@ the command will automatically try to find your tests in a `spec/`, `test/`, or
`tests/` directory.

```shell
$ nvim -u NONE -l ./scripts/test-runner.lua tests/my_spec.lua
$ nvim -l ./scripts/test-runner.lua tests/my_spec.lua
```

#### Test via rockspec
Expand All @@ -173,6 +174,11 @@ test = {
}
```

## Debugging tests

`neotest-busted` has support for debugging tests via [`local-lua-debugger-vscode`](https://github.com/tomblind/local-lua-debugger-vscode)
which can be set up via [`nvim-dap`](https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation#lua). Once set up, you can set a breakpoint and run the test with the `dap` strategy. Please refer to the [`neotest`](https://github.com/nvim-neotest/neotest) documentation for more information.

## FAQ

#### Q: Can I run async tests with neotest-busted?
Expand Down
180 changes: 140 additions & 40 deletions lua/neotest-busted/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ local lib = require("neotest.lib")
local logger = require("neotest.logging")
local types = require("neotest.types")

local function get_strategy_config(strategy)
local strategy_configs = {
dap = nil, -- TODO: Add dap config
}
if strategy_configs[strategy] then
return strategy_configs[strategy]()
local log_methods = {
"debug",
"info",
"warn",
"error",
}

---@param message string
---@param level 1 | 2 | 3 | 4
local function log_and_notify(message, level)
local log_method = log_methods[level]

if not log_method then
return
end

logger[log_method](message)
vim.notify(message, level)
end

---@type neotest.Adapter
Expand Down Expand Up @@ -120,6 +131,11 @@ local function get_reporter_path()
return table.concat({ script_path(), "output_handler.lua" })
end

---@return string
local function get_debug_start_script()
return table.concat({ script_path(), "start_debug.lua" })
end

--- Escape special characters in a lua pattern
---@param filter string
---@return string
Expand All @@ -141,22 +157,27 @@ local function escape_test_pattern_filter(filter)
)
end

---@param string string
local function quote_string(string)
return '"' .. string .. '"'
end

---@param results_path string?
---@param paths string[]
---@param filters string[]
---@param options neotest-busted.BustedCommandOptions?
---@return neotest-busted.BustedCommandConfig?
function BustedNeotestAdapter.create_busted_command(results_path, paths, filters, options)
---@param options neotest-busted.TestCommandOptions?
---@return neotest-busted.TestCommandConfig?
function BustedNeotestAdapter.create_test_command(results_path, paths, filters, options)
local busted = BustedNeotestAdapter.find_busted_command()

if not busted then
log_and_notify("Could not find a busted command", vim.log.levels.ERROR)
return
end

-- stylua: ignore start
---@type string[]
local command = {
vim.loop.exepath(),
local arguments = {
"--headless",
"-i", "NONE", -- no shada
"-n", -- no swapfile, always in-memory
Expand All @@ -167,6 +188,7 @@ function BustedNeotestAdapter.create_busted_command(results_path, paths, filters
---@type string[], string[]
local lua_paths, lua_cpaths = {}, {}

-- TODO: Should paths be quoted? Try seeing if a path with a space works
-- Append custom paths from config
if vim.tbl_islist(config.busted_paths) then
vim.list_extend(lua_paths, config.busted_paths)
Expand All @@ -185,8 +207,8 @@ function BustedNeotestAdapter.create_busted_command(results_path, paths, filters
vim.list_extend(lua_cpaths, busted.lua_cpaths)

-- Create '-c' arguments for updating package paths in neovim
vim.list_extend(command, util.create_package_path_argument("package.path", lua_paths))
vim.list_extend(command, util.create_package_path_argument("package.cpath", lua_cpaths))
vim.list_extend(arguments, util.create_package_path_argument("package.path", lua_paths))
vim.list_extend(arguments, util.create_package_path_argument("package.cpath", lua_cpaths))

local _options = options or {}

Expand All @@ -199,15 +221,15 @@ function BustedNeotestAdapter.create_busted_command(results_path, paths, filters
"--verbose",
}

if _options.output_handler then
if _options.busted_output_handler then
vim.list_extend(busted_command, {
"--output",
_options.output_handler,
_options.busted_output_handler,
})

if _options.output_handler_options then
if _options.busted_output_handler_options then
table.insert(busted_command, "-Xoutput")
vim.list_extend(busted_command, _options.output_handler_options)
vim.list_extend(busted_command, _options.busted_output_handler_options)
end
else
if not results_path then
Expand All @@ -222,27 +244,90 @@ function BustedNeotestAdapter.create_busted_command(results_path, paths, filters
})
end

vim.list_extend(command, busted_command)
vim.list_extend(arguments, busted_command)

if vim.tbl_islist(config.busted_args) and #config.busted_args > 0 then
vim.list_extend(command, config.busted_args)
vim.list_extend(arguments, config.busted_args)
end

-- Add test filters
for _, filter in ipairs(filters) do
vim.list_extend(command, { "--filter", escape_test_pattern_filter(filter) })
local escaped_filter = escape_test_pattern_filter(filter)

if _options.quote_strings then
escaped_filter = quote_string(escaped_filter)
end

vim.list_extend(arguments, { "--filter", escaped_filter })
end

-- Add test files
vim.list_extend(command, paths)
if _options.quote_strings then
vim.list_extend(arguments, vim.tbl_map(quote_string, paths))
else
vim.list_extend(arguments, paths)
end

return {
command = command,
path = lua_paths,
cpath = lua_cpaths,
nvim_command = vim.loop.exepath(),
arguments = arguments,
paths = lua_paths,
cpaths = lua_cpaths,
}
end

---@param strategy string
---@param results_path string
---@param paths string[]
---@param filters string[]
---@return table?
local function get_strategy_config(strategy, results_path, paths, filters)
if strategy == "dap" then
vim.list_extend(paths, { "--helper", get_debug_start_script() }, 1)

local test_command_info = BustedNeotestAdapter.create_test_command(
results_path,
paths,
filters,
-- NOTE: When run via dap, passing arguments such as the one for
-- busted's '--filter' need to be escaped since the command is run
-- using node's child_process.spawn with { shell: true } that will
-- run via a shell and split arguments on spaces. This will break
-- the command if a filter contains spaces.
--
-- On the other hand, we don't need to quote when running the integrated
-- strategy (through vim.fn.jobstart) because it runs with command as a
-- list which does not run through a shell
{ quote_strings = true }
)

if not test_command_info then
log_and_notify("Failed to construct test command for debugging", vim.log.levels.ERROR)
return nil
end

local lua_paths = util.normalize_and_create_lua_path(unpack(test_command_info.paths))
local lua_cpaths = util.normalize_and_create_lua_path(unpack(test_command_info.cpaths))

return {
name = "Debug busted tests",
type = "local-lua",
cwd = "${workspaceFolder}",
request = "launch",
env = {
LUA_PATH = lua_paths,
LUA_CPATH = lua_cpaths,
},
program = {
command = test_command_info.nvim_command,
},
args = test_command_info.arguments,
}
end

return nil
end

BustedNeotestAdapter.root =
lib.files.match_root_pattern(".busted", ".luarocks", "lua_modules", "*.rockspec")

Expand Down Expand Up @@ -380,27 +465,29 @@ function BustedNeotestAdapter.build_spec(args)
end

local results_path = async.fn.tempname() .. ".json"
local busted = BustedNeotestAdapter.create_busted_command(results_path, paths, filters)

if not busted then
local message = "Could not find a busted executable"
logger.error(message)
vim.notify(message, vim.log.levels.ERROR)
local test_command = BustedNeotestAdapter.create_test_command(results_path, paths, filters)

if not test_command then
log_and_notify("Could not find a busted executable", vim.log.levels.ERROR)
return
end

---@type string[]
local command = vim.list_extend({ test_command.nvim_command }, test_command.arguments)

-- Extra arguments for busted
if vim.tbl_islist(args.extra_args) then
vim.list_extend(busted.command, args.extra_args)
vim.list_extend(command, args.extra_args)
end

return {
command = busted.command,
command = command,
context = {
results_path = results_path,
pos = pos,
position_ids = position_ids,
},
strategy = get_strategy_config(args.strategy, results_path, paths, filters),
}
end

Expand All @@ -423,11 +510,19 @@ local function create_error_info(test_result)
return nil
end

---@param test_result neotest-busted.BustedResult | neotest-busted.BustedFailureResult
---@param test_result neotest-busted.BustedResult | neotest-busted.BustedFailureResult | neotest-busted.BustedErrorResult
---@param status neotest.ResultStatus
---@param output string
---@param is_error boolean
local function test_result_to_neotest_result(test_result, status, output, is_error)
if test_result.isError == true then
-- This is an internal error in busted, not a test that threw
return nil, {
message = test_result.message,
line = 0,
}
end

local pos_id = create_pos_id_key(
test_result.element.trace.source:sub(2), -- Strip the "@" from the source path
test_result.name,
Expand All @@ -441,6 +536,7 @@ local function test_result_to_neotest_result(test_result, status, output, is_err
}

if is_error then
---@cast test_result -neotest-busted.BustedErrorResult
result.errors = create_error_info(test_result)
end

Expand All @@ -458,19 +554,20 @@ function BustedNeotestAdapter.results(spec, strategy_result, tree)
local ok, data = pcall(lib.files.read, results_path)

if not ok then
logger.error("Failed to read json test output file ", results_path, " with error: ", data)
log_and_notify(
("Failed to read json test output file %s with error: %s"):format(results_path, data),
vim.log.levels.ERROR
)
return {}
end

---@diagnostic disable-next-line: cast-local-type
local json_ok, parsed = pcall(vim.json.decode, data, { luanil = { object = true } })

if not json_ok then
logger.error(
"Failed to parse json test output file ",
results_path,
" with error: ",
parsed
log_and_notify(
("Failed to parse json test output file %s with error: %s"):format(results_path, parsed),
vim.log.levels.ERROR
)
return {}
end
Expand Down Expand Up @@ -502,7 +599,10 @@ function BustedNeotestAdapter.results(spec, strategy_result, tree)
local pos_id = position_ids[pos_id_key]

if not pos_id then
logger.error("Failed to find matching position id for key ", pos_id_key)
log_and_notify(
("Failed to find matching position id for key %s"):format(pos_id_key),
vim.log.levels.ERROR
)
else
results[position_ids[pos_id_key]] = result
end
Expand Down
3 changes: 3 additions & 0 deletions lua/neotest-busted/start_debug.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then
require("lldebugger").start()
end
18 changes: 15 additions & 3 deletions lua/neotest-busted/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
---@field lua_paths string[]
---@field lua_cpaths string[]

---@class neotest-busted.BustedCommandOptions
---@field output_handler string?
---@field output_handler_options string[]?
---@class neotest-busted.TestCommandOptions
---@field busted_output_handler string?
---@field busted_output_handler_options string[]?
---@field quote_strings boolean

---@class neotest-busted.TestCommandConfig
---@field nvim_command string
---@field arguments string[]
---@field paths string[]
---@field cpaths string[]

---@class neotest-busted.BustedTrace
---@field what string
Expand Down Expand Up @@ -47,6 +54,11 @@
---@field trace neotest-busted.BustedTrace
---@field element neotest-busted.BustedElement

---@class neotest-busted.BustedErrorResult
---@field isError true
---@field message string
---@field name string

---@class neotest-busted.BustedResultObject
---@field errors neotest-busted.BustedFailureResult[]
---@field pendings neotest-busted.BustedResult[]
Expand Down
Loading

0 comments on commit e4c872f

Please sign in to comment.