From e14eb002d5839387a9b58e9a7ebc0a2047d1c1e5 Mon Sep 17 00:00:00 2001 From: yetone Date: Fri, 10 Jan 2025 00:23:59 +0800 Subject: [PATCH] feat: add luatest (#1064) --- .github/workflows/lua.yaml | 38 +++++++++- Makefile | 15 +++- lua/avante/utils/init.lua | 8 ++- tests/utils/file_spec.lua | 141 +++++++++++++++++++++++++++++++++++++ tests/utils/init_spec.lua | 123 ++++++++++++++++++++++++++++++++ 5 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 tests/utils/file_spec.lua create mode 100644 tests/utils/init_spec.lua diff --git a/.github/workflows/lua.yaml b/.github/workflows/lua.yaml index 38d314dd7..a1f97b2bd 100644 --- a/.github/workflows/lua.yaml +++ b/.github/workflows/lua.yaml @@ -13,6 +13,41 @@ on: - "**/*.lua" jobs: + # reference from: https://github.com/nvim-lua/plenary.nvim/blob/2d9b06177a975543726ce5c73fca176cedbffe9d/.github/workflows/default.yml#L6C3-L43C20 + run_tests: + name: unit tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + rev: v0.10.0/nvim-linux64.tar.gz + steps: + - uses: actions/checkout@v3 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v3 + with: + path: _neovim + key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }} + + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + + - name: Run tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + # install nvim-lua/plenary.nvim + git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim + nvim --version + make luatest + stylua: name: Check Lua style runs-on: ubuntu-latest @@ -24,7 +59,8 @@ jobs: crate: stylua features: lua54 - run: stylua --version - - run: stylua --check ./lua/ ./plugin/ + - run: stylua --color always --check ./lua/ ./plugin/ ./tests/ + luacheck: name: Lint Lua runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index b80436482..cc429a76f 100644 --- a/Makefile +++ b/Makefile @@ -66,8 +66,8 @@ clean: luacheck: @luacheck `find -name "*.lua"` --codes -stylecheck: - @stylua --check lua/ plugin/ +luastylecheck: + @stylua --check lua/ plugin/ tests/ stylefix: @stylua lua/ plugin/ @@ -81,3 +81,14 @@ ruststylecheck: rustlint: @rustup component add clippy 2> /dev/null @cargo clippy -F luajit --all -- -F clippy::dbg-macro -D warnings + +.PHONY: rusttest +rusttest: + @cargo test --features luajit + +.PHONY: luatest +luatest: + nvim --headless -c "PlenaryBustedDirectory tests/" + +.PHONY: lint +lint: luacheck luastylecheck ruststylecheck rustlint diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index b709f39f2..92b8be237 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -153,12 +153,12 @@ end ---@param str string ---@param opts? {suffix?: string, prefix?: string} function M.trim(str, opts) - if not opts then return str end local res = str + if not opts then return res end if opts.suffix then - res = str:sub(#str - #opts.suffix + 1) == opts.suffix and str:sub(1, #str - #opts.suffix) or str + res = res:sub(#res - #opts.suffix + 1) == opts.suffix and res:sub(1, #res - #opts.suffix) or res end - if opts.prefix then res = str:sub(1, #opts.prefix) == opts.prefix and str:sub(#opts.prefix + 1) or str end + if opts.prefix then res = res:sub(1, #opts.prefix) == opts.prefix and res:sub(#opts.prefix + 1) or res end return res end @@ -463,6 +463,8 @@ function M.url_join(...) ::continue:: end + if result:sub(-1) == "/" then result = result:sub(1, -2) end + return result end diff --git a/tests/utils/file_spec.lua b/tests/utils/file_spec.lua new file mode 100644 index 000000000..264ea46d0 --- /dev/null +++ b/tests/utils/file_spec.lua @@ -0,0 +1,141 @@ +local File = require("avante.utils.file") +local mock = require("luassert.mock") +local stub = require("luassert.stub") + +describe("File", function() + local test_file = "test.txt" + local test_content = "test content\nline 2" + + -- Mock vim API + local api_mock + local loop_mock + + before_each(function() + -- Setup mocks + api_mock = mock(vim.api, true) + loop_mock = mock(vim.loop, true) + end) + + after_each(function() + -- Clean up mocks + mock.revert(api_mock) + mock.revert(loop_mock) + end) + + describe("read_content", function() + it("should read file content", function() + vim.fn.readfile = stub().returns({ "test content", "line 2" }) + + local content = File.read_content(test_file) + assert.equals(test_content, content) + assert.stub(vim.fn.readfile).was_called_with(test_file) + end) + + it("should return nil for non-existent file", function() + vim.fn.readfile = stub().returns(nil) + + local content = File.read_content("nonexistent.txt") + assert.is_nil(content) + end) + + it("should use cache for subsequent reads", function() + vim.fn.readfile = stub().returns({ "test content", "line 2" }) + local new_test_file = "test1.txt" + + -- First read + local content1 = File.read_content(new_test_file) + assert.equals(test_content, content1) + + -- Second read (should use cache) + local content2 = File.read_content(new_test_file) + assert.equals(test_content, content2) + + -- readfile should only be called once + assert.stub(vim.fn.readfile).was_called(1) + end) + end) + + describe("exists", function() + it("should return true for existing file", function() + loop_mock.fs_stat.returns({ type = "file" }) + + assert.is_true(File.exists(test_file)) + assert.stub(loop_mock.fs_stat).was_called_with(test_file) + end) + + it("should return false for non-existent file", function() + loop_mock.fs_stat.returns(nil) + + assert.is_false(File.exists("nonexistent.txt")) + end) + end) + + describe("get_file_icon", function() + local Filetype + local devicons_mock + + before_each(function() + -- Mock plenary.filetype + Filetype = mock(require("plenary.filetype"), true) + -- Prepare devicons mock + devicons_mock = { + get_icon = stub().returns(""), + } + -- Reset _G.MiniIcons + _G.MiniIcons = nil + end) + + after_each(function() mock.revert(Filetype) end) + + it("should get icon using nvim-web-devicons", function() + Filetype.detect.returns("lua") + devicons_mock.get_icon.returns("") + + -- Mock require for nvim-web-devicons + local old_require = _G.require + _G.require = function(module) + if module == "nvim-web-devicons" then return devicons_mock end + return old_require(module) + end + + local icon = File.get_file_icon("test.lua") + assert.equals("", icon) + assert.stub(Filetype.detect).was_called_with("test.lua", {}) + assert.stub(devicons_mock.get_icon).was_called() + + _G.require = old_require + end) + + it("should get icon using MiniIcons if available", function() + _G.MiniIcons = { + get = stub().returns("", "color", "name"), + } + + Filetype.detect.returns("lua") + + local icon = File.get_file_icon("test.lua") + assert.equals("", icon) + assert.stub(Filetype.detect).was_called_with("test.lua", {}) + assert.stub(_G.MiniIcons.get).was_called_with("filetype", "lua") + + _G.MiniIcons = nil + end) + + it("should handle unknown filetypes", function() + Filetype.detect.returns(nil) + devicons_mock.get_icon.returns("") + + -- Mock require for nvim-web-devicons + local old_require = _G.require + _G.require = function(module) + if module == "nvim-web-devicons" then return devicons_mock end + return old_require(module) + end + + local icon = File.get_file_icon("unknown.xyz") + assert.equals("", icon) + + _G.require = old_require + end) + end) +end) diff --git a/tests/utils/init_spec.lua b/tests/utils/init_spec.lua new file mode 100644 index 000000000..6863e4a54 --- /dev/null +++ b/tests/utils/init_spec.lua @@ -0,0 +1,123 @@ +local Utils = require("avante.utils") + +describe("Utils", function() + describe("trim", function() + it("should trim prefix", function() assert.equals("test", Utils.trim("prefix_test", { prefix = "prefix_" })) end) + + it("should trim suffix", function() assert.equals("test", Utils.trim("test_suffix", { suffix = "_suffix" })) end) + + it( + "should trim both prefix and suffix", + function() assert.equals("test", Utils.trim("prefix_test_suffix", { prefix = "prefix_", suffix = "_suffix" })) end + ) + + it( + "should return original string if no match", + function() assert.equals("test", Utils.trim("test", { prefix = "xxx", suffix = "yyy" })) end + ) + end) + + describe("url_join", function() + it("should join url parts correctly", function() + assert.equals("http://example.com/path", Utils.url_join("http://example.com", "path")) + assert.equals("http://example.com/path", Utils.url_join("http://example.com/", "/path")) + assert.equals("http://example.com/path/to", Utils.url_join("http://example.com", "path", "to")) + assert.equals("http://example.com/path", Utils.url_join("http://example.com/", "/path/")) + end) + + it("should handle empty parts", function() + assert.equals("http://example.com", Utils.url_join("http://example.com", "")) + assert.equals("http://example.com", Utils.url_join("http://example.com", nil)) + end) + end) + + describe("is_type", function() + it("should check basic types correctly", function() + assert.is_true(Utils.is_type("string", "test")) + assert.is_true(Utils.is_type("number", 123)) + assert.is_true(Utils.is_type("boolean", true)) + assert.is_true(Utils.is_type("table", {})) + assert.is_true(Utils.is_type("function", function() end)) + assert.is_true(Utils.is_type("nil", nil)) + end) + + it("should check list type correctly", function() + assert.is_true(Utils.is_type("list", { 1, 2, 3 })) + assert.is_false(Utils.is_type("list", { a = 1, b = 2 })) + end) + + it("should check map type correctly", function() + assert.is_true(Utils.is_type("map", { a = 1, b = 2 })) + assert.is_false(Utils.is_type("map", { 1, 2, 3 })) + end) + end) + + describe("get_indentation", function() + it("should get correct indentation", function() + assert.equals(" ", Utils.get_indentation(" test")) + assert.equals("\t", Utils.get_indentation("\ttest")) + assert.equals("", Utils.get_indentation("test")) + end) + + it("should handle empty or nil input", function() + assert.equals("", Utils.get_indentation("")) + assert.equals("", Utils.get_indentation(nil)) + end) + end) + + describe("remove_indentation", function() + it("should remove indentation correctly", function() + assert.equals("test", Utils.remove_indentation(" test")) + assert.equals("test", Utils.remove_indentation("\ttest")) + assert.equals("test", Utils.remove_indentation("test")) + end) + + it("should handle empty or nil input", function() + assert.equals("", Utils.remove_indentation("")) + assert.equals(nil, Utils.remove_indentation(nil)) + end) + end) + + describe("is_first_letter_uppercase", function() + it("should detect uppercase first letter", function() + assert.is_true(Utils.is_first_letter_uppercase("Test")) + assert.is_true(Utils.is_first_letter_uppercase("ABC")) + end) + + it("should detect lowercase first letter", function() + assert.is_false(Utils.is_first_letter_uppercase("test")) + assert.is_false(Utils.is_first_letter_uppercase("abc")) + end) + end) + + describe("extract_mentions", function() + it("should extract @codebase mention", function() + local result = Utils.extract_mentions("test @codebase") + assert.equals("test ", result.new_content) + assert.is_true(result.enable_project_context) + assert.is_false(result.enable_diagnostics) + end) + + it("should extract @diagnostics mention", function() + local result = Utils.extract_mentions("test @diagnostics") + assert.equals("test @diagnostics", result.new_content) + assert.is_false(result.enable_project_context) + assert.is_true(result.enable_diagnostics) + end) + + it("should handle multiple mentions", function() + local result = Utils.extract_mentions("test @codebase @diagnostics") + assert.equals("test @diagnostics", result.new_content) + assert.is_true(result.enable_project_context) + assert.is_true(result.enable_diagnostics) + end) + end) + + describe("get_mentions", function() + it("should return valid mentions", function() + local mentions = Utils.get_mentions() + assert.equals("codebase", mentions[1].command) + assert.equals("diagnostics", mentions[2].command) + end) + end) +end)