Skip to content

Commit

Permalink
Merge pull request #7 from fidgetingbits/lua-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
saidelike authored Jul 25, 2024
2 parents 027f188 + 1be17b8 commit 4c0ea88
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 18 deletions.
17 changes: 17 additions & 0 deletions .github/actions/test-neovim-lua/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: "Neovim Lua Tests"
description: "Set up Neovim Lua environment and run Busted tests"
runs:
using: "composite"
steps:
- uses: leafo/gh-actions-lua@v9
with:
luaVersion: "luajit-2.1.0-beta3"
- uses: leafo/gh-actions-luarocks@v4
- shell: bash
run: |
luarocks install busted
luarocks install luafilesystem
- shell: bash
run: |
cd cursorless.nvim
busted --run unit
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ jobs:
if: runner.os == 'Linux'
env:
NEOVIM_PATH: ${{ steps.vim.outputs.executable }}
- uses: ./.github/actions/test-neovim-lua/
if: runner.os == 'Linux' && matrix.app_version == 'stable'
- name: Create vscode dist that can be installed locally
run: pnpm -F @cursorless/cursorless-vscode populate-dist --local-install
if: runner.os == 'Linux' && matrix.app_version == 'stable'
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
"Lua.diagnostics.globals": ["vim", "talon"],
"[lua]": {
"editor.defaultFormatter": "JohnnyMorganz.stylua"
},
"files.associations": {
".busted": "lua"
}
}
8 changes: 8 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@
// NOTE: We don't have a way on Windows atm due to command with argument inside Run() not working
// so we need to show logs outside of vscode (see #2454)
},
{
"label": "Neovim: Launch neovim (lua test)",
"type": "shell",
"command": "busted --run unit",
"options": {
"cwd": "cursorless.nvim"
}
},

