From 2c0d1cbd4da6e9fa7383c8c621bc2f5c3d5d3781 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 15 Jun 2026 15:28:46 +0000 Subject: [PATCH] fix: speed up coverage test runtime --- .github/workflows/test.yml | 2 +- lua/claudecode/config.lua | 18 +++++------ lua/claudecode/server/tcp.lua | 23 +++++++------- lua/claudecode/server/utils.lua | 33 ++++--------------- mise.toml | 4 +-- tests/unit/config_spec.lua | 10 ++++++ tests/unit/server/tcp_spec.lua | 56 +++++++++++++++++++++++++++++++++ tests/unit/terminal_spec.lua | 1 + 8 files changed, 96 insertions(+), 51 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8375d2d..cc57e9a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: - name: Generate coverage report run: | if [ -f "luacov.stats.out" ]; then - mise exec -- luacov + ./.luarocks/bin/luacov echo "Creating lcov.info from luacov.report.out" { diff --git a/lua/claudecode/config.lua b/lua/claudecode/config.lua index c77577be..43824ece 100644 --- a/lua/claudecode/config.lua +++ b/lua/claudecode/config.lua @@ -39,7 +39,15 @@ M.defaults = { { name = "Claude Haiku (Latest)", value = "haiku" }, { name = "Default (account recommended)", value = "default" }, }, - terminal = nil, -- Will be lazy-loaded to avoid circular dependency + -- Keep a minimal terminal config here instead of requiring claudecode.terminal + -- during config.apply(). Loading the terminal module pulls in the server/main + -- module graph and makes coverage-enabled config validation unexpectedly slow. + terminal = { + provider = "auto", + provider_opts = { + external_terminal_cmd = nil, + }, + }, } ---Validates the provided configuration table. @@ -191,14 +199,6 @@ end function M.apply(user_config) local config = vim.deepcopy(M.defaults) - -- Lazy-load terminal defaults to avoid circular dependency - if config.terminal == nil then - local terminal_ok, terminal_module = pcall(require, "claudecode.terminal") - if terminal_ok and terminal_module.defaults then - config.terminal = terminal_module.defaults - end - end - if user_config then -- Use vim.tbl_deep_extend if available, otherwise simple merge if vim.tbl_deep_extend then diff --git a/lua/claudecode/server/tcp.lua b/lua/claudecode/server/tcp.lua index 4aac69e6..695d02ef 100644 --- a/lua/claudecode/server/tcp.lua +++ b/lua/claudecode/server/tcp.lua @@ -1,6 +1,5 @@ ---@brief TCP server implementation using vim.loop local client_manager = require("claudecode.server.client") -local utils = require("claudecode.server.utils") local M = {} @@ -19,20 +18,21 @@ local M = {} ---@param max_port number Maximum port to try ---@return number|nil port Available port number, or nil if none found function M.find_available_port(min_port, max_port) - if min_port > max_port then - return nil -- Or handle error appropriately - end + assert(type(min_port) == "number", "min_port must be a number") + assert(type(max_port) == "number", "max_port must be a number") - local ports = {} - for i = min_port, max_port do - table.insert(ports, i) + if min_port > max_port then + return nil end - -- Shuffle the ports - utils.shuffle_array(ports) + local port_count = max_port - min_port + 1 + local start_offset = math.random(port_count) - 1 - -- Try to bind to a port from the shuffled list - for _, port in ipairs(ports) do + -- Pick a random starting point, then scan the range once. This keeps the + -- selection spread across the configured range without building and shuffling + -- a 55k-entry table for the default 10000-65535 range on every startup. + for checked = 0, port_count - 1 do + local port = min_port + ((start_offset + checked) % port_count) local test_server = vim.loop.new_tcp() if test_server then local success = test_server:bind("127.0.0.1", port) @@ -42,7 +42,6 @@ function M.find_available_port(min_port, max_port) return port end end - -- Continue to next port if test_server creation failed or bind failed end return nil diff --git a/lua/claudecode/server/utils.lua b/lua/claudecode/server/utils.lua index 9e53b325..6ae94302 100644 --- a/lua/claudecode/server/utils.lua +++ b/lua/claudecode/server/utils.lua @@ -44,6 +44,9 @@ local function bxor(a, b) return result end +local bit_ok, bit = pcall(require, "bit") +local native_bxor = bit_ok and bit and bit.bxor or nil + local function bnot(a) return bxor(a, 0xFFFFFFFF) end @@ -364,32 +367,6 @@ function M.bytes_to_uint64(bytes) return num end ----XOR lookup table for faster operations -local xor_table = {} -for i = 0, 255 do - xor_table[i] = {} - for j = 0, 255 do - local result = 0 - local a, b = i, j - local bit_val = 1 - - while a > 0 or b > 0 do - local a_bit = a % 2 - local b_bit = b % 2 - - if a_bit ~= b_bit then - result = result + bit_val - end - - a = math.floor(a / 2) - b = math.floor(b / 2) - bit_val = bit_val * 2 - end - - xor_table[i][j] = result - end -end - ---Apply XOR mask to payload data ---@param data string The data to mask/unmask ---@param mask string The 4-byte mask @@ -401,7 +378,9 @@ function M.apply_mask(data, mask) for i = 1, #data do local mask_idx = ((i - 1) % 4) + 1 local data_byte = data:byte(i) - result[i] = string.char(xor_table[data_byte][mask_bytes[mask_idx]]) + local mask_byte = mask_bytes[mask_idx] + local masked_byte = native_bxor and native_bxor(data_byte, mask_byte) or bxor(data_byte, mask_byte) + result[i] = string.char(masked_byte) end return table.concat(result) diff --git a/mise.toml b/mise.toml index b4e36f36..9311b007 100644 --- a/mise.toml +++ b/mise.toml @@ -103,7 +103,7 @@ echo "Checking Lua files for syntax errors..." # (macOS) too — `find -exec ... \;` swallows the child's status there. find lua -name "*.lua" -type f -print0 | xargs -0 -I{} luajit -e "assert(loadfile('{}'))" echo "Running luacheck..." -luacheck lua/ tests/ --no-unused-args --no-max-line-length +./.luarocks/bin/luacheck lua/ tests/ --no-unused-args --no-max-line-length ''' [tasks.test] @@ -115,7 +115,7 @@ TEST_FILES=$(find tests -type f -name "*_test.lua" -o -name "*_spec.lua" | sort) echo "Found test files:" echo "$TEST_FILES" if [ -n "$TEST_FILES" ]; then - busted --coverage -v $TEST_FILES + ./.luarocks/bin/busted --coverage -v $TEST_FILES else echo "No test files found" fi diff --git a/tests/unit/config_spec.lua b/tests/unit/config_spec.lua index 4d3c642f..e858beea 100644 --- a/tests/unit/config_spec.lua +++ b/tests/unit/config_spec.lua @@ -47,6 +47,16 @@ describe("Configuration", function() expect(final_config.env).to_be_table() -- Should inherit default empty table end) + it("should not load terminal module while applying configuration", function() + package.loaded["claudecode.terminal"] = nil + + local final_config = config.apply({ auto_start = false, log_level = "info" }) + + expect(final_config.terminal).to_be_table() + expect(final_config.terminal.provider).to_be("auto") + expect(package.loaded["claudecode.terminal"]).to_be_nil() + end) + it("should reject invalid port range", function() local invalid_config = { port_range = { min = -1, max = 65536 }, diff --git a/tests/unit/server/tcp_spec.lua b/tests/unit/server/tcp_spec.lua index 83a96e3d..9adcce03 100644 --- a/tests/unit/server/tcp_spec.lua +++ b/tests/unit/server/tcp_spec.lua @@ -16,6 +16,62 @@ describe("TCP server disconnect handling", function() client_manager.process_data = original_process_data end) + describe("find_available_port", function() + local original_new_tcp + local original_random + + before_each(function() + original_new_tcp = vim.loop.new_tcp + original_random = math.random + end) + + after_each(function() + vim.loop.new_tcp = original_new_tcp + rawset(math, "random", original_random) + end) + + it("should not build the whole default range when the first candidate is available", function() + local bind_count = 0 + vim.loop.new_tcp = function() + return { + bind = function(self, host, port) + bind_count = bind_count + 1 + return true + end, + close = function(self) end, + } + end + + local port = tcp.find_available_port(10000, 65535) + + assert.is_true(type(port) == "number") + assert.is_true(port >= 10000 and port <= 65535) + assert.are.equal(1, bind_count) + end) + + it("should wrap and scan each port at most once", function() + local tried_ports = {} + rawset(math, "random", function(max) + assert.are.equal(3, max) + return 3 + end) + vim.loop.new_tcp = function() + return { + bind = function(self, host, port) + table.insert(tried_ports, port) + return port == 10000 + end, + close = function(self) end, + } + end + + local port = tcp.find_available_port(10000, 10002) + + assert.are.equal(10000, port) + assert.are.same({ 10002, 10000 }, tried_ports) + end) + end) + it("should call on_disconnect and remove client on EOF", function() local callbacks = { on_message = spy.new(function() end), diff --git a/tests/unit/terminal_spec.lua b/tests/unit/terminal_spec.lua index 0028366f..73945815 100644 --- a/tests/unit/terminal_spec.lua +++ b/tests/unit/terminal_spec.lua @@ -65,6 +65,7 @@ describe("claudecode.terminal (wrapper for Snacks.nvim)", function() end before_each(function() + package.loaded["tests.mocks.vim"] = nil _G.vim = require("tests.mocks.vim") local spy_instance_methods = {}