repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/tests/std/project_spec.lua
Rawlocal project = require('std.project')
local join = require('std.table').join
local function tmpdir()
return vim.fs.normalize(vim.fn.tempname())
end
local function mkdir(path)
vim.fn.mkdir(path, 'p')
end
local function touch(path)
local f = io.open(path, 'w')
if f then f:close() end
end
describe('std.project', function()
before_each(function()
for k in pairs(project.cache) do
project.cache[k] = nil
end
end)
describe('cache_key', function()
-- cache_key is internal, but we verify it through cache behavior:
-- if cache_key is broken (collisions), subsequent tests would fail
-- because find_root would return wrong results from cache.
-- We test the cache indirectly via find_root correctness.
end)
describe('find_root', function()
local root_path
local stop_path
before_each(function()
root_path = tmpdir()
stop_path = tmpdir()
mkdir(root_path .. '/a/b/c')
mkdir(stop_path)
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
vim.fn.delete(stop_path, 'rf')
end)
it('returns nil when no markers exist', function()
local result = project.find_root(root_path .. '/a/b/c/file.lua', {}, stop_path)
assert.is_nil(result)
end)
it('returns directory with highest-scoring markers', function()
touch(root_path .. '/.git')
local markers = { { name = '.git', weight = 3 } }
local result = project.find_root(root_path .. '/a/b/c/file.lua', markers, stop_path)
assert.equal(root_path, result)
end)
it('prefers closer markers when weights are equal', function()
-- root has .git (weight 3), but subdir has .git closer
-- score = weight * (1 / distance)
-- root: 3 * (1/3) = 1
-- subdir: 3 * (1/1) = 3 -> should win
local markers = { { name = '.git', weight = 3 } }
mkdir(root_path .. '/a/.git')
touch(root_path .. '/a/.git')
local result = project.find_root(root_path .. '/a/b/c/file.lua', markers, stop_path)
assert.equal(vim.fs.joinpath(root_path, 'a'), result)
end)
it('does not traverse beyond stop directory', function()
touch(stop_path .. '/.git')
local result = project.find_root(root_path, {}, stop_path)
assert.is_nil(result)
end)
it('returns nil for empty path', function()
local ok, result = pcall(project.find_root, '', {}, stop_path)
assert.is_true(ok)
assert.is_nil(result)
end)
it('errors when stop directory does not exist', function()
local ok, err = pcall(project.find_root, root_path, {}, '/nonexistent/path')
assert.is_false(ok)
assert.is_string(err)
end)
it('returns cached result on second call', function()
touch(root_path .. '/.git')
local markers = { { name = '.git', weight = 3 } }
-- first call
local first = project.find_root(root_path .. '/a/file.lua', markers, stop_path)
assert.equal(root_path, first)
-- second call should return cached value without error
local second = project.find_root(root_path .. '/a/file.lua', markers, stop_path)
assert.equal(root_path, second)
end)
it('markers with weight 0 do not contribute to score', function()
-- Create a scenario where only a weight-0 marker exists at root
-- and a real marker exists deeper -- the weight-0 should be ignored
local markers_with_zero = {
{ name = '.git', weight = 0 },
{ name = 'Makefile', weight = 3 },
}
touch(root_path .. '/Makefile')
local result = project.find_root(root_path .. '/a/file.lua', markers_with_zero, stop_path)
assert.equal(root_path, result)
end)
it('returns path at stop boundary without traversal', function()
-- path == stop should return nil (no traversal possible)
local ok, result = pcall(project.find_root, stop_path, { { name = '.git', weight = 3 } }, stop_path)
if ok then
assert.is_nil(result)
end
end)
end)
describe('find_project_root', function()
local root_path
local stop_path
before_each(function()
root_path = tmpdir()
stop_path = tmpdir()
mkdir(root_path .. '/a/b')
mkdir(stop_path)
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
vim.fn.delete(stop_path, 'rf')
end)
it('finds project root using default markers', function()
touch(root_path .. '/.git')
local result = project.find_project_root(root_path .. '/a/b/file.lua')
assert.equal(root_path, result)
end)
it('finds project root with custom markers', function()
touch(root_path .. '/Makefile')
local result = project.find_project_root(root_path .. '/a/file.lua', {
{ name = 'Makefile', weight = 3 },
})
assert.equal(root_path, result)
end)
it('returns nil when no project markers exist', function()
local result = project.find_project_root(root_path .. '/a/file.lua')
assert.is_nil(result)
end)
it('uses empty markers when passed explicitly', function()
touch(root_path .. '/.git')
local result = project.find_project_root(root_path .. '/a/file.lua', {})
-- empty markers means no markers to find, so should be nil
-- (the function will fall back to vcs project root which still uses default markers internally)
-- Actually find_vcs_project_root calls find_project_root(path, {}) which still uses default markers
-- So this will find .git
assert.equal(root_path, result)
end)
it('cache hit returns same result without re-traversal', function()
touch(root_path .. '/.git')
local first = project.find_project_root(root_path .. '/a/file.lua')
assert.equal(root_path, first)
local second = project.find_project_root(root_path .. '/a/file.lua')
assert.equal(root_path, second)
end)
end)
describe('language-specific finders', function()
local root_path
local stop_path
before_each(function()
root_path = tmpdir()
stop_path = tmpdir()
mkdir(root_path .. '/a')
mkdir(stop_path)
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
vim.fn.delete(stop_path, 'rf')
end)
it('find_lua_project_root finds lua markers', function()
mkdir(root_path .. '/lua')
touch(root_path .. '/.luarc.json')
local result = project.find_lua_project_root(root_path .. '/a/file.lua')
assert.equal(root_path, result)
end)
it('find_lua_project_root returns nil when no lua markers', function()
local result = project.find_lua_project_root(root_path .. '/a/file.lua')
assert.is_nil(result)
end)
it('find_java_project_root finds java markers', function()
touch(root_path .. '/pom.xml')
mkdir(root_path .. '/.idea')
local result = project.find_java_project_root(root_path .. '/a/file.java')
assert.equal(root_path, result)
end)
it('find_java_project_root returns nil when no java markers', function()
local result = project.find_java_project_root(root_path .. '/a/file.java')
assert.is_nil(result)
end)
it('find_zig_project_root finds zig markers', function()
touch(root_path .. '/build.zig')
local result = project.find_zig_project_root(root_path .. '/a/file.zig')
assert.equal(root_path, result)
end)
it('find_zig_project_root returns nil when no zig markers', function()
local result = project.find_zig_project_root(root_path .. '/a/file.zig')
assert.is_nil(result)
end)
it('find_vcs_project_root finds vcs markers', function()
touch(root_path .. '/.jj')
local result = project.find_vcs_project_root(root_path .. '/a/file.lua')
assert.equal(root_path, result)
end)
end)
describe('find_root_by_filetype', function()
local root_path
before_each(function()
root_path = tmpdir()
mkdir(root_path .. '/a')
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
end)
it('returns correct rooter for lua filetype', function()
mkdir(root_path .. '/lua')
touch(root_path .. '/.luarc.json')
local result = project.find_root_by_filetype(root_path .. '/a/file.lua', 'lua')
assert.equal(root_path, result)
end)
it('returns correct rooter for java filetype', function()
touch(root_path .. '/pom.xml')
local result = project.find_root_by_filetype(root_path .. '/a/file.java', 'java')
assert.equal(root_path, result)
end)
it('returns correct rooter for zig filetype', function()
touch(root_path .. '/build.zig')
local result = project.find_root_by_filetype(root_path .. '/a/file.zig', 'zig')
assert.equal(root_path, result)
end)
it('falls back to generic rooter for unknown filetype', function()
touch(root_path .. '/.git')
local result = project.find_root_by_filetype(root_path .. '/a/file.foo', 'unknown_lang')
-- should fall back to vcs -> find_project_root with default markers
assert.equal(root_path, result)
end)
it('falls back to find_project_root when no vcs markers for unknown filetype', function()
-- no vcs markers, find_project_root will find nothing
local result = project.find_root_by_filetype(root_path .. '/a/file.foo', 'unknown_lang')
assert.is_nil(result)
end)
end)
describe('weights (MAYBE, LIKELY, DEFINITELY)', function()
local root_path
local stop_path
before_each(function()
root_path = tmpdir()
stop_path = tmpdir()
mkdir(root_path .. '/a/b')
mkdir(stop_path)
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
vim.fn.delete(stop_path, 'rf')
end)
it('DEFINITELY marker beats multiple LIKELY markers', function()
-- root has .lsp.settings.lua (weight=7), subdir has .git+.editorconfig (weight=3+3=6)
-- score(root) = 7 * (1/2) = 3.5
-- score(subdir) = 6 * (1/1) = 6 -- would win without DEFINITELY
-- But since root has DEFINITELY(7) * (1/2) = 3.5 vs subdir 6, subdir still wins!
-- Let's use a case where DEFINITELY at root wins:
-- root: .lsp.settings.lua (7) at distance 2 -> 3.5
-- subdir: .git (3) at distance 1 -> 3
-- 3.5 > 3, so root wins
local markers = {
{ name = '.lsp.settings.lua', weight = 7 },
{ name = '.git', weight = 3 },
}
touch(root_path .. '/.lsp.settings.lua')
mkdir(root_path .. '/a/.git')
touch(root_path .. '/a/.git')
local result = project.find_root(root_path .. '/a/b/file.lua', markers, stop_path)
assert.equal(root_path, result)
end)
end)
describe('cache correctness (sha256 replacement verification)', function()
-- These tests verify the cache produces correct results
-- after the sha256 -> string concat change.
-- If cache keys collide, these tests will return wrong results.
local root_path
local stop_path
before_each(function()
root_path = tmpdir()
stop_path = tmpdir()
mkdir(root_path .. '/a/b')
mkdir(stop_path)
touch(root_path .. '/.git')
end)
after_each(function()
vim.fn.delete(root_path, 'rf')
vim.fn.delete(stop_path, 'rf')
end)
it('find_root returns correct result with cache populated', function()
local markers = { { name = '.git', weight = 3 } }
-- Call multiple times to populate cache
local r1 = project.find_root(root_path .. '/a/b/file.lua', markers, stop_path)
assert.equal(root_path, r1)
-- Same call - should be cache hit
local r2 = project.find_root(root_path .. '/a/b/file.lua', markers, stop_path)
assert.equal(root_path, r2)
-- Different subdir - same root, cache miss but same traversal result
local r3 = project.find_root(root_path .. '/a/file.lua', markers, stop_path)
assert.equal(root_path, r3)
end)
it('different markers produce different results', function()
local root2 = tmpdir()
mkdir(root2 .. '/a')
touch(root2 .. '/Makefile')
-- Use find_root with explicit markers to avoid default marker fallback
-- root_path has .git but no Makefile
-- root2 has Makefile but no .git
local markers = { { name = 'Makefile', weight = 3 } }
local r1 = project.find_root(root_path .. '/a/file.lua', markers, stop_path)
local r2 = project.find_root(root2 .. '/a/file.lua', markers, stop_path)
assert.is_nil(r1)
assert.equal(root2, r2)
vim.fn.delete(root2, 'rf')
end)
end)
describe('_select_best_root (pure scoring)', function()
local select_best_root
before_each(function()
select_best_root = project._select_best_root
end)
it('returns nil for empty directory_scores', function()
local result = select_best_root({})
assert.is_nil(result)
end)
it('returns the only scored directory', function()
local scores = {
{ path = '/project', marker_count = 3, distance_to_buffer_file = 2 },
}
local result = select_best_root(scores)
assert.equal('/project', result)
end)
it('prefers higher likelihood (marker_count * 1/distance)', function()
local scores = {
{ path = '/far', marker_count = 6, distance_to_buffer_file = 3 },
{ path = '/close', marker_count = 3, distance_to_buffer_file = 1 },
}
-- /far: 6 * (1/3) = 2
-- /close: 3 * (1/1) = 3 -> wins
local result = select_best_root(scores)
assert.equal('/close', result)
end)
it('DEFINITELY (7) at root beats LIKELY (3) close', function()
local scores = {
{ path = '/project', marker_count = 7, distance_to_buffer_file = 3 },
{ path = '/project/sub', marker_count = 3, distance_to_buffer_file = 1 },
}
-- /project: 7 * (1/3) = 2.33
-- /project/sub: 3 * (1/1) = 3 -> wins without DEFINITELY bonus
-- But if we adjust to make DEFINITELY win:
local scores2 = {
{ path = '/project', marker_count = 7, distance_to_buffer_file = 2 },
{ path = '/project/sub', marker_count = 6, distance_to_buffer_file = 1 },
}
-- /project: 7 * (1/2) = 3.5
-- /project/sub: 6 * (1/1) = 6 -> still wins
-- So DEFINITELY needs closer distance to win:
local scores3 = {
{ path = '/project', marker_count = 7, distance_to_buffer_file = 1 },
{ path = '/project/sub', marker_count = 6, distance_to_buffer_file = 2 },
}
-- /project: 7 * 1 = 7
-- /project/sub: 6 * 0.5 = 3 -> /project wins
local result = select_best_root(scores3)
assert.equal('/project', result)
end)
it('multiple LIKELY (3) close beats single DEFINITELY (7) far', function()
local scores = {
{ path = '/project', marker_count = 7, distance_to_buffer_file = 4 },
{ path = '/project/sub', marker_count = 6, distance_to_buffer_file = 1 },
}
-- /project: 7 * 0.25 = 1.75
-- /project/sub: 6 * 1 = 6 -> sub wins
local result = select_best_root(scores)
assert.equal('/project/sub', result)
end)
it('equal likelihood picks first (closest to root in traversal order)', function()
local scores = {
{ path = '/first', marker_count = 3, distance_to_buffer_file = 1 },
{ path = '/second', marker_count = 3, distance_to_buffer_file = 1 },
}
-- Equal likelihood, first in traversal order wins
local result = select_best_root(scores)
assert.equal('/first', result)
end)
it('marker_count 0 at any distance produces 0 likelihood', function()
local scores = {
{ path = '/zero', marker_count = 0, distance_to_buffer_file = 1 },
{ path = '/has_markers', marker_count = 3, distance_to_buffer_file = 3 },
}
-- /zero: 0 * 1 = 0
-- /has_markers: 3 * 0.33 = 1 -> wins
local result = select_best_root(scores)
assert.equal('/has_markers', result)
end)
it('distance 1 (closest possible) maximizes score multiplier', function()
local scores = {
{ path = '/root', marker_count = 3, distance_to_buffer_file = 1 },
{ path = '/deep', marker_count = 3, distance_to_buffer_file = 5 },
}
-- /root: 3 * 1 = 3
-- /deep: 3 * 0.2 = 0.6 -> /root wins
local result = select_best_root(scores)
assert.equal('/root', result)
end)
it('handles very large marker counts', function()
local scores = {
{ path = '/project', marker_count = 100, distance_to_buffer_file = 10 },
{ path = '/project/sub', marker_count = 99, distance_to_buffer_file = 1 },
}
-- /project: 100 * 0.1 = 10
-- /project/sub: 99 * 1 = 99 -> sub wins
local result = select_best_root(scores)
assert.equal('/project/sub', result)
end)
it('handles fractional likelihood from large distance', function()
local scores = {
{ path = '/project', marker_count = 7, distance_to_buffer_file = 100 },
{ path = '/project/sub', marker_count = 1, distance_to_buffer_file = 1 },
}
-- /project: 7 * 0.01 = 0.07
-- /project/sub: 1 * 1 = 1 -> sub wins easily
local result = select_best_root(scores)
assert.equal('/project/sub', result)
end)
end)
end)