diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b0c0db2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test on Neovim ${{ matrix.neovim }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + neovim: ['v0.9.5', 'v0.10.0', 'stable', 'nightly'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: ${{ matrix.neovim }} + + - name: Run tests + run: | + nvim --version + nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal_init.lua'}" + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check formatting with stylua + uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --check lua/ tests/ diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..b1aeb38 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Tabs" +indent_width = 4 +quote_style = "AutoPreferDouble" +call_parentheses = "Always" diff --git a/tests/auto_activate_venv_spec.lua b/tests/auto_activate_venv_spec.lua index e795cd8..b8554e6 100644 --- a/tests/auto_activate_venv_spec.lua +++ b/tests/auto_activate_venv_spec.lua @@ -1,63 +1,51 @@ --- Tests for granular auto_activate_venv setting --- Run with: nvim --headless -u tests/minimal_init.lua -c "luafile tests/auto_activate_venv_spec.lua" -c "qa!" - local uv = require("uv") -local function assert_eq(expected, actual, message) - if expected ~= actual then - error(string.format("%s: expected %s, got %s", message or "Assertion failed", vim.inspect(expected), vim.inspect(actual))) - end - print(string.format("PASS: %s", message or "assertion")) -end - local function reset_state() vim.g.uv_auto_activate_venv = nil vim.b.uv_auto_activate_venv = nil uv.config.auto_activate_venv = true end -print("\n=== Testing auto_activate_venv setting ===\n") - --- Test 1: is_auto_activate_enabled() respects global vim variable -print("Test 1: Global vim variable") -reset_state() -uv.setup({ auto_activate_venv = true }) -assert_eq(true, uv.is_auto_activate_enabled(), "Default from config") - -vim.g.uv_auto_activate_venv = false -assert_eq(false, uv.is_auto_activate_enabled(), "Global set to false") -reset_state() - --- Test 2: Buffer-local takes precedence over global -print("\nTest 2: Buffer-local precedence") -reset_state() -vim.g.uv_auto_activate_venv = true -vim.b.uv_auto_activate_venv = false -assert_eq(false, uv.is_auto_activate_enabled(), "Buffer false overrides global true") -reset_state() - --- Test 3: toggle_auto_activate_venv() works -print("\nTest 3: Toggle function") -reset_state() -uv.setup({ auto_activate_venv = true }) -assert_eq(true, uv.is_auto_activate_enabled(), "Initial true") - -uv.toggle_auto_activate_venv() -assert_eq(false, uv.is_auto_activate_enabled(), "After toggle: false") - -uv.toggle_auto_activate_venv() -assert_eq(true, uv.is_auto_activate_enabled(), "After second toggle: true") -reset_state() - --- Test 4: Buffer-local toggle -print("\nTest 4: Buffer-local toggle") -reset_state() -vim.g.uv_auto_activate_venv = true - -uv.toggle_auto_activate_venv(true) -assert_eq(false, uv.is_auto_activate_enabled(), "Buffer toggle to false") -assert_eq(false, vim.b.uv_auto_activate_venv, "Buffer var is false") -assert_eq(true, vim.g.uv_auto_activate_venv, "Global unchanged") -reset_state() - -print("\n=== All tests passed! ===\n") +describe("auto_activate_venv setting", function() + after_each(function() + reset_state() + end) + + it("respects global vim variable", function() + reset_state() + uv.setup({ auto_activate_venv = true }) + assert.is_true(uv.is_auto_activate_enabled()) + + vim.g.uv_auto_activate_venv = false + assert.is_false(uv.is_auto_activate_enabled()) + end) + + it("buffer-local takes precedence over global", function() + reset_state() + vim.g.uv_auto_activate_venv = true + vim.b.uv_auto_activate_venv = false + assert.is_false(uv.is_auto_activate_enabled()) + end) + + it("toggle_auto_activate_venv works", function() + reset_state() + uv.setup({ auto_activate_venv = true }) + assert.is_true(uv.is_auto_activate_enabled()) + + uv.toggle_auto_activate_venv() + assert.is_false(uv.is_auto_activate_enabled()) + + uv.toggle_auto_activate_venv() + assert.is_true(uv.is_auto_activate_enabled()) + end) + + it("buffer-local toggle works", function() + reset_state() + vim.g.uv_auto_activate_venv = true + + uv.toggle_auto_activate_venv(true) + assert.is_false(uv.is_auto_activate_enabled()) + assert.is_false(vim.b.uv_auto_activate_venv) + assert.is_true(vim.g.uv_auto_activate_venv) + end) +end) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 4c0b2c5..e1ff9a6 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,6 +1,11 @@ --- Minimal init for running tests --- Add the plugin to the runtime path -vim.opt.rtp:prepend(".") +-- Minimal init.lua for running plenary tests +vim.opt.runtimepath:prepend(vim.fn.getcwd()) --- Load the plugin -require("uv") +local plenary_path = vim.fn.stdpath("data") .. "/site/pack/test/start/plenary.nvim" +if vim.fn.isdirectory(plenary_path) == 0 then + vim.fn.system({ "git", "clone", "https://github.com/nvim-lua/plenary.nvim", plenary_path }) +end +vim.opt.runtimepath:append(plenary_path) + +vim.g.mapleader = " " +vim.opt.swapfile = false diff --git a/tests/remove_package_spec.lua b/tests/remove_package_spec.lua index dffd2f4..e695955 100644 --- a/tests/remove_package_spec.lua +++ b/tests/remove_package_spec.lua @@ -1,79 +1,18 @@ --- Tests for remove_package function (PR #21) --- Run with: nvim --headless -u NONE -c "lua dofile('tests/remove_package_spec.lua')" -c "qa!" - --- Tests the public contract only - no mocking internals - -assert(vim and vim.fn, "This test must be run in Neovim") - --- Minimal test framework -local tests_passed = 0 -local tests_failed = 0 - -local function describe(name, fn) - print("\n=== " .. name .. " ===") - fn() -end - -local function it(name, fn) - local ok, err = pcall(fn) - if ok then - tests_passed = tests_passed + 1 - print(" ✓ " .. name) - else - tests_failed = tests_failed + 1 - print(" ✗ " .. name) - print(" Error: " .. tostring(err)) - end -end - -local function assert_equal(expected, actual, msg) - if expected ~= actual then - error((msg or "Assertion failed") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual)) - end -end - -local function assert_true(value, msg) - if not value then - error((msg or "Assertion failed") .. ": expected true, got " .. tostring(value)) - end -end - --- Load the module -package.path = package.path .. ";./lua/?.lua;./lua/?/init.lua" local uv = require("uv") -describe("remove_package()", function() - it("should be exported as a function", function() - assert_equal("function", type(uv.remove_package), "remove_package should be a function") - end) -end) - -describe("keymap setup", function() - it("should set up 'd' keymap for remove package when keymaps enabled", function() +describe("remove_package", function() + it("sets up keymap when enabled", function() uv.setup({ keymaps = { prefix = "u", remove_package = true } }) local keymaps = vim.api.nvim_get_keymap("n") local found = false for _, km in ipairs(keymaps) do - -- Check for keymap ending in 'd' with UV Remove Package description if km.desc == "UV Remove Package" then found = true - assert_true( - km.rhs:match("remove_package") or km.callback ~= nil, - "keymap should invoke remove_package" - ) + assert.is_truthy(km.rhs:match("remove_package") or km.callback) break end end - assert_true(found, "should have remove_package keymap defined") + assert.is_true(found, "should have remove_package keymap defined") end) end) - --- Print summary -print("\n" .. string.rep("=", 40)) -print(string.format("Tests: %d passed, %d failed", tests_passed, tests_failed)) -print(string.rep("=", 40)) - -if tests_failed > 0 then - os.exit(1) -end diff --git a/tests/statusline_spec.lua b/tests/statusline_spec.lua index a23c326..6893e89 100644 --- a/tests/statusline_spec.lua +++ b/tests/statusline_spec.lua @@ -1,66 +1,9 @@ --- Tests for statusline helper functions --- Run with: nvim --headless -u NONE -c "lua dofile('tests/statusline_spec.lua')" -c "qa!" - --- Ensure we're running in Neovim -assert(vim and vim.fn, "This test must be run in Neovim") - --- Minimal test framework -local tests_passed = 0 -local tests_failed = 0 - -local function describe(name, fn) - print("\n=== " .. name .. " ===") - fn() -end - -local function it(name, fn) - local ok, err = pcall(fn) - if ok then - tests_passed = tests_passed + 1 - print(" ✓ " .. name) - else - tests_failed = tests_failed + 1 - print(" ✗ " .. name) - print(" Error: " .. tostring(err)) - end -end - -local function assert_equal(expected, actual, msg) - if expected ~= actual then - error((msg or "Assertion failed") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual)) - end -end - -local function assert_true(value, msg) - if not value then - error((msg or "Assertion failed") .. ": expected true, got " .. tostring(value)) - end -end - -local function assert_false(value, msg) - if value then - error((msg or "Assertion failed") .. ": expected false, got " .. tostring(value)) - end -end - -local function assert_nil(value, msg) - if value ~= nil then - error((msg or "Assertion failed") .. ": expected nil, got " .. tostring(value)) - end -end - --- Store original VIRTUAL_ENV -local original_venv = vim.env.VIRTUAL_ENV - --- Load the module -package.path = package.path .. ";./lua/?.lua;./lua/?/init.lua" local uv = require("uv") --- Test directory for creating real files +local original_venv = vim.env.VIRTUAL_ENV local test_dir = vim.fn.tempname() vim.fn.mkdir(test_dir, "p") --- Helper to create a test venv with pyvenv.cfg local function create_test_venv(venv_name, prompt) local venv_dir = test_dir .. "/" .. venv_name vim.fn.mkdir(venv_dir, "p") @@ -77,69 +20,70 @@ local function create_test_venv(venv_name, prompt) return venv_dir end --- Run tests describe("is_venv_active()", function() - it("should return false when no venv is active", function() + after_each(function() + vim.env.VIRTUAL_ENV = original_venv + end) + + it("returns false when no venv is active", function() vim.env.VIRTUAL_ENV = nil - assert_false(uv.is_venv_active(), "is_venv_active should be false when VIRTUAL_ENV is nil") + assert.is_false(uv.is_venv_active()) end) - it("should return true when a venv is active", function() + it("returns true when a venv is active", function() vim.env.VIRTUAL_ENV = test_dir .. "/some-project/.venv" - assert_true(uv.is_venv_active(), "is_venv_active should be true when VIRTUAL_ENV is set") + assert.is_true(uv.is_venv_active()) end) end) describe("get_venv()", function() - it("should return nil when no venv is active", function() + after_each(function() + vim.env.VIRTUAL_ENV = original_venv + end) + + it("returns nil when no venv is active", function() vim.env.VIRTUAL_ENV = nil - assert_nil(uv.get_venv(), "get_venv should return nil when VIRTUAL_ENV is nil") + assert.is_nil(uv.get_venv()) end) - it("should return prompt from pyvenv.cfg", function() + it("returns prompt from pyvenv.cfg", function() local venv_path = create_test_venv("test-venv", "my-awesome-project") vim.env.VIRTUAL_ENV = venv_path - assert_equal("my-awesome-project", uv.get_venv(), "get_venv should return prompt from pyvenv.cfg") + assert.are.equal("my-awesome-project", uv.get_venv()) end) - it("should return venv folder name when no prompt in pyvenv.cfg", function() + it("returns venv folder name when no prompt in pyvenv.cfg", function() local venv_path = create_test_venv("custom-env", nil) vim.env.VIRTUAL_ENV = venv_path - assert_equal("custom-env", uv.get_venv(), "get_venv should return venv folder name when no prompt") + assert.are.equal("custom-env", uv.get_venv()) end) - it("should return venv folder name when no pyvenv.cfg exists", function() + it("returns venv folder name when no pyvenv.cfg exists", function() local venv_dir = test_dir .. "/no-cfg" vim.fn.mkdir(venv_dir, "p") vim.env.VIRTUAL_ENV = venv_dir - assert_equal("no-cfg", uv.get_venv(), "get_venv should return venv folder name as fallback") + assert.are.equal("no-cfg", uv.get_venv()) end) end) describe("get_venv_path()", function() - it("should return nil when no venv is active", function() + after_each(function() + vim.env.VIRTUAL_ENV = original_venv + end) + + it("returns nil when no venv is active", function() vim.env.VIRTUAL_ENV = nil - assert_nil(uv.get_venv_path(), "get_venv_path should return nil when VIRTUAL_ENV is nil") + assert.is_nil(uv.get_venv_path()) end) - it("should return the full venv path when active", function() + it("returns the full venv path when active", function() local expected_path = test_dir .. "/test-project/.venv" vim.fn.mkdir(expected_path, "p") vim.env.VIRTUAL_ENV = expected_path - assert_equal(expected_path, uv.get_venv_path(), "get_venv_path should return full path") + assert.are.equal(expected_path, uv.get_venv_path()) end) end) --- Cleanup +-- Cleanup at end vim.env.VIRTUAL_ENV = original_venv vim.fn.delete(test_dir, "rf") - --- Print summary -print("\n" .. string.rep("=", 40)) -print(string.format("Tests: %d passed, %d failed", tests_passed, tests_failed)) -print(string.rep("=", 40)) - --- Exit with appropriate code -if tests_failed > 0 then - os.exit(1) -end diff --git a/tests/uv_spec.lua b/tests/uv_spec.lua new file mode 100644 index 0000000..5b9c7f9 --- /dev/null +++ b/tests/uv_spec.lua @@ -0,0 +1,124 @@ +local uv = require("uv") + +local original_path = vim.env.PATH +local original_venv = vim.env.VIRTUAL_ENV +local original_cwd = vim.fn.getcwd() + +local function reset_env() + vim.env.PATH = original_path + vim.env.VIRTUAL_ENV = original_venv + vim.cmd("cd " .. vim.fn.fnameescape(original_cwd)) +end + +local function fresh_uv() + package.loaded["uv"] = nil + return require("uv") +end + +describe("uv.nvim", function() + after_each(function() + reset_env() + end) + + describe("configuration", function() + it("has correct defaults", function() + local m = fresh_uv() + assert.is_true(m.config.auto_activate_venv) + assert.are.equal("x", m.config.keymaps.prefix) + assert.are.equal("uv run python", m.config.execution.run_command) + assert.are.equal("split", m.config.execution.terminal) + end) + + it("merges custom config", function() + local m = fresh_uv() + m.setup({ auto_activate_venv = false, auto_commands = false, keymaps = false, picker_integration = false }) + assert.is_false(m.config.auto_activate_venv) + assert.is_true(m.config.notify_activate_venv) + end) + + it("accepts custom execution config", function() + local m = fresh_uv() + m.setup({ + execution = { run_command = "python3", terminal = "vsplit" }, + auto_commands = false, + keymaps = false, + picker_integration = false, + }) + assert.are.equal("python3", m.config.execution.run_command) + assert.are.equal("vsplit", m.config.execution.terminal) + end) + end) + + describe("commands", function() + it("registers user commands", function() + local m = fresh_uv() + m.setup({ auto_commands = false, keymaps = false, picker_integration = false }) + local cmds = vim.api.nvim_get_commands({}) + assert.is_not_nil(cmds.UVInit) + assert.is_not_nil(cmds.UVRunFile) + assert.is_not_nil(cmds.UVRunSelection) + assert.is_not_nil(cmds.UVRunFunction) + end) + end) + + describe("activate_venv", function() + it("sets VIRTUAL_ENV", function() + local m = fresh_uv() + m.config.notify_activate_venv = false + local test_path = vim.fn.tempname() + vim.fn.mkdir(test_path .. "/bin", "p") + + m.activate_venv(test_path) + assert.are.equal(test_path, vim.env.VIRTUAL_ENV) + + vim.fn.delete(test_path, "rf") + end) + + it("prepends to PATH", function() + local m = fresh_uv() + m.config.notify_activate_venv = false + local test_path = vim.fn.tempname() + vim.fn.mkdir(test_path .. "/bin", "p") + + m.activate_venv(test_path) + assert.are.equal(1, vim.env.PATH:find(test_path .. "/bin", 1, true)) + + vim.fn.delete(test_path, "rf") + end) + end) + + describe("auto_activate_venv", function() + it("returns false when no .venv exists", function() + local m = fresh_uv() + m.config.notify_activate_venv = false + local temp_dir = vim.fn.tempname() + vim.fn.mkdir(temp_dir, "p") + vim.cmd("cd " .. vim.fn.fnameescape(temp_dir)) + + assert.is_false(m.auto_activate_venv()) + + vim.fn.delete(temp_dir, "rf") + end) + + it("returns true when .venv exists", function() + local m = fresh_uv() + m.config.notify_activate_venv = false + local temp_dir = vim.fn.tempname() + vim.fn.mkdir(temp_dir .. "/.venv/bin", "p") + vim.cmd("cd " .. vim.fn.fnameescape(temp_dir)) + + assert.is_true(m.auto_activate_venv()) + + vim.fn.delete(temp_dir, "rf") + end) + end) + + describe("setup", function() + it("exposes run_command globally", function() + local m = fresh_uv() + _G.run_command = nil + m.setup({ auto_commands = false, keymaps = false, picker_integration = false }) + assert.are.equal("function", type(_G.run_command)) + end) + end) +end)