diff --git a/README.md b/README.md index 74dd888..734d533 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,13 @@ require("neotest").setup({ -- !!EXPERIMENTAL!! Enable shelling out to `pytest` to discover test -- instances for files containing a parametrize mark (default: false) pytest_discover_instances = true, + -- Allow redirection of pytest execution inside a docker container + -- Can also be a function to return dynamic value. + -- !! Only pytest is supported [and](and) requires installation of pytest-json package inside the container + use_docker = false, + -- The name of the docker to use for execution. + -- Can also be a function to return dynamic value. + containers = "docker-name-1", }) } }) diff --git a/lua/neotest-python/adapter.lua b/lua/neotest-python/adapter.lua index 1d9e143..52f105e 100644 --- a/lua/neotest-python/adapter.lua +++ b/lua/neotest-python/adapter.lua @@ -1,3 +1,4 @@ +local Path = require("plenary.path") local nio = require("nio") local lib = require("neotest.lib") local pytest = require("neotest-python.pytest") @@ -10,6 +11,8 @@ local base = require("neotest-python.base") ---@field get_python_command fun(root: string):string[] ---@field get_args fun(runner: string, position: neotest.Position, strategy: string): string[] ---@field get_runner fun(python_command: string[]): string +---@field use_docker? fun(): boolean +---@field get_container fun(): string ---@param config neotest-python._AdapterConfig ---@return neotest.Adapter @@ -50,6 +53,32 @@ return function(config) return script_args end + ---@param run_args neotest.RunArgs + ---@param results_path string + ---@param runner string + ---@return string[] + local function build_docker_args(run_args, results_path, runner) + local script_args = { "exec" , config.get_container(), runner, "--json="..results_path } + + local position = run_args.tree:data() + + vim.list_extend(script_args, config.get_args(runner, position, run_args.strategy)) + + if run_args.extra_args then + vim.list_extend(script_args, run_args.extra_args) + end + + if position then + local relpath = Path:new(position.path):make_relative(vim.loop.cwd()) + table.insert(script_args, relpath) + if position.type == "test" then + vim.list_extend(script_args, {'-k', position.name}) + end + end + + return script_args + end + ---@type neotest.Adapter return { name = "neotest-python", @@ -77,30 +106,40 @@ return function(config) ---@param args neotest.RunArgs ---@return neotest.RunSpec build_spec = function(args) - local position = args.tree:data() + local command + local results_path + local script_args + local script_path + local position = args.tree:data() local root = base.get_root(position.path) or vim.loop.cwd() or "" - - local python_command = config.get_python_command(root) - local runner = config.get_runner(python_command) - - local results_path = nio.fn.tempname() local stream_path = nio.fn.tempname() lib.files.write(stream_path, "") local stream_data, stop_stream = lib.files.stream_lines(stream_path) - - local script_args = build_script_args(args, results_path, stream_path, runner) - local script_path = base.get_script_path() + local runner = config.get_runner(command) + + if config.use_docker() == false then + command = config.get_python_command(root) + + results_path = nio.fn.tempname() + script_args = build_script_args(args, results_path, stream_path, runner) + script_path = base.get_script_path() + else + command = {"docker"} + script_path = "container" + results_path = "report.json" + script_args = build_docker_args(args, results_path, runner) + end local strategy_config if args.strategy == "dap" then - strategy_config = - base.create_dap_config(python_command, script_path, script_args, config.dap_args) + strategy_config = base.create_dap_config(command, script_path, script_args, config.dap_args) end + ---@type neotest.RunSpec return { - command = vim.iter({ python_command, script_path, script_args }):flatten():totable(), + command = vim.iter({ command, script_path, script_args }):flatten():totable(), context = { results_path = results_path, stop_stream = stop_stream, @@ -122,16 +161,41 @@ return function(config) ---@param spec neotest.RunSpec ---@param result neotest.StrategyResult ---@return neotest.Result[] - results = function(spec, result) + results = function(spec, result, tree) + local results = {} spec.context.stop_stream() local success, data = pcall(lib.files.read, spec.context.results_path) if not success then data = "{}" end - local results = vim.json.decode(data, { luanil = { object = true } }) - for _, pos_result in pairs(results) do - result.output_path = pos_result.output_path + local report = vim.json.decode(data, { luanil = { object = true } }) + + -- Native pytest execution + if config.use_docker() == false then + for _, pos_result in pairs(results) do + result.output_path = pos_result.output_path + end + + -- docker delegated executionù + else + -- the path must be recomposed because docker has no the same absolute path + local path = vim.loop.cwd() + + for _, v in pairs(report['report']['tests']) do + if v['outcome'] == 'failed' then + results[path .. "/" .. v['name']] = { + status = v['outcome'], + short = v['call']['longrepr'] + } + else + results[path .. "/" .. v['name']] = { + status = v['outcome'], + short = "" + } + end + end end + return results end, } diff --git a/lua/neotest-python/base.lua b/lua/neotest-python/base.lua index 90cd6da..4e1e9e3 100644 --- a/lua/neotest-python/base.lua +++ b/lua/neotest-python/base.lua @@ -159,4 +159,13 @@ function M.get_runner(python_path) return runner end +function M.use_docker() + return false +end + +function M.get_container() + -- Unknown container error will be throw if the field "container" is not defined in configuration + return '' +end + return M diff --git a/lua/neotest-python/init.lua b/lua/neotest-python/init.lua index 73cfc6d..a9a8448 100644 --- a/lua/neotest-python/init.lua +++ b/lua/neotest-python/init.lua @@ -8,6 +8,8 @@ local create_adapter = require("neotest-python.adapter") ---@field python? string|string[]|fun(root: string):string[] ---@field args? string[]|fun(runner: string, position: neotest.Position, strategy: string): string[] ---@field runner? string|fun(python_command: string[]): string +---@field use_docker? boolean|fun(): boolean +---@field container? string|fun(): string local is_callable = function(obj) return type(obj) == "function" or (type(obj) == "table" and obj.__call) @@ -56,6 +58,24 @@ local augment_config = function(config) end end + local use_docker = base.use_docker + if is_callable(config.use_docker) then + use_docker = config.use_docker + else + use_docker = function () + return config.use_docker + end + end + + local get_container = base.get_container + if is_callable(config.container) then + get_container = config.container + elseif config.container then + get_container = function () + return config.container + end + end + ---@type neotest-python._AdapterConfig return { pytest_discovery = config.pytest_discover_instances, @@ -64,6 +84,8 @@ local augment_config = function(config) get_args = get_args, is_test_file = config.is_test_file or base.is_test_file, get_python_command = get_python_command, + use_docker = use_docker, + get_container = get_container, } end