orjangj / neotest-ctest

A neotest adapter for C/C++ using CTest as a test runner. Supports GoogleTest, Catch2 and doctest test frameworks.
MIT License
8 stars 4 forks source link

neotest-ctest

A neotest adapter for C/C++ using CTest as a test runner

[![Neovim][neovim-shield]][neovim-url] [![Lua][lua-shield]][lua-url] [![CTest][ctest-shield]][ctest-url] ![cpp-shield] [![MIT License][license-shield]][license-url] [![Issues][issues-shield]][issues-url] [![Build Status][ci-shield]][ci-url]

Quick links

Features

The framework versions listed above are the ones that have been tested, but older versions may work as well.

Limitations

Requirements

Installation

See Neotest Installation Instructions.

The following example is based on lazy.nvim:

{
  "nvim-neotest/neotest",
  dependencies = {
    "nvim-lua/plenary.nvim",
    -- Other neotest dependencies here
    "orjangj/neotest-ctest",
  },
  config = function()
    -- Optional, but recommended, if you have enabled neotest's diagnostic option
    local neotest_ns = vim.api.nvim_create_namespace("neotest")
    vim.diagnostic.config({
      virtual_text = {
        format = function(diagnostic)
          -- Convert newlines, tabs and whitespaces into a single whitespace
          -- for improved virtual text readability
          local message = diagnostic.message:gsub("[\r\n\t%s]+", " ")
          return message
        end,
      },
    }, neotest_ns)

    require("neotest").setup({
      adapters = {
        -- Load with default config
        require("neotest-ctest").setup({})
      }
    })
  end
}

Configuration

require("neotest-ctest").setup({
  -- fun(string) -> string: Find the project root directory given a current directory
  -- to work from.
  root = function(dir)
    -- by default, it will use neotest.lib.files.match_root_pattern with the following entries
    return require("neotest.lib").files.match_root_pattern(
      -- NOTE: CMakeLists.txt is not a good candidate as it can be found in
      -- more than one directory
      "CMakePresets.json",
      "compile_commands.json",
      ".clangd",
      ".clang-format",
      ".clang-tidy",
      "build",
      "out",
      ".git"
    )(dir)
  end
  ),
  -- fun(string) -> bool: Takes a file path as string and returns true if it contains tests.
  -- This function is called often by neotest, so make sure you don't do any heavy duty work.
  is_test_file = function(file)
    -- by default, returns true if the file stem ends with _test and the file extension is
    -- one of cpp/cc/cxx.
  end,
  -- fun(string, string, string) -> bool: Filter directories when searching for test files.
  -- Best to keep this as-is and set per-project settings in neotest instead.
  -- See :h neotest.Config.discovery.
  filter_dir = function(name, rel_path, root)
    -- If you don't configure filter_dir through neotest, and you leave it as-is,
    -- it will filter the following directories by default: build, cmake, doc,
    -- docs, examples, out, scripts, tools, venv.
  end,
  -- What frameworks to consider when performing auto-detection of test files.
  -- Priority can be configured by ordering/removing list items to your needs.
  -- By default, each test file will be queried with the given frameworks in the
  -- following order.
  frameworks = { "gtest", "catch2", "doctest"},
  -- What extra args should ALWAYS be sent to CTest? Note that most of CTest arguments
  -- are not expected to be used (or work) with this plugin, but some might be useful
  -- depending on your needs. For instance:
  --   extra_args = {
  --     "--stop-on-failure",
  --     "--schedule-random",
  --     "--timeout",
  --     "<seconds>",
  --   }
  -- If you want to send extra_args for one given invocation only, send them to
  -- `neotest.run.run({extra_args = ...})` instead. see :h neotest.RunArgs for details.
  extra_args = {},
})

It's possible to configure the adapter per project using Neotest's projects option if you need more fine-grained control:

require("neotest").setup({
  -- other options
  projects = {
    ["~/path/to/some/project"] = {
      discovery = {
        filter_dir = function(name, rel_path, root)
          -- Do not look for tests in `build` folder for this specific project
          return name ~= "build"
        end,
      },
      adapters = {
        require("neotest-ctest").setup({
          is_test_file = function(file_path)
            -- your implementation
          end,
          frameworks = { "catch2" },
        }),
      },
    },
  },
})

Usage

See Neotest Usage. The following example of keybindings can be used as a starting point:

{
  "nvim-neotest/neotest",
  dependencies = {
    "nvim-lua/plenary.nvim",
    -- Other neotest dependencies here
    "orjangj/neotest-ctest",
  },
  keys = function()
    local neotest = require("neotest")

    return {
      { "<leader>tf", function() neotest.run.run(vim.fn.expand("%")) end, desc = "Run File" },
      { "<leader>tt", function() neotest.run.run() end, desc = "Run Nearest" },
      { "<leader>tw", function() neotest.run.run(vim.loop.cwd()) end, desc = "Run Workspace" },
      {
        "<leader>tr",
        function()
          -- This will only show the output from the test framework
          neotest.output.open({ short = true, auto_close = true })
        end,
        desc = "Results (short)",
      },
      {
        "<leader>tR",
        function()
          -- This will show the classic CTest log output.
          -- The output usually spans more than can fit the neotest floating window,
          -- so using 'enter = true' to enable normal navigation within the window
          -- is recommended.
          neotest.output.open({ enter = true })
        end,
        desc = "Results (full)",
      },
      -- Other keybindings
    }
  end,
  config = function()
    require("neotest").setup({
      adapters = {
        -- Load with default config
        require("neotest-ctest").setup({})
      }
    })
  end
}