import subprocess import pytest from furtka import dockerops class FakeProc: def __init__(self, stdout=b"", stderr=b"", returncode=0): self.stdout = stdout self.stderr = stderr self.returncode = returncode def test_compose_exec_builds_command(tmp_path, monkeypatch): recorded = {} def fake_run(cmd, **kwargs): recorded["cmd"] = cmd recorded["kwargs"] = kwargs return FakeProc(stdout="ok\n", returncode=0) monkeypatch.setattr(subprocess, "run", fake_run) out = dockerops.compose_exec(tmp_path, "myproj", "svc", ["echo", "hi"]) assert out == "ok\n" cmd = recorded["cmd"] # docker compose --project-name myproj --file /docker-compose.yaml exec -T svc echo hi assert cmd[0] == "docker" assert cmd[1] == "compose" assert "--project-name" in cmd and "myproj" in cmd assert "exec" in cmd assert "-T" in cmd # -T must come before the service name assert cmd.index("-T") < cmd.index("svc") # argv appended after service assert cmd[-2:] == ["echo", "hi"] def test_compose_exec_propagates_env(tmp_path, monkeypatch): recorded = {} def fake_run(cmd, **kwargs): recorded["cmd"] = cmd return FakeProc() monkeypatch.setattr(subprocess, "run", fake_run) dockerops.compose_exec(tmp_path, "p", "s", ["true"], env={"A": "1", "B": "two"}) cmd = recorded["cmd"] # `--env A=1 --env B=two` should appear before the service name. s_idx = cmd.index("s") env_args = cmd[:s_idx] assert env_args.count("--env") == 2 assert "A=1" in env_args assert "B=two" in env_args def test_compose_exec_raises_on_nonzero(tmp_path, monkeypatch): def fake_run(cmd, **kwargs): return FakeProc(stdout="", stderr="boom", returncode=2) monkeypatch.setattr(subprocess, "run", fake_run) with pytest.raises(dockerops.DockerError, match="exited 2"): dockerops.compose_exec(tmp_path, "p", "s", ["fail"]) def test_compose_exec_raises_on_timeout(tmp_path, monkeypatch): def fake_run(cmd, **kwargs): raise subprocess.TimeoutExpired(cmd, timeout=kwargs.get("timeout")) monkeypatch.setattr(subprocess, "run", fake_run) with pytest.raises(dockerops.DockerError, match="timed out"): dockerops.compose_exec(tmp_path, "p", "s", ["sleep", "9999"], timeout=1) def test_compose_exec_script_streams_via_stdin(tmp_path, monkeypatch): script = tmp_path / "hook.sh" body = b"#!/bin/sh\necho hello\n" script.write_bytes(body) recorded = {} def fake_run(cmd, **kwargs): recorded["cmd"] = cmd recorded["input"] = kwargs["input"] return FakeProc(stdout=b"hello\n", returncode=0) monkeypatch.setattr(subprocess, "run", fake_run) out = dockerops.compose_exec_script(tmp_path, "p", "s", script) assert out == "hello\n" # exec ... s sh -s (script body comes in on stdin) cmd = recorded["cmd"] assert cmd[-3:] == ["s", "sh", "-s"] assert recorded["input"] == body def test_compose_exec_script_raises_on_nonzero(tmp_path, monkeypatch): script = tmp_path / "fail.sh" script.write_bytes(b"exit 1\n") def fake_run(cmd, **kwargs): return FakeProc(stdout=b"", stderr=b"hook says no", returncode=1) monkeypatch.setattr(subprocess, "run", fake_run) with pytest.raises(dockerops.DockerError, match="hook fail.sh exited 1"): dockerops.compose_exec_script(tmp_path, "p", "s", script) def test_compose_exec_script_raises_on_timeout(tmp_path, monkeypatch): script = tmp_path / "slow.sh" script.write_bytes(b"sleep 10\n") def fake_run(cmd, **kwargs): raise subprocess.TimeoutExpired(cmd, timeout=kwargs.get("timeout")) monkeypatch.setattr(subprocess, "run", fake_run) with pytest.raises(dockerops.DockerError, match="hook slow.sh timed out"): dockerops.compose_exec_script(tmp_path, "p", "s", script, timeout=1)