initial commit after one day coding agent session
This commit is contained in:
commit
372af75b90
88 changed files with 22694 additions and 0 deletions
304
tests/builder/test_uv_installer.py
Normal file
304
tests/builder/test_uv_installer.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"""Tests for UV Package Installer module."""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, mock_open
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from mcp_forge.builder.uv_installer import UVInstaller
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache_path(tmp_path):
|
||||
"""Temporary cache directory."""
|
||||
cache_dir = tmp_path / "uv_cache"
|
||||
return cache_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def installer(cache_path):
|
||||
"""UVInstaller instance."""
|
||||
return UVInstaller(cache_path)
|
||||
|
||||
|
||||
def test_init_creates_cache_directory(cache_path):
|
||||
"""Test that __init__ creates cache directory."""
|
||||
assert not cache_path.exists()
|
||||
|
||||
installer = UVInstaller(cache_path)
|
||||
|
||||
assert cache_path.exists()
|
||||
assert cache_path.is_dir()
|
||||
|
||||
|
||||
def test_init_with_existing_cache(cache_path):
|
||||
"""Test initialization with existing cache directory."""
|
||||
cache_path.mkdir(parents=True)
|
||||
|
||||
installer = UVInstaller(cache_path)
|
||||
|
||||
assert cache_path.exists()
|
||||
|
||||
|
||||
def test_generate_requirements_single_package(installer):
|
||||
"""Test generating requirements.txt with single package."""
|
||||
packages = ["numpy>=1.24.0"]
|
||||
|
||||
requirements = installer.generate_requirements(packages)
|
||||
|
||||
assert requirements == "numpy>=1.24.0"
|
||||
|
||||
|
||||
def test_generate_requirements_multiple_packages(installer):
|
||||
"""Test generating requirements.txt with multiple packages."""
|
||||
packages = ["numpy>=1.24.0", "pandas==2.0.0", "requests"]
|
||||
|
||||
requirements = installer.generate_requirements(packages)
|
||||
|
||||
lines = requirements.strip().split('\n')
|
||||
assert len(lines) == 3
|
||||
assert "numpy>=1.24.0" in lines
|
||||
assert "pandas==2.0.0" in lines
|
||||
assert "requests" in lines
|
||||
|
||||
|
||||
def test_generate_requirements_empty_list(installer):
|
||||
"""Test generating requirements.txt with empty list."""
|
||||
packages = []
|
||||
|
||||
requirements = installer.generate_requirements(packages)
|
||||
|
||||
assert requirements == ""
|
||||
|
||||
|
||||
def test_generate_containerfile_structure(installer):
|
||||
"""Test Containerfile has correct structure."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy>=1.24.0", "pandas"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Check key sections are present
|
||||
assert f"FROM {base_image}" in containerfile
|
||||
assert "pip install" in containerfile and "uv" in containerfile
|
||||
assert "COPY" in containerfile and "requirements.txt" in containerfile
|
||||
assert "RUN uv pip install" in containerfile
|
||||
assert "WORKDIR" in containerfile
|
||||
|
||||
|
||||
def test_generate_containerfile_with_python_version(installer):
|
||||
"""Test Containerfile generation with specific Python version."""
|
||||
base_image = "docker.io/python:3.12-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
containerfile = installer.generate_containerfile(
|
||||
base_image, packages, python_version="3.12"
|
||||
)
|
||||
|
||||
assert "3.12" in containerfile or "python:3.12" in base_image
|
||||
|
||||
|
||||
def test_generate_containerfile_uses_requirements(installer):
|
||||
"""Test Containerfile copies and uses requirements.txt."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy", "pandas"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Should copy requirements.txt for layer caching
|
||||
assert "COPY" in containerfile and "requirements.txt" in containerfile
|
||||
assert "-r requirements.txt" in containerfile
|
||||
|
||||
|
||||
def test_generate_containerfile_creates_user(installer):
|
||||
"""Test Containerfile creates non-root user."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Should create user for security
|
||||
assert "useradd" in containerfile.lower() or "USER" in containerfile
|
||||
|
||||
|
||||
def test_create_build_context_creates_directory(installer):
|
||||
"""Test that build context directory is created."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy", "pandas"]
|
||||
|
||||
context_path = installer.create_build_context(base_image, packages)
|
||||
|
||||
try:
|
||||
assert context_path.exists()
|
||||
assert context_path.is_dir()
|
||||
finally:
|
||||
if context_path.exists():
|
||||
shutil.rmtree(context_path)
|
||||
|
||||
|
||||
def test_create_build_context_contains_containerfile(installer):
|
||||
"""Test that build context contains Containerfile."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
context_path = installer.create_build_context(base_image, packages)
|
||||
|
||||
try:
|
||||
containerfile_path = context_path / "Containerfile"
|
||||
assert containerfile_path.exists()
|
||||
|
||||
content = containerfile_path.read_text()
|
||||
assert f"FROM {base_image}" in content
|
||||
finally:
|
||||
if context_path.exists():
|
||||
shutil.rmtree(context_path)
|
||||
|
||||
|
||||
def test_create_build_context_contains_requirements(installer):
|
||||
"""Test that build context contains requirements.txt."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy>=1.24.0", "pandas"]
|
||||
|
||||
context_path = installer.create_build_context(base_image, packages)
|
||||
|
||||
try:
|
||||
requirements_path = context_path / "requirements.txt"
|
||||
assert requirements_path.exists()
|
||||
|
||||
content = requirements_path.read_text()
|
||||
assert "numpy>=1.24.0" in content
|
||||
assert "pandas" in content
|
||||
finally:
|
||||
if context_path.exists():
|
||||
shutil.rmtree(context_path)
|
||||
|
||||
|
||||
def test_create_build_context_returns_temp_directory(installer):
|
||||
"""Test that build context is in temp directory."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
context_path = installer.create_build_context(base_image, packages)
|
||||
|
||||
try:
|
||||
# Should be in system temp directory
|
||||
temp_dir = Path(tempfile.gettempdir())
|
||||
assert temp_dir in context_path.parents
|
||||
finally:
|
||||
if context_path.exists():
|
||||
shutil.rmtree(context_path)
|
||||
|
||||
|
||||
def test_get_cache_volume_mount_returns_dict(installer):
|
||||
"""Test cache volume mount returns proper dict."""
|
||||
mount_config = installer.get_cache_volume_mount()
|
||||
|
||||
assert isinstance(mount_config, dict)
|
||||
assert "bind" in mount_config
|
||||
assert "mode" in mount_config
|
||||
assert mount_config["mode"] == "rw"
|
||||
|
||||
|
||||
def test_get_cache_volume_mount_includes_cache_path(installer, cache_path):
|
||||
"""Test cache volume mount includes cache path."""
|
||||
mount_config = installer.get_cache_volume_mount()
|
||||
|
||||
# The bind target should reference UV cache location
|
||||
assert "bind" in mount_config
|
||||
assert "/cache" in mount_config["bind"] or "uv" in mount_config["bind"].lower()
|
||||
|
||||
|
||||
def test_generate_containerfile_security_practices(installer):
|
||||
"""Test Containerfile follows security best practices."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Should not run as root
|
||||
assert "USER" in containerfile
|
||||
|
||||
# Should set working directory
|
||||
assert "WORKDIR" in containerfile
|
||||
|
||||
|
||||
def test_generate_requirements_preserves_version_specs(installer):
|
||||
"""Test that version specifiers are preserved exactly."""
|
||||
packages = [
|
||||
"numpy>=1.24.0,<2.0.0",
|
||||
"pandas==2.0.0",
|
||||
"requests~=2.28.0",
|
||||
"scipy!=1.10.0"
|
||||
]
|
||||
|
||||
requirements = installer.generate_requirements(packages)
|
||||
|
||||
for package in packages:
|
||||
assert package in requirements
|
||||
|
||||
|
||||
def test_generate_containerfile_with_extras(installer):
|
||||
"""Test Containerfile works with package extras."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["requests[security]>=2.28.0", "pandas[excel]"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Should handle extras in requirements
|
||||
assert "COPY" in containerfile and "requirements.txt" in containerfile
|
||||
assert "RUN uv pip install" in containerfile
|
||||
|
||||
|
||||
def test_create_build_context_with_custom_python_version(installer):
|
||||
"""Test build context creation with custom Python version."""
|
||||
base_image = "docker.io/python:3.12-slim"
|
||||
packages = ["numpy"]
|
||||
|
||||
context_path = installer.create_build_context(
|
||||
base_image, packages, python_version="3.12"
|
||||
)
|
||||
|
||||
try:
|
||||
containerfile = (context_path / "Containerfile").read_text()
|
||||
assert "3.12" in containerfile or "python:3.12" in containerfile
|
||||
finally:
|
||||
if context_path.exists():
|
||||
shutil.rmtree(context_path)
|
||||
|
||||
|
||||
def test_generate_containerfile_optimizes_layer_caching(installer):
|
||||
"""Test that Containerfile structure optimizes Docker layer caching."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = ["numpy", "pandas", "scipy"]
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
lines = containerfile.split('\n')
|
||||
|
||||
# UV install should come before requirements copy
|
||||
uv_install_idx = None
|
||||
requirements_idx = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if 'uv' in line.lower() and ('pip install' in line.lower() or 'ADD' in line):
|
||||
uv_install_idx = i
|
||||
if 'COPY' in line and 'requirements.txt' in line:
|
||||
requirements_idx = i
|
||||
|
||||
# UV installation should be cached separately
|
||||
assert uv_install_idx is not None
|
||||
# Requirements copy should happen for cache busting
|
||||
assert requirements_idx is not None
|
||||
|
||||
|
||||
def test_empty_packages_list_creates_valid_containerfile(installer):
|
||||
"""Test that empty package list still creates valid Containerfile."""
|
||||
base_image = "docker.io/python:3.11-slim"
|
||||
packages = []
|
||||
|
||||
containerfile = installer.generate_containerfile(base_image, packages)
|
||||
|
||||
# Should still have base structure
|
||||
assert f"FROM {base_image}" in containerfile
|
||||
assert "WORKDIR" in containerfile
|
||||
Loading…
Add table
Add a link
Reference in a new issue