From 6fca450d7eb0449a3472580a23d0e514821be998 Mon Sep 17 00:00:00 2001 From: Carsten Date: Tue, 21 Apr 2026 22:31:24 +0200 Subject: [PATCH] feat(agent): Shell.run wrapper for testable external commands --- agent/lib/proxmox_agent/shell.ex | 24 ++++++++++++++++++++++++ agent/test/proxmox_agent/shell_test.exs | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 agent/lib/proxmox_agent/shell.ex create mode 100644 agent/test/proxmox_agent/shell_test.exs diff --git a/agent/lib/proxmox_agent/shell.ex b/agent/lib/proxmox_agent/shell.ex new file mode 100644 index 0000000..abb84be --- /dev/null +++ b/agent/lib/proxmox_agent/shell.ex @@ -0,0 +1,24 @@ +defmodule ProxmoxAgent.Shell do + @moduledoc """ + Thin wrapper over System.cmd for testability. Collectors accept an optional + :runner function of this shape so tests can inject fixture-backed fakes. + """ + + @type result :: {:ok, String.t()} | {:error, term()} + + @spec run(String.t(), [String.t()]) :: result + def run(command, args) do + try do + case System.cmd(command, args, stderr_to_stdout: true) do + {output, 0} -> {:ok, output} + {output, code} -> {:error, {:nonzero_exit, code, output}} + end + rescue + e in ErlangError -> + case e.original do + :enoent -> {:error, {:enoent, command}} + other -> {:error, {:system_error, other}} + end + end + end +end diff --git a/agent/test/proxmox_agent/shell_test.exs b/agent/test/proxmox_agent/shell_test.exs new file mode 100644 index 0000000..ea9cab0 --- /dev/null +++ b/agent/test/proxmox_agent/shell_test.exs @@ -0,0 +1,19 @@ +defmodule ProxmoxAgent.ShellTest do + use ExUnit.Case, async: true + + alias ProxmoxAgent.Shell + + test "run/2 returns {:ok, output} on zero exit" do + assert {:ok, output} = Shell.run("/bin/echo", ["hello"]) + assert String.trim(output) == "hello" + end + + test "run/2 returns {:error, {:nonzero_exit, code, output}} on non-zero exit" do + assert {:error, {:nonzero_exit, code, _}} = Shell.run("/bin/sh", ["-c", "exit 7"]) + assert code == 7 + end + + test "run/2 returns {:error, {:enoent, _}} when binary is missing" do + assert {:error, {:enoent, _}} = Shell.run("/does/not/exist/nope", []) + end +end