Skip to content

Commit 9ecf9f3

Browse files
authored
Speed up coverage test runtime (#282)
## Summary - Speed up coverage-enabled tests by removing expensive module-load work from the server utilities and config path. - Avoid building/shuffling the entire default port range on every server startup. - Keep terminal specs isolated so mock spy wrappers do not accumulate across examples. - Make local/CI test tasks use the project-local LuaRocks binaries explicitly. ## Runtime impact - Linked CI test step baseline: ~296.9s for `mise run test`. - Local post-change `mise run test`: ~8.8s with coverage enabled. ## Dogfooding / quality gates - `mise run check` - `mise run test` - `./scripts/run_integration_tests_individually.sh` - `prettier --check .github/workflows/test.yml` - `actionlint .github/workflows/test.yml` - `git diff --check` --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-5.5` • Thinking: `xhigh`_
1 parent 453920f commit 9ecf9f3

8 files changed

Lines changed: 96 additions & 51 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
- name: Generate coverage report
6363
run: |
6464
if [ -f "luacov.stats.out" ]; then
65-
mise exec -- luacov
65+
./.luarocks/bin/luacov
6666
6767
echo "Creating lcov.info from luacov.report.out"
6868
{

lua/claudecode/config.lua

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,15 @@ M.defaults = {
3939
{ name = "Claude Haiku (Latest)", value = "haiku" },
4040
{ name = "Default (account recommended)", value = "default" },
4141
},
42-
terminal = nil, -- Will be lazy-loaded to avoid circular dependency
42+
-- Keep a minimal terminal config here instead of requiring claudecode.terminal
43+
-- during config.apply(). Loading the terminal module pulls in the server/main
44+
-- module graph and makes coverage-enabled config validation unexpectedly slow.
45+
terminal = {
46+
provider = "auto",
47+
provider_opts = {
48+
external_terminal_cmd = nil,
49+
},
50+
},
4351
}
4452

4553
---Validates the provided configuration table.
@@ -191,14 +199,6 @@ end
191199
function M.apply(user_config)
192200
local config = vim.deepcopy(M.defaults)
193201

194-
-- Lazy-load terminal defaults to avoid circular dependency
195-
if config.terminal == nil then
196-
local terminal_ok, terminal_module = pcall(require, "claudecode.terminal")
197-
if terminal_ok and terminal_module.defaults then
198-
config.terminal = terminal_module.defaults
199-
end
200-
end
201-
202202
if user_config then
203203
-- Use vim.tbl_deep_extend if available, otherwise simple merge
204204
if vim.tbl_deep_extend then

lua/claudecode/server/tcp.lua

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---@brief TCP server implementation using vim.loop
22
local client_manager = require("claudecode.server.client")
3-
local utils = require("claudecode.server.utils")
43

54
local M = {}
65

@@ -19,20 +18,21 @@ local M = {}
1918
---@param max_port number Maximum port to try
2019
---@return number|nil port Available port number, or nil if none found
2120
function M.find_available_port(min_port, max_port)
22-
if min_port > max_port then
23-
return nil -- Or handle error appropriately
24-
end
21+
assert(type(min_port) == "number", "min_port must be a number")
22+
assert(type(max_port) == "number", "max_port must be a number")
2523

26-
local ports = {}
27-
for i = min_port, max_port do
28-
table.insert(ports, i)
24+
if min_port > max_port then
25+
return nil
2926
end
3027

31-
-- Shuffle the ports
32-
utils.shuffle_array(ports)
28+
local port_count = max_port - min_port + 1
29+
local start_offset = math.random(port_count) - 1
3330

34-
-- Try to bind to a port from the shuffled list
35-
for _, port in ipairs(ports) do
31+
-- Pick a random starting point, then scan the range once. This keeps the
32+
-- selection spread across the configured range without building and shuffling
33+
-- a 55k-entry table for the default 10000-65535 range on every startup.
34+
for checked = 0, port_count - 1 do
35+
local port = min_port + ((start_offset + checked) % port_count)
3636
local test_server = vim.loop.new_tcp()
3737
if test_server then
3838
local success = test_server:bind("127.0.0.1", port)
@@ -42,7 +42,6 @@ function M.find_available_port(min_port, max_port)
4242
return port
4343
end
4444
end
45-
-- Continue to next port if test_server creation failed or bind failed
4645
end
4746

4847
return nil

lua/claudecode/server/utils.lua

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ local function bxor(a, b)
4444
return result
4545
end
4646

47+
local bit_ok, bit = pcall(require, "bit")
48+
local native_bxor = bit_ok and bit and bit.bxor or nil
49+
4750
local function bnot(a)
4851
return bxor(a, 0xFFFFFFFF)
4952
end
@@ -364,32 +367,6 @@ function M.bytes_to_uint64(bytes)
364367
return num
365368
end
366369

367-
---XOR lookup table for faster operations
368-
local xor_table = {}
369-
for i = 0, 255 do
370-
xor_table[i] = {}
371-
for j = 0, 255 do
372-
local result = 0
373-
local a, b = i, j
374-
local bit_val = 1
375-
376-
while a > 0 or b > 0 do
377-
local a_bit = a % 2
378-
local b_bit = b % 2
379-
380-
if a_bit ~= b_bit then
381-
result = result + bit_val
382-
end
383-
384-
a = math.floor(a / 2)
385-
b = math.floor(b / 2)
386-
bit_val = bit_val * 2
387-
end
388-
389-
xor_table[i][j] = result
390-
end
391-
end
392-
393370
---Apply XOR mask to payload data
394371
---@param data string The data to mask/unmask
395372
---@param mask string The 4-byte mask
@@ -401,7 +378,9 @@ function M.apply_mask(data, mask)
401378
for i = 1, #data do
402379
local mask_idx = ((i - 1) % 4) + 1
403380
local data_byte = data:byte(i)
404-
result[i] = string.char(xor_table[data_byte][mask_bytes[mask_idx]])
381+
local mask_byte = mask_bytes[mask_idx]
382+
local masked_byte = native_bxor and native_bxor(data_byte, mask_byte) or bxor(data_byte, mask_byte)
383+
result[i] = string.char(masked_byte)
405384
end
406385

407386
return table.concat(result)

mise.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ echo "Checking Lua files for syntax errors..."
103103
# (macOS) too — `find -exec ... \;` swallows the child's status there.
104104
find lua -name "*.lua" -type f -print0 | xargs -0 -I{} luajit -e "assert(loadfile('{}'))"
105105
echo "Running luacheck..."
106-
luacheck lua/ tests/ --no-unused-args --no-max-line-length
106+
./.luarocks/bin/luacheck lua/ tests/ --no-unused-args --no-max-line-length
107107
'''
108108

109109
[tasks.test]
@@ -115,7 +115,7 @@ TEST_FILES=$(find tests -type f -name "*_test.lua" -o -name "*_spec.lua" | sort)
115115
echo "Found test files:"
116116
echo "$TEST_FILES"
117117
if [ -n "$TEST_FILES" ]; then
118-
busted --coverage -v $TEST_FILES
118+
./.luarocks/bin/busted --coverage -v $TEST_FILES
119119
else
120120
echo "No test files found"
121121
fi

tests/unit/config_spec.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ describe("Configuration", function()
4747
expect(final_config.env).to_be_table() -- Should inherit default empty table
4848
end)
4949

50+
it("should not load terminal module while applying configuration", function()
51+
package.loaded["claudecode.terminal"] = nil
52+
53+
local final_config = config.apply({ auto_start = false, log_level = "info" })
54+
55+
expect(final_config.terminal).to_be_table()
56+
expect(final_config.terminal.provider).to_be("auto")
57+
expect(package.loaded["claudecode.terminal"]).to_be_nil()
58+
end)
59+
5060
it("should reject invalid port range", function()
5161
local invalid_config = {
5262
port_range = { min = -1, max = 65536 },

tests/unit/server/tcp_spec.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,62 @@ describe("TCP server disconnect handling", function()
1616
client_manager.process_data = original_process_data
1717
end)
1818

19+
describe("find_available_port", function()
20+
local original_new_tcp
21+
local original_random
22+
23+
before_each(function()
24+
original_new_tcp = vim.loop.new_tcp
25+
original_random = math.random
26+
end)
27+
28+
after_each(function()
29+
vim.loop.new_tcp = original_new_tcp
30+
rawset(math, "random", original_random)
31+
end)
32+
33+
it("should not build the whole default range when the first candidate is available", function()
34+
local bind_count = 0
35+
vim.loop.new_tcp = function()
36+
return {
37+
bind = function(self, host, port)
38+
bind_count = bind_count + 1
39+
return true
40+
end,
41+
close = function(self) end,
42+
}
43+
end
44+
45+
local port = tcp.find_available_port(10000, 65535)
46+
47+
assert.is_true(type(port) == "number")
48+
assert.is_true(port >= 10000 and port <= 65535)
49+
assert.are.equal(1, bind_count)
50+
end)
51+
52+
it("should wrap and scan each port at most once", function()
53+
local tried_ports = {}
54+
rawset(math, "random", function(max)
55+
assert.are.equal(3, max)
56+
return 3
57+
end)
58+
vim.loop.new_tcp = function()
59+
return {
60+
bind = function(self, host, port)
61+
table.insert(tried_ports, port)
62+
return port == 10000
63+
end,
64+
close = function(self) end,
65+
}
66+
end
67+
68+
local port = tcp.find_available_port(10000, 10002)
69+
70+
assert.are.equal(10000, port)
71+
assert.are.same({ 10002, 10000 }, tried_ports)
72+
end)
73+
end)
74+
1975
it("should call on_disconnect and remove client on EOF", function()
2076
local callbacks = {
2177
on_message = spy.new(function() end),

tests/unit/terminal_spec.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ describe("claudecode.terminal (wrapper for Snacks.nvim)", function()
6565
end
6666

6767
before_each(function()
68+
package.loaded["tests.mocks.vim"] = nil
6869
_G.vim = require("tests.mocks.vim")
6970

7071
local spy_instance_methods = {}

0 commit comments

Comments
 (0)