Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/tests/std/project_spec.lua

Raw
local 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)