feat(tests)!: new infrastructure based on makefile

Problem: Not easy to run all checks and tests locally. Redundant CI
workflows.

Solution: Separate CI into two workflows:
 * lint: Lua files (stylua, luals), query files (valid captures,
   predicates, directives using tsqueryls), docs
   (SUPPORTED_LANGUAGES.md) -- does not need parser installation
 * tests: parsers (ABI compatibility), query files (tsqueryls on
   Linux/macOS; nvim on Windows), highlight and indent tests (separated
   for better readability) -- needs parser installation (but only once)

Switch to https://github.com/nvim-treesitter/highlight-assertions fork
with ABI 15 support.

Run all tests (on Linux and macOS) through `make` (`formatlua`,
`checklua`, `lintquery`, `formatquery`, `checkquery`, `docs`, `tests`),
which downloads and caches all necessary dependencies.

Remove `update-readme` workflow (replaced by lint job on PRs).
This commit is contained in:
Christian Clason
2025-04-29 19:40:18 +02:00
parent 4e906caca3
commit 53dccb3a77
19 changed files with 269 additions and 272 deletions

View File

@@ -1,52 +1,50 @@
name: Lint
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
workflow_dispatch:
jobs:
luacheck:
name: Luacheck
lua:
name: Lint Lua files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare
- name: Format
run: |
sudo apt-get update
sudo apt-get install luarocks -y
sudo luarocks install luacheck
- name: Run Luacheck
run: luacheck .
stylua:
name: StyLua
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint with stylua
uses: JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --check .
format-queries:
name: Format queries
runs-on: ubuntu-latest
env:
NVIM_TAG: nightly
steps:
- uses: actions/checkout@v4
- uses: tree-sitter/setup-action/cli@v1
- name: Prepare
run: |
bash ./scripts/ci-install.sh
make formatlua
git diff --exit-code
- name: Lint
run: make checklua
queries:
name: Lint query files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Format
run: |
nvim -l scripts/install-parsers.lua query
nvim -l scripts/format-queries.lua
make formatquery
git diff --exit-code
- name: Lint
run: make lintquery
readme:
name: Lint docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check SUPPORTED_LANGUAGES
run: |
make docs
git diff --exit-code

View File

@@ -35,7 +35,7 @@ jobs:
name: Generate and compile parsers
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=2
- if: inputs.type == 'queries'
- if: inputs.type == 'build'
name: Setup Parsers Cache
id: parsers-cache
uses: actions/cache@v4
@@ -47,9 +47,25 @@ jobs:
'./lua/nvim-treesitter/install.lua',
'./lua/nvim-treesitter/parsers.lua') }}
- if: inputs.type == 'queries'
- if: inputs.type == 'build'
name: Compile parsers
run: $NVIM -l ./scripts/install-parsers.lua
- name: Check query files
- name: Check parsers
run: $NVIM -l ./scripts/check-parsers.lua
- name: Check queries (nvim)
if: ${{ matrix.os == 'windows-latest' }}
run: $NVIM -l ./scripts/check-queries.lua
- name: Check queries (tsqueryls)
if: ${{ matrix.os != 'windows-latest' }}
run: make checkquery
- name: Run highlight tests
if: ${{ matrix.os != 'windows-latest' }}
run: make tests TESTS=query NVIM_BIN=$NVIM
- name: Run indents tests
if: ${{ matrix.os != 'windows-latest' }}
run: make tests TESTS=indent NVIM_BIN=$NVIM

View File

@@ -1,4 +1,4 @@
name: Generate from grammar
name: Tests
on:
pull_request:
@@ -8,12 +8,12 @@ on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-generate-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check_compilation:
name: Build
name: Generate
if: contains(github.event.pull_request.labels.*.name, 'ci:generate') || github.event_name == 'workflow_dispatch'
uses: ./.github/workflows/test-core.yml
with:

View File

@@ -1,12 +1,16 @@
name: Check queries
name: Tests
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-build-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
@@ -14,4 +18,4 @@ jobs:
name: Build
uses: ./.github/workflows/test-core.yml
with:
type: "queries"
type: "build"

View File

