Skip to content

Commit

Permalink
feat: support for the sydtest framework
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcjkb committed Mar 31, 2023
1 parent ca91051 commit 1f07a37
Show file tree
Hide file tree
Showing 25 changed files with 727 additions and 207 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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 the [`sydtest`](https://hackage.haskell.org/package/sydtest) test framework.
- Move position queries to `queries/haskell/<framework>-positions.scm`
and `queries/haskell/<framework>-test`. This allows the addition of extra
queries to `$XDG_CONFIG_HOME/nvim/after/queries/haskell/<framework>-positions.scm`
### Fixed
- Hspec: support `context`, `xcontext`, `specify` and `xspecify`

Expand Down
55 changes: 26 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ A [Neotest](https://github.com/nvim-neotest/neotest) adapter for Haskell.

- [x] Supports [Cabal](https://www.haskell.org/cabal/) (single/multi-package) projects.
- [x] Supports [Stack](https://docs.haskellstack.org/en/stable/) (single/multi-package) projects.
- [x] Parses [Hspec](https://hackage.haskell.org/package/hspec) `--match` filters for the cursor's position using tree-sitter.
- [x] Parses [Hspec](https://hackage.haskell.org/package/hspec) and [Sydtest](https://hackage.haskell.org/package/sydtest) `--match` filters for the cursor's position using tree-sitter.
- [x] Parses [Tasty](https://hackage.haskell.org/package/tasty) `--pattern` filters for the cursor's position using tree-sitter.
- [x] Parses test results and displays error messages as diagnostics.

Expand Down Expand Up @@ -89,7 +89,7 @@ require('neotest').setup {
-- Default: Use stack if possible and then try cabal
build_tools = { 'stack', 'cabal' },
-- Default: Check for tasty first and then try hspec
frameworks = { 'tasty', 'hspec' },
frameworks = { 'tasty', 'hspec', 'sydtest' },
},
},
}
Expand All @@ -110,6 +110,7 @@ require('neotest').setup {
frameworks = {
{ framework = 'tasty', modules = { 'Test.Tasty', 'MyTestModule' }, },
'hspec',
'sydtest',
},
},
},
Expand All @@ -121,6 +122,22 @@ used for framework identification:

* `tasty`: `modules = { 'Test.Tasty' }`
* `hspec`: `modules = { 'Test.Hspec' }`
* `sydtest`: `modules = { 'Test.Syd' }`


## Advanced configuration

This plugin uses tree-sitter queries in files that match
`<runtimepath>/queries/haskell/<framework>-positions.scm`

For example, to add position queries for this plugin for `tasty`, without
having to fork this plugin, you can add them to
`$XDG_CONFIG_HOME/nvim/after/queries/haskell/tasty-positions.scm`.

> **Note**
>
> * `:h runtimepath`
> * See examples in [`queries/haskell/`](./queries/haskell/).

## Examples
Expand Down Expand Up @@ -185,44 +202,24 @@ stack test my_package --ta "--match \"/Prelude.head/\""

## TODO

### Cabal support

- [x] Run cabal v2 tests with Hspec
- [x] Support both single + multi-package cabal v2 projects
- [x] Support cabal v2 projects with more than one test suite per package
- [x] Parse cabal v2 Hspec test results


### Stack support

- [x] Run stack tests with Hspec
- [x] Support both single + multi-package stack projects
- [x] Support stack projects with more than one test suite per package
- [x] Parse stack Hspec test results


### Testing frameworks

- [x] [hspec](https://hackage.haskell.org/package/hspec)
- [x] [tasty](https://hackage.haskell.org/package/tasty)
- [ ] [sydtest](https://github.com/NorfairKing/sydtest)
- [ ] [yesod-test](https://hackage.haskell.org/package/yesod-test)

### Other
- [ ] Provide `nvim-dap` configuration
See [issues](https://github.com/mrcjkb/neotest-haskell/issues).


## Troubleshooting

To run a health check, run `:checkhealth neotest-haskell` in Neovim.

## Limitations

* To run `sydtest` tests of type `'file'`, `sydtest >= 0.13.0.4` is required,
if the file has more than one top-level namespace (`describe`, `context`, ..).

## Recommendations

Here are some other plugins I recommend for Haskell development:

* [mrcjkb/haskell-tools.nvim](https://github.com/mrcjkb/haskell-tools.nvim): Toolset to improve the Haskell experience in Neovim
* [luc-tielen/telescope_hoogle](https://github.com/luc-tielen/telescope_hoogle): Hoogle search
* [mrcjkb/haskell-tools.nvim](https://github.com/mrcjkb/haskell-tools.nvim): Toolset to improve the Haskell experience in Neovim.
* [luc-tielen/telescope_hoogle](https://github.com/luc-tielen/telescope_hoogle): Hoogle search.


## Contributors ✨
Expand Down
9 changes: 5 additions & 4 deletions doc/neotest-haskell.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ You can also pass a config to the setup. The following are the defaults:
require('neotest-haskell') {
-- Default: Use stack if possible and then try cabal
build_tools = { 'stack', 'cabal' },
-- Default: Check for tasty first and then try hspec
frameworks = { 'tasty', 'hspec' },
-- Default: Check for tasty first, then try hspec, and finally 'sydtest'
frameworks = { 'tasty', 'hspec', 'sydtest' },
},
},
}
Expand All @@ -48,6 +48,7 @@ used to identify the respective framework in a test file:
frameworks = {
{ framework = 'tasty', modules = { 'Test.Tasty', 'MyTestModule' }, },
'hspec',
'sydtest',
},
},
},
Expand All @@ -61,7 +62,7 @@ NeotestHaskellOpts *NeotestHaskellOpts*

Fields: ~
{build_tools} (build_tool[]|nil) The build tools, ordered by priority. Default: `{ 'stack', 'cabal' }`.
{frameworks} (framework_opt[]|nil) List of frameworks or framework specs, ordered by priority. Default: `{ 'tasty', 'hspec' }`.
{frameworks} (framework_opt[]|nil) List of frameworks or framework specs, ordered by priority. Default: `{ 'tasty', 'hspec', 'sydtest' }`.
{is_test_file} (fun(name:string):boolean|nil) Used to detect if a file is a test file.
{filter_dir} (fun(name:string,rel_path:string,root:string):boolean|nil) Filter directories when searching for test files

Expand All @@ -84,7 +85,7 @@ framework_opt *framework_opt*
test_framework *test_framework*

Type: ~
"tasty"|"hspec"
"tasty"|"hspec"|"sydtest"


FrameworkSpec *FrameworkSpec*
Expand Down
44 changes: 3 additions & 41 deletions lua/neotest-haskell/hspec.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
local treesitter = require('neotest-haskell.treesitter')
local util = require('neotest-haskell.util')
local position = require('neotest-haskell.position')
local results = require('neotest-haskell.results')
Expand All @@ -7,53 +8,14 @@ local hspec = {}

hspec.default_modules = { 'Test.Hspec' }

---Tree-sitter query to find namespaces
hspec.namespace_query = [[
;; query
;; describe (unqualified)
(_ (_ (exp_apply
(exp_name (variable) @func_name)
(exp_literal) @namespace.name
)
(#any-of? @func_name "describe" "xdescribe" "context" "xcontext")
)) @namespace.definition
;; describe (qualified)
(_ (_ (exp_apply
(exp_name (qualified_variable (variable) @func_name))
(exp_literal) @namespace.name
)
(#any-of? @func_name "describe" "xdescribe" "context" "xcontext")
)) @namespace.definition
]]

---Tree-sitter query to find tests
hspec.tests_query = [[
;; query
;; test (unqualified)
((exp_apply
(exp_name (variable) @func_name)
(exp_literal) @test.name
)
(#any-of? @func_name "it" "xit" "prop" "xprop" "specify" "xspecify")
) @test.definition
;; test (qualified)
((exp_apply
(exp_name (qualified_variable (variable) @func_name))
(exp_literal) @test.name
)
(#any-of? @func_name "it" "xit" "prop" "xprop" "specify" "xspecify")
) @test.definition
]]
hspec.position_query = treesitter.get_position_query('hspec')

---Parse the positions in a test file.
---@async
---@param path string Test file path
---@return neotest.Tree
function hspec.parse_positions(path)
local query = hspec.namespace_query .. hspec.tests_query
return position.parse_positions(path, query)
return position.parse_positions(path, hspec.position_query)
end

---Parses hspec --match filter expressions for the top-level test positions.
Expand Down
9 changes: 5 additions & 4 deletions lua/neotest-haskell/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
--- require('neotest-haskell') {
--- -- Default: Use stack if possible and then try cabal
--- build_tools = { 'stack', 'cabal' },
--- -- Default: Check for tasty first and then try hspec
--- frameworks = { 'tasty', 'hspec' },
--- -- Default: Check for tasty first, then try hspec, and finally 'sydtest'
--- frameworks = { 'tasty', 'hspec', 'sydtest' },
--- },
--- },
--- }
Expand All @@ -44,6 +44,7 @@
--- frameworks = {
--- { framework = 'tasty', modules = { 'Test.Tasty', 'MyTestModule' }, },
--- 'hspec',
--- 'sydtest',
--- },
--- },
--- },
Expand All @@ -55,7 +56,7 @@

---@class NeotestHaskellOpts
---@field build_tools build_tool[] | nil The build tools, ordered by priority. Default: `{ 'stack', 'cabal' }`.
---@field frameworks framework_opt[] | nil List of frameworks or framework specs, ordered by priority. Default: `{ 'tasty', 'hspec' }`.
---@field frameworks framework_opt[] | nil List of frameworks or framework specs, ordered by priority. Default: `{ 'tasty', 'hspec', 'sydtest' }`.
---@field is_test_file (fun(name:string):boolean) | nil Used to detect if a file is a test file.
---@field filter_dir (fun(name:string, rel_path:string, root:string):boolean) | nil Filter directories when searching for test files
---@see neotest
Expand All @@ -64,7 +65,7 @@

---@alias framework_opt test_framework | FrameworkSpec

---@alias test_framework 'tasty' | 'hspec'
---@alias test_framework 'tasty' | 'hspec' | 'sydtest'

---@class FrameworkSpec
---@field framework test_framework
Expand Down
3 changes: 1 addition & 2 deletions lua/neotest-haskell/internal_types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

---@class TestFrameworkHandler
---@field default_modules string[] Default list of qualified modules used to determine if this handler can be used.
---@field namespace_query string Tree-sitter query for namespace positions.
---@field test_query string Tree-sitter query for test positions.
---@field position_query string Tree-sitter query for test and namespace positions.
---@field parse_positions fun(file_path:string):neotest.Tree Function that parses the positions in a test file.
---@field get_cabal_test_opts fun(pos:neotest.Position):string[] Function that constructs the options for a cabal test command.
---@field get_stack_test_opts fun(pos:neotest.Position):string[] Function that constructs the options for a stack test command.
Expand Down
12 changes: 7 additions & 5 deletions lua/neotest-haskell/results.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ local function get_file_root(tree)
return tree
end

---NOTE: The order of the `get_*_name` params is the order in which they are checked.
---
---@async
---@param parse_errors fun(raw_lines:string[], test_name:string):neotest.Error[]
---@param get_failed_name fun(line:string, lines:string[], idx:integer):string? Function to extract a failed test name
Expand Down Expand Up @@ -67,20 +69,20 @@ function results.mk_result_parser(parse_errors, get_failed_name, get_succeeded_n
if not success then
return {}
end
local lines = vim.split(data, '\n')
local lines = vim.split(data, '\n') or {}
local failure_positions = {}
local success_positions = {}
local skipped_positions = {}
for idx, line in ipairs(lines) do
local skipped = get_skipped_name(line, lines, idx)
local failed = get_failed_name(line, lines, idx)
local succeeded = get_succeeded_name(line, lines, idx)
if skipped then
skipped_positions[#skipped_positions + 1] = skipped
elseif failed then
local skipped = get_skipped_name(line, lines, idx)
if failed then
failure_positions[#failure_positions + 1] = failed
elseif succeeded then
success_positions[#success_positions + 1] = succeeded
elseif skipped then
skipped_positions[#skipped_positions + 1] = skipped
end
end

Expand Down
6 changes: 5 additions & 1 deletion lua/neotest-haskell/runner.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local async = require('neotest.async')
local lib = require('neotest.lib')
local Path = require('plenary.path')
local logger = require('neotest.logging')
local treesitter_hs = require('neotest-haskell.treesitter')

local runner = {}
Expand Down Expand Up @@ -83,7 +84,7 @@ end
runner.supported_build_tools = { 'stack', 'cabal' }

---@type test_framework[]
runner.supported_frameworks = { 'tasty', 'hspec' }
runner.supported_frameworks = { 'tasty', 'hspec', 'sydtest' }

---@param framework test_framework
---@return TestFrameworkHandler
Expand Down Expand Up @@ -118,6 +119,7 @@ function runner.select_framework(test_file_path, frameworks)
end
for _, spec in pairs(framework_specs) do
if has_module(content, spec.modules) then
logger.debug('Selected Haskell framework: ' .. spec.framework)
return get_handler(spec.framework)
end
end
Expand Down Expand Up @@ -160,6 +162,8 @@ function runner.select_build_tool(handler, test_file_path, build_tools)
error('Cannot run tests for configured build tools: ' .. vim.inspect(build_tools))
end

logger.debug('Selected Haskell build tool: ' .. selected_build_tool)

local command = { selected_build_tool, 'test' }
if is_multi_package_project then
local package_name = get_package_name(package_root)
Expand Down
Loading

0 comments on commit 1f07a37

Please sign in to comment.