db1d565b64
Integration Tests / melodic (push) Has been cancelled
Integration Tests / noetic (push) Has been cancelled
Integration Tests / humble (push) Has been cancelled
Integration Tests / jazzy (push) Has been cancelled
Ruff Lint & Format / ruff (push) Has been cancelled
Sync main to develop / Check if sync is needed (push) Has been cancelled
Sync main to develop / Sync main to develop (push) Has been cancelled
192 lines
5.0 KiB
Python
192 lines
5.0 KiB
Python
"""
|
|
Pytest configuration and fixtures for installation tests.
|
|
|
|
These tests use Docker to verify installation methods in clean environments.
|
|
All tests install from git (current branch) to validate before publishing.
|
|
"""
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Default repository URL for git-based installation
|
|
DEFAULT_REPO_URL = "https://github.com/robotmcp/ros-mcp-server.git"
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
"""Add command line options for installation tests."""
|
|
parser.addoption(
|
|
"--branch",
|
|
action="store",
|
|
default=None,
|
|
help="Git branch/tag to test installation from (default: current branch)",
|
|
)
|
|
parser.addoption(
|
|
"--repo-url",
|
|
action="store",
|
|
default=DEFAULT_REPO_URL,
|
|
help=f"Git repository URL (default: {DEFAULT_REPO_URL})",
|
|
)
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Register custom markers."""
|
|
config.addinivalue_line(
|
|
"markers", "installation: marks tests as installation tests (may be slow)"
|
|
)
|
|
|
|
|
|
def get_current_git_branch(repo_path: Path) -> str:
|
|
"""Get the current git branch name from the repository."""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=repo_path,
|
|
timeout=10,
|
|
)
|
|
if result.returncode == 0:
|
|
return result.stdout.strip()
|
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
pass
|
|
return "main" # Fallback to main if git command fails
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def repo_root() -> Path:
|
|
"""Return path to repository root."""
|
|
return Path(__file__).parent.parent.parent
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def docker_dir() -> Path:
|
|
"""Return path to docker directory containing Dockerfiles."""
|
|
return Path(__file__).parent / "docker"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def git_branch(request, repo_root) -> str:
|
|
"""Return the git branch to test installation from."""
|
|
branch = request.config.getoption("--branch")
|
|
if branch is None:
|
|
branch = get_current_git_branch(repo_root)
|
|
return branch
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def repo_url(request) -> str:
|
|
"""Return the git repository URL."""
|
|
return request.config.getoption("--repo-url")
|
|
|
|
|
|
def docker_available() -> bool:
|
|
"""Check if Docker is available and functional on the system."""
|
|
try:
|
|
# First check if docker command exists
|
|
result = subprocess.run(
|
|
["docker", "version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
if result.returncode != 0:
|
|
return False
|
|
|
|
# Also verify we can actually use Docker (not just have the command)
|
|
# This catches WSL2 cases where docker command exists but isn't connected
|
|
result = subprocess.run(
|
|
["docker", "info"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
return False
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def require_docker():
|
|
"""Skip all tests if Docker is not available."""
|
|
if not docker_available():
|
|
pytest.skip("Docker is not available or not functional")
|
|
|
|
|
|
def build_docker_image(
|
|
dockerfile_path: Path,
|
|
context_path: Path,
|
|
tag: str | None = None,
|
|
build_args: dict | None = None,
|
|
timeout: int = 300,
|
|
) -> subprocess.CompletedProcess:
|
|
"""
|
|
Build a Docker image from a Dockerfile.
|
|
|
|
Args:
|
|
dockerfile_path: Path to the Dockerfile
|
|
context_path: Path to the build context (usually repo root)
|
|
tag: Optional tag for the image
|
|
build_args: Optional dict of build arguments
|
|
timeout: Build timeout in seconds (default 5 minutes)
|
|
|
|
Returns:
|
|
CompletedProcess with build output
|
|
"""
|
|
cmd = ["docker", "build", "-f", str(dockerfile_path)]
|
|
|
|
if tag:
|
|
cmd.extend(["-t", tag])
|
|
|
|
if build_args:
|
|
for key, value in build_args.items():
|
|
cmd.extend(["--build-arg", f"{key}={value}"])
|
|
|
|
cmd.append(str(context_path))
|
|
|
|
return subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def run_docker_container(
|
|
image: str,
|
|
command: str | None = None,
|
|
timeout: int = 60,
|
|
) -> subprocess.CompletedProcess:
|
|
"""
|
|
Run a Docker container.
|
|
|
|
Args:
|
|
image: Image name or tag to run
|
|
command: Optional command to run in container
|
|
timeout: Run timeout in seconds
|
|
|
|
Returns:
|
|
CompletedProcess with run output
|
|
"""
|
|
cmd = ["docker", "run", "--rm", image]
|
|
|
|
if command:
|
|
cmd.extend(["sh", "-c", command])
|
|
|
|
return subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def cleanup_docker_image(tag: str) -> None:
|
|
"""Remove a Docker image by tag."""
|
|
subprocess.run(
|
|
["docker", "rmi", "-f", tag],
|
|
capture_output=True,
|
|
timeout=30,
|
|
)
|