@@ -1,60 +0,0 @@
name: Tests
on:
# push:
# branches:
# - "main"
pull_request:
branches:
- "main"
workflow_dispatch:
# Cancel any in-progress CI runs for a PR if it is updated
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
jobs:
check_compilation:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
name: Run tests
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: tree-sitter/setup-action/cli@v1
- name: Test Dependencies
run: |
mkdir -p ~/.local/share/nvim/site/pack/plenary.nvim/start
cd ~/.local/share/nvim/site/pack/plenary.nvim/start
git clone https://github.com/nvim-lua/plenary.nvim
curl -L https://github.com/theHamsta/highlight-assertions/releases/download/v0.1.6/highlight-assertions_v0.1.6_x86_64-unknown-linux-gnu.tar.gz | tar -xz
cp highlight-assertions /usr/local/bin
- name: Install and prepare Neovim
env:
NVIM_TAG: nightly
TREE_SITTER_CLI_TAG: v0.20.8
run: |
bash ./scripts/ci-install.sh
- name: Setup Parsers Cache
id: parsers-cache
uses: actions/cache@v4
with:
path: |
~/.local/share/nvim/site/parser/
~/AppData/Local/nvim-data/site/parser/
key: parsers-${{ join(matrix.*, '-') }}-${{ hashFiles(
'./lua/nvim-treesitter/install.lua',
'./lua/nvim-treesitter/parsers.lua') }}
- name: Compile parsers
run: nvim -l ./scripts/install-parsers.lua
- name: Tests
run: PATH=/usr/local/bin:$PATH ./scripts/run_tests.sh

View File

@@ -2,7 +2,7 @@ name: Update parsers
on:
schedule:
- cron: "30 6 * * *"
- cron: "30 6 * * 6"
workflow_dispatch:
env:

View File

@@ -1,47 +0,0 @@
name: Update README
on:
push:
branches:
- main
workflow_dispatch:
jobs:
update-readme:
name: Update README
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.TOKEN_ID }}
private-key: ${{ secrets.TOKEN_PRIVATE_KEY }}
- name: Prepare
env:
NVIM_TAG: nightly
run: |
bash ./scripts/ci-install.sh
- name: Check README
run: |
nvim -l scripts/update-readme.lua || echo 'Needs update'
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
add-paths: SUPPORTED_LANGUAGES.md
token: ${{ steps.app-token.outputs.token }}
sign-commits: true
commit-message: "bot(readme): update"
title: Update SUPPORTED_LANGUAGES.md
body: "[beep boop](https://github.com/peter-evans/create-pull-request)"
branch: update-readme-pr
base: ${{ github.head_ref }}
- name: Enable Pull Request Automerge
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: gh pr merge --rebase --auto update-readme-pr

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.test-deps
doc/tags
.luacheckcache
/tags

View File

@@ -1,21 +0,0 @@
-- Rerun tests only if their modification time changed.
cache = true
codes = true
exclude_files = {
"tests/indent/lua/"
}
-- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html
ignore = {
"212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off.
"411", -- Redefining a local variable.
"412", -- Redefining an argument.
"422", -- Shadowing an argument
"122" -- Indirectly setting a readonly global
}
-- Global objects defined by the C code
read_globals = {
"vim",
}

View File

@@ -9,6 +9,7 @@
"${3rd}/busted/library"
],
"ignoreDir": [
".test-deps",
"tests"
],
"checkThirdParty": "Disable"

142
Makefile Normal file
View File

