diff --git a/README.md b/README.md index cd653d4..1f6b0d6 100644 --- a/README.md +++ b/README.md @@ -3,45 +3,91 @@ This plugin provides a [Pest](https://pestphp.com) adapter for the [Neotest](https://github.com/nvim-neotest/neotest) framework. This is a fork of `neotest-pest` originally by [@theutz](https://github.com/theutz/neotest-pest), with some fixes and updates: + - Updated to work with [Pest](https://pestphp.com) 2.0 - Support for (and automatic detection of) Laravel Sail - Note: This also moves junit output files into `storage/app/` +- Parallel testing support :warning: _Ive only focused on making this work for me. Please test against your Pest tests_ :warning: ## :package: Installation -Install the plugin using packer: +Install the plugin using your favorite package manager. + +Here's an example using lazy.nvim: ```lua -use({ - 'nvim-neotest/neotest', - requires = { - ..., - 'V13Axel/neotest-pest', - }, - config = function() - require('neotest').setup({ - ..., - adapters = { - require('neotest-pest'), - } - }) - end -}) +{ + 'nvim-neotest/neotest', + dependencies = { + ..., + 'V13Axel/neotest-pest', + }, + config = function() + require('neotest').setup({ + ..., + adapters = { + require('neotest-pest'), + } + }) + end +} ``` ## :wrench: Configuration -The plugin may be configured as below: +> [!TIP] +> Any of these options can be set to a lua function that returns the desired result. For example, wanna run tests in parallel, one for each CPU core? +> `parallel = function() return #vim.loop.cpu_info() end,` ```lua adapters = { - require('neotest-pest')({ - pest_cmd = function() - return "vendor/bin/pest" - end - }), + require('neotest-pest')({ + -- Ignore these directories when looking for tests + -- -- Default: { "vendor", "node_modules" } + ignore_dirs = { "vendor", "node_modules" } + + -- Ignore any projects containing "phpunit-only.tests" + -- -- Default: {} + root_ignore_files = { "phpunit-only.tests" }, + + -- Specify suffixes for files that should be considered tests + -- -- Default: { "Test.php" } + test_file_suffixes = { "Test.php", "_test.php", "PestTest.php" }, + + -- Sail not properly detected? Explicitly enable it. + -- -- Default: function() that checks for sail presence + sail_enabled = false, + + -- Custom sail executable. Not running in Sail, but running bare Docker? + -- Set `sail_enabled` = true and `sail_executable` to { "docker", "exec", "[somecontainer]" } + -- -- Default: "vendor/bin/sail" + sail_executable = "vendor/bin/sail", + + -- Custom pest binary. + -- -- Default: function that checks for sail presence + pest_cmd = "vendor/bin/pest", + + -- Run N tests in parallel, <=1 doesn't pass --parallel to pest at all + -- -- Default: 0 + parallel = 16 + + -- Enable ["compact" output printer](https://pestphp.com/docs/optimizing-tests#content-compact-printer) + -- -- Default: false + compact = false, + + -- Set a custom path for the results XML file, parsed by this adapter + -- + ------------------------------------------------------------------------------------ + -- NOTE: This must be a path accessible by both your test runner AND your editor! -- + ------------------------------------------------------------------------------------ + -- + -- -- Default: function that checks for sail presence. + -- -- - If no sail: Numbered file in randomized /tmp/ directory (using async.fn.tempname()) + -- -- - If sail: "storage/app/" .. os.date("junit-%Y%m%d-%H%M%S") + results_path = function() "/some/accessible/path" end, + }), } ``` @@ -62,6 +108,7 @@ vim.keymap.set('n', 'tn', function() require('neotest').run.run() end) To test a file run `lua require('neotest').run.run(vim.fn.expand('%'))` Example - t(est)f(ile): + ```lua vim.keymap.set('n', 'tf', function() require('neotest').run.run(vim.fn.expand('%')) end) ``` @@ -76,7 +123,7 @@ To test the full test suite run `lua require('neotest').run.run({ suite = true } ## :gift: Contributing -This fork is maintained by one guy, in my spare time and when I can get to it. Please raise a PR if you are interested in adding new functionality or fixing any bugs. When submitting a bug, please include an example test that I can test against. +I'm just one guy, maintaining this in my spare time and when I can get to it. Please raise a PR if you are interested in adding new functionality or fixing any bugs. When submitting a bug, please include an example test that I can test against. To trigger the tests for the adapter, run: diff --git a/lua/neotest-pest/config.lua b/lua/neotest-pest/config.lua new file mode 100644 index 0000000..9e33d2e --- /dev/null +++ b/lua/neotest-pest/config.lua @@ -0,0 +1,87 @@ +local logger = require('neotest.logging') +local ok, async = pcall(require, "nio") +if not ok then + async = require("neotest.async") +end + +local is_callable = function(obj) + return type(obj) == "function" or (type(obj) == "table" and obj.__call) +end + +local M = { + env = { + root_ignore_files = {}, + root_files = { "tests/Pest.php" }, + ignore_dirs = { "vendor", "node_modules" }, + test_file_suffixes = { "Test.php" }, + sail_executable = "vendor/bin/sail", + parallel = 0, + compact = false, + }, + + _sail_error = false, + _sail_enabled = false, +} + +function M.env.sail_enabled() + -- Cache and short-circuit so we don't have to check the disk for + -- vnedor/bin/sail every time. If the user removes the sail executable + -- out from under us, that's their problem. + if M._sail_enabled then + return true + end + + return M.sail_available() +end + +function M.env.is_parallel() + return M('parallel') > 0 +end + +function M.env.pest_cmd() + if M('sail_enabled') then + return { "vendor/bin/sail", "bin", "pest" } + end + + return { "vendor/bin/pest" } +end + +function M.env.results_path() + if M('sail_enabled') then + return "storage/app/" .. os.date("junit-%Y%m%d-%H%M%S") + end + + return async.fn.tempname() +end + +function M.sail_error() + return M._sail_error +end + +function M.sail_available() + if vim.fn.filereadable(M('sail_executable')) == 1 then + M._sail_enabled = true + return true + end + + M._sail_error = true + logger.debug("Sail executable not found") +end + +function M.merge(env) + for key, value in pairs(env) do + M.env[key] = value + end +end + +setmetatable(M, { + __call = function(_, key) + if is_callable(M.env[key] or nil) then + return M.env[key]() + end + + return M.env[key] or {} + end +}) + +return M diff --git a/lua/neotest-pest/init.lua b/lua/neotest-pest/init.lua index 672fe1f..1aac6d7 100644 --- a/lua/neotest-pest/init.lua +++ b/lua/neotest-pest/init.lua @@ -1,7 +1,22 @@ local lib = require('neotest.lib') -local async = require('neotest.async') local logger = require('neotest.logging') local utils = require('neotest-pest.utils') +local config = require('neotest-pest.config') +local debug = logger.debug + +local notify = function(msg, level) + vim.notify(msg, level, { + title = "neotest-pest", + }) +end + +local error = function(msg) + notify(msg, "error") +end + +local info = function(msg) + notify(msg, "info") +end ---@class neotest.Adapter ---@field name string @@ -13,7 +28,37 @@ local NeotestAdapter = { name = "neotest-pest" } ---@async ---@param dir string @Directory to treat as cwd ---@return string | nil @Absolute root dir of test suite -NeotestAdapter.root = lib.files.match_root_pattern("tests/Pest.php") +function NeotestAdapter.root(dir) + local result = nil + + debug("Finding root...") + + for _, root_ignore_file in ipairs(config("root_ignore_files")) do + debug("Checking root ignore file", root_ignore_file) + + result = lib.files.match_root_pattern(root_ignore_file)(dir) + + if result then + debug("Ignoring root because file", root_ignore_file) + return nil + end + end + + for _, root_file in ipairs(config("root_files")) do + debug("Checking root file", root_file) + + result = lib.files.match_root_pattern(root_file)(dir) + + if result then + debug("Found root", result) + break + end + end + + debug("Root not found") + + return result +end ---Filter directories when searching for test files ---@async @@ -22,14 +67,22 @@ NeotestAdapter.root = lib.files.match_root_pattern("tests/Pest.php") ---@param root string Root directory of project ---@return boolean True when matching function NeotestAdapter.filter_dir(name, rel_path, root) - return vim.startswith(rel_path, "tests") + for _, filter_dir in ipairs(config("ignore_dirs")) do + if name == filter_dir then return false end + end + + return true end ---@async ---@param file_path string ---@return boolean function NeotestAdapter.is_test_file(file_path) - return vim.endswith(file_path, "Test.php") + for _, suffix in ipairs(config("test_file_suffixes")) do + if vim.endswith(file_path, suffix) then return true end + end + + return false end function NeotestAdapter.discover_positions(path) @@ -52,57 +105,56 @@ function NeotestAdapter.discover_positions(path) }) end -local function get_pest_cmd() - local binary = "pest" - - if vim.fn.filereadable("vendor/bin/pest") == 1 then - binary = "vendor/bin/pest" - end - - return binary -end - -local is_callable = function(obj) - return type(obj) == "function" or (type(obj) == "table" and obj.__call) -end - ---@param args neotest.RunArgs ---@return neotest.RunSpec | nil function NeotestAdapter.build_spec(args) local position = args.tree:data() - local results_path = "storage/app/" .. os.date("junit-%Y%m%d-%H%M%S") + local results_path = config('results_path') - local binary = get_pest_cmd() + debug("Building spec for:", position) + debug("Results path:", results_path) - local command = {} + local path = position.path; - if vim.fn.filereadable("vendor/bin/sail") == 1 then - command = vim.tbl_flatten({ - "vendor/bin/sail", "bin", "pest", - position.name ~= "tests" and ("/var/www/html" .. string.sub(position.path, string.len(vim.loop.cwd()) + 1)), - "--log-junit=" .. results_path, - }) - else - command = vim.tbl_flatten({ - binary, - position.name ~= "tests" and position.path, - "--log-junit=" .. results_path, - }) + if config('sail_enabled') then + debug("Sail enabled, adjusting path") + path = "/var/www/html" .. string.sub(position.path, string.len(vim.loop.cwd() or "") + 1) end + local command = vim.tbl_flatten({ + config('pest_cmd'), + path, + "--log-junit=" .. results_path, + }) if position.type == "test" then - local script_args = vim.tbl_flatten({ + command = vim.tbl_flatten({ + command, "--filter", position.name, }) + else + debug("Position type:", position.type) + end + if config('is_parallel') then command = vim.tbl_flatten({ command, - script_args, + "--parallel", + "--processes=" .. config('parallel'), }) end + if config('compact') == true then + info("Using compact output") + command = vim.tbl_flatten({ + command, + "--compact", + }) + end + + debug("Command:", command) + return { command = command, context = { @@ -121,18 +173,21 @@ function NeotestAdapter.results(test, result, tree) local ok, data = pcall(lib.files.read, output_file) if not ok then + error("No test output file found! Should have been at: " .. output_file) logger.error("No test output file found:", output_file) return {} end local ok, parsed_data = pcall(lib.xml.parse, data) if not ok then + error("Failed to parse test output!") logger.error("Failed to parse test output:", output_file) return {} end local ok, results = pcall(utils.get_test_results, parsed_data, output_file) if not ok then + error("Could not get test results!") logger.error("Could not get test results", output_file) return {} end @@ -142,13 +197,8 @@ end setmetatable(NeotestAdapter, { __call = function(_, opts) - if is_callable(opts.pest_cmd) then - get_pest_cmd = opts.pest_cmd - elseif opts.pest_cmd then - get_pest_cmd = function() - return opts.pest_cmd - end - end + config.merge(opts or {}) + return NeotestAdapter end, }) diff --git a/lua/neotest-pest/utils.lua b/lua/neotest-pest/utils.lua index e238680..f649021 100644 --- a/lua/neotest-pest/utils.lua +++ b/lua/neotest-pest/utils.lua @@ -15,8 +15,8 @@ M.make_test_id = function(position) local path = string.sub(position.path, string.len(vim.loop.cwd()) + 2) local id = path .. separator .. position.name - logger.info("Path to test file:", { position.path }) - logger.info("Treesitter id:", { id }) + logger.debug("Path to test file:", { position.path }) + logger.debug("Treesitter id:", { id }) return id end @@ -65,14 +65,14 @@ end ---@param output_file string ---@return table local function make_outputs(test, output_file) - logger.info("Pre-output test:", test) + logger.debug("Pre-output test:", test) local test_attr = test["_attr"] or test[1]["_attr"] local name = string.gsub(test_attr.name, "it (.*)", "%1") -- Difference to neotest-phpunit as of PHPUnit 10: -- Pest's test IDs are in the format "path/to/test/file::test name" local test_id = string.gsub(test_attr.file, "(.*)::(.*)", "%1") .. separator .. name - logger.info("Pest id:", { test_id }) + logger.debug("Pest id:", { test_id }) local test_output = { status = "passed", @@ -83,7 +83,7 @@ local function make_outputs(test, output_file) local test_failed, errors, fails = errors_or_fails(test) if test_failed then - logger.info("test_failed:", { test_failed, errors, fails }) + logger.debug("test_failed:", { test_failed, errors, fails }) test_output.status = "failed" if #errors > 0 then diff --git a/phpunit.xml b/phpunit.xml index 39c5df8..2023371 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,14 @@ - - - - ./tests - - - - - ./app - ./src - - + + + + ./tests + + + + + ./app + ./src + + diff --git a/phpunit.xml.bak b/phpunit.xml.bak new file mode 100644 index 0000000..39c5df8 --- /dev/null +++ b/phpunit.xml.bak @@ -0,0 +1,18 @@ + + + + + ./tests + + + + + ./app + ./src + + +