nvim-neotest / neotest

An extensible framework for interacting with tests within NeoVim.
MIT License
2.45k stars 123 forks source link

[feature]: statusline integration #110

Closed rebelot closed 1 year ago

rebelot commented 2 years ago

Hi, is it possible to retrieve a table of informations to be displayed in custom statuslines? something like:

local function my_neotest_statuline_component()
    local dict = require'neotest'.status_dict
    return "OK " .. dict.passed .. "FAILED " .. dict.failed .. " / " .. dict.total
end

maybe it is already possible given the large API of neotest but I couldn't find a way to access tests results

rcarriga commented 2 years ago

This is not yet possible, it would require a new consumer. Happy to take contributions :smile:

rebelot commented 2 years ago

I'm trying to write a consumer, which kinda works so far. However, the client:get_results() method returns the results for single tests and for the entire test file and I haven't found a "comfortable" way to separate the two kind of results

rcarriga commented 2 years ago

You probably want to iterate through the tree and get the results for only tests

local results = client:get_results(adapter_id)
local tree = client:get_position(root_id, { adapter = adapter_id })
for _, pos in tree:iter() do
  if pos.type == "test" then
     ...
  end
end

Results can come in very often when streamed so I'd recommend having some sort of caching to prevent a tonne of work being done. You could just mark the cache as invalid on a results event and then recalculate when the status is requested

rebelot commented 2 years ago
    consumers = {
        statusline = function(client)
            return {
                statusline = function()
                    local adapters = client:get_adapters()
                    local statuses = {}
                    local root_id = vim.api.nvim_buf_get_name(0)
                    for _, adapter_id in ipairs(adapters) do
                        local results = client:get_results(adapter_id)
                        local tree = client:get_position(root_id, { adapter = adapter_id })
                        if not tree then
                            return
                        end
                        for _, pos in tree:iter() do
                            if pos.type == "test" then
                                local id = pos.id
                                if results[id] then
                                    table.insert(statuses, results[id].status)
                                else
                                    table.insert(statuses, "none")
                                end
                            end
                        end
                    end
                    local status_dict = { total = 0, failed = 0, passed = 0 }
                    for _, s in ipairs(statuses) do
                        if s == "failed" then
                            status_dict.failed = status_dict.failed + 1
                        elseif s == "passed" then
                            status_dict.passed = status_dict.passed + 1
                        elseif s == "none" then
                            status_dict.total = status_dict.total + 1
                        end
                    end
                    status_dict.total = status_dict.total + status_dict.failed + status_dict.passed
                    return status_dict
                end,
            }
        end,
    },

I came up with something like this, but I am not yet so sure it's worth it given the completeness of the summary consumer...

rcarriga commented 2 years ago

Your logic around adapters means that if it doesn't match the first adapter, it will always return nil. Also you can simplify the status handling a bit.

---@param client neotest.Client
statusline = function(client)
  return {
    statusline = function()
      local adapters = client:get_adapters()
      if not client:has_started() then
        return
      end
      local root_id = vim.api.nvim_buf_get_name(0)
      for _, adapter_id in ipairs(adapters) do
        local results = client:get_results(adapter_id)
        local tree = client:get_position(root_id, { adapter = adapter_id })
        if tree then
          local status_dict = { total = 0, failed = 0, passed = 0, skipped = 0 }
          for _, pos in tree:iter() do
            if pos.type == "test" then
              status_dict.total = status_dict.total + 1
              if results[pos.id] then
                status_dict[results[pos.id].status] = status_dict[results[pos.id].status] + 1
              end
            end
          end
          return status_dict
        end
      end
    end,
  }
end,

Depending on statusline integration this will be called thousands of times within a minute or two, hence why the suggestion of caching. I tested locally and my fans started spinning just from adding this :sweat_smile:

rcarriga commented 1 year ago

Closing this as the new state consumer provides a cache for results