@@ -0,0 +1,142 @@
NVIM_VERSION ?= nightly
LUALS_VERSION := 3.14.0
DEPDIR ?= .test-deps
CURL ?= curl -sL --create-dirs
ifeq ($(shell uname -s),Darwin)
NVIM_ARCH ?= macos-arm64
LUALS_ARCH ?= darwin-arm64
STYLUA_ARCH ?= macos-aarch64
RUST_ARCH ?= aarch64-apple-darwin
else
NVIM_ARCH ?= linux-x86_64
LUALS_ARCH ?= linux-x64
STYLUA_ARCH ?= linux-x86_64
RUST_ARCH ?= x86_64-unknown-linux-gnu
endif
.DEFAULT_GOAL := all
# download test dependencies
NVIM := $(DEPDIR)/nvim-$(NVIM_ARCH)
NVIM_TARBALL := $(NVIM).tar.gz
NVIM_URL := https://github.com/neovim/neovim/releases/download/$(NVIM_VERSION)/$(notdir $(NVIM_TARBALL))
NVIM_BIN := $(NVIM)/nvim-$(NVIM_ARCH)/bin/nvim
NVIM_RUNTIME=$(NVIM)/nvim-$(NVIM_ARCH)/share/nvim/runtime
.PHONY: nvim
nvim: $(NVIM)
$(NVIM):
$(CURL) $(NVIM_URL) -o $(NVIM_TARBALL)
mkdir $@
tar -xf $(NVIM_TARBALL) -C $@
rm -rf $(NVIM_TARBALL)
LUALS := $(DEPDIR)/lua-language-server-$(LUALS_VERSION)-$(LUALS_ARCH)
LUALS_TARBALL := $(LUALS).tar.gz
LUALS_URL := https://github.com/LuaLS/lua-language-server/releases/download/$(LUALS_VERSION)/$(notdir $(LUALS_TARBALL))
.PHONY: luals
luals: $(LUALS)
$(LUALS):
$(CURL) $(LUALS_URL) -o $(LUALS_TARBALL)
mkdir $@
tar -xf $(LUALS_TARBALL) -C $@
rm -rf $(LUALS_TARBALL)
STYLUA := $(DEPDIR)/stylua-$(STYLUA_ARCH)
STYLUA_TARBALL := $(STYLUA).zip
STYLUA_URL := https://github.com/JohnnyMorganz/StyLua/releases/latest/download/$(notdir $(STYLUA_TARBALL))
.PHONY: stylua
stylua: $(STYLUA)
$(STYLUA):
$(CURL) $(STYLUA_URL) -o $(STYLUA_TARBALL)
unzip $(STYLUA_TARBALL) -d $(STYLUA)
rm -rf $(STYLUA_TARBALL)
TSQUERYLS := $(DEPDIR)/ts_query_ls-$(RUST_ARCH)
TSQUERYLS_TARBALL := $(TSQUERYLS).tar.gz
TSQUERYLS_URL := https://github.com/ribru17/ts_query_ls/releases/latest/download/$(notdir $(TSQUERYLS_TARBALL))
.PHONY: tsqueryls
tsqueryls: $(TSQUERYLS)
$(TSQUERYLS):
$(CURL) $(TSQUERYLS_URL) -o $(TSQUERYLS_TARBALL)
mkdir $@
tar -xf $(TSQUERYLS_TARBALL) -C $@
rm -rf $(TSQUERYLS_TARBALL)
HLASSERT := $(DEPDIR)/highlight-assertions-$(RUST_ARCH)
HLASSERT_TARBALL := $(HLASSERT).tar.gz
HLASSERT_URL := https://github.com/nvim-treesitter/highlight-assertions/releases/latest/download/$(notdir $(HLASSERT_TARBALL))
.PHONY: hlassert
hlassert: $(HLASSERT)
$(HLASSERT):
$(CURL) $(HLASSERT_URL) -o $(HLASSERT_TARBALL)
mkdir $@
tar -xf $(HLASSERT_TARBALL) -C $@
rm -rf $(HLASSERT_TARBALL)
PLENARY := $(DEPDIR)/plenary.nvim
.PHONY: plenary
plenary: $(PLENARY)
$(PLENARY):
git clone --filter=blob:none https://github.com/nvim-lua/plenary.nvim $(PLENARY)
# actual test targets
.PHONY: lua
lua: formatlua checklua
.PHONY: formatlua
formatlua: $(STYLUA)
$(STYLUA)/stylua .
.PHONY: checklua
checklua: $(LUALS) $(NVIM)
VIMRUNTIME=$(NVIM_RUNTIME) $(LUALS)/bin/lua-language-server \
--configpath=../.luarc.json \
--check=./
.PHONY: query
query: formatquery lintquery checkquery
.PHONY: lintquery
lintquery: $(TSQUERYLS)
$(TSQUERYLS)/ts_query_ls lint runtime/queries
.PHONY: formatquery
formatquery: $(TSQUERYLS)
$(TSQUERYLS)/ts_query_ls format runtime/queries
.PHONY: checkquery
checkquery: $(TSQUERYLS)
$(TSQUERYLS)/ts_query_ls check runtime/queries
.PHONY: docs
docs: $(NVIM)
$(NVIM_BIN) -l scripts/update-readme.lua
.PHONY: tests
tests: $(NVIM) $(HLASSERT) $(PLENARY)
HLASSERT=$(HLASSERT)/highlight-assertions PLENARY=$(PLENARY) \
$(NVIM_BIN) --headless --clean -u scripts/minimal_init.lua \
-c "PlenaryBustedDirectory tests/$(TESTS) { minimal_init = './scripts/minimal_init.lua' }"
.PHONY: all
all: lua query docs tests
.PHONY: clean
clean:
rm -rf $(DEPDIR)

10
TODO.md
View File

