|
|
"""Tests for Docker utilities.""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
from typing import TYPE_CHECKING |
|
|
from unittest.mock import MagicMock, patch |
|
|
|
|
|
import pytest |
|
|
|
|
|
from stroke_deepisles_demo.core.exceptions import DockerNotAvailableError |
|
|
from stroke_deepisles_demo.inference.docker import ( |
|
|
build_docker_command, |
|
|
check_docker_available, |
|
|
ensure_docker_available, |
|
|
run_container, |
|
|
) |
|
|
|
|
|
if TYPE_CHECKING: |
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
class TestCheckDockerAvailable: |
|
|
"""Tests for check_docker_available.""" |
|
|
|
|
|
def test_returns_true_when_docker_responds(self) -> None: |
|
|
"""Returns True when 'docker info' succeeds.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=0) |
|
|
|
|
|
result = check_docker_available() |
|
|
|
|
|
assert result is True |
|
|
|
|
|
def test_returns_false_when_docker_not_found(self) -> None: |
|
|
"""Returns False when docker command not found.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.side_effect = FileNotFoundError() |
|
|
|
|
|
result = check_docker_available() |
|
|
|
|
|
assert result is False |
|
|
|
|
|
def test_returns_false_when_daemon_not_running(self) -> None: |
|
|
"""Returns False when docker daemon not running.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=1) |
|
|
|
|
|
result = check_docker_available() |
|
|
|
|
|
assert result is False |
|
|
|
|
|
|
|
|
class TestEnsureDockerAvailable: |
|
|
"""Tests for ensure_docker_available.""" |
|
|
|
|
|
def test_raises_when_docker_not_available(self) -> None: |
|
|
"""Raises DockerNotAvailableError when Docker not available.""" |
|
|
with ( |
|
|
patch( |
|
|
"stroke_deepisles_demo.inference.docker.check_docker_available", |
|
|
return_value=False, |
|
|
), |
|
|
pytest.raises(DockerNotAvailableError), |
|
|
): |
|
|
ensure_docker_available() |
|
|
|
|
|
def test_no_error_when_docker_available(self) -> None: |
|
|
"""No exception when Docker is available.""" |
|
|
with patch( |
|
|
"stroke_deepisles_demo.inference.docker.check_docker_available", |
|
|
return_value=True, |
|
|
): |
|
|
ensure_docker_available() |
|
|
|
|
|
|
|
|
class TestBuildDockerCommand: |
|
|
"""Tests for build_docker_command.""" |
|
|
|
|
|
def test_basic_command(self) -> None: |
|
|
"""Builds basic docker run command.""" |
|
|
cmd = build_docker_command("myimage:latest") |
|
|
|
|
|
assert cmd[0] == "docker" |
|
|
assert "run" in cmd |
|
|
assert "myimage:latest" in cmd |
|
|
|
|
|
def test_includes_rm_flag(self) -> None: |
|
|
"""Includes --rm when remove=True.""" |
|
|
cmd = build_docker_command("myimage", remove=True) |
|
|
|
|
|
assert "--rm" in cmd |
|
|
|
|
|
def test_excludes_rm_flag(self) -> None: |
|
|
"""Excludes --rm when remove=False.""" |
|
|
cmd = build_docker_command("myimage", remove=False) |
|
|
|
|
|
assert "--rm" not in cmd |
|
|
|
|
|
def test_includes_gpu_flag(self) -> None: |
|
|
"""Includes --gpus all when gpu=True.""" |
|
|
cmd = build_docker_command("myimage", gpu=True) |
|
|
|
|
|
assert "--gpus" in cmd |
|
|
gpu_index = cmd.index("--gpus") |
|
|
assert cmd[gpu_index + 1] == "all" |
|
|
|
|
|
def test_volume_mounts(self, temp_dir: Path) -> None: |
|
|
"""Includes volume mounts.""" |
|
|
volumes = {temp_dir: "/data"} |
|
|
cmd = build_docker_command("myimage", volumes=volumes) |
|
|
|
|
|
assert "-v" in cmd |
|
|
|
|
|
v_index = cmd.index("-v") |
|
|
assert f"{temp_dir}:/data" in cmd[v_index + 1] |
|
|
|
|
|
def test_custom_command(self) -> None: |
|
|
"""Appends custom command arguments.""" |
|
|
cmd = build_docker_command("myimage", command=["--input", "/data", "--fast", "True"]) |
|
|
|
|
|
assert "--input" in cmd |
|
|
assert "--fast" in cmd |
|
|
|
|
|
def test_match_user_on_linux(self) -> None: |
|
|
"""Adds --user flag on Linux when match_user=True.""" |
|
|
|
|
|
with ( |
|
|
patch("os.name", "posix"), |
|
|
patch("sys.platform", "linux"), |
|
|
patch("os.getuid", return_value=1000, create=True), |
|
|
patch("os.getgid", return_value=1000, create=True), |
|
|
): |
|
|
cmd = build_docker_command("myimage", match_user=True) |
|
|
assert "--user" in cmd |
|
|
assert "1000:1000" in cmd |
|
|
|
|
|
def test_no_match_user_on_mac(self) -> None: |
|
|
"""Does NOT add --user flag on Darwin.""" |
|
|
with patch("sys.platform", "darwin"): |
|
|
cmd = build_docker_command("myimage", match_user=True) |
|
|
assert "--user" not in cmd |
|
|
|
|
|
|
|
|
class TestRunContainer: |
|
|
"""Tests for run_container.""" |
|
|
|
|
|
def test_calls_subprocess_with_built_command(self) -> None: |
|
|
"""Calls subprocess.run with built command.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="output", stderr="") |
|
|
with patch("stroke_deepisles_demo.inference.docker.ensure_docker_available"): |
|
|
run_container("myimage") |
|
|
|
|
|
mock_run.assert_called_once() |
|
|
|
|
|
def test_returns_result_with_exit_code(self) -> None: |
|
|
"""Returns DockerRunResult with correct exit code.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=42, stdout="out", stderr="err") |
|
|
with patch("stroke_deepisles_demo.inference.docker.ensure_docker_available"): |
|
|
result = run_container("myimage") |
|
|
|
|
|
assert result.exit_code == 42 |
|
|
|
|
|
def test_captures_stdout_stderr(self) -> None: |
|
|
"""Captures stdout and stderr from container.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="hello", stderr="warning") |
|
|
with patch("stroke_deepisles_demo.inference.docker.ensure_docker_available"): |
|
|
result = run_container("myimage") |
|
|
|
|
|
assert result.stdout == "hello" |
|
|
assert result.stderr == "warning" |
|
|
|
|
|
def test_respects_timeout(self) -> None: |
|
|
"""Passes timeout to subprocess.""" |
|
|
with patch("subprocess.run") as mock_run: |
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") |
|
|
with patch("stroke_deepisles_demo.inference.docker.ensure_docker_available"): |
|
|
run_container("myimage", timeout=60.0) |
|
|
|
|
|
call_kwargs = mock_run.call_args.kwargs |
|
|
assert call_kwargs.get("timeout") == 60.0 |
|
|
|
|
|
|
|
|
@pytest.mark.integration |
|
|
class TestDockerIntegration: |
|
|
"""Integration tests requiring real Docker.""" |
|
|
|
|
|
def test_docker_actually_available(self) -> None: |
|
|
"""Docker is actually available on this system.""" |
|
|
|
|
|
|
|
|
available = check_docker_available() |
|
|
if not available: |
|
|
pytest.skip("Docker not available") |
|
|
|
|
|
assert available is True |
|
|
|
|
|
def test_can_run_hello_world(self) -> None: |
|
|
"""Can run docker hello-world container.""" |
|
|
if not check_docker_available(): |
|
|
pytest.skip("Docker not available") |
|
|
|
|
|
result = run_container("hello-world", timeout=60.0) |
|
|
|
|
|
assert result.exit_code == 0 |
|
|
assert "Hello from Docker!" in result.stdout |
|
|
|