Skip to content

Latest commit

 

History

History
284 lines (254 loc) · 10 KB

README.md

File metadata and controls

284 lines (254 loc) · 10 KB

neotest-ctest

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

Neovim Lua CTest cpp-shield

MIT License Issues Build Status

Quick links

Features

  • Supported Test Frameworks
    • GoogleTest (v1.11.0+): Supports macros TEST, TEST_F and TEST_P
      • For TEST_P, only Range, Values and Bool parameter generators are supported. The name generator is not supported either. See INSTANTIATE_TEST_SUITE_P for more.
    • Catch2 (v3.3.0+): Supports macros TEST_CASE, TEST_CASE_METHOD, SCENARIO
    • doctest (v2.4.8+): Supports macros TEST_CASE, TEST_CASE_FIXTURE, SCENARIO
      • Decorators not supported yet
  • Automatically detects test framework used in a test file (see limitations)
    • Using multiple test frameworks is supported as each test file is evaluated separately
    • What frameworks to include in the detection is configurable
  • Automatically detects CTest test directory (see limitations)
  • Parses test results and displays errors as diagnostics (if you have enabled neotest's diagnostic option)

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

Limitations

  • Does not compile any source or tests (cmake-tools is highly recommended as a companion plugin).
  • While CTest does not directly depend on the usage of CMake, this plugin assumes you have enumerated your tests with CMake integrations such as gtest_discover_tests(), catch_discover_tests(), etc.
  • CTestTestfile.cmake is expected to be on path from project root (max two levels deep)
    • For instance <dir>/CTestTestfile.cmake or <dir>/<config>/CTestTestfile.cmake.
    • For multi-config projects, the first CTest enabled configuration found will be selected.
  • Currently, framework tests are only detected if the system lib include pattern is used, such as #include <gtest/gtest.h> as opposed to #include "gtest/gtest.h".
  • Some of the frameworks, such as catch2 and doctest, enumerates test case names interchangeably. This makes it impossible for neotest-ctest to reliably map them to neotest positions. Please ensure that test case names are uniquely defined if you use multiple frameworks together!
  • Does not support neotest's dap strategy for debugging tests (yet)

Requirements

  • Neovim v0.9.1+, v0.10.x, or nightly.
  • nvim-neotest v5.0.0+
  • Tree-sitter parser for C++ to be installed (preferably latest).
  • CMake v3.21 or higher (CTest is bundled with CMake)

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
}