@@ -4,17 +4,14 @@ This document lists the planned and finished changes in this rewrite towards [Nv
## TODO
- [ ] **`install.lua`:** simply skip Tier 4 parsers (`get_install_info`)
- [ ] **`parsers.lua`:** allow specifying version in addition to commit hash (for Tier 1)
- [ ] **`parsers.lua`:** track versioned releases for tier 1
- [ ] **`parsers.lua`:** add WASM support (tier 1)
- [ ] **`install.lua`:** migrate to async v2
- [ ] **tests:** fix, update (remove custom crate, plenary dependency)
- [ ] **CI:** switch to ts_query_ls, add update readme as check (remove update job)
- [ ] **tests:** remove custom crate, plenary dependency
- [ ] **documentation:** consolidate, autogenerate?
- [ ] **documentation:** migration guide
- [ ] **indents:** rewrite (Helix compatible)
- [ ] **indents:** rewrite (Helix or Zed compatible)
- [ ] **textobjects:** include simple(!) `node`, `scope` (using `locals`) objects
- [ ] **downstream:** adapt to breaking changes (`nvim-treesitter-refactor`)
## DONE
@@ -31,3 +28,4 @@ This document lists the planned and finished changes in this rewrite towards [Nv
- [X] switch to upstream injection format
- [X] remove locals from highlighting (cf. https://github.com/nvim-treesitter/nvim-treesitter/issues/3944#issuecomment-1458782497)
- [X] drop ensure_install (replace with install)
- [X] **CI:** switch to ts_query_ls, add update readme as check (remove update job)

40
scripts/check-parsers.lua Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env -S nvim -l
vim.opt.runtimepath:append('.')
local configs = require('nvim-treesitter.parsers')
local parsers = #_G.arg > 0 and { unpack(_G.arg) }
or require('nvim-treesitter.config').installed_parsers()
local data = {} ---@type table[]
local errors = {} ---@type string[]
for _, lang in pairs(parsers) do
if configs[lang] and configs[lang].install_info then
local ok, info = pcall(vim.treesitter.language.inspect, lang)
if not ok then
errors[#errors + 1] = string.format('%s: %s', lang, info)
else
data[#data + 1] = { lang = lang, abi = info.abi_version, state_count = info.state_count }
end
end
end
if #errors > 0 then
print('::group::Errors')
for _, err in ipairs(errors) do
print(err)
end
print('::endgroup::')
print('Check failed!\n')
vim.cmd.cq()
else
print('::group::State counts')
table.sort(data, function(a, b)
return a.state_count < b.state_count
end)
for i, val in ipairs(data) do
print(string.format('%i.\t%d\t%s (ABI %d)', #data - i + 1, val.state_count, val.lang, val.abi))
end
print('::endgroup::')
print('Check successful!')
end

View File

@@ -6,29 +6,6 @@ local configs = require('nvim-treesitter.parsers')
local parsers = #_G.arg > 0 and { unpack(_G.arg) }
or require('nvim-treesitter.config').installed_parsers()
-- Extract captures from documentation for validation
local captures = {} ---@type table[]
do
local current_query ---@type string
for line in io.lines('CONTRIBUTING.md') do
if vim.startswith(line, '### ') then
current_query = line:sub(5):lower() ---@type string
elseif vim.startswith(line, '@') and current_query then
if not captures[current_query] then
captures[current_query] = {}
end
table.insert(captures[current_query], vim.split(line:sub(2), ' ')[1])
end
end
-- Complete captures for injections.
for lang, _ in pairs(configs) do
table.insert(captures['injections'], lang)
end
end
-- Check queries for each installed parser in parsers
local errors = {} ---@type string[]
local timings = {} ---@type { duration: number, lang: string, query_type: string }[]
@@ -46,19 +23,6 @@ do
print(string.format('Checking %s %s (%.02fms)', lang, query_type, duration * 1e-6))
if not ok then
errors[#errors + 1] = string.format('%s (%s): %s', lang, query_type, query)
else
if query then
for _, capture in ipairs(query.captures) do
local is_valid = (
vim.startswith(capture, '_') -- Helpers.
or vim.list_contains(captures[query_type], capture)
)
if not is_valid then
errors[#errors + 1] =
string.format('%s (%s): invalid capture "@%s"', lang, query_type, capture)
end
end
end
end
end
end

View File

@@ -10,19 +10,12 @@ if [[ $os == Linux ]]; then
tar -zxf nvim-linux-x86_64.tar.gz
sudo ln -s "$PWD"/nvim-linux-x86_64/bin/nvim /usr/local/bin
rm -rf "$PWD"/nvim-linu-x86_x64/lib/nvim/parser
mkdir -p ~/.local/share/nvim/site/pack/nvim-treesitter/start
ln -s "$PWD" ~/.local/share/nvim/site/pack/nvim-treesitter/start
elif [[ $os == Darwin ]]; then
RELEASE_NAME="nvim-macos-$(uname -m)"
curl -L "https://github.com/neovim/neovim/releases/download/${NVIM_TAG}/$RELEASE_NAME.tar.gz" | tar -xz
sudo ln -s "$PWD/$RELEASE_NAME/bin/nvim" /usr/local/bin
rm -rf "$PWD/$RELEASE_NAME/lib/nvim/parser"
mkdir -p ~/.local/share/nvim/site/pack/nvim-treesitter/start
ln -s "$PWD" ~/.local/share/nvim/site/pack/nvim-treesitter/start
else
curl -L "https://github.com/neovim/neovim/releases/download/${NVIM_TAG}/nvim-win64.zip" -o nvim-win64.zip
unzip nvim-win64
mkdir -p ~/AppData/Local/nvim/pack/nvim-treesitter/start
mkdir -p ~/AppData/Local/nvim-data
cp -r "$PWD" ~/AppData/Local/nvim/pack/nvim-treesitter/start
fi

View File

@@ -1,3 +1,4 @@
vim.opt.runtimepath:append(os.getenv('PLENARY'))
vim.opt.runtimepath:append('.')
vim.cmd.runtime({ 'plugin/plenary.vim', bang = true })
vim.cmd.runtime({ 'plugin/query_predicates.lua', bang = true })

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
HERE="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
cd $HERE/..
run() {
nvim --headless --noplugin -u scripts/minimal_init.lua \
-c "PlenaryBustedDirectory $1 { minimal_init = './scripts/minimal_init.lua' }"
}
if [[ $2 = '--summary' ]]; then
## really simple results summary by filtering plenary busted output
run tests/$1 2> /dev/null | grep -E '^\S*(Testing|Success|Failed|Errors)\s*:'
else
run tests/$1
fi

View File

@@ -1,3 +1,4 @@
local config = require('nvim-treesitter.config')
local ts = vim.treesitter
local COMMENT_NODES = {
@@ -6,27 +7,20 @@ local COMMENT_NODES = {
}
local function check_assertions(file)
assert.same(
1,
vim.fn.executable('highlight-assertions'),
'"highlight-assertions" not executable!'
.. ' Get it via "cargo install --git https://github.com/theHamsta/highlight-assertions"'
)
local buf = vim.fn.bufadd(file)
vim.fn.bufload(file)
local ft = vim.bo[buf].filetype
local lang = vim.treesitter.language.get_lang(ft) or ft
local comment_node = COMMENT_NODES[lang] or 'comment'
local assertions = vim.fn.json_decode(
vim.fn.system(
"highlight-assertions -p '"
.. vim.api.nvim_get_runtime_file('parser/' .. lang .. '.so', false)[1]
.. "' -s '"
.. file
.. "' -c "
.. comment_node
)
)
local assertions = vim.fn.json_decode(vim.fn.system({
os.getenv('HLASSERT'),
'-p',
config.get_install_dir('parser') .. '/' .. lang .. '.so',
'-s',
file,
'-c',
comment_node,
}))
assert.True(#assertions > 0, 'No assertions detected!')
local parser = ts.get_parser(buf)

View File

@@ -6,24 +6,13 @@ local function check_assertions(file)
vim.fn.bufload(file)
local ft = vim.bo[buf].filetype
local lang = vim.treesitter.language.get_lang(ft) or ft
assert.same(
1,
vim.fn.executable('highlight-assertions'),
'"highlight-assertions" not executable!'
.. ' Get it via "cargo install --git https://github.com/theHamsta/highlight-assertions"'
)
local assertions = vim.fn.json_decode(
vim.fn.system(
"highlight-assertions -p '"
.. config.get_install_dir('parser')
.. '/'
.. lang
.. ".so'"
.. " -s '"
.. file
.. "'"
)
)
local assertions = vim.fn.json_decode(vim.fn.system({
os.getenv('HLASSERT'),
'-p',
config.get_install_dir('parser') .. '/' .. lang .. '.so',
'-s',
file,
}))
local parser = ts.get_parser(buf, lang)
local top_level_root = parser:parse(true)[1]:root()