// cursorless.org
{
Expand Down
10 changes: 10 additions & 0 deletions cursorless.nvim/.busted
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
return {
_all = {
lua = './test/nvim-shim.sh',
output = "TAP",
["defer-print"] = false,
},
unit = {
ROOT = {'./test/unit/'},
},
}
15 changes: 5 additions & 10 deletions cursorless.nvim/lua/cursorless/cursorless.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,14 @@ function M.buffer_get_selection()
end

-- https://github.com/nvim-treesitter/nvim-treesitter/blob/master/lua/nvim-treesitter/ts_utils.lua#L278
-- luacheck:ignore 631
-- https://github.com/nvim-treesitter/nvim-treesitter-textobjects/blob/master/lua/nvim-treesitter/textobjects/select.lua#L114
-- as an example if you put that in a vim buffer and do the following you can do a selection:
-- :w c:\work\tmp\test.lua
-- :so %
-- :lua select_range(5, 12, 5, 30)
-- for example it will highlight the last function name (nvim_win_set_cursor).
-- another example is :tmap <c-b> <Cmd>lua require("talon.cursorless").select_range(4, 0, 4, 38)<Cr>
-- If you have a buffer with the line: "hello world"
-- :lua require("cursorless.cursorless").select_range(1, 2, 1, 4)
-- will highlight "llo"
-- NOTE: works for any mode (n,i,v,nt) except in t mode
function M.select_range(start_line, start_col, end_line, end_col)
vim.cmd([[normal! :noh]])
vim.cmd([[silent! normal! :noh]])
vim.api.nvim_win_set_cursor(0, { start_line, start_col })
vim.cmd([[normal v]])
vim.cmd([[silent! normal v]])
vim.api.nvim_win_set_cursor(0, { end_line, end_col })
end

Expand Down
2 changes: 1 addition & 1 deletion cursorless.nvim/lua/cursorless/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ end
-- https://www.baeldung.com/linux/vim-paste-text
-- e.g. run in command mode :imap <c-b> <Cmd>lua require('cursorless.utils').paste()<Cr>
function M.paste()
vim.cmd([[normal! "+p]])
vim.cmd([[silent! normal! "+p]])
end

return M
16 changes: 16 additions & 0 deletions cursorless.nvim/test/helpers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- This file gets linked into plugin/helpers.lua of busted nvim config
-- Functions that are exposed to all tests

function _G.get_selected_text()
local _, ls, cs = unpack(vim.fn.getpos("v"))
local _, le, ce = unpack(vim.fn.getpos("."))
return vim.api.nvim_buf_get_text(0, ls - 1, cs - 1, le - 1, ce, {})
end

function _G.convert_table_entries(tbl, func)
local mapped = {}
for k, v in pairs(tbl) do
mapped[k] = func(v)
end
return mapped
end
28 changes: 28 additions & 0 deletions cursorless.nvim/test/nvim-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

if ! [[ "${PWD}" == *"cursorless.nvim" ]]; then
echo "ERROR: This script must be run from inside cursorless.nvim/ directory"
exit 1
fi

test_folder=$(mktemp -d "${TMPDIR-/tmp}"/cursorless-busted-test-XXXXX)
export XDG_CONFIG_HOME="${test_folder}/xdg/config/"
export XDG_STATE_HOME="${test_folder}/xdg/local/state/"
export XDG_DATA_HOME="${test_folder}/xdg/local/share/"
dependency_folder="${XDG_DATA_HOME}/nvim/site/pack/testing/start/"
plugin_folder="${XDG_CONFIG_HOME}/nvim/plugin/"

mkdir -p "${plugin_folder}" "${XDG_STATE_HOME}" "${dependency_folder}"
ln -sf "${PWD}" "${dependency_folder}/cursorless.nvim"

# Link in standalone helper functions we want all tests to be able to call
ln -sf "${PWD}/test/helpers.lua" "${plugin_folder}/helpers.lua"

# shellcheck disable=SC2068
command nvim --cmd 'set loadplugins' -l $@
exit_code=$?

rm -rf "${test_folder}"

exit $exit_code
79 changes: 79 additions & 0 deletions cursorless.nvim/test/unit/cursorless_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
describe("", function()
local cursorless = require("cursorless.cursorless")

describe("window_get_visible_lines() ->", function()
it("can read one visible line", function()
local pos = vim.api.nvim_win_get_cursor(0)[2]
local line = vim.api.nvim_get_current_line()
local nline = line:sub(0, pos) .. "hello" .. line:sub(pos + 1)
vim.api.nvim_set_current_line(nline)

local visible = cursorless.window_get_visible_lines()
assert(table.concat(visible) == table.concat({ 1, 1 }))
end)

it("can read all lines visible on the window", function()
local maxlines = vim.api.nvim_win_get_height(0)
local lines = {}
for _ = 1, (maxlines + 1) do
table.insert(lines, "hello ")
end
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
local visible = cursorless.window_get_visible_lines()
assert(table.concat(visible) == table.concat({ 1, maxlines }))
end)
end)
describe("select_range() ->", function()
it("selects the specified range", function()
local lines = "hello world"
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(lines, "\n"))
cursorless.select_range(1, 2, 1, 4)

assert(table.concat(_G.get_selected_text()) == "llo")
end)
end)
describe("buffer_get_selection() ->", function()
it(
"can get the forward selection in a format expected by cursorless",
function()
local lines = "hello world"
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(lines, "\n"))
cursorless.select_range(1, 2, 1, 4)
assert(
table.concat(
_G.convert_table_entries(
cursorless.buffer_get_selection(),
tostring
),
", "
)
== table.concat(
_G.convert_table_entries({ 1, 3, 1, 5, false }, tostring),
", "
)
)
end
)
it(
"can get the backward selection in a format expected by cursorless",
function()
local lines = "hello world"
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(lines, "\n"))
cursorless.select_range(1, 4, 1, 2)
assert(
table.concat(
_G.convert_table_entries(
cursorless.buffer_get_selection(),
tostring
),
", "
)
== table.concat(
_G.convert_table_entries({ 1, 3, 1, 5, true }, tostring),
", "
)
)
end
)
end)
end)
59 changes: 56 additions & 3 deletions docs/contributing/architecture/neovim-test-infrastructure.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Neovim test infrastructure

We'll start with a high-level overview of the architecture of the Cursorless tests for neovim, and then we'll dive into the details. Here is the call path when running Neovim tests locally. Note that `->` indicates one file calling another file:
We'll start with a high-level overview of the architecture of the Cursorless tests for neovim, and then we'll dive into the details.

## Neovim tests

Here is the call path when running Neovim tests locally. Note that `->` indicates one file calling another file:

```
launch.json -> .vscode/tasks.json -> nvim -u init.lua
Expand All @@ -25,7 +29,7 @@ packages/test-harness/src/config/init.lua
-> TestHarnessRun() -> run() -> runAllTests() -> Mocha + packages/cursorless-neovim-e2e/src/suite/recorded.neovim.test.ts
```

## Running Neovim tests locally
### Running Neovim tests locally

This is supported on Windows, Linux and OSX.

Expand Down Expand Up @@ -273,4 +277,53 @@ NOTE: CI uses `dist/cursorless.nvim/` (and not `cursorless.nvim/`), since the sy
## Lua unit tests
XXX
This is supported on Linux only, both locally and on CI.
Here is the call path when running lua unit tests locally. Note that `->` indicates one file calling another file:
```
launch.json -> .vscode/tasks.json -> cd cursorless.nvim && busted --run unit
cursorless.nvim/.busted
-> lua interpreter: cursorless.nvim/test/nvim-shim.sh -> nvim -l <spec_script>
-> test specification files: cursorless.nvim/test/unit/*_spec.lua
```
And here is the call path when running lua unit tests on CI:
```
.github/workflows/test.yml -> .github/actions/test-neovim-lua/action.yml -> cd cursorless.nvim && busted --run unit
cursorless.nvim/.busted
-> lua interpreter: cursorless.nvim/test/nvim-shim.sh -> nvim -l <spec_script>
-> test specification files: cursorless.nvim/test/unit/*_spec.lua
```
### Running lua unit tests
Many of the cursorless.nvim lua functions are run in order to complete Cursorless actions and so are already
indirectly tested by the tests described in the [previous section](#3-cursorless-tests-for-neovim). Nevertheless, we run
more specific unit tests in order to give better visibility into exactly which functions are failing.
The [busted](https://github.com/lunarmodules/busted) framework is used to test lua functions defined in cursorless.nvim.
This relies on a `cursorless.nvim/.busted` file which directs busted to use a lua interpreter and test specifications files:
```bash
return {
_all = {
lua = './test/nvim-shim.sh'
},
unit = {
ROOT = {'./test/unit/'},
},
}
```
The `.busted` file declares the `cursorless.nvim/test/nvim-shim.sh` shell wrapper as its lua interpreter. This script sets up an enclosed neovim environment by using [XDG Base
Directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) environment variables (Linux
only) pointing to a temp directory. This allows loading cursorless.nvim, any helpers and (optional) plugins needed to run
the tests. Consequently, the cursorless.nvim lua functions are exposed to the tests. Afterwards, the shim will use `nvim -l <spec_script>` for each of the [lua test specifications scripts](https://neovim.io/doc/user/starting.html#-l).
The `.busted` file declares that test specifications files are in
`cursorless.nvim/test/unit/`. Any file in that folder ending with `_spec.lua` contains tests and will be executed
by neovim's lua interpreter.
NOTE: Different tests rely on
the same custom test helper functions. These functions are exposed as globals in a file called `helpers.lua` placed in `nvim/plugin/` inside the isolated XDG environment. These helpers themselves also have their own unit tests that will be run by busted.
This busted setup was inspired by this [blog
post](https://hiphish.github.io/blog/2024/01/29/testing-neovim-plugins-with-busted/), which goes into greater detail.
10 changes: 8 additions & 2 deletions docs/contributing/cursorless-in-neovim.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Follow the steps in [CONTRIBUTING.md](./CONTRIBUTING.md#initial-setup).

Follow the installation steps in [cursorless.nvim](https://github.com/hands-free-vim/cursorless.nvim/tree/main#prerequisites).

Confirm that production cursorless.nvim is working in neovim, eg say `"take first paint"` in a nonempty document.
Confirm that production cursorless.nvim is working in neovim, eg say `"take first paint"` in a non-empty document.

### 3. Add nvim executable path to your PATH

Expand Down Expand Up @@ -42,7 +42,13 @@ debug mode. To do so you need to run the `workbench.action.debug.selectandstart`

The debug logs are written in `C:\path\to\cursorless\packages\cursorless-neovim\out\nvim_node.log`.

NOTE: This will spawn a standalone nvim instance that is independent of VSCode. Consequently after you're done debugging, you need to close nvim.
NOTE: This will spawn a standalone nvim instance that is independent of VSCode. Consequently after you're done
debugging, you need to close nvim.

### Running lua tests

Their are separate cursorless and lua tests. You can run the lua tests by entering the `cursorless.nvim` folder and
running: `busted --run unit`. These tests currently only work on Linux.

## Sending pull requests

Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ We run the above tests in various contexts, both locally and in CI. The contexts
- **VSCode**: Today, many of our tests must run within a VSCode context. For some of our tests, this is desirable, because they are designed to test that our code works in VSCode. However, many of our tests (such as scope tests and recorded tests) are not really VSCode-specific, but we haven't yet built the machinery to run them in a more isolated context, which would be much faster.
- **Unit tests**: Many of our tests can run in a neutral context, without requiring an actual ide with editors, etc. Most of these are unit tests in the traditional sense of the word, testing the logic of a small unit of code, such as a function.
- **Talon**: For each of our recorded tests, we test that saying the spoken form of the command in Talon results in the command payload that we expect. Note that these tests can only be run locally today.
- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. To learn more about our Neovim test infrastracture, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md).
- **Neovim**: We run a subset of our recorded tests within Neovim to ensure that the given subset of Cursorless works within Neovim. We also have a few lua unit tests that must be run in Neovim. These test the lua functions that Cursorless needs in order to interact with Neovim. To learn more about our Neovim test infrastructure, see [Neovim test infrastructure](./architecture/neovim-test-infrastructure.md).

You can get an overview of the various test contexts that exist locally by looking at our VSCode launch configs, which include not only our VSCode tests, but all of our tests.
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@
'';
}))
python

pkgs.neovim
pkgs.luajitPackages.busted # for lua testing
pkgs.luarocks # pre-commit doesn't auto-install luarocks
];
# To prevent weird broken non-interactive bash terminal
buildInputs = [ pkgs.bashInteractive ];
Expand Down
2 changes: 1 addition & 1 deletion init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ if not (vim.uv or vim.loop).fs_stat(lazypath) then
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
vim.opt.runtimepath:prepend(lazypath)
require("lazy").setup({
-- Allows title detection by neovim-talon while testing
"hands-free-vim/talon.nvim",
Expand Down

0 comments on commit 4c0ea88

Please sign in to comment.