From b7dfdcc63cacd88c5dc7b9171f57b74e70d72fe4 Mon Sep 17 00:00:00 2001 From: Dmitry Dementyev-Dedelis Date: Mon, 15 Sep 2025 07:07:53 +0300 Subject: [PATCH 1/4] docker support impl. --- lua/neotest-python/adapter.lua | 105 +++++++++++++++++++++++++++----- lua/neotest-python/base.lua | 106 ++++++++++++++++++++++++++++++++- lua/neotest-python/init.lua | 22 ++++++- 3 files changed, 211 insertions(+), 22 deletions(-) diff --git a/lua/neotest-python/adapter.lua b/lua/neotest-python/adapter.lua index d0b5999..cd9bbe2 100644 --- a/lua/neotest-python/adapter.lua +++ b/lua/neotest-python/adapter.lua @@ -1,7 +1,7 @@ local nio = require("nio") local lib = require("neotest.lib") -local pytest = require("neotest-python.pytest") -local base = require("neotest-python.base") +local pytest = require("plugins.neotest-python-local.lua.neotest-python.pytest") +local base = require("plugins.neotest-python-local.lua.neotest-python.base") ---@class neotest-python._AdapterConfig ---@field dap_args? table @@ -10,6 +10,7 @@ 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 docker? neotest-python.DockerConfig ---@param config neotest-python._AdapterConfig ---@return neotest.Adapter @@ -37,7 +38,10 @@ return function(config) table.insert(script_args, "--") - vim.list_extend(script_args, config.get_args(runner, position, run_args.strategy)) + -- Handle case where position might be nil for full project runs + if position then + vim.list_extend(script_args, config.get_args(runner, position, run_args.strategy)) + end if run_args.extra_args then vim.list_extend(script_args, run_args.extra_args) @@ -45,6 +49,9 @@ return function(config) if position then table.insert(script_args, position.id) + elseif run_args.tree then + -- For full project runs, use the root directory + table.insert(script_args, vim.loop.cwd()) end return script_args @@ -85,34 +92,58 @@ return function(config) 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, "") + -- Handle Docker paths and script location + local results_path, stream_path, script_path + if config.docker then + -- Use container paths for temp files + results_path = "/tmp/neotest_results_" .. vim.fn.localtime() + stream_path = "/tmp/neotest_stream_" .. vim.fn.localtime() + script_path = base.copy_script_to_container(config.docker) + + -- Create empty stream file in container + if config.docker.container then + local create_stream_cmd = vim.list_extend({}, {"docker", "exec", config.docker.container, "touch", stream_path}) + lib.process.run(create_stream_cmd) + end + else + results_path = nio.fn.tempname() + stream_path = nio.fn.tempname() + script_path = base.get_script_path() + lib.files.write(stream_path, "") + end 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 strategy_config if args.strategy == "dap" then strategy_config = base.create_dap_config(python_command, script_path, script_args, config.dap_args) end + ---@type neotest.RunSpec return { command = vim.iter({ python_command, script_path, script_args }):flatten():totable(), context = { results_path = results_path, stop_stream = stop_stream, + docker = config.docker, }, stream = function() return function() - local lines = stream_data() + local lines = {} + pcall(function() + lines = stream_data() + end) local results = {} for _, line in ipairs(lines) do - local result = vim.json.decode(line, { luanil = { object = true } }) - results[result.id] = result.result + if line and line ~= "" then + local success, result = pcall(vim.json.decode, line, { luanil = { object = true } }) + if success and result and result.id then + results[result.id] = result.result + end + end end return results end @@ -124,14 +155,56 @@ return function(config) ---@param result neotest.StrategyResult ---@return neotest.Result[] results = function(spec, result) - spec.context.stop_stream() - local success, data = pcall(lib.files.read, spec.context.results_path) - if not success then - data = "{}" + pcall(function() + spec.context.stop_stream() + end) + + local data = "{}" + + if spec.context.docker then + -- Copy results from container to host + local host_results_path = nio.fn.tempname() + if spec.context.docker.container then + local copy_cmd = { + "docker", "cp", + spec.context.docker.container .. ":" .. spec.context.results_path, + host_results_path + } + local copy_success = lib.process.run(copy_cmd) == 0 + if copy_success then + local read_success, file_data = pcall(lib.files.read, host_results_path) + if read_success then + data = file_data + end + end + end + else + local success, file_data = pcall(lib.files.read, spec.context.results_path) + if success then + data = file_data + end end - local results = vim.json.decode(data, { luanil = { object = true } }) + + local results = {} + local parse_success, parsed_results = pcall(vim.json.decode, data, { luanil = { object = true } }) + if parse_success and type(parsed_results) == "table" then + results = parsed_results + end + + -- Translate container paths back to host paths for Docker + if spec.context.docker then + for test_id, test_result in pairs(results) do + if test_result.output_path then + test_result.output_path = base.translate_path_to_host(test_result.output_path, spec.context.docker) + end + results[test_id] = test_result + end + end + for _, pos_result in pairs(results) do - result.output_path = pos_result.output_path + if pos_result.output_path then + result.output_path = pos_result.output_path + end end return results end, diff --git a/lua/neotest-python/base.lua b/lua/neotest-python/base.lua index 6d4a3d3..d3d8af1 100644 --- a/lua/neotest-python/base.lua +++ b/lua/neotest-python/base.lua @@ -80,8 +80,12 @@ function M.get_python_command(root) { stdout = true } ) if success and exit_code == 0 then - python_command_mem[root] = { Path:new(data).filename } - return python_command_mem[root] + local python_path = type(data) == "table" and data.stdout or tostring(data) + python_path = python_path:gsub("\r?\n", "") + if python_path and python_path ~= "" then + python_command_mem[root] = { python_path } + return python_command_mem[root] + end end end @@ -101,7 +105,8 @@ function M.get_script_path() end end - error("neotest.py not found") + vim.notify("neotest.py not found", vim.log.levels.ERROR) + return "" end ---@param python_command string[] @@ -176,6 +181,101 @@ function M.create_dap_config(python_path, script_path, script_args, dap_args) }, dap_args or {}) end +-- Docker support functions +function M.get_docker_python_command(root, docker_config) + root = root or vim.loop.cwd() + + local container = docker_config.container + local image = docker_config.image + local command = docker_config.command or { "docker", "exec" } + local args = docker_config.args or {} + local workdir = docker_config.workdir or "/app" + + if not container and not image then + vim.notify("Docker config must specify either 'container' or 'image'", vim.log.levels.ERROR) + return M.get_python_command(root) + end + + local docker_cmd = vim.list_extend({}, command) + + if container then + vim.list_extend(docker_cmd, args) + table.insert(docker_cmd, container) + elseif image then + docker_cmd = { "docker", "run", "--rm" } + vim.list_extend(docker_cmd, args) + vim.list_extend(docker_cmd, { "-v", root .. ":" .. workdir, "-w", workdir, image }) + end + + table.insert(docker_cmd, "python") + return docker_cmd +end + +function M.translate_path_to_container(host_path, docker_config) + if not docker_config then + return host_path + end + + local workdir = docker_config.workdir or "/app" + local cwd = vim.loop.cwd() + + if vim.startswith(host_path, cwd) then + local relative_path = host_path:sub(#cwd + 2) + return workdir .. "/" .. relative_path + end + + return host_path +end + +function M.translate_path_to_host(container_path, docker_config) + if not docker_config then + return container_path + end + + local workdir = docker_config.workdir or "/app" + local cwd = vim.loop.cwd() + + if vim.startswith(container_path, workdir) then + local relative_path = container_path:sub(#workdir + 2) + return cwd .. "/" .. relative_path + end + + return container_path +end + +function M.copy_script_to_container(docker_config) + if not docker_config or not docker_config.container then + return M.get_script_path() + end + + local script_path = M.get_script_path() + local script_dir = vim.fn.fnamemodify(script_path, ":h") + local container_script_path = "/tmp/neotest.py" + local container_dir_path = "/tmp/neotest_python" + + -- Copy the script file + local copy_cmd = { + "docker", "cp", script_path, + docker_config.container .. ":" .. container_script_path + } + + local success = lib.process.run(copy_cmd) == 0 + if not success then + vim.notify("Failed to copy neotest script to container", vim.log.levels.ERROR) + return script_path + end + + -- Copy the neotest_python directory + local copy_dir_cmd = { + "docker", "cp", script_dir .. "/neotest_python", + docker_config.container .. ":" .. container_dir_path + } + + lib.process.run(copy_dir_cmd) + + return container_script_path +end + local stored_runners = {} function M.get_runner(python_path) diff --git a/lua/neotest-python/init.lua b/lua/neotest-python/init.lua index 73cfc6d..200d37c 100644 --- a/lua/neotest-python/init.lua +++ b/lua/neotest-python/init.lua @@ -1,5 +1,12 @@ -local base = require("neotest-python.base") -local create_adapter = require("neotest-python.adapter") +local base = require("plugins.neotest-python-local.lua.neotest-python.base") +local create_adapter = require("plugins.neotest-python-local.lua.neotest-python.adapter") + +---@class neotest-python.DockerConfig +---@field container? string Container name or ID +---@field image? string Docker image name (alternative to container) +---@field command? string[] Custom docker command prefix (default: ["docker", "exec"]) +---@field args? string[] Additional docker arguments (e.g., ["-i", "-w", "/app"]) +---@field workdir? string Working directory inside container (default: "/app") ---@class neotest-python.AdapterConfig ---@field dap? table @@ -8,6 +15,7 @@ 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 docker? neotest-python.DockerConfig Docker configuration for containerized execution local is_callable = function(obj) return type(obj) == "function" or (type(obj) == "table" and obj.__call) @@ -31,7 +39,14 @@ local augment_config = function(config) return python end - return base.get_python(root) + return base.get_python_command(root) + end + end + + -- Handle Docker configuration + if config.docker then + get_python_command = function(root) + return base.get_docker_python_command(root, config.docker) end end @@ -64,6 +79,7 @@ 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, + docker = config.docker, } end From b3dc3baf19ef394df4ec10a8377225a4f42a381a Mon Sep 17 00:00:00 2001 From: Dmitry Dementyev-Dedelis Date: Mon, 15 Sep 2025 07:29:55 +0300 Subject: [PATCH 2/4] Adjustments for full test run --- lua/neotest-python/adapter.lua | 78 +++++++++++++++++++++++++--------- lua/neotest-python/base.lua | 19 +++++++-- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/lua/neotest-python/adapter.lua b/lua/neotest-python/adapter.lua index cd9bbe2..0608f28 100644 --- a/lua/neotest-python/adapter.lua +++ b/lua/neotest-python/adapter.lua @@ -19,8 +19,9 @@ return function(config) ---@param results_path string ---@param stream_path string ---@param runner string + ---@param docker_config neotest-python.DockerConfig? ---@return string[] - local function build_script_args(run_args, results_path, stream_path, runner) + local function build_script_args(run_args, results_path, stream_path, runner, docker_config) local script_args = { "--results-file", results_path, @@ -41,17 +42,29 @@ return function(config) -- Handle case where position might be nil for full project runs if position then vim.list_extend(script_args, config.get_args(runner, position, run_args.strategy)) + else + -- For full project runs, call get_args with nil position + vim.list_extend(script_args, config.get_args(runner, nil, run_args.strategy)) end if run_args.extra_args then vim.list_extend(script_args, run_args.extra_args) end - if position then - table.insert(script_args, position.id) - elseif run_args.tree then + if position and position.id then + local test_path = position.id + -- Translate host path to container path for Docker + if docker_config then + test_path = base.translate_path_to_container(position.id, docker_config) + end + table.insert(script_args, test_path) + else -- For full project runs, use the root directory - table.insert(script_args, vim.loop.cwd()) + local root_path = vim.loop.cwd() + if docker_config then + root_path = base.translate_path_to_container(root_path, docker_config) + end + table.insert(script_args, root_path) end return script_args @@ -93,28 +106,33 @@ return function(config) local runner = config.get_runner(python_command) -- Handle Docker paths and script location - local results_path, stream_path, script_path + local results_path, stream_path, script_path, container_results_path, container_stream_path if config.docker then - -- Use container paths for temp files - results_path = "/tmp/neotest_results_" .. vim.fn.localtime() - stream_path = "/tmp/neotest_stream_" .. vim.fn.localtime() + -- Create temp files on host for streaming + results_path = nio.fn.tempname() + stream_path = nio.fn.tempname() script_path = base.copy_script_to_container(config.docker) - -- Create empty stream file in container - if config.docker.container then - local create_stream_cmd = vim.list_extend({}, {"docker", "exec", config.docker.container, "touch", stream_path}) - lib.process.run(create_stream_cmd) - end + -- Create container paths that will be passed to the script + local unique_id = tostring(math.random(1000000, 9999999)) + container_results_path = "/tmp/neotest_results_" .. unique_id + container_stream_path = "/tmp/neotest_stream_" .. unique_id + + -- Create empty files on host for streaming + lib.files.write(stream_path, "") + lib.files.write(results_path, "{}") else results_path = nio.fn.tempname() stream_path = nio.fn.tempname() script_path = base.get_script_path() + container_results_path = results_path + container_stream_path = stream_path lib.files.write(stream_path, "") end 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_args = build_script_args(args, container_results_path, container_stream_path, runner, config.docker) local strategy_config if args.strategy == "dap" then @@ -127,11 +145,22 @@ return function(config) command = vim.iter({ python_command, script_path, script_args }):flatten():totable(), context = { results_path = results_path, + container_results_path = container_results_path, stop_stream = stop_stream, docker = config.docker, }, stream = function() return function() + -- For Docker, copy stream file from container to host before reading + if config.docker and config.docker.container then + local copy_stream_cmd = { + "docker", "cp", + config.docker.container .. ":" .. container_stream_path, + stream_path + } + pcall(lib.process.run, copy_stream_cmd) + end + local lines = {} pcall(function() lines = stream_data() @@ -162,17 +191,16 @@ return function(config) local data = "{}" if spec.context.docker then - -- Copy results from container to host - local host_results_path = nio.fn.tempname() + -- Copy results from container to host file if spec.context.docker.container then local copy_cmd = { "docker", "cp", - spec.context.docker.container .. ":" .. spec.context.results_path, - host_results_path + spec.context.docker.container .. ":" .. spec.context.container_results_path, + spec.context.results_path } local copy_success = lib.process.run(copy_cmd) == 0 if copy_success then - local read_success, file_data = pcall(lib.files.read, host_results_path) + local read_success, file_data = pcall(lib.files.read, spec.context.results_path) if read_success then data = file_data end @@ -193,12 +221,20 @@ return function(config) -- Translate container paths back to host paths for Docker if spec.context.docker then + local translated_results = {} for test_id, test_result in pairs(results) do + -- Translate the test ID (path) from container to host + local host_test_id = base.translate_path_to_host(test_id, spec.context.docker) + + -- Translate output_path if it exists if test_result.output_path then test_result.output_path = base.translate_path_to_host(test_result.output_path, spec.context.docker) end - results[test_id] = test_result + + -- Use the translated test ID as the key + translated_results[host_test_id] = test_result end + results = translated_results end for _, pos_result in pairs(results) do diff --git a/lua/neotest-python/base.lua b/lua/neotest-python/base.lua index d3d8af1..611e93a 100644 --- a/lua/neotest-python/base.lua +++ b/lua/neotest-python/base.lua @@ -98,6 +98,13 @@ end ---@return string function M.get_script_path() + -- First check our local neotest-python copy + local local_script = vim.fn.stdpath("config") .. "/lua/plugins/neotest-python-local/neotest.py" + if vim.fn.filereadable(local_script) == 1 then + return local_script + end + + -- Fallback to runtime file search local paths = vim.api.nvim_get_runtime_file("neotest.py", true) for _, path in ipairs(paths) do if vim.endswith(path, ("neotest-python%sneotest.py"):format(lib.files.sep)) then @@ -105,7 +112,9 @@ function M.get_script_path() end end - vim.notify("neotest.py not found", vim.log.levels.ERROR) + vim.schedule(function() + vim.notify("neotest.py not found", vim.log.levels.ERROR) + end) return "" end @@ -192,7 +201,9 @@ function M.get_docker_python_command(root, docker_config) local workdir = docker_config.workdir or "/app" if not container and not image then - vim.notify("Docker config must specify either 'container' or 'image'", vim.log.levels.ERROR) + vim.schedule(function() + vim.notify("Docker config must specify either 'container' or 'image'", vim.log.levels.ERROR) + end) return M.get_python_command(root) end @@ -261,7 +272,9 @@ function M.copy_script_to_container(docker_config) local success = lib.process.run(copy_cmd) == 0 if not success then - vim.notify("Failed to copy neotest script to container", vim.log.levels.ERROR) + vim.schedule(function() + vim.notify("Failed to copy neotest script to container", vim.log.levels.ERROR) + end) return script_path end From e06b94cabdf69d75ab01314dc1bb51d58e0ecb69 Mon Sep 17 00:00:00 2001 From: Dmitry Dementyev-Dedelis Date: Mon, 15 Sep 2025 22:50:40 +0300 Subject: [PATCH 3/4] Ready for commit --- lua/neotest-python/adapter.lua | 16 ++-------------- lua/neotest-python/base.lua | 10 ---------- lua/neotest-python/init.lua | 5 ++--- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/lua/neotest-python/adapter.lua b/lua/neotest-python/adapter.lua index 0608f28..6b9e6ab 100644 --- a/lua/neotest-python/adapter.lua +++ b/lua/neotest-python/adapter.lua @@ -1,7 +1,7 @@ local nio = require("nio") local lib = require("neotest.lib") -local pytest = require("plugins.neotest-python-local.lua.neotest-python.pytest") -local base = require("plugins.neotest-python-local.lua.neotest-python.base") +local pytest = require("neotest-python.pytest") +local base = require("neotest-python.base") ---@class neotest-python._AdapterConfig ---@field dap_args? table @@ -39,7 +39,6 @@ return function(config) table.insert(script_args, "--") - -- Handle case where position might be nil for full project runs if position then vim.list_extend(script_args, config.get_args(runner, position, run_args.strategy)) else @@ -53,7 +52,6 @@ return function(config) if position and position.id then local test_path = position.id - -- Translate host path to container path for Docker if docker_config then test_path = base.translate_path_to_container(position.id, docker_config) end @@ -105,20 +103,16 @@ return function(config) local python_command = config.get_python_command(root) local runner = config.get_runner(python_command) - -- Handle Docker paths and script location local results_path, stream_path, script_path, container_results_path, container_stream_path if config.docker then - -- Create temp files on host for streaming results_path = nio.fn.tempname() stream_path = nio.fn.tempname() script_path = base.copy_script_to_container(config.docker) - -- Create container paths that will be passed to the script local unique_id = tostring(math.random(1000000, 9999999)) container_results_path = "/tmp/neotest_results_" .. unique_id container_stream_path = "/tmp/neotest_stream_" .. unique_id - -- Create empty files on host for streaming lib.files.write(stream_path, "") lib.files.write(results_path, "{}") else @@ -151,7 +145,6 @@ return function(config) }, stream = function() return function() - -- For Docker, copy stream file from container to host before reading if config.docker and config.docker.container then local copy_stream_cmd = { "docker", "cp", @@ -191,7 +184,6 @@ return function(config) local data = "{}" if spec.context.docker then - -- Copy results from container to host file if spec.context.docker.container then local copy_cmd = { "docker", "cp", @@ -219,19 +211,15 @@ return function(config) results = parsed_results end - -- Translate container paths back to host paths for Docker if spec.context.docker then local translated_results = {} for test_id, test_result in pairs(results) do - -- Translate the test ID (path) from container to host local host_test_id = base.translate_path_to_host(test_id, spec.context.docker) - -- Translate output_path if it exists if test_result.output_path then test_result.output_path = base.translate_path_to_host(test_result.output_path, spec.context.docker) end - -- Use the translated test ID as the key translated_results[host_test_id] = test_result end results = translated_results diff --git a/lua/neotest-python/base.lua b/lua/neotest-python/base.lua index 611e93a..b4b3603 100644 --- a/lua/neotest-python/base.lua +++ b/lua/neotest-python/base.lua @@ -98,13 +98,6 @@ end ---@return string function M.get_script_path() - -- First check our local neotest-python copy - local local_script = vim.fn.stdpath("config") .. "/lua/plugins/neotest-python-local/neotest.py" - if vim.fn.filereadable(local_script) == 1 then - return local_script - end - - -- Fallback to runtime file search local paths = vim.api.nvim_get_runtime_file("neotest.py", true) for _, path in ipairs(paths) do if vim.endswith(path, ("neotest-python%sneotest.py"):format(lib.files.sep)) then @@ -190,7 +183,6 @@ function M.create_dap_config(python_path, script_path, script_args, dap_args) }, dap_args or {}) end --- Docker support functions function M.get_docker_python_command(root, docker_config) root = root or vim.loop.cwd() @@ -264,7 +256,6 @@ function M.copy_script_to_container(docker_config) local container_script_path = "/tmp/neotest.py" local container_dir_path = "/tmp/neotest_python" - -- Copy the script file local copy_cmd = { "docker", "cp", script_path, docker_config.container .. ":" .. container_script_path @@ -278,7 +269,6 @@ function M.copy_script_to_container(docker_config) return script_path end - -- Copy the neotest_python directory local copy_dir_cmd = { "docker", "cp", script_dir .. "/neotest_python", docker_config.container .. ":" .. container_dir_path diff --git a/lua/neotest-python/init.lua b/lua/neotest-python/init.lua index 200d37c..53fb7c5 100644 --- a/lua/neotest-python/init.lua +++ b/lua/neotest-python/init.lua @@ -1,5 +1,5 @@ -local base = require("plugins.neotest-python-local.lua.neotest-python.base") -local create_adapter = require("plugins.neotest-python-local.lua.neotest-python.adapter") +local base = require("neotest-python.base") +local create_adapter = require("neotest-python.adapter") ---@class neotest-python.DockerConfig ---@field container? string Container name or ID @@ -43,7 +43,6 @@ local augment_config = function(config) end end - -- Handle Docker configuration if config.docker then get_python_command = function(root) return base.get_docker_python_command(root, config.docker) From d4efd4dcd73569735f2995eb735f1155ab06ef8a Mon Sep 17 00:00:00 2001 From: Dmitry Dementyev-Dedelis Date: Mon, 15 Sep 2025 22:52:05 +0300 Subject: [PATCH 4/4] Updated readme.md --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 74dd888..c445c50 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,78 @@ 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, + -- Docker configuration for running tests in containers + docker = { + -- Docker container name or ID (required if using container-based execution) + container = "my-python-container", + -- OR use Docker image name instead of container + -- image = "python:3.9", + -- Custom docker command prefix (default: ["docker", "exec"]) + command = {"docker", "exec"}, + -- Additional docker arguments (e.g., interactive mode, working directory) + args = {"-i", "-w", "/app"}, + -- Working directory inside container (default: "/app") + workdir = "/app", + }, }) } }) ``` + +## Docker Support + +This adapter supports running tests inside Docker containers, which is useful for projects that are developed or deployed in containerized environments. + +### Configuration + +To enable Docker support, configure the `docker` option in your neotest setup: + +```lua +require("neotest").setup({ + adapters = { + require("neotest-python")({ + docker = { + container = "my-python-container", -- Required: container name or ID + args = {"-i", "-w", "/app"}, -- Optional: additional docker arguments + workdir = "/app", -- Optional: working directory (default: "/app") + } + }) + } +}) +``` + +### Docker Options + +- **`container`** (string): Name or ID of a running Docker container where tests will be executed +- **`image`** (string): Alternatively, specify a Docker image name to run tests in a new container +- **`command`** (table): Custom docker command prefix (default: `{"docker", "exec"}`) +- **`args`** (table): Additional arguments passed to the docker command (e.g., `{"-i", "-w", "/app"}`) +- **`workdir`** (string): Working directory inside the container (default: `"/app"`) + +### Requirements + +- Docker must be installed and accessible from your system +- The specified container must be running (when using `container` option) +- The container must have Python and your test dependencies installed +- Your project files must be mounted or copied into the container + +### Example Configurations + +#### Using an existing container: +```lua +docker = { + container = "web-app", + args = {"-i"}, + workdir = "/workspace" +} +``` + +#### Using a Docker image (creates temporary containers): +```lua +docker = { + image = "python:3.9-slim", + args = {"-v", "/host/project:/app", "-w", "/app"}, + workdir = "/app" +} +```