Skip to content

Commit

Permalink
feat: Add support for watch flag
Browse files Browse the repository at this point in the history
This commit introduces a feature to support the watch mode in neotest. In the previous implementation, the user had the ability to run tests but there was no provision to watch the files and run the tests when changes occur. This feature is particularly helpful while doing test-driven development where code changes frequently.

1. Added key mappings examples in README.md to trigger watch mode.
2. Refactored the build_spec function in init.lua to support watch mode. It now includes command flag "--watch=false" in the command list instead of --run.
3. Added a new function stream in util.lua which continuously reads file data and triggers a callback function for new data. This is used to watch the test files and run tests on file change.

closes #27

Signed-off-by: marcoSven <[email protected]>
  • Loading branch information
marcoSven committed Feb 27, 2024
1 parent c0ea475 commit dfe56ba
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 51 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,26 @@ filter_dir = function(name, rel_path, root)
end
```

### Watch mode mappings

```lua

vim.api.nvim_set_keymap(
"n",
"<leader>twr",
"<cmd>lua require('neotest').run.run({ vitestCommand = 'vitest --watch ' })<cr>",
{desc = "Run Watch"}
)

vim.api.nvim_set_keymap(
"n",
"<leader>twf",
"<cmd>lua require('neotest').run.run({ vim.fn.expand("%"), vitestCommand = 'vitest --watch ' })<cr>",
{desc = "Run Watch File"}
)

```

## Usage

![usage preview](https://user-images.githubusercontent.com/32909388/185812063-d05d9cc7-b9aa-43ed-915b-cf156e3f0c52.gif)
Expand Down
130 changes: 79 additions & 51 deletions lua/neotest-vitest/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ local function escapeTestPattern(s)
)
end

local function get_strategy_config(strategy, command)
local function get_strategy_config(strategy, command, cwd)
local config = {
dap = function()
return {
Expand All @@ -225,6 +225,7 @@ local function get_strategy_config(strategy, command)
runtimeExecutable = command[1],
console = "integratedTerminal",
internalConsoleOptions = "neverOpen",
cwd = cwd or "${workspaceFolder}",
}
end,
}
Expand All @@ -243,56 +244,6 @@ local function getCwd(path)
return nil
end

---@param args neotest.RunArgs
---@return neotest.RunSpec | nil
function adapter.build_spec(args)
local results_path = async.fn.tempname() .. ".json"
local tree = args.tree

if not tree then
return
end

local pos = args.tree:data()
local testNamePattern = ".*"

if pos.type == "test" then
testNamePattern = escapeTestPattern(pos.name) .. "$"
end

if pos.type == "namespace" then
testNamePattern = "^ " .. escapeTestPattern(pos.name)
end

local binary = getVitestCommand(pos.path)
local config = getVitestConfig(pos.path) or "vitest.config.js"
local command = vim.split(binary, "%s+")
if util.path.exists(config) then
-- only use config if available
table.insert(command, "--config=" .. config)
end

vim.list_extend(command, {
"--run",
"--reporter=verbose",
"--reporter=json",
"--outputFile=" .. results_path,
"--testNamePattern=" .. testNamePattern .. "",
pos.path,
})

return {
command = command,
cwd = getCwd(pos.path),
context = {
results_path = results_path,
file = pos.path,
},
strategy = get_strategy_config(args.strategy, command),
env = getEnv(args[2] and args[2].env or {}),
}
end

local function cleanAnsi(s)
return s:gsub("\x1b%[%d+;%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+;%d+m", "")
Expand Down Expand Up @@ -359,10 +310,87 @@ local function parsed_json_to_results(data, output_file, consoleOut)
return tests
end

---@param args neotest.RunArgs
---@return neotest.RunSpec | nil
function adapter.build_spec(args)
local results_path = async.fn.tempname() .. ".json"
local tree = args.tree

if not tree then
return
end

local pos = args.tree:data()
local testNamePattern = ".*"

if pos.type == "test" then
testNamePattern = escapeTestPattern(pos.name) .. "$"
end

if pos.type == "namespace" then
testNamePattern = "^ " .. escapeTestPattern(pos.name)
end

local binary = args.vitestCommand or getVitestCommand(pos.path)
local config = getVitestConfig(pos.path) or "vitest.config.js"
local command = vim.split(binary, "%s+")

if util.path.exists(config) then
-- only use config if available
table.insert(command, "--config=" .. config)
end

vim.list_extend(command, {
"--watch=false",
"--reporter=verbose",
"--reporter=json",
"--outputFile=" .. results_path,
"--testNamePattern=" .. testNamePattern,
vim.fs.normalize(pos.path),
})

local cwd = getCwd(pos.path)

-- creating empty file for streaming results
lib.files.write(results_path, "")
local stream_data, stop_stream = util.stream(results_path)

return {
command = command,
cwd = cwd,
context = {
results_path = results_path,
file = pos.path,
stop_stream = stop_stream,
},
stream = function()
return function()
local new_results = stream_data()

if not new_results or new_results == "" then
return {}
end

local ok, parsed = pcall(vim.json.decode, new_results, { luanil = { object = true } })

if not ok or not parsed.testResults then
return {}
end

return parsed_json_to_results(parsed, results_path, nil)
end
end,
strategy = get_strategy_config(args.strategy, command, cwd),
env = getEnv(args[2] and args[2].env or {}),
}
end

---@async
---@param spec neotest.RunSpec
---@return neotest.Result[]
function adapter.results(spec, b, tree)
spec.context.stop_stream()

local output_file = spec.context.results_path

local success, data = pcall(lib.files.read, output_file)
Expand Down
41 changes: 41 additions & 0 deletions lua/neotest-vitest/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ local vim = vim
local validate = vim.validate
local uv = vim.loop

local async = require("neotest.async")

local M = {}

-- Some path utilities
Expand Down Expand Up @@ -184,4 +186,43 @@ function M.find_package_json_ancestor(startpath)
end)
end

function M.stream(file_path)
local queue = async.control.queue()
local read_semaphore = async.control.semaphore(1)

local open_err, file_fd = async.uv.fs_open(file_path, "r", 438)
assert(not open_err, open_err)

local exit_future = async.control.future()
local read = function()
read_semaphore.with(function()
local stat_err, stat = async.uv.fs_fstat(file_fd)

assert(not stat_err, stat_err)
local read_err, data = async.uv.fs_read(file_fd, stat.size, 0)

assert(not read_err, read_err)
queue.put(data)
end)
end

read()
local event = vim.loop.new_fs_event()
event:start(file_path, {}, function(err, _, _)
assert(not err)
async.run(read)
end)

local function stop()
exit_future.wait()
event:stop()
local close_err = async.uv.fs_close(file_fd)
assert(not close_err, close_err)
end

async.run(stop)

return queue.get, exit_future.set
end

return M

0 comments on commit dfe56ba

Please sign in to comment.