Skip to content

Commit

Permalink
feat: add support for test positions of type 'file'
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcjkb committed Feb 27, 2023
1 parent 2badca5 commit 48811a2
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 27 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Support for test files (See [#45](https://github.com/mrcjkb/neotest-haskell/issues/45)).
Running `neotes.run.run(vim.api.nvim_buf_get_name)` will now run a single process for the top-level Hspec node.

## [0.2.2] - 2023-01-14
### Fixed
Expand Down
2 changes: 1 addition & 1 deletion lua/neotest-haskell/cabal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function cabal.build_command(package_root, pos)
logger.debug('Building spec for Cabal project...')
local command = {
'cabal',
'new-test',
'test',
}
local package_file_path = get_package_file(package_root)
local package_file_name = vim.fn.fnamemodify(package_file_path, ':t')
Expand Down
111 changes: 86 additions & 25 deletions lua/neotest-haskell/hspec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local util = require('neotest-haskell.util')
local lib = require('neotest.lib')
local logger = require('neotest.logging')

local hspec = {}

Expand Down Expand Up @@ -109,10 +110,25 @@ local function mk_parent_query(test_name)
)
end

local describe_query = [[
;; describe (unqualified)
((exp_apply
(exp_name (variable) @func_name)
(exp_literal) @test.name
) (#eq? @func_name "describe")) @test.definition
;; describe (qualified)
((exp_apply
(exp_name (qualified_variable (variable) @func_name))
(exp_literal) @test.name
) (#eq? @func_name "describe")) @test.definition
]]

-- @param path: Test file path
-- @type neotest.Tree
function hspec.parse_positions(path)
local tests_query = [[
local tests_query = describe_query
.. [[
;; describe (unqualified)
((exp_apply
(exp_name (variable) @func_name)
Expand Down Expand Up @@ -160,11 +176,25 @@ local function hspec_format(test_name)
return test_name:gsub('"', '')
end

-- Helper function for 'M.get_hspec_match(position)'
-- @param position the position of the test to get the match for
-- @return the hspec match for the test
-- @type string
local function parse_hspec_match(position)
--- Parses the top level hspec node in a file
--- @param path string The test file path
--- @return string hspec_match_path The hspec match path for the top level node of the hspec tree
local function parse_top_level_hspec_node(path)
local positions = util.parse_positions(path, describe_query)
local top_level
for _, node in positions:iter_nodes() do
local data = node:data()
if data.type == 'test' then
top_level = (top_level and data.range[1] < top_level.range[1] and data or top_level) or data
end
end
return top_level and hspec_format(top_level.name) or ''
end

--- Recursively parses the hspec tree, starting at a child node, up to its parents.
--- @param position table neotest.Position The position of the test to get the match for
--- @return string hspec_match_path The hspec match path for the test
local function parse_hspec_tree(position)
local test_name = position.name
local path = position.path
local row = position.range[1]
Expand All @@ -174,7 +204,7 @@ local function parse_hspec_match(position)
for _, parent_node in parent_tree:iter_nodes() do
local data = parent_node:data()
if data.type == 'test' then
if data.range and data.range[1] <= row - 1 then
if data.range[1] <= row - 1 then
nearest = parent_node
else
break
Expand All @@ -185,24 +215,30 @@ local function parse_hspec_match(position)
return hspec_format(test_name)
end
local parent_position = nearest:data()
return parse_hspec_match(parent_position) .. '/' .. hspec_format(test_name)
return parse_hspec_tree(parent_position) .. '/' .. hspec_format(test_name)
end

-- Runs a treesitter query for the tests in the test file 'path',
-- and if there is a test that matches 'test_name',
-- prepends any parent 'describe's to the test name.
-- Example:
-- - position.name: "My test"
-- - Hspec tests in path:
-- ```
-- describe "Run" $ do
-- it "My test" $ do
-- ...
-- ```
-- - Result: "/Run/My test"
-- @param pos the position of the test to get the match for
-- @return the hspec match for the test (see example).
-- @type string
local function parse_hspec_match(position)
if position.type == 'file' then
return parse_top_level_hspec_node(position.path)
end
return parse_hspec_tree(position)
end

--- Runs a treesitter query for tests at a `neotest.Position`,
--- and if there is a test that matches <test_name>,
--- prepends any parent <describe>s to the test name.
--- Example:
--- - position.name: "My test"
--- - Hspec tests in path:
--- ```
--- describe "Run" $ do
--- it "My test" $ do
--- ...
--- ```
--- - Result: "/Run/My test"
--- @param pos table (neotest.Position) The position of the test to get the match for
--- @return string hspec_match The hspec match for the test (see example).
local function get_hspec_match(pos)
local hspec_match = '/' .. parse_hspec_match(pos) .. '/'
vim.notify('HSpec: --match: ' .. hspec_match, vim.log.levels.INFO)
Expand Down Expand Up @@ -256,6 +292,29 @@ local function get_hspec_errors(raw_lines, test_name)
return {}
end

--- Initialise an empty results table for each test node
--- in the given path. This is necessary to prevent neotest
--- from displaying parent nodes of succeeded tests as 'passed'
--- if an unrelated test has failed.
--- @param path string The test file path.
--- @return table initial_result A neotest result table with all positions initialised as empty.
local function init_empty_result(path)
local init_result = {}
local positions = hspec.parse_positions(path)
if not positions then
logger.warn('Could not get positions to initialise result for ' .. path)
return init_result
end
for _, node in positions:iter_nodes() do
local pos = node:data()
if pos.type == 'test' then
init_result[pos.id] = {}
end
end
vim.pretty_print(init_result)
return init_result
end

---@async
---@param context table: Spec context with the following fields:
--- - file: Absolute path to the test file
Expand Down Expand Up @@ -284,9 +343,11 @@ function hspec.parse_results(context, out_path)
success_positions[#success_positions + 1] = succeeded
end
end
local result = { [pos_id] = {
local result = init_empty_result(context.pos_path)

vim.tbl_extend('force', result, { [pos_id] = {
status = 'failed',
} }
} })
for _, pos in ipairs(failure_positions) do
local failure = {
[pos_path .. '::"' .. pos .. '"'] = {
Expand Down
3 changes: 2 additions & 1 deletion lua/neotest-haskell/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ end

---@async
function HaskellNeotestAdapter.build_spec(args)
local supported_types = { 'test', 'file' }
local tree = args and args.tree
if not tree then
return nil
end
local pos = args.tree:data()
if pos.type ~= 'test' then
if not vim.tbl_contains(supported_types, pos.type) then
return nil
end

Expand Down

0 comments on commit 48811a2

Please sign in to comment.