diff --git a/.github/workflows/lua.yml b/.github/workflows/lua.yml index 5da62a387..35b15376e 100644 --- a/.github/workflows/lua.yml +++ b/.github/workflows/lua.yml @@ -22,6 +22,18 @@ jobs: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Validate Neovim Configuration (Dev Shell) run: make lua-check-neovim-dev + lua-neovim-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Run Neovim Tests (Dev Shell) + run: make neovim-test-dev lua-hammerspoon: runs-on: macos-latest timeout-minutes: 300 @@ -38,6 +50,7 @@ jobs: if: always() needs: - lua-neovim + - lua-neovim-test - lua-hammerspoon runs-on: ubuntu-latest timeout-minutes: 3 diff --git a/Makefile b/Makefile index fec2cc1e8..3e50dbff7 100644 --- a/Makefile +++ b/Makefile @@ -491,6 +491,17 @@ neovim-sync: ## Sync Neovim plugins. @nvim --headless +"lua vim.cmd('source ' .. vim.fn.stdpath('config') .. '/init.lua')" +qa @echo "✅ Neovim plugins synced" +.PHONY: neovim-test +neovim-test: ## Run Neovim tests using plenary.nvim. + @echo "🧪 Running Neovim tests..." + @$(PWD)/home-manager/programs/neovim/tests/run_tests.sh + @echo "✅ Neovim tests completed" + +.PHONY: neovim-test-dev +neovim-test-dev: ## Run Neovim tests inside the Nix dev shell (mirrors CI). + @echo "🧪 Running Neovim tests inside the Nix dev shell..." + @DEVENV_ROOT=$(CURDIR) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) develop $(NIX_FLAGS) .# --command $(MAKE) neovim-test + ##@ Lua .PHONY: lua-check diff --git a/home-manager/programs/neovim/tests/config_spec.lua b/home-manager/programs/neovim/tests/config_spec.lua new file mode 100644 index 000000000..620c58c13 --- /dev/null +++ b/home-manager/programs/neovim/tests/config_spec.lua @@ -0,0 +1,63 @@ +-- Tests for Neovim configuration loading +-- Verifies that the config modules can be loaded without errors + +describe("config", function() + describe("settings", function() + it("should load without errors", function() + assert.has_no.errors(function() + package.loaded["settings"] = nil + require("settings") + end) + end) + end) + + describe("keymaps", function() + it("should define leader key", function() + assert.equals(" ", vim.g.mapleader) + end) + end) + + describe("nvim API", function() + it("should have working buffer API", function() + local buf = vim.api.nvim_get_current_buf() + assert.is_number(buf) + assert.is_true(vim.api.nvim_buf_is_valid(buf)) + end) + + it("should have working window API", function() + local win = vim.api.nvim_get_current_win() + assert.is_number(win) + assert.is_true(vim.api.nvim_win_is_valid(win)) + end) + + it("should be able to create and manipulate buffers", function() + -- Create a new buffer + local buf = vim.api.nvim_create_buf(false, true) + assert.is_true(vim.api.nvim_buf_is_valid(buf)) + + -- Set lines + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "hello", "world" }) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + assert.are.same({ "hello", "world" }, lines) + + -- Clean up + vim.api.nvim_buf_delete(buf, { force = true }) + end) + + it("should handle feedkeys for keymap testing", function() + -- Create a scratch buffer for testing + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(buf) + + -- Enter insert mode and type + vim.api.nvim_feedkeys("itest", "x", false) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "x", false) + + local line = vim.api.nvim_get_current_line() + assert.equals("test", line) + + -- Clean up + vim.api.nvim_buf_delete(buf, { force = true }) + end) + end) +end) diff --git a/home-manager/programs/neovim/tests/minimal_init.lua b/home-manager/programs/neovim/tests/minimal_init.lua new file mode 100644 index 000000000..5da0e0232 --- /dev/null +++ b/home-manager/programs/neovim/tests/minimal_init.lua @@ -0,0 +1,49 @@ +-- Minimal init.lua for Neovim testing +-- This provides a clean environment isolated from the full config + +-- Set leader key (required by some tests) +vim.g.mapleader = " " +vim.g.maplocalleader = " " + +-- Get the directory of this init file +local init_path = debug.getinfo(1, "S").source:sub(2) +local tests_dir = vim.fn.fnamemodify(init_path, ":h") +local nvim_dir = vim.fn.fnamemodify(tests_dir, ":h") + +-- Add lua module paths +package.path = package.path .. ";" .. nvim_dir .. "/lua/?.lua" +package.path = package.path .. ";" .. nvim_dir .. "/lua/?/init.lua" + +-- Set up runtimepath to include plenary +vim.opt.runtimepath:append(".") +vim.opt.runtimepath:append(nvim_dir) + +-- Add plenary to runtimepath if installed via vim.pack or available +local plenary_paths = { + vim.fn.stdpath("data") .. "/site/pack/plugins/start/plenary.nvim", + vim.fn.stdpath("data") .. "/site/pack/plugins/opt/plenary.nvim", + vim.fn.expand("~/.local/share/nvim/site/pack/plugins/start/plenary.nvim"), + vim.fn.expand("~/.local/share/nvim/site/pack/plugins/opt/plenary.nvim"), + -- CI/test environment paths + "/tmp/plenary.nvim", + tests_dir .. "/plenary.nvim", +} + +for _, path in ipairs(plenary_paths) do + if vim.fn.isdirectory(path) == 1 then + vim.opt.runtimepath:append(path) + break + end +end + +-- Basic settings for testing +vim.opt.swapfile = false +vim.opt.backup = false +vim.opt.writebackup = false +vim.opt.undofile = false + +-- Disable shada (shared data) to avoid file conflicts in parallel tests +vim.opt.shadafile = "NONE" + +-- Set a reasonable timeout for async operations +vim.opt.updatetime = 100 diff --git a/home-manager/programs/neovim/tests/run_tests.sh b/home-manager/programs/neovim/tests/run_tests.sh new file mode 100755 index 000000000..f45daae96 --- /dev/null +++ b/home-manager/programs/neovim/tests/run_tests.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Neovim Test Runner +# Runs plenary-based tests for the Neovim configuration + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NVIM_DIR="$(dirname "$SCRIPT_DIR")" +PLENARY_DIR="${PLENARY_DIR:-/tmp/plenary.nvim}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Running Neovim tests...${NC}" + +# Check if nvim is available +if ! command -v nvim &>/dev/null; then + echo -e "${RED}Error: Neovim is not installed or not in PATH${NC}" + exit 1 +fi + +# Ensure plenary.nvim is available +if [ ! -d "$PLENARY_DIR" ]; then + echo -e "${YELLOW}Cloning plenary.nvim to $PLENARY_DIR...${NC}" + git clone --depth 1 https://github.com/nvim-lua/plenary.nvim "$PLENARY_DIR" +fi + +# Export plenary directory for minimal_init.lua +export PLENARY_DIR + +# Change to the nvim config directory +cd "$NVIM_DIR" + +# Run tests +echo -e "${YELLOW}Executing tests...${NC}" +nvim --headless \ + -u tests/minimal_init.lua \ + -c "lua require('plenary.test_harness').test_directory('tests/', { minimal_init = 'tests/minimal_init.lua', sequential = true })" + +EXIT_CODE=$? + +if [ $EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" +else + echo -e "${RED}Tests failed with exit code: $EXIT_CODE${NC}" +fi + +exit $EXIT_CODE diff --git a/home-manager/programs/neovim/tests/utils_spec.lua b/home-manager/programs/neovim/tests/utils_spec.lua new file mode 100644 index 000000000..13c882907 --- /dev/null +++ b/home-manager/programs/neovim/tests/utils_spec.lua @@ -0,0 +1,109 @@ +-- Tests for lua/utils.lua +-- Run with: nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/ { minimal_init = './tests/minimal_init.lua' }" + +describe("utils", function() + local utils + + before_each(function() + -- Clear any cached module to get fresh state + package.loaded["utils"] = nil + utils = require("utils") + end) + + describe("cycle_buffer", function() + it("should be a function", function() + assert.is_function(utils.cycle_buffer) + end) + + it("should not error with no buffers", function() + -- With only one buffer (current), should not error + assert.has_no.errors(function() + utils.cycle_buffer("next") + end) + end) + + it("should not error when cycling previous", function() + assert.has_no.errors(function() + utils.cycle_buffer("prev") + end) + end) + + it("should handle multiple buffers", function() + -- Create a second buffer + local buf1 = vim.api.nvim_get_current_buf() + vim.cmd("enew") + local buf2 = vim.api.nvim_get_current_buf() + + -- Should be able to cycle between them + assert.has_no.errors(function() + utils.cycle_buffer("next") + end) + + -- Clean up + vim.api.nvim_buf_delete(buf2, { force = true }) + end) + end) + + describe("copen", function() + it("should be a function", function() + assert.is_function(utils.copen) + end) + + it("should handle empty quickfix list", function() + -- Clear quickfix list first + vim.fn.setqflist({}, "r") + + assert.has_no.errors(function() + utils.copen() + end) + end) + + it("should open quickfix window when items exist", function() + -- Add items to quickfix list + vim.fn.setqflist({ + { filename = "test.lua", lnum = 1, text = "Test item 1" }, + { filename = "test.lua", lnum = 2, text = "Test item 2" }, + }, "r") + + utils.copen() + + -- Check if quickfix window is open + local qf_open = false + for _, win in ipairs(vim.api.nvim_list_wins()) do + local buf = vim.api.nvim_win_get_buf(win) + if vim.bo[buf].buftype == "quickfix" then + qf_open = true + break + end + end + + assert.is_true(qf_open) + + -- Clean up + vim.cmd("cclose") + vim.fn.setqflist({}, "r") + end) + end) + + describe("cclear", function() + it("should be a function", function() + assert.is_function(utils.cclear) + end) + + it("should clear the quickfix list", function() + -- Add items to quickfix list + vim.fn.setqflist({ + { filename = "test.lua", lnum = 1, text = "Test item" }, + }, "r") + + -- Verify it's not empty + assert.is_true(vim.fn.getqflist({ size = 0 }).size > 0) + + -- Clear it + utils.cclear() + + -- Verify it's empty + assert.equals(0, vim.fn.getqflist({ size = 0 }).size) + end) + end) +end)