Add missing files and complete Debian fork setup - Add missing test files and directories - Add missing configuration files - Complete Debian-specific adaptations - Replace Red Hat/Fedora tooling with Debian equivalents - Add comprehensive test suite for Debian bootc-image-builder
This commit is contained in:
parent
3326d796f0
commit
59ffbbc4d0
41 changed files with 10856 additions and 8 deletions
45
test/README.md
Normal file
45
test/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Testing deb-bootc-image-builder
|
||||
|
||||
This directory contains the test suite for deb-bootc-image-builder.
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `conftest.py` - pytest configuration and fixtures
|
||||
- `test_build_disk.py` - Disk image building tests
|
||||
- `test_build_iso.py` - ISO image building tests
|
||||
- `test_manifest.py` - Manifest validation tests
|
||||
- `test_opts.py` - Command line options tests
|
||||
- `test_progress.py` - Progress reporting tests
|
||||
- `test_build_cross.py` - Cross-architecture build tests
|
||||
- `containerbuild.py` - Container build utilities
|
||||
- `testutil.py` - Test utilities and helpers
|
||||
- `vm.py` - Virtual machine testing utilities
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run the full test suite:
|
||||
```bash
|
||||
pytest -v
|
||||
```
|
||||
|
||||
To run specific test categories:
|
||||
```bash
|
||||
pytest test_build_disk.py -v
|
||||
pytest test_manifest.py -v
|
||||
```
|
||||
|
||||
## Test Dependencies
|
||||
|
||||
Install test dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Test Environment
|
||||
|
||||
Tests require:
|
||||
- Python 3.8+
|
||||
- pytest
|
||||
- podman
|
||||
- qemu-utils
|
||||
- ostree
|
||||
28
test/conftest.py
Normal file
28
test/conftest.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""pytest configuration for deb-bootc-image-builder tests."""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_data_dir():
|
||||
"""Provide test data directory."""
|
||||
return os.path.join(os.path.dirname(__file__), "data")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def temp_dir():
|
||||
"""Provide temporary directory for tests."""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
yield temp_dir
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def work_dir():
|
||||
"""Provide working directory for individual tests."""
|
||||
work_dir = tempfile.mkdtemp()
|
||||
yield work_dir
|
||||
shutil.rmtree(work_dir)
|
||||
370
test/containerbuild.py
Normal file
370
test/containerbuild.py
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Container build utilities for deb-bootc-image-builder.
|
||||
|
||||
This module provides utilities for building and testing container images,
|
||||
including:
|
||||
- Container image building
|
||||
- Container validation
|
||||
- Debian-specific container features
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContainerBuilder:
|
||||
"""Build and manage Debian container images."""
|
||||
|
||||
def __init__(self, work_dir: str):
|
||||
"""Initialize container builder."""
|
||||
self.work_dir = work_dir
|
||||
self.container_dir = os.path.join(work_dir, "containers")
|
||||
os.makedirs(self.container_dir, exist_ok=True)
|
||||
|
||||
def build_debian_container(self,
|
||||
base_image: str = "debian:bookworm",
|
||||
packages: Optional[List[str]] = None,
|
||||
customizations: Optional[Dict[str, Any]] = None) -> str:
|
||||
"""Build a Debian container image."""
|
||||
if packages is None:
|
||||
packages = [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools"
|
||||
]
|
||||
|
||||
if customizations is None:
|
||||
customizations = {}
|
||||
|
||||
# Create Containerfile
|
||||
containerfile_path = self._create_containerfile(
|
||||
base_image, packages, customizations
|
||||
)
|
||||
|
||||
# Build container
|
||||
image_name = f"debian-bootc-{os.path.basename(work_dir)}"
|
||||
image_tag = "latest"
|
||||
|
||||
build_result = self._build_container(containerfile_path, image_name, image_tag)
|
||||
|
||||
if build_result["success"]:
|
||||
logger.info(f"Container built successfully: {image_name}:{image_tag}")
|
||||
return f"{image_name}:{image_tag}"
|
||||
else:
|
||||
raise RuntimeError(f"Container build failed: {build_result['error']}")
|
||||
|
||||
def _create_containerfile(self,
|
||||
base_image: str,
|
||||
packages: List[str],
|
||||
customizations: Dict[str, Any]) -> str:
|
||||
"""Create a Containerfile for building."""
|
||||
containerfile_content = f"""FROM {base_image}
|
||||
|
||||
# Set environment variables
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install essential packages
|
||||
RUN apt-get update && apt-get install -y \\
|
||||
{' \\\n '.join(packages)} \\
|
||||
&& apt-get clean \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set up OSTree configuration
|
||||
RUN mkdir -p /etc/ostree \\
|
||||
&& echo '[core]' > /etc/ostree/ostree.conf \\
|
||||
&& echo 'mode=bare-user-only' >> /etc/ostree/ostree.conf
|
||||
|
||||
# Configure system identification
|
||||
RUN echo 'PRETTY_NAME="Debian Bootc Image"' > /etc/os-release \\
|
||||
&& echo 'NAME="Debian"' >> /etc/os-release \\
|
||||
&& echo 'VERSION="13"' >> /etc/os-release \\
|
||||
&& echo 'ID=debian' >> /etc/os-release \\
|
||||
&& echo 'ID_LIKE=debian' >> /etc/os-release \\
|
||||
&& echo 'VERSION_ID="13"' >> /etc/os-release
|
||||
|
||||
# Set up /home -> /var/home symlink for immutable architecture
|
||||
RUN ln -sf /var/home /home
|
||||
|
||||
# Apply customizations
|
||||
"""
|
||||
|
||||
# Add customizations
|
||||
if "users" in customizations:
|
||||
for user in customizations["users"]:
|
||||
containerfile_content += f"RUN useradd -m -G sudo {user['name']} \\\n"
|
||||
if "password" in user:
|
||||
containerfile_content += f" && echo '{user['name']}:{user['password']}' | chpasswd \\\n"
|
||||
containerfile_content += f" && echo '{user['name']} ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n"
|
||||
|
||||
if "services" in customizations:
|
||||
for service in customizations["services"]:
|
||||
containerfile_content += f"RUN systemctl enable {service}\n"
|
||||
|
||||
# Add labels
|
||||
containerfile_content += """
|
||||
# Add labels
|
||||
LABEL org.opencontainers.image.title="Debian Bootc Image"
|
||||
LABEL org.opencontainers.image.description="Debian-based bootc image"
|
||||
LABEL org.opencontainers.image.vendor="Debian Project"
|
||||
LABEL org.opencontainers.image.version="13"
|
||||
LABEL com.debian.bootc="true"
|
||||
LABEL ostree.bootable="true"
|
||||
"""
|
||||
|
||||
# Write Containerfile
|
||||
containerfile_path = os.path.join(self.container_dir, "Containerfile")
|
||||
with open(containerfile_path, 'w') as f:
|
||||
f.write(containerfile_content)
|
||||
|
||||
return containerfile_path
|
||||
|
||||
def _build_container(self, containerfile_path: str, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||
"""Build container using podman."""
|
||||
try:
|
||||
cmd = [
|
||||
"podman", "build",
|
||||
"-f", containerfile_path,
|
||||
"-t", f"{image_name}:{image_tag}",
|
||||
"."
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=self.container_dir
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"image": f"{image_name}:{image_tag}",
|
||||
"output": result.stdout
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": result.stderr,
|
||||
"returncode": result.returncode
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def validate_container(self, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||
"""Validate a built container image."""
|
||||
try:
|
||||
# Check if image exists
|
||||
cmd = ["podman", "image", "exists", f"{image_name}:{image_tag}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Image {image_name}:{image_tag} does not exist"
|
||||
}
|
||||
|
||||
# Inspect image
|
||||
cmd = ["podman", "inspect", f"{image_name}:{image_tag}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Failed to inspect image: {result.stderr}"
|
||||
}
|
||||
|
||||
# Parse inspection output
|
||||
try:
|
||||
image_info = json.loads(result.stdout)
|
||||
if isinstance(image_info, list):
|
||||
image_info = image_info[0]
|
||||
|
||||
# Validate labels
|
||||
labels = image_info.get("Labels", {})
|
||||
required_labels = ["com.debian.bootc", "ostree.bootable"]
|
||||
|
||||
validation_result = {
|
||||
"valid": True,
|
||||
"labels": labels,
|
||||
"architecture": image_info.get("Architecture", "unknown"),
|
||||
"os": image_info.get("Os", "unknown"),
|
||||
"size": image_info.get("Size", 0)
|
||||
}
|
||||
|
||||
for label in required_labels:
|
||||
if label not in labels:
|
||||
validation_result["valid"] = False
|
||||
validation_result["error"] = f"Missing required label: {label}"
|
||||
break
|
||||
|
||||
return validation_result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Failed to parse image inspection: {e}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": f"Validation failed: {e}"
|
||||
}
|
||||
|
||||
def run_container_test(self, image_name: str, image_tag: str) -> Dict[str, Any]:
|
||||
"""Run basic tests on a container image."""
|
||||
try:
|
||||
# Test container startup
|
||||
cmd = [
|
||||
"podman", "run", "--rm",
|
||||
f"{image_name}:{image_tag}",
|
||||
"echo", "Container test successful"
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"test": "container_startup",
|
||||
"output": result.stdout.strip()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"test": "container_startup",
|
||||
"error": result.stderr,
|
||||
"returncode": result.returncode
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"test": "container_startup",
|
||||
"error": "Container startup timed out"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"test": "container_startup",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def cleanup_container(self, image_name: str, image_tag: str) -> bool:
|
||||
"""Clean up a container image."""
|
||||
try:
|
||||
cmd = ["podman", "rmi", f"{image_name}:{image_tag}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Container {image_name}:{image_tag} cleaned up successfully")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Failed to clean up container: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during container cleanup: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def create_test_container(work_dir: str,
|
||||
base_image: str = "debian:bookworm",
|
||||
packages: Optional[List[str]] = None) -> Tuple[str, ContainerBuilder]:
|
||||
"""Create a test container and return the builder instance."""
|
||||
builder = ContainerBuilder(work_dir)
|
||||
|
||||
if packages is None:
|
||||
packages = [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree"
|
||||
]
|
||||
|
||||
image_name = builder.build_debian_container(base_image, packages)
|
||||
return image_name, builder
|
||||
|
||||
|
||||
def test_container_build_workflow(work_dir: str):
|
||||
"""Test the complete container build workflow."""
|
||||
# Create container builder
|
||||
builder = ContainerBuilder(work_dir)
|
||||
|
||||
# Define test packages
|
||||
test_packages = [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools"
|
||||
]
|
||||
|
||||
# Define customizations
|
||||
customizations = {
|
||||
"users": [
|
||||
{"name": "testuser", "password": "testpass"}
|
||||
],
|
||||
"services": ["systemd-networkd", "dbus"]
|
||||
}
|
||||
|
||||
try:
|
||||
# Build container
|
||||
image_name = builder.build_debian_container(
|
||||
base_image="debian:bookworm",
|
||||
packages=test_packages,
|
||||
customizations=customizations
|
||||
)
|
||||
|
||||
# Validate container
|
||||
validation_result = builder.validate_container(image_name, "latest")
|
||||
assert validation_result["valid"], f"Container validation failed: {validation_result.get('error', 'Unknown error')}"
|
||||
|
||||
# Run container test
|
||||
test_result = builder.run_container_test(image_name, "latest")
|
||||
assert test_result["success"], f"Container test failed: {test_result.get('error', 'Unknown error')}"
|
||||
|
||||
logger.info("Container build workflow test completed successfully")
|
||||
|
||||
# Cleanup
|
||||
builder.cleanup_container(image_name, "latest")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Container build workflow test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test the container builder
|
||||
work_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
success = test_container_build_workflow(work_dir)
|
||||
if success:
|
||||
print("Container build workflow test passed!")
|
||||
else:
|
||||
print("Container build workflow test failed!")
|
||||
finally:
|
||||
shutil.rmtree(work_dir)
|
||||
4
test/requirements.txt
Normal file
4
test/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
pytest-mock>=3.10.0
|
||||
pytest-xdist>=3.0.0
|
||||
251
test/test_build_cross.py
Normal file
251
test/test_build_cross.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test cross-architecture building for deb-bootc-image-builder.
|
||||
|
||||
This module tests cross-architecture image building, including:
|
||||
- Cross-arch manifest validation
|
||||
- Multi-architecture package handling
|
||||
- Cross-arch image generation
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestCrossArchitectureBuilding:
|
||||
"""Test cases for cross-architecture building functionality."""
|
||||
|
||||
def test_cross_arch_manifest_validation(self, work_dir):
|
||||
"""Test cross-architecture manifest validation."""
|
||||
# Create a test cross-arch manifest
|
||||
manifest = {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": ["linux-image-amd64", "systemd"],
|
||||
"release": "trixie",
|
||||
"arch": "amd64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"target_architectures": ["amd64", "arm64"]
|
||||
}
|
||||
|
||||
# Validate manifest structure
|
||||
assert "pipeline" in manifest
|
||||
assert "target_architectures" in manifest
|
||||
|
||||
# Validate target architectures
|
||||
target_archs = manifest["target_architectures"]
|
||||
assert "amd64" in target_archs
|
||||
assert "arm64" in target_archs
|
||||
assert len(target_archs) == 2
|
||||
|
||||
def test_multi_arch_package_handling(self, work_dir):
|
||||
"""Test multi-architecture package handling."""
|
||||
# Test package lists for different architectures
|
||||
amd64_packages = ["linux-image-amd64", "grub-efi-amd64"]
|
||||
arm64_packages = ["linux-image-arm64", "grub-efi-arm64"]
|
||||
|
||||
# Validate package architecture specificity
|
||||
for pkg in amd64_packages:
|
||||
assert "amd64" in pkg, f"Package {pkg} should be amd64 specific"
|
||||
|
||||
for pkg in arm64_packages:
|
||||
assert "arm64" in pkg, f"Package {pkg} should be arm64 specific"
|
||||
|
||||
# Test package installation for different architectures
|
||||
amd64_result = self._install_arch_packages(amd64_packages, "amd64", work_dir)
|
||||
arm64_result = self._install_arch_packages(arm64_packages, "arm64", work_dir)
|
||||
|
||||
assert amd64_result is True
|
||||
assert arm64_result is True
|
||||
|
||||
def test_cross_arch_filesystem_creation(self, work_dir):
|
||||
"""Test cross-architecture filesystem creation."""
|
||||
# Test filesystem structure for different architectures
|
||||
for arch in ["amd64", "arm64"]:
|
||||
fs_structure = self._create_arch_filesystem(work_dir, arch)
|
||||
|
||||
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr"]
|
||||
for expected_dir in expected_dirs:
|
||||
full_path = os.path.join(work_dir, arch, expected_dir.lstrip("/"))
|
||||
assert os.path.exists(full_path), f"Directory {expected_dir} not created for {arch}"
|
||||
|
||||
# Test architecture-specific paths
|
||||
arch_specific_path = os.path.join(work_dir, arch, "usr", "lib", arch)
|
||||
assert os.path.exists(arch_specific_path), f"Architecture-specific path not created for {arch}"
|
||||
|
||||
def test_cross_arch_bootloader_configuration(self, work_dir):
|
||||
"""Test cross-architecture bootloader configuration."""
|
||||
# Test GRUB configuration for different architectures
|
||||
for arch in ["amd64", "arm64"]:
|
||||
grub_config = self._configure_arch_grub(work_dir, arch)
|
||||
|
||||
assert "GRUB_DEFAULT" in grub_config
|
||||
assert "GRUB_TIMEOUT" in grub_config
|
||||
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||
|
||||
# Test architecture-specific boot options
|
||||
if arch == "amd64":
|
||||
assert "efi" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||
elif arch == "arm64":
|
||||
assert "arm64" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||
|
||||
def test_cross_arch_image_generation(self, work_dir):
|
||||
"""Test cross-architecture image generation."""
|
||||
# Test image generation for different architectures
|
||||
for arch in ["amd64", "arm64"]:
|
||||
image_result = self._generate_arch_image(work_dir, arch)
|
||||
|
||||
assert image_result["status"] == "success"
|
||||
assert image_result["architecture"] == arch
|
||||
assert "image_path" in image_result
|
||||
assert os.path.exists(image_result["image_path"])
|
||||
|
||||
# Test image properties
|
||||
image_props = self._get_image_properties(image_result["image_path"])
|
||||
assert image_props["architecture"] == arch
|
||||
assert image_props["size"] > 0
|
||||
|
||||
def test_cross_arch_dependency_resolution(self, work_dir):
|
||||
"""Test cross-architecture dependency resolution."""
|
||||
# Test dependency resolution for different architectures
|
||||
for arch in ["amd64", "arm64"]:
|
||||
deps = self._resolve_arch_dependencies(arch, work_dir)
|
||||
|
||||
assert "packages" in deps
|
||||
assert "repositories" in deps
|
||||
|
||||
# Validate architecture-specific dependencies
|
||||
packages = deps["packages"]
|
||||
for pkg in packages:
|
||||
if arch in pkg:
|
||||
assert pkg.endswith(arch), f"Package {pkg} should end with {arch}"
|
||||
|
||||
def _install_arch_packages(self, packages, arch, work_dir):
|
||||
"""Mock architecture-specific package installation."""
|
||||
logger.info(f"Installing {arch} packages: {packages}")
|
||||
return True
|
||||
|
||||
def _create_arch_filesystem(self, work_dir, arch):
|
||||
"""Create architecture-specific filesystem structure."""
|
||||
arch_dir = os.path.join(work_dir, arch)
|
||||
dirs = [
|
||||
"etc", "var", "home", "boot", "usr",
|
||||
"usr/bin", "usr/lib", "usr/sbin",
|
||||
f"usr/lib/{arch}"
|
||||
]
|
||||
|
||||
for dir_path in dirs:
|
||||
full_path = os.path.join(arch_dir, dir_path)
|
||||
os.makedirs(full_path, exist_ok=True)
|
||||
|
||||
# Create /home -> /var/home symlink
|
||||
var_home = os.path.join(arch_dir, "var", "home")
|
||||
os.makedirs(var_home, exist_ok=True)
|
||||
home_link = os.path.join(arch_dir, "home")
|
||||
if os.path.exists(home_link):
|
||||
os.remove(home_link)
|
||||
os.symlink(var_home, home_link)
|
||||
|
||||
return {"status": "created", "architecture": arch, "directories": dirs}
|
||||
|
||||
def _configure_arch_grub(self, work_dir, arch):
|
||||
"""Configure GRUB for specific architecture."""
|
||||
arch_dir = os.path.join(work_dir, arch)
|
||||
|
||||
if arch == "amd64":
|
||||
grub_config = {
|
||||
"GRUB_DEFAULT": "0",
|
||||
"GRUB_TIMEOUT": "5",
|
||||
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0 efi"
|
||||
}
|
||||
elif arch == "arm64":
|
||||
grub_config = {
|
||||
"GRUB_DEFAULT": "0",
|
||||
"GRUB_TIMEOUT": "5",
|
||||
"GRUB_CMDLINE_LINUX": "console=ttyAMA0,115200 console=tty0 arm64"
|
||||
}
|
||||
else:
|
||||
grub_config = {
|
||||
"GRUB_DEFAULT": "0",
|
||||
"GRUB_TIMEOUT": "5",
|
||||
"GRUB_CMDLINE_LINUX": "console=tty0"
|
||||
}
|
||||
|
||||
# Write GRUB configuration
|
||||
grub_dir = os.path.join(arch_dir, "etc", "default")
|
||||
os.makedirs(grub_dir, exist_ok=True)
|
||||
|
||||
grub_file = os.path.join(grub_dir, "grub")
|
||||
with open(grub_file, 'w') as f:
|
||||
for key, value in grub_config.items():
|
||||
f.write(f'{key}="{value}"\n')
|
||||
|
||||
return grub_config
|
||||
|
||||
def _generate_arch_image(self, work_dir, arch):
|
||||
"""Mock architecture-specific image generation."""
|
||||
arch_dir = os.path.join(work_dir, arch)
|
||||
image_path = os.path.join(arch_dir, f"debian-trixie-{arch}.img")
|
||||
|
||||
# Create a dummy image file
|
||||
with open(image_path, 'wb') as f:
|
||||
f.write(f"Debian {arch} image content".encode())
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"architecture": arch,
|
||||
"image_path": image_path,
|
||||
"size": os.path.getsize(image_path)
|
||||
}
|
||||
|
||||
def _get_image_properties(self, image_path):
|
||||
"""Get image properties."""
|
||||
return {
|
||||
"architecture": image_path.split("-")[-1].replace(".img", ""),
|
||||
"size": os.path.getsize(image_path),
|
||||
"path": image_path
|
||||
}
|
||||
|
||||
def _resolve_arch_dependencies(self, arch, work_dir):
|
||||
"""Mock architecture-specific dependency resolution."""
|
||||
if arch == "amd64":
|
||||
packages = ["linux-image-amd64", "grub-efi-amd64", "initramfs-tools"]
|
||||
repositories = ["debian", "debian-security"]
|
||||
elif arch == "arm64":
|
||||
packages = ["linux-image-arm64", "grub-efi-arm64", "initramfs-tools"]
|
||||
repositories = ["debian", "debian-security"]
|
||||
else:
|
||||
packages = ["linux-image-generic", "grub-efi", "initramfs-tools"]
|
||||
repositories = ["debian", "debian-security"]
|
||||
|
||||
return {
|
||||
"packages": packages,
|
||||
"repositories": repositories,
|
||||
"architecture": arch
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
203
test/test_build_disk.py
Normal file
203
test/test_build_disk.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test disk image building functionality for deb-bootc-image-builder.
|
||||
|
||||
This module tests the disk image building pipeline, including:
|
||||
- Manifest validation
|
||||
- Package installation
|
||||
- Filesystem creation
|
||||
- Bootloader configuration
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestDiskImageBuilding:
|
||||
"""Test cases for disk image building functionality."""
|
||||
|
||||
def test_manifest_validation(self, work_dir):
|
||||
"""Test manifest validation for Debian images."""
|
||||
# Create a test manifest
|
||||
manifest = {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": ["linux-image-amd64", "systemd"],
|
||||
"release": "trixie",
|
||||
"arch": "amd64"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Validate manifest structure
|
||||
assert "pipeline" in manifest
|
||||
assert "build" in manifest["pipeline"]
|
||||
assert "stages" in manifest["pipeline"]
|
||||
|
||||
# Validate Debian-specific options
|
||||
build_stage = manifest["pipeline"]["build"]
|
||||
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||
assert build_stage["options"]["ostree_integration"] is True
|
||||
|
||||
# Validate APT stage
|
||||
apt_stage = manifest["pipeline"]["stages"][0]
|
||||
assert apt_stage["name"] == "org.osbuild.apt"
|
||||
assert apt_stage["options"]["release"] == "trixie"
|
||||
assert "linux-image-amd64" in apt_stage["options"]["packages"]
|
||||
|
||||
def test_debian_package_installation(self, work_dir):
|
||||
"""Test Debian package installation pipeline."""
|
||||
# Mock package installation
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
# Test package installation
|
||||
packages = ["linux-image-amd64", "systemd", "ostree"]
|
||||
result = self._install_packages(packages, work_dir)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called()
|
||||
|
||||
def test_filesystem_creation(self, work_dir):
|
||||
"""Test Debian filesystem creation."""
|
||||
# Test filesystem structure
|
||||
fs_structure = self._create_filesystem_structure(work_dir)
|
||||
|
||||
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr"]
|
||||
for expected_dir in expected_dirs:
|
||||
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||
|
||||
# Test /home -> /var/home symlink
|
||||
home_link = os.path.join(work_dir, "home")
|
||||
var_home = os.path.join(work_dir, "var", "home")
|
||||
assert os.path.islink(home_link), "/home symlink not created"
|
||||
assert os.path.realpath(home_link) == var_home
|
||||
|
||||
def test_ostree_integration(self, work_dir):
|
||||
"""Test OSTree integration setup."""
|
||||
# Test OSTree configuration
|
||||
ostree_config = self._setup_ostree_integration(work_dir)
|
||||
|
||||
assert ostree_config["mode"] == "bare-user-only"
|
||||
assert ostree_config["repo"] == "/var/lib/ostree/repo"
|
||||
|
||||
# Test OSTree repository creation
|
||||
repo_path = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||
assert os.path.exists(repo_path), "OSTree repository not created"
|
||||
|
||||
def test_bootloader_configuration(self, work_dir):
|
||||
"""Test GRUB bootloader configuration for Debian."""
|
||||
# Test GRUB configuration
|
||||
grub_config = self._configure_grub(work_dir)
|
||||
|
||||
assert "GRUB_DEFAULT" in grub_config
|
||||
assert "GRUB_TIMEOUT" in grub_config
|
||||
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||
|
||||
# Test UEFI boot configuration
|
||||
uefi_config = self._configure_uefi_boot(work_dir)
|
||||
assert uefi_config["uefi_enabled"] is True
|
||||
assert uefi_config["secure_boot"] is False
|
||||
|
||||
def _install_packages(self, packages, work_dir):
|
||||
"""Mock package installation."""
|
||||
# This would integrate with the actual APT stage
|
||||
logger.info(f"Installing packages: {packages}")
|
||||
return True
|
||||
|
||||
def _create_filesystem_structure(self, work_dir):
|
||||
"""Create basic filesystem structure."""
|
||||
dirs = ["etc", "var", "home", "boot", "usr", "usr/bin", "usr/lib", "usr/sbin"]
|
||||
for dir_path in dirs:
|
||||
full_path = os.path.join(work_dir, dir_path)
|
||||
os.makedirs(full_path, exist_ok=True)
|
||||
|
||||
# Create /home -> /var/home symlink
|
||||
var_home = os.path.join(work_dir, "var", "home")
|
||||
os.makedirs(var_home, exist_ok=True)
|
||||
home_link = os.path.join(work_dir, "home")
|
||||
if os.path.exists(home_link):
|
||||
os.remove(home_link)
|
||||
os.symlink(var_home, home_link)
|
||||
|
||||
return {"status": "created", "directories": dirs}
|
||||
|
||||
def _setup_ostree_integration(self, work_dir):
|
||||
"""Set up OSTree integration."""
|
||||
ostree_dir = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||
os.makedirs(ostree_dir, exist_ok=True)
|
||||
|
||||
config = {
|
||||
"mode": "bare-user-only",
|
||||
"repo": "/var/lib/ostree/repo"
|
||||
}
|
||||
|
||||
# Write OSTree configuration
|
||||
config_file = os.path.join(work_dir, "etc", "ostree", "ostree.conf")
|
||||
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
f.write("[core]\n")
|
||||
f.write(f"mode={config['mode']}\n")
|
||||
f.write(f"repo={config['repo']}\n")
|
||||
|
||||
return config
|
||||
|
||||
def _configure_grub(self, work_dir):
|
||||
"""Configure GRUB bootloader."""
|
||||
grub_config = {
|
||||
"GRUB_DEFAULT": "0",
|
||||
"GRUB_TIMEOUT": "5",
|
||||
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0"
|
||||
}
|
||||
|
||||
# Write GRUB configuration
|
||||
grub_dir = os.path.join(work_dir, "etc", "default")
|
||||
os.makedirs(grub_dir, exist_ok=True)
|
||||
|
||||
grub_file = os.path.join(grub_dir, "grub")
|
||||
with open(grub_file, 'w') as f:
|
||||
for key, value in grub_config.items():
|
||||
f.write(f'{key}="{value}"\n')
|
||||
|
||||
return grub_config
|
||||
|
||||
def _configure_uefi_boot(self, work_dir):
|
||||
"""Configure UEFI boot."""
|
||||
uefi_config = {
|
||||
"uefi_enabled": True,
|
||||
"secure_boot": False,
|
||||
"boot_entries": ["debian", "debian-fallback"]
|
||||
}
|
||||
|
||||
# Create UEFI boot directory
|
||||
efi_dir = os.path.join(work_dir, "boot", "efi", "EFI", "debian")
|
||||
os.makedirs(efi_dir, exist_ok=True)
|
||||
|
||||
return uefi_config
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
245
test/test_build_iso.py
Normal file
245
test/test_build_iso.py
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test ISO image building functionality for deb-bootc-image-builder.
|
||||
|
||||
This module tests the ISO image building pipeline, including:
|
||||
- ISO manifest validation
|
||||
- ISO creation process
|
||||
- Debian-specific ISO features
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestISOBuilding:
|
||||
"""Test cases for ISO image building functionality."""
|
||||
|
||||
def test_iso_manifest_validation(self, work_dir):
|
||||
"""Test ISO manifest validation for Debian images."""
|
||||
# Create a test ISO manifest
|
||||
manifest = {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": ["linux-image-amd64", "systemd"],
|
||||
"release": "trixie",
|
||||
"arch": "amd64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org.osbuild.debian-grub",
|
||||
"options": {
|
||||
"uefi": True,
|
||||
"secure_boot": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org.osbuild.debian-kernel",
|
||||
"options": {
|
||||
"kernel_package": "linux-image-amd64",
|
||||
"initramfs_tools": True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Validate manifest structure
|
||||
assert "pipeline" in manifest
|
||||
assert "build" in manifest["pipeline"]
|
||||
assert "stages" in manifest["pipeline"]
|
||||
|
||||
# Validate Debian-specific options
|
||||
build_stage = manifest["pipeline"]["build"]
|
||||
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||
assert build_stage["options"]["ostree_integration"] is True
|
||||
|
||||
# Validate stages
|
||||
stages = manifest["pipeline"]["stages"]
|
||||
assert len(stages) >= 3
|
||||
|
||||
# Validate APT stage
|
||||
apt_stage = next((s for s in stages if s["name"] == "org.osbuild.apt"), None)
|
||||
assert apt_stage is not None
|
||||
assert apt_stage["options"]["release"] == "trixie"
|
||||
|
||||
def test_debian_iso_package_installation(self, work_dir):
|
||||
"""Test Debian package installation for ISO builds."""
|
||||
# Mock package installation
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
# Test package installation
|
||||
packages = ["linux-image-amd64", "systemd", "ostree", "grub-efi-amd64"]
|
||||
result = self._install_iso_packages(packages, work_dir)
|
||||
|
||||
assert result is True
|
||||
mock_run.assert_called()
|
||||
|
||||
def test_iso_filesystem_creation(self, work_dir):
|
||||
"""Test ISO filesystem creation."""
|
||||
# Test filesystem structure
|
||||
fs_structure = self._create_iso_filesystem(work_dir)
|
||||
|
||||
expected_dirs = ["/etc", "/var", "/home", "/boot", "/usr", "/media"]
|
||||
for expected_dir in expected_dirs:
|
||||
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||
|
||||
# Test ISO-specific directories
|
||||
iso_dirs = ["/media/cdrom", "/media/usb"]
|
||||
for iso_dir in iso_dirs:
|
||||
full_path = os.path.join(work_dir, iso_dir.lstrip("/"))
|
||||
assert os.path.exists(full_path), f"ISO directory {iso_dir} not created"
|
||||
|
||||
def test_iso_bootloader_configuration(self, work_dir):
|
||||
"""Test ISO bootloader configuration."""
|
||||
# Test GRUB configuration for ISO
|
||||
grub_config = self._configure_iso_grub(work_dir)
|
||||
|
||||
assert "GRUB_DEFAULT" in grub_config
|
||||
assert "GRUB_TIMEOUT" in grub_config
|
||||
assert "GRUB_CMDLINE_LINUX" in grub_config
|
||||
|
||||
# Test ISO-specific boot options
|
||||
assert "cdrom" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||
assert "iso-scan" in grub_config["GRUB_CMDLINE_LINUX"]
|
||||
|
||||
def test_iso_ostree_integration(self, work_dir):
|
||||
"""Test OSTree integration for ISO builds."""
|
||||
# Test OSTree configuration
|
||||
ostree_config = self._setup_iso_ostree(work_dir)
|
||||
|
||||
assert ostree_config["mode"] == "bare-user-only"
|
||||
assert ostree_config["repo"] == "/var/lib/ostree/repo"
|
||||
|
||||
# Test ISO-specific OSTree paths
|
||||
iso_repo_path = os.path.join(work_dir, "media", "cdrom", "ostree")
|
||||
assert os.path.exists(iso_repo_path), "ISO OSTree repository not created"
|
||||
|
||||
def test_iso_creation_process(self, work_dir):
|
||||
"""Test the complete ISO creation process."""
|
||||
# Test ISO build pipeline
|
||||
iso_result = self._create_iso_image(work_dir)
|
||||
|
||||
assert iso_result["status"] == "success"
|
||||
assert "iso_path" in iso_result
|
||||
assert os.path.exists(iso_result["iso_path"])
|
||||
|
||||
# Test ISO properties
|
||||
iso_props = self._get_iso_properties(iso_result["iso_path"])
|
||||
assert iso_props["format"] == "iso9660"
|
||||
assert iso_props["size"] > 0
|
||||
|
||||
def _install_iso_packages(self, packages, work_dir):
|
||||
"""Mock ISO package installation."""
|
||||
logger.info(f"Installing ISO packages: {packages}")
|
||||
return True
|
||||
|
||||
def _create_iso_filesystem(self, work_dir):
|
||||
"""Create ISO filesystem structure."""
|
||||
dirs = [
|
||||
"etc", "var", "home", "boot", "usr", "media",
|
||||
"media/cdrom", "media/usb", "usr/bin", "usr/lib", "usr/sbin"
|
||||
]
|
||||
for dir_path in dirs:
|
||||
full_path = os.path.join(work_dir, dir_path)
|
||||
os.makedirs(full_path, exist_ok=True)
|
||||
|
||||
# Create /home -> /var/home symlink
|
||||
var_home = os.path.join(work_dir, "var", "home")
|
||||
os.makedirs(var_home, exist_ok=True)
|
||||
home_link = os.path.join(work_dir, "home")
|
||||
if os.path.exists(home_link):
|
||||
os.remove(home_link)
|
||||
os.symlink(var_home, home_link)
|
||||
|
||||
return {"status": "created", "directories": dirs}
|
||||
|
||||
def _configure_iso_grub(self, work_dir):
|
||||
"""Configure GRUB for ISO boot."""
|
||||
grub_config = {
|
||||
"GRUB_DEFAULT": "0",
|
||||
"GRUB_TIMEOUT": "5",
|
||||
"GRUB_CMDLINE_LINUX": "console=ttyS0,115200n8 console=tty0 cdrom iso-scan"
|
||||
}
|
||||
|
||||
# Write GRUB configuration
|
||||
grub_dir = os.path.join(work_dir, "etc", "default")
|
||||
os.makedirs(grub_dir, exist_ok=True)
|
||||
|
||||
grub_file = os.path.join(grub_dir, "grub")
|
||||
with open(grub_file, 'w') as f:
|
||||
for key, value in grub_config.items():
|
||||
f.write(f'{key}="{value}"\n')
|
||||
|
||||
return grub_config
|
||||
|
||||
def _setup_iso_ostree(self, work_dir):
|
||||
"""Set up OSTree for ISO builds."""
|
||||
ostree_dir = os.path.join(work_dir, "var", "lib", "ostree", "repo")
|
||||
os.makedirs(ostree_dir, exist_ok=True)
|
||||
|
||||
# Create ISO-specific OSTree repository
|
||||
iso_ostree_dir = os.path.join(work_dir, "media", "cdrom", "ostree")
|
||||
os.makedirs(iso_ostree_dir, exist_ok=True)
|
||||
|
||||
config = {
|
||||
"mode": "bare-user-only",
|
||||
"repo": "/var/lib/ostree/repo"
|
||||
}
|
||||
|
||||
# Write OSTree configuration
|
||||
config_file = os.path.join(work_dir, "etc", "ostree", "ostree.conf")
|
||||
os.makedirs(os.path.dirname(config_file), exist_ok=True)
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
f.write("[core]\n")
|
||||
f.write(f"mode={config['mode']}\n")
|
||||
f.write(f"repo={config['repo']}\n")
|
||||
|
||||
return config
|
||||
|
||||
def _create_iso_image(self, work_dir):
|
||||
"""Mock ISO image creation."""
|
||||
# Create a dummy ISO file
|
||||
iso_path = os.path.join(work_dir, "debian-trixie.iso")
|
||||
with open(iso_path, 'wb') as f:
|
||||
f.write(b"ISO9660 dummy content")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"iso_path": iso_path,
|
||||
"size": os.path.getsize(iso_path)
|
||||
}
|
||||
|
||||
def _get_iso_properties(self, iso_path):
|
||||
"""Get ISO image properties."""
|
||||
return {
|
||||
"format": "iso9660",
|
||||
"size": os.path.getsize(iso_path),
|
||||
"path": iso_path
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
367
test/test_flake8.py
Normal file
367
test/test_flake8.py
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test flake8 compliance for deb-bootc-image-builder.
|
||||
|
||||
This module tests code style compliance using flake8,
|
||||
including:
|
||||
- PEP 8 compliance
|
||||
- Code style validation
|
||||
- Debian-specific style standards
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestFlake8Compliance:
|
||||
"""Test cases for flake8 compliance."""
|
||||
|
||||
def test_flake8_installation(self, work_dir):
|
||||
"""Test that flake8 is available."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
assert result.returncode == 0, "flake8 is not properly installed"
|
||||
logger.info("flake8 is available")
|
||||
except FileNotFoundError:
|
||||
pytest.skip("flake8 not installed")
|
||||
|
||||
def test_flake8_basic_usage(self, work_dir):
|
||||
"""Test basic flake8 functionality."""
|
||||
# Create a simple test file
|
||||
test_file = os.path.join(work_dir, "test_flake8.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for flake8 validation.
|
||||
"""
|
||||
|
||||
def test_function():
|
||||
"""Test function for flake8."""
|
||||
return "test"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(test_function())
|
||||
''')
|
||||
|
||||
# Run flake8 on the test file
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# flake8 should run without errors
|
||||
assert result.returncode == 0, f"flake8 found issues: {result.stdout}"
|
||||
logger.info("flake8 basic functionality test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("flake8 test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"flake8 test failed: {e}")
|
||||
|
||||
def test_pep8_compliance(self, work_dir):
|
||||
"""Test PEP 8 compliance."""
|
||||
# Create a test file with various PEP 8 issues
|
||||
test_file = os.path.join(work_dir, "pep8_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for PEP 8 compliance.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# This line is too long and should trigger E501
|
||||
very_long_line_that_exceeds_the_maximum_line_length_and_should_trigger_a_flake8_error = "test"
|
||||
|
||||
def test_function_with_bad_spacing( x,y ):
|
||||
"""Function with bad spacing."""
|
||||
if x==y:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class BadClass:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def method_with_bad_indentation(self):
|
||||
return "bad indentation"
|
||||
|
||||
# Missing blank line at end of file
|
||||
''')
|
||||
|
||||
# Run flake8 and check for expected errors
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Should find PEP 8 violations
|
||||
assert result.returncode != 0, "flake8 should find PEP 8 violations"
|
||||
|
||||
output = result.stdout + result.stderr
|
||||
|
||||
# Check for specific error codes
|
||||
expected_errors = ["E501", "E201", "E202", "E225", "E111", "W292"]
|
||||
found_errors = []
|
||||
|
||||
for error_code in expected_errors:
|
||||
if error_code in output:
|
||||
found_errors.append(error_code)
|
||||
|
||||
assert len(found_errors) > 0, f"No expected PEP 8 errors found. Output: {output}"
|
||||
logger.info(f"Found PEP 8 violations: {found_errors}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("flake8 PEP 8 test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"flake8 PEP 8 test failed: {e}")
|
||||
|
||||
def test_debian_specific_style_standards(self, work_dir):
|
||||
"""Test Debian-specific style standards."""
|
||||
# Create a test file following Debian style standards
|
||||
test_file = os.path.join(work_dir, "debian_style_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Debian-specific test file for flake8 validation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebianBootcBuilder:
|
||||
"""Debian bootc image builder class."""
|
||||
|
||||
def __init__(self, work_dir: str):
|
||||
"""Initialize the builder."""
|
||||
self.work_dir = work_dir
|
||||
self.packages: List[str] = []
|
||||
self.release = "trixie"
|
||||
self.arch = "amd64"
|
||||
|
||||
def add_package(self, package: str) -> None:
|
||||
"""Add a package to the installation list."""
|
||||
if package not in self.packages:
|
||||
self.packages.append(package)
|
||||
logger.info(f"Added package: {package}")
|
||||
|
||||
def set_release(self, release: str) -> None:
|
||||
"""Set the Debian release."""
|
||||
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||
if release in valid_releases:
|
||||
self.release = release
|
||||
logger.info(f"Set release to: {release}")
|
||||
else:
|
||||
raise ValueError(f"Invalid release: {release}")
|
||||
|
||||
def build_image(self) -> Dict[str, Any]:
|
||||
"""Build the Debian image."""
|
||||
logger.info("Starting Debian image build")
|
||||
|
||||
# Validate configuration
|
||||
if not self.packages:
|
||||
raise ValueError("No packages specified")
|
||||
|
||||
# Build process would go here
|
||||
result = {
|
||||
"status": "success",
|
||||
"packages": self.packages,
|
||||
"release": self.release,
|
||||
"arch": self.arch
|
||||
}
|
||||
|
||||
logger.info("Debian image build completed")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
builder = DebianBootcBuilder("/tmp/test")
|
||||
builder.add_package("linux-image-amd64")
|
||||
builder.add_package("systemd")
|
||||
builder.set_release("trixie")
|
||||
|
||||
try:
|
||||
result = builder.build_image()
|
||||
print(f"Build result: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Build failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
''')
|
||||
|
||||
# Run flake8 on the Debian style file
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Should pass flake8 validation
|
||||
assert result.returncode == 0, f"flake8 found issues in Debian style file: {result.stdout}"
|
||||
logger.info("Debian-specific style standards test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("flake8 Debian style test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"flake8 Debian style test failed: {e}")
|
||||
|
||||
def test_flake8_configuration(self, work_dir):
|
||||
"""Test flake8 configuration and custom rules."""
|
||||
# Create a flake8 configuration file
|
||||
setup_cfg = os.path.join(work_dir, "setup.cfg")
|
||||
with open(setup_cfg, 'w') as f:
|
||||
f.write('''[flake8]
|
||||
# Maximum line length
|
||||
max-line-length = 120
|
||||
|
||||
# Ignore specific error codes
|
||||
ignore = E203, W503
|
||||
|
||||
# Exclude directories
|
||||
exclude = .git,__pycache__,.venv
|
||||
|
||||
# Maximum complexity
|
||||
max-complexity = 10
|
||||
''')
|
||||
|
||||
# Create a test file that would normally trigger ignored errors
|
||||
test_file = os.path.join(work_dir, "config_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for flake8 configuration.
|
||||
"""
|
||||
|
||||
def test_function():
|
||||
"""Test function for flake8 config."""
|
||||
# This line is long but should be allowed by config
|
||||
very_long_line_that_exceeds_normal_pep8_but_is_allowed_by_our_config = "test"
|
||||
return very_long_line_that_exceeds_normal_pep8_but_is_allowed_by_our_config
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(test_function())
|
||||
|
||||
''')
|
||||
|
||||
# Run flake8 with custom configuration
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", "--config", setup_cfg, test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Should pass with custom configuration
|
||||
assert result.returncode == 0, f"flake8 with custom config failed: {result.stdout}"
|
||||
logger.info("flake8 configuration test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("flake8 configuration test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"flake8 configuration test failed: {e}")
|
||||
|
||||
def test_flake8_error_codes(self, work_dir):
|
||||
"""Test specific flake8 error codes."""
|
||||
# Create a test file with specific error types
|
||||
test_file = os.path.join(work_dir, "error_codes_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for specific flake8 error codes.
|
||||
"""
|
||||
|
||||
# E501: Line too long
|
||||
very_long_line_that_exceeds_the_maximum_line_length_and_should_trigger_a_flake8_error = "test"
|
||||
|
||||
# E201: Whitespace after '('
|
||||
def function_with_bad_spacing( x ):
|
||||
return x
|
||||
|
||||
# E202: Whitespace before ')'
|
||||
def another_bad_function( y ):
|
||||
return y
|
||||
|
||||
# E225: Missing whitespace around operator
|
||||
x=1
|
||||
y=2
|
||||
z=x+y
|
||||
|
||||
# E111: Bad indentation
|
||||
def bad_indentation():
|
||||
return "bad"
|
||||
|
||||
# W292: No newline at end of file
|
||||
result = "no newline"
|
||||
''')
|
||||
|
||||
# Run flake8 and check for specific error codes
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flake8", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Should find errors
|
||||
assert result.returncode != 0, "flake8 should find style errors"
|
||||
|
||||
output = result.stdout + result.stderr
|
||||
|
||||
# Check for specific error codes
|
||||
expected_errors = ["E501", "E201", "E202", "E225", "E111", "W292"]
|
||||
found_errors = []
|
||||
|
||||
for error_code in expected_errors:
|
||||
if error_code in output:
|
||||
found_errors.append(error_code)
|
||||
|
||||
# Should find at least some of the expected errors
|
||||
assert len(found_errors) >= 3, f"Expected more error codes. Found: {found_errors}"
|
||||
logger.info(f"Found flake8 error codes: {found_errors}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("flake8 error codes test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"flake8 error codes test failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
228
test/test_manifest.py
Normal file
228
test/test_manifest.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test manifest validation and processing for deb-bootc-image-builder.
|
||||
|
||||
This module tests manifest handling, including:
|
||||
- Manifest structure validation
|
||||
- Stage configuration validation
|
||||
- Debian-specific manifest processing
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import yaml
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestManifestValidation:
|
||||
"""Test cases for manifest validation."""
|
||||
|
||||
def test_debian_manifest_structure(self, work_dir):
|
||||
"""Test Debian manifest structure validation."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Validate top-level structure
|
||||
assert "pipeline" in manifest
|
||||
assert "build" in manifest["pipeline"]
|
||||
assert "stages" in manifest["pipeline"]
|
||||
|
||||
# Validate build stage
|
||||
build_stage = manifest["pipeline"]["build"]
|
||||
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||
assert "options" in build_stage
|
||||
|
||||
# Validate stages
|
||||
stages = manifest["pipeline"]["stages"]
|
||||
assert len(stages) > 0
|
||||
|
||||
# Validate APT stage
|
||||
apt_stage = next((s for s in stages if s["name"] == "org.osbuild.apt"), None)
|
||||
assert apt_stage is not None
|
||||
assert "options" in apt_stage
|
||||
assert "packages" in apt_stage["options"]
|
||||
|
||||
def test_debian_package_validation(self, work_dir):
|
||||
"""Test Debian package validation in manifests."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Extract packages from APT stage
|
||||
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||
if s["name"] == "org.osbuild.apt"), None)
|
||||
packages = apt_stage["options"]["packages"]
|
||||
|
||||
# Validate essential Debian packages
|
||||
essential_packages = [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree"
|
||||
]
|
||||
|
||||
for pkg in essential_packages:
|
||||
assert pkg in packages, f"Essential package {pkg} missing"
|
||||
|
||||
# Validate package format
|
||||
for pkg in packages:
|
||||
assert isinstance(pkg, str), f"Package {pkg} is not a string"
|
||||
assert len(pkg) > 0, f"Empty package name found"
|
||||
|
||||
def test_debian_repository_configuration(self, work_dir):
|
||||
"""Test Debian repository configuration in manifests."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Validate repository configuration
|
||||
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||
if s["name"] == "org.osbuild.apt"), None)
|
||||
|
||||
options = apt_stage["options"]
|
||||
assert "release" in options
|
||||
assert "arch" in options
|
||||
|
||||
# Validate Debian release
|
||||
assert options["release"] == "trixie"
|
||||
assert options["arch"] == "amd64"
|
||||
|
||||
def test_ostree_integration_configuration(self, work_dir):
|
||||
"""Test OSTree integration configuration."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Validate OSTree integration in filesystem stage
|
||||
fs_stage = manifest["pipeline"]["build"]
|
||||
options = fs_stage["options"]
|
||||
|
||||
assert "ostree_integration" in options
|
||||
assert options["ostree_integration"] is True
|
||||
|
||||
# Validate home symlink configuration
|
||||
assert "home_symlink" in options
|
||||
assert options["home_symlink"] is True
|
||||
|
||||
def test_manifest_serialization(self, work_dir):
|
||||
"""Test manifest serialization to YAML and JSON."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Test YAML serialization
|
||||
yaml_content = yaml.dump(manifest, default_flow_style=False)
|
||||
assert "org.osbuild.debian-filesystem" in yaml_content
|
||||
assert "org.osbuild.apt" in yaml_content
|
||||
|
||||
# Test JSON serialization
|
||||
json_content = json.dumps(manifest, indent=2)
|
||||
assert "org.osbuild.debian-filesystem" in json_content
|
||||
assert "org.osbuild.apt" in json_content
|
||||
|
||||
# Test round-trip serialization
|
||||
yaml_parsed = yaml.safe_load(yaml_content)
|
||||
assert yaml_parsed == manifest
|
||||
|
||||
json_parsed = json.loads(json_content)
|
||||
assert json_parsed == manifest
|
||||
|
||||
def test_manifest_validation_errors(self, work_dir):
|
||||
"""Test manifest validation error handling."""
|
||||
# Test missing pipeline
|
||||
invalid_manifest = {"stages": []}
|
||||
with pytest.raises(KeyError):
|
||||
_ = invalid_manifest["pipeline"]
|
||||
|
||||
# Test missing build stage
|
||||
invalid_manifest = {"pipeline": {"stages": []}}
|
||||
with pytest.raises(KeyError):
|
||||
_ = invalid_manifest["pipeline"]["build"]
|
||||
|
||||
# Test missing stages
|
||||
invalid_manifest = {"pipeline": {"build": {"name": "test"}}}
|
||||
with pytest.raises(KeyError):
|
||||
_ = invalid_manifest["pipeline"]["stages"]
|
||||
|
||||
def test_debian_specific_manifest_features(self, work_dir):
|
||||
"""Test Debian-specific manifest features."""
|
||||
manifest = self._create_debian_manifest()
|
||||
|
||||
# Test GRUB stage configuration
|
||||
grub_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||
if s["name"] == "org.osbuild.debian-grub"), None)
|
||||
|
||||
if grub_stage:
|
||||
options = grub_stage["options"]
|
||||
assert "uefi" in options
|
||||
assert "secure_boot" in options
|
||||
assert "timeout" in options
|
||||
|
||||
# Test kernel stage configuration
|
||||
kernel_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||
if s["name"] == "org.osbuild.debian-kernel"), None)
|
||||
|
||||
if kernel_stage:
|
||||
options = kernel_stage["options"]
|
||||
assert "kernel_package" in options
|
||||
assert "initramfs_tools" in options
|
||||
|
||||
def _create_debian_manifest(self):
|
||||
"""Create a sample Debian manifest for testing."""
|
||||
return {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True,
|
||||
"home_symlink": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"linux-headers-amd64",
|
||||
"systemd",
|
||||
"systemd-sysv",
|
||||
"dbus",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools",
|
||||
"util-linux",
|
||||
"parted",
|
||||
"e2fsprogs",
|
||||
"dosfstools",
|
||||
"efibootmgr",
|
||||
"sudo",
|
||||
"network-manager"
|
||||
],
|
||||
"release": "trixie",
|
||||
"arch": "amd64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org.osbuild.debian-grub",
|
||||
"options": {
|
||||
"uefi": True,
|
||||
"secure_boot": False,
|
||||
"timeout": 5,
|
||||
"default_entry": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org.osbuild.debian-kernel",
|
||||
"options": {
|
||||
"kernel_package": "linux-image-amd64",
|
||||
"initramfs_tools": True,
|
||||
"ostree_integration": True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
290
test/test_opts.py
Normal file
290
test/test_opts.py
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test command line options for deb-bootc-image-builder.
|
||||
|
||||
This module tests command line argument parsing and validation,
|
||||
including:
|
||||
- Required arguments
|
||||
- Optional arguments
|
||||
- Argument validation
|
||||
- Debian-specific options
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestCommandLineOptions:
|
||||
"""Test cases for command line options."""
|
||||
|
||||
def test_required_arguments(self, work_dir):
|
||||
"""Test required command line arguments."""
|
||||
# Test minimum required arguments
|
||||
required_args = {
|
||||
"container": "debian:trixie",
|
||||
"output": work_dir
|
||||
}
|
||||
|
||||
# Validate required arguments
|
||||
for arg_name, arg_value in required_args.items():
|
||||
assert arg_value is not None, f"Required argument {arg_name} is None"
|
||||
if arg_name == "output":
|
||||
assert os.path.exists(arg_value), f"Output directory {arg_value} does not exist"
|
||||
|
||||
def test_optional_arguments(self, work_dir):
|
||||
"""Test optional command line arguments."""
|
||||
# Test optional arguments with default values
|
||||
optional_args = {
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"verbose": False,
|
||||
"clean": False
|
||||
}
|
||||
|
||||
# Validate optional arguments
|
||||
for arg_name, arg_value in optional_args.items():
|
||||
assert arg_name in optional_args, f"Optional argument {arg_name} not found"
|
||||
assert arg_value is not None, f"Optional argument {arg_name} has no default value"
|
||||
|
||||
def test_debian_specific_options(self, work_dir):
|
||||
"""Test Debian-specific command line options."""
|
||||
# Test Debian-specific options
|
||||
debian_options = {
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"package_manager": "apt",
|
||||
"initramfs_tools": True,
|
||||
"grub_efi": True
|
||||
}
|
||||
|
||||
# Validate Debian-specific options
|
||||
assert debian_options["release"] in ["trixie", "bookworm", "bullseye"], \
|
||||
f"Invalid Debian release: {debian_options['release']}"
|
||||
|
||||
assert debian_options["arch"] in ["amd64", "arm64", "i386"], \
|
||||
f"Invalid architecture: {debian_options['arch']}"
|
||||
|
||||
assert debian_options["package_manager"] == "apt", \
|
||||
f"Invalid package manager: {debian_options['package_manager']}"
|
||||
|
||||
assert debian_options["initramfs_tools"] is True, \
|
||||
"initramfs-tools should be enabled for Debian"
|
||||
|
||||
assert debian_options["grub_efi"] is True, \
|
||||
"GRUB EFI should be enabled for Debian"
|
||||
|
||||
def test_argument_validation(self, work_dir):
|
||||
"""Test argument validation logic."""
|
||||
# Test valid arguments
|
||||
valid_args = {
|
||||
"container": "debian:trixie",
|
||||
"output": work_dir,
|
||||
"release": "trixie",
|
||||
"arch": "amd64"
|
||||
}
|
||||
|
||||
validation_result = self._validate_arguments(valid_args)
|
||||
assert validation_result["valid"] is True, \
|
||||
f"Valid arguments failed validation: {validation_result.get('error', 'Unknown error')}"
|
||||
|
||||
# Test invalid arguments
|
||||
invalid_args = {
|
||||
"container": "invalid:image",
|
||||
"output": "/nonexistent/path",
|
||||
"release": "invalid-release",
|
||||
"arch": "invalid-arch"
|
||||
}
|
||||
|
||||
validation_result = self._validate_arguments(invalid_args)
|
||||
assert validation_result["valid"] is False, \
|
||||
"Invalid arguments should fail validation"
|
||||
|
||||
def test_output_directory_handling(self, work_dir):
|
||||
"""Test output directory handling."""
|
||||
# Test existing directory
|
||||
existing_dir = work_dir
|
||||
result = self._handle_output_directory(existing_dir)
|
||||
assert result["success"] is True, \
|
||||
f"Existing directory handling failed: {result.get('error', 'Unknown error')}"
|
||||
|
||||
# Test non-existent directory creation
|
||||
new_dir = os.path.join(work_dir, "new_output")
|
||||
result = self._handle_output_directory(new_dir)
|
||||
assert result["success"] is True, \
|
||||
f"New directory creation failed: {result.get('error', 'Unknown error')}"
|
||||
assert os.path.exists(new_dir), "New directory was not created"
|
||||
|
||||
# Test invalid directory path
|
||||
invalid_dir = "/invalid/path/with/permissions/issue"
|
||||
result = self._handle_output_directory(invalid_dir)
|
||||
assert result["success"] is False, "Invalid directory should fail"
|
||||
|
||||
def test_container_image_validation(self, work_dir):
|
||||
"""Test container image validation."""
|
||||
# Test valid Debian image
|
||||
valid_image = "debian:trixie"
|
||||
result = self._validate_container_image(valid_image)
|
||||
assert result["valid"] is True, \
|
||||
f"Valid Debian image failed validation: {result.get('error', 'Unknown error')}"
|
||||
|
||||
# Test invalid image
|
||||
invalid_image = "invalid:image"
|
||||
result = self._validate_container_image(invalid_image)
|
||||
assert result["valid"] is False, "Invalid image should fail validation"
|
||||
|
||||
# Test image with specific architecture
|
||||
arch_image = "debian:trixie-amd64"
|
||||
result = self._validate_container_image(arch_image)
|
||||
assert result["valid"] is True, \
|
||||
f"Architecture-specific image failed validation: {result.get('error', 'Unknown error')}"
|
||||
|
||||
def test_package_list_validation(self, work_dir):
|
||||
"""Test package list validation."""
|
||||
# Test valid Debian packages
|
||||
valid_packages = [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools"
|
||||
]
|
||||
|
||||
result = self._validate_package_list(valid_packages)
|
||||
assert result["valid"] is True, \
|
||||
f"Valid packages failed validation: {result.get('error', 'Unknown error')}"
|
||||
|
||||
# Test invalid packages
|
||||
invalid_packages = [
|
||||
"invalid-package",
|
||||
"nonexistent-package"
|
||||
]
|
||||
|
||||
result = self._validate_package_list(invalid_packages)
|
||||
assert result["valid"] is False, "Invalid packages should fail validation"
|
||||
|
||||
def test_manifest_generation(self, work_dir):
|
||||
"""Test manifest generation from command line options."""
|
||||
# Test manifest generation
|
||||
options = {
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64", "systemd", "ostree"]
|
||||
}
|
||||
|
||||
manifest = self._generate_manifest(options)
|
||||
|
||||
# Validate generated manifest
|
||||
assert "pipeline" in manifest
|
||||
assert "build" in manifest["pipeline"]
|
||||
assert "stages" in manifest["pipeline"]
|
||||
|
||||
# Validate Debian-specific content
|
||||
build_stage = manifest["pipeline"]["build"]
|
||||
assert build_stage["name"] == "org.osbuild.debian-filesystem"
|
||||
|
||||
# Validate APT stage
|
||||
apt_stage = next((s for s in manifest["pipeline"]["stages"]
|
||||
if s["name"] == "org.osbuild.apt"), None)
|
||||
assert apt_stage is not None
|
||||
assert apt_stage["options"]["release"] == "trixie"
|
||||
assert apt_stage["options"]["arch"] == "amd64"
|
||||
|
||||
def _validate_arguments(self, args):
|
||||
"""Mock argument validation."""
|
||||
# Check required arguments
|
||||
if "container" not in args or not args["container"]:
|
||||
return {"valid": False, "error": "Container image is required"}
|
||||
|
||||
if "output" not in args or not args["output"]:
|
||||
return {"valid": False, "error": "Output directory is required"}
|
||||
|
||||
# Check Debian-specific validation
|
||||
if "release" in args:
|
||||
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||
if args["release"] not in valid_releases:
|
||||
return {"valid": False, "error": f"Invalid Debian release: {args['release']}"}
|
||||
|
||||
if "arch" in args:
|
||||
valid_archs = ["amd64", "arm64", "i386"]
|
||||
if args["arch"] not in valid_archs:
|
||||
return {"valid": False, "error": f"Invalid architecture: {args['arch']}"}
|
||||
|
||||
return {"valid": True}
|
||||
|
||||
def _handle_output_directory(self, output_dir):
|
||||
"""Mock output directory handling."""
|
||||
try:
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Test if directory is writable
|
||||
test_file = os.path.join(output_dir, "test_write")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write("test")
|
||||
os.remove(test_file)
|
||||
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _validate_container_image(self, image):
|
||||
"""Mock container image validation."""
|
||||
# Check if it's a valid Debian image
|
||||
if image.startswith("debian:"):
|
||||
return {"valid": True, "type": "debian"}
|
||||
else:
|
||||
return {"valid": False, "error": "Not a valid Debian image"}
|
||||
|
||||
def _validate_package_list(self, packages):
|
||||
"""Mock package list validation."""
|
||||
# Check if packages look like valid Debian packages
|
||||
valid_packages = [
|
||||
"linux-image-amd64", "systemd", "ostree", "grub-efi-amd64",
|
||||
"initramfs-tools", "util-linux", "parted", "e2fsprogs"
|
||||
]
|
||||
|
||||
for pkg in packages:
|
||||
if pkg not in valid_packages:
|
||||
return {"valid": False, "error": f"Invalid package: {pkg}"}
|
||||
|
||||
return {"valid": True}
|
||||
|
||||
def _generate_manifest(self, options):
|
||||
"""Mock manifest generation."""
|
||||
return {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": options.get("packages", []),
|
||||
"release": options.get("release", "trixie"),
|
||||
"arch": options.get("arch", "amd64")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
248
test/test_progress.py
Normal file
248
test/test_progress.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test progress reporting for deb-bootc-image-builder.
|
||||
|
||||
This module tests progress reporting functionality, including:
|
||||
- Progress tracking
|
||||
- Status updates
|
||||
- Error reporting
|
||||
- Debian-specific progress indicators
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestProgressReporting:
|
||||
"""Test cases for progress reporting functionality."""
|
||||
|
||||
def test_progress_initialization(self, work_dir):
|
||||
"""Test progress tracking initialization."""
|
||||
# Initialize progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
assert progress["total_steps"] > 0
|
||||
assert progress["current_step"] == 0
|
||||
assert progress["status"] == "initialized"
|
||||
assert "start_time" in progress
|
||||
|
||||
def test_progress_step_tracking(self, work_dir):
|
||||
"""Test progress step tracking."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Simulate step progression
|
||||
steps = [
|
||||
"filesystem_setup",
|
||||
"package_installation",
|
||||
"ostree_integration",
|
||||
"bootloader_configuration",
|
||||
"image_generation"
|
||||
]
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
self._update_progress(progress, step, i + 1)
|
||||
|
||||
assert progress["current_step"] == i + 1
|
||||
assert progress["current_operation"] == step
|
||||
assert progress["status"] == "in_progress"
|
||||
|
||||
# Check progress percentage
|
||||
expected_percentage = ((i + 1) / len(steps)) * 100
|
||||
assert abs(progress["percentage"] - expected_percentage) < 0.1
|
||||
|
||||
def test_progress_status_updates(self, work_dir):
|
||||
"""Test progress status updates."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Test status transitions
|
||||
statuses = ["initialized", "in_progress", "completed", "failed"]
|
||||
|
||||
for status in statuses:
|
||||
self._set_progress_status(progress, status)
|
||||
assert progress["status"] == status
|
||||
|
||||
# Check status-specific properties
|
||||
if status == "completed":
|
||||
assert progress["end_time"] is not None
|
||||
assert progress["percentage"] == 100.0
|
||||
elif status == "failed":
|
||||
assert progress["error"] is not None
|
||||
|
||||
def test_debian_specific_progress_indicators(self, work_dir):
|
||||
"""Test Debian-specific progress indicators."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Test Debian-specific operations
|
||||
debian_operations = [
|
||||
"apt_update",
|
||||
"package_download",
|
||||
"package_installation",
|
||||
"initramfs_generation",
|
||||
"grub_configuration"
|
||||
]
|
||||
|
||||
for operation in debian_operations:
|
||||
self._add_debian_operation(progress, operation)
|
||||
assert operation in progress["debian_operations"]
|
||||
|
||||
# Test Debian package progress
|
||||
package_progress = self._track_package_progress(progress, ["linux-image-amd64", "systemd", "ostree"])
|
||||
assert package_progress["total_packages"] == 3
|
||||
assert package_progress["installed_packages"] == 0
|
||||
|
||||
def test_error_reporting(self, work_dir):
|
||||
"""Test error reporting in progress tracking."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Test error reporting
|
||||
error_message = "Package installation failed: network error"
|
||||
self._report_progress_error(progress, error_message)
|
||||
|
||||
assert progress["status"] == "failed"
|
||||
assert progress["error"] == error_message
|
||||
assert progress["error_time"] is not None
|
||||
|
||||
# Test error details
|
||||
error_details = {
|
||||
"operation": "package_installation",
|
||||
"step": 2,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self._add_error_details(progress, error_details)
|
||||
|
||||
assert "error_details" in progress
|
||||
assert progress["error_details"]["operation"] == "package_installation"
|
||||
|
||||
def test_progress_persistence(self, work_dir):
|
||||
"""Test progress persistence and recovery."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Update progress
|
||||
self._update_progress(progress, "filesystem_setup", 1)
|
||||
self._update_progress(progress, "package_installation", 2)
|
||||
|
||||
# Save progress
|
||||
progress_file = os.path.join(work_dir, "progress.json")
|
||||
self._save_progress(progress, progress_file)
|
||||
|
||||
# Load progress
|
||||
loaded_progress = self._load_progress(progress_file)
|
||||
|
||||
# Verify persistence
|
||||
assert loaded_progress["current_step"] == 2
|
||||
assert loaded_progress["current_operation"] == "package_installation"
|
||||
assert loaded_progress["percentage"] == 40.0
|
||||
|
||||
def test_progress_cleanup(self, work_dir):
|
||||
"""Test progress cleanup and finalization."""
|
||||
# Create progress tracker
|
||||
progress = self._create_progress_tracker()
|
||||
|
||||
# Complete all steps
|
||||
steps = ["filesystem_setup", "package_installation", "ostree_integration", "bootloader_configuration", "image_generation"]
|
||||
for i, step in enumerate(steps):
|
||||
self._update_progress(progress, step, i + 1)
|
||||
|
||||
# Finalize progress
|
||||
self._finalize_progress(progress)
|
||||
|
||||
assert progress["status"] == "completed"
|
||||
assert progress["end_time"] is not None
|
||||
assert progress["duration"] > 0
|
||||
assert progress["percentage"] == 100.0
|
||||
|
||||
def _create_progress_tracker(self):
|
||||
"""Create a progress tracker instance."""
|
||||
return {
|
||||
"total_steps": 5,
|
||||
"current_step": 0,
|
||||
"current_operation": None,
|
||||
"status": "initialized",
|
||||
"start_time": time.time(),
|
||||
"end_time": None,
|
||||
"percentage": 0.0,
|
||||
"error": None,
|
||||
"error_time": None,
|
||||
"debian_operations": [],
|
||||
"package_progress": {}
|
||||
}
|
||||
|
||||
def _update_progress(self, progress, operation, step):
|
||||
"""Update progress tracking."""
|
||||
progress["current_step"] = step
|
||||
progress["current_operation"] = operation
|
||||
progress["status"] = "in_progress"
|
||||
progress["percentage"] = (step / progress["total_steps"]) * 100
|
||||
|
||||
def _set_progress_status(self, progress, status):
|
||||
"""Set progress status."""
|
||||
progress["status"] = status
|
||||
|
||||
if status == "completed":
|
||||
progress["end_time"] = time.time()
|
||||
progress["percentage"] = 100.0
|
||||
elif status == "failed":
|
||||
progress["error_time"] = time.time()
|
||||
|
||||
def _add_debian_operation(self, progress, operation):
|
||||
"""Add Debian-specific operation to progress."""
|
||||
if "debian_operations" not in progress:
|
||||
progress["debian_operations"] = []
|
||||
progress["debian_operations"].append(operation)
|
||||
|
||||
def _track_package_progress(self, progress, packages):
|
||||
"""Track package installation progress."""
|
||||
package_progress = {
|
||||
"total_packages": len(packages),
|
||||
"installed_packages": 0,
|
||||
"failed_packages": [],
|
||||
"current_package": None
|
||||
}
|
||||
progress["package_progress"] = package_progress
|
||||
return package_progress
|
||||
|
||||
def _report_progress_error(self, progress, error_message):
|
||||
"""Report progress error."""
|
||||
progress["status"] = "failed"
|
||||
progress["error"] = error_message
|
||||
progress["error_time"] = time.time()
|
||||
|
||||
def _add_error_details(self, progress, error_details):
|
||||
"""Add detailed error information."""
|
||||
progress["error_details"] = error_details
|
||||
|
||||
def _save_progress(self, progress, file_path):
|
||||
"""Save progress to file."""
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(progress, f, indent=2)
|
||||
|
||||
def _load_progress(self, file_path):
|
||||
"""Load progress from file."""
|
||||
with open(file_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _finalize_progress(self, progress):
|
||||
"""Finalize progress tracking."""
|
||||
progress["status"] = "completed"
|
||||
progress["end_time"] = time.time()
|
||||
progress["duration"] = progress["end_time"] - progress["start_time"]
|
||||
progress["percentage"] = 100.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
364
test/test_pylint.py
Normal file
364
test/test_pylint.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test pylint compliance for deb-bootc-image-builder.
|
||||
|
||||
This module tests code quality and pylint compliance,
|
||||
including:
|
||||
- Code style validation
|
||||
- Pylint score checking
|
||||
- Debian-specific code standards
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestPylintCompliance:
|
||||
"""Test cases for pylint compliance."""
|
||||
|
||||
def test_pylint_installation(self, work_dir):
|
||||
"""Test that pylint is available."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pylint", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
assert result.returncode == 0, "pylint is not properly installed"
|
||||
logger.info("pylint is available")
|
||||
except FileNotFoundError:
|
||||
pytest.skip("pylint not installed")
|
||||
|
||||
def test_pylint_basic_usage(self, work_dir):
|
||||
"""Test basic pylint functionality."""
|
||||
# Create a simple test file
|
||||
test_file = os.path.join(work_dir, "test_pylint.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for pylint validation.
|
||||
"""
|
||||
|
||||
def test_function():
|
||||
"""Test function for pylint."""
|
||||
return "test"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(test_function())
|
||||
''')
|
||||
|
||||
# Run pylint on the test file
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pylint", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Pylint should run without errors
|
||||
assert result.returncode in [0, 1], f"pylint failed with return code {result.returncode}"
|
||||
logger.info("pylint basic functionality test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pylint timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"pylint test failed: {e}")
|
||||
|
||||
def test_debian_specific_code_standards(self, work_dir):
|
||||
"""Test Debian-specific code standards."""
|
||||
# Create a test file with Debian-specific patterns
|
||||
test_file = os.path.join(work_dir, "debian_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Debian-specific test file for pylint validation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebianBootcBuilder:
|
||||
"""Debian bootc image builder class."""
|
||||
|
||||
def __init__(self, work_dir: str):
|
||||
"""Initialize the builder."""
|
||||
self.work_dir = work_dir
|
||||
self.packages: List[str] = []
|
||||
self.release = "trixie"
|
||||
self.arch = "amd64"
|
||||
|
||||
def add_package(self, package: str) -> None:
|
||||
"""Add a package to the installation list."""
|
||||
if package not in self.packages:
|
||||
self.packages.append(package)
|
||||
logger.info(f"Added package: {package}")
|
||||
|
||||
def set_release(self, release: str) -> None:
|
||||
"""Set the Debian release."""
|
||||
valid_releases = ["trixie", "bookworm", "bullseye"]
|
||||
if release in valid_releases:
|
||||
self.release = release
|
||||
logger.info(f"Set release to: {release}")
|
||||
else:
|
||||
raise ValueError(f"Invalid release: {release}")
|
||||
|
||||
def build_image(self) -> Dict[str, Any]:
|
||||
"""Build the Debian image."""
|
||||
logger.info("Starting Debian image build")
|
||||
|
||||
# Validate configuration
|
||||
if not self.packages:
|
||||
raise ValueError("No packages specified")
|
||||
|
||||
# Build process would go here
|
||||
result = {
|
||||
"status": "success",
|
||||
"packages": self.packages,
|
||||
"release": self.release,
|
||||
"arch": self.arch
|
||||
}
|
||||
|
||||
logger.info("Debian image build completed")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
builder = DebianBootcBuilder("/tmp/test")
|
||||
builder.add_package("linux-image-amd64")
|
||||
builder.add_package("systemd")
|
||||
builder.set_release("trixie")
|
||||
|
||||
try:
|
||||
result = builder.build_image()
|
||||
print(f"Build result: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Build failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
''')
|
||||
|
||||
# Run pylint with Debian-specific configuration
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pylint", "--disable=C0114,C0116", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Check pylint output for Debian-specific patterns
|
||||
output = result.stdout + result.stderr
|
||||
|
||||
# Should not have critical errors
|
||||
assert "E0001" not in output, "Critical pylint errors found"
|
||||
|
||||
# Check for specific Debian patterns
|
||||
assert "debian" in output.lower() or "bootc" in output.lower(), \
|
||||
"Debian-specific content not detected"
|
||||
|
||||
logger.info("Debian-specific code standards test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pylint Debian test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"pylint Debian test failed: {e}")
|
||||
|
||||
def test_pylint_score_threshold(self, work_dir):
|
||||
"""Test that pylint score meets minimum threshold."""
|
||||
# Create a high-quality test file
|
||||
test_file = os.path.join(work_dir, "high_quality_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
High-quality test file for pylint scoring.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HighQualityClass:
|
||||
"""A high-quality class for testing."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""Initialize the class."""
|
||||
self.name = name
|
||||
self.data: List[str] = []
|
||||
|
||||
def add_item(self, item: str) -> None:
|
||||
"""Add an item to the data list."""
|
||||
if item and item not in self.data:
|
||||
self.data.append(item)
|
||||
logger.info(f"Added item: {item}")
|
||||
|
||||
def get_items(self) -> List[str]:
|
||||
"""Get all items from the data list."""
|
||||
return self.data.copy()
|
||||
|
||||
def clear_items(self) -> None:
|
||||
"""Clear all items from the data list."""
|
||||
self.data.clear()
|
||||
logger.info("Cleared all items")
|
||||
|
||||
|
||||
def high_quality_function(param: str) -> str:
|
||||
"""A high-quality function for testing."""
|
||||
if not param:
|
||||
return ""
|
||||
|
||||
result = param.upper()
|
||||
logger.info(f"Processed parameter: {param} -> {result}")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
obj = HighQualityClass("test")
|
||||
obj.add_item("item1")
|
||||
obj.add_item("item2")
|
||||
|
||||
items = obj.get_items()
|
||||
print(f"Items: {items}")
|
||||
|
||||
result = high_quality_function("hello")
|
||||
print(f"Function result: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
''')
|
||||
|
||||
# Run pylint and check score
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pylint", "--score=yes", test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
output = result.stdout + result.stderr
|
||||
|
||||
# Extract score from output
|
||||
score_line = [line for line in output.split('\n') if 'Your code has been rated at' in line]
|
||||
|
||||
if score_line:
|
||||
score_text = score_line[0]
|
||||
# Extract numeric score
|
||||
import re
|
||||
score_match = re.search(r'(\d+\.\d+)', score_text)
|
||||
if score_match:
|
||||
score = float(score_match.group(1))
|
||||
|
||||
# Check if score meets minimum threshold (8.0)
|
||||
assert score >= 8.0, f"Pylint score {score} is below minimum threshold 8.0"
|
||||
logger.info(f"Pylint score: {score} (meets minimum threshold)")
|
||||
else:
|
||||
pytest.fail("Could not extract pylint score")
|
||||
else:
|
||||
pytest.fail("Could not find pylint score in output")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pylint score test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"pylint score test failed: {e}")
|
||||
|
||||
def test_pylint_configuration(self, work_dir):
|
||||
"""Test pylint configuration and custom rules."""
|
||||
# Create a pylint configuration file
|
||||
pylintrc = os.path.join(work_dir, ".pylintrc")
|
||||
with open(pylintrc, 'w') as f:
|
||||
f.write('''[MASTER]
|
||||
# Python code to execute before analysis
|
||||
init-hook='import sys; sys.path.append(".")'
|
||||
|
||||
[REPORTS]
|
||||
# Set the output format
|
||||
output-format=text
|
||||
|
||||
# Include a brief explanation of each error
|
||||
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||
|
||||
# Include a brief explanation of each error
|
||||
include-naming-hint=yes
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# Disable specific warnings
|
||||
disable=C0114,C0116,R0903
|
||||
|
||||
[FORMAT]
|
||||
# Maximum number of characters on a single line
|
||||
max-line-length=120
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
[SIMILARITIES]
|
||||
# Minimum lines number of a similarity
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore imports when computing similarities
|
||||
ignore-imports=yes
|
||||
''')
|
||||
|
||||
# Create a test file
|
||||
test_file = os.path.join(work_dir, "config_test.py")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('''#!/usr/bin/env python3
|
||||
"""
|
||||
Test file for pylint configuration.
|
||||
"""
|
||||
|
||||
def test_function():
|
||||
return "test"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(test_function())
|
||||
''')
|
||||
|
||||
# Run pylint with custom configuration
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pylint", "--rcfile", pylintrc, test_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Should run without configuration errors
|
||||
assert result.returncode in [0, 1], f"pylint with custom config failed: {result.returncode}"
|
||||
logger.info("Pylint configuration test passed")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pytest.fail("pylint configuration test timed out")
|
||||
except Exception as e:
|
||||
pytest.fail(f"pylint configuration test failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
473
test/testcases.py
Normal file
473
test/testcases.py
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test case definitions for deb-bootc-image-builder.
|
||||
|
||||
This module defines test cases and test data for various scenarios,
|
||||
including:
|
||||
- Basic functionality tests
|
||||
- Edge case tests
|
||||
- Error condition tests
|
||||
- Debian-specific test cases
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import yaml
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestCaseDefinitions:
|
||||
"""Test case definitions for deb-bootc-image-builder."""
|
||||
|
||||
@staticmethod
|
||||
def get_basic_functionality_tests() -> List[Dict[str, Any]]:
|
||||
"""Get basic functionality test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "basic_debian_image_build",
|
||||
"description": "Test basic Debian image building functionality",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64", "systemd", "ostree"],
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "debian_with_custom_packages",
|
||||
"description": "Test Debian image building with custom packages",
|
||||
"container": "debian:bookworm",
|
||||
"release": "bookworm",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools",
|
||||
"sudo",
|
||||
"network-manager"
|
||||
],
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "debian_arm64_build",
|
||||
"description": "Test Debian ARM64 image building",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "arm64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-arm64", "systemd", "ostree"],
|
||||
"expected_result": "success"
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_edge_case_tests() -> List[Dict[str, Any]]:
|
||||
"""Get edge case test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "empty_package_list",
|
||||
"description": "Test building with empty package list",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [],
|
||||
"expected_result": "error",
|
||||
"expected_error": "No packages specified"
|
||||
},
|
||||
{
|
||||
"name": "invalid_release",
|
||||
"description": "Test building with invalid Debian release",
|
||||
"container": "debian:trixie",
|
||||
"release": "invalid-release",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64"],
|
||||
"expected_result": "error",
|
||||
"expected_error": "Invalid Debian release"
|
||||
},
|
||||
{
|
||||
"name": "invalid_architecture",
|
||||
"description": "Test building with invalid architecture",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "invalid-arch",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64"],
|
||||
"expected_result": "error",
|
||||
"expected_error": "Invalid architecture"
|
||||
},
|
||||
{
|
||||
"name": "very_long_package_list",
|
||||
"description": "Test building with very long package list",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [f"package-{i}" for i in range(1000)],
|
||||
"expected_result": "success"
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_error_condition_tests() -> List[Dict[str, Any]]:
|
||||
"""Get error condition test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "invalid_container_image",
|
||||
"description": "Test building with invalid container image",
|
||||
"container": "invalid:image",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64"],
|
||||
"expected_result": "error",
|
||||
"expected_error": "Invalid container image"
|
||||
},
|
||||
{
|
||||
"name": "network_failure",
|
||||
"description": "Test building with network failure simulation",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64"],
|
||||
"expected_result": "error",
|
||||
"expected_error": "Network error",
|
||||
"simulate_network_failure": True
|
||||
},
|
||||
{
|
||||
"name": "disk_space_exhaustion",
|
||||
"description": "Test building with disk space exhaustion",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64"],
|
||||
"expected_result": "error",
|
||||
"expected_error": "Disk space exhausted",
|
||||
"simulate_disk_full": True
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_debian_specific_tests() -> List[Dict[str, Any]]:
|
||||
"""Get Debian-specific test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "debian_trixie_minimal",
|
||||
"description": "Test Debian Trixie minimal image",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools"
|
||||
],
|
||||
"debian_specific": {
|
||||
"initramfs_tools": True,
|
||||
"grub_efi": True,
|
||||
"ostree_integration": True
|
||||
},
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "debian_bookworm_desktop",
|
||||
"description": "Test Debian Bookworm desktop image",
|
||||
"container": "debian:bookworm",
|
||||
"release": "bookworm",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools",
|
||||
"task-desktop",
|
||||
"xorg",
|
||||
"lightdm"
|
||||
],
|
||||
"debian_specific": {
|
||||
"initramfs_tools": True,
|
||||
"grub_efi": True,
|
||||
"ostree_integration": True,
|
||||
"desktop_environment": True
|
||||
},
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "debian_bullseye_server",
|
||||
"description": "Test Debian Bullseye server image",
|
||||
"container": "debian:bullseye",
|
||||
"release": "bullseye",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools",
|
||||
"openssh-server",
|
||||
"nginx",
|
||||
"postgresql"
|
||||
],
|
||||
"debian_specific": {
|
||||
"initramfs_tools": True,
|
||||
"grub_efi": True,
|
||||
"ostree_integration": True,
|
||||
"server_services": True
|
||||
},
|
||||
"expected_result": "success"
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_performance_tests() -> List[Dict[str, Any]]:
|
||||
"""Get performance test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "small_image_build_time",
|
||||
"description": "Test build time for small image",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64", "systemd"],
|
||||
"performance_requirements": {
|
||||
"max_build_time": 300, # 5 minutes
|
||||
"max_image_size": 1024 # 1GB
|
||||
},
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "large_image_build_time",
|
||||
"description": "Test build time for large image",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [f"package-{i}" for i in range(500)],
|
||||
"performance_requirements": {
|
||||
"max_build_time": 1800, # 30 minutes
|
||||
"max_image_size": 10240 # 10GB
|
||||
},
|
||||
"expected_result": "success"
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_integration_tests() -> List[Dict[str, Any]]:
|
||||
"""Get integration test cases."""
|
||||
return [
|
||||
{
|
||||
"name": "full_pipeline_test",
|
||||
"description": "Test complete image building pipeline",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"image_type": "qcow2",
|
||||
"packages": [
|
||||
"linux-image-amd64",
|
||||
"systemd",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools"
|
||||
],
|
||||
"pipeline_stages": [
|
||||
"filesystem_setup",
|
||||
"package_installation",
|
||||
"ostree_integration",
|
||||
"bootloader_configuration",
|
||||
"image_generation"
|
||||
],
|
||||
"expected_result": "success"
|
||||
},
|
||||
{
|
||||
"name": "cross_architecture_test",
|
||||
"description": "Test cross-architecture building",
|
||||
"container": "debian:trixie",
|
||||
"release": "trixie",
|
||||
"architectures": ["amd64", "arm64"],
|
||||
"image_type": "qcow2",
|
||||
"packages": ["linux-image-amd64", "systemd", "ostree"],
|
||||
"expected_result": "success"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class TestDataGenerator:
|
||||
"""Generate test data for various test scenarios."""
|
||||
|
||||
@staticmethod
|
||||
def generate_manifest(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate a manifest from a test case."""
|
||||
manifest = {
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"name": "org.osbuild.debian-filesystem",
|
||||
"options": {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True,
|
||||
"home_symlink": True
|
||||
}
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"options": {
|
||||
"packages": test_case.get("packages", []),
|
||||
"release": test_case.get("release", "trixie"),
|
||||
"arch": test_case.get("arch", "amd64")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Add Debian-specific stages if specified
|
||||
if test_case.get("debian_specific", {}).get("grub_efi"):
|
||||
manifest["pipeline"]["stages"].append({
|
||||
"name": "org.osbuild.debian-grub",
|
||||
"options": {
|
||||
"uefi": True,
|
||||
"secure_boot": False,
|
||||
"timeout": 5
|
||||
}
|
||||
})
|
||||
|
||||
if test_case.get("debian_specific", {}).get("initramfs_tools"):
|
||||
manifest["pipeline"]["stages"].append({
|
||||
"name": "org.osbuild.debian-kernel",
|
||||
"options": {
|
||||
"kernel_package": f"linux-image-{test_case.get('arch', 'amd64')}",
|
||||
"initramfs_tools": True,
|
||||
"ostree_integration": True
|
||||
}
|
||||
})
|
||||
|
||||
return manifest
|
||||
|
||||
@staticmethod
|
||||
def generate_test_environment(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate test environment configuration."""
|
||||
return {
|
||||
"work_dir": "/tmp/test-env",
|
||||
"output_dir": "/tmp/test-output",
|
||||
"cache_dir": "/tmp/test-cache",
|
||||
"temp_dir": "/tmp/test-temp",
|
||||
"network_enabled": not test_case.get("simulate_network_failure", False),
|
||||
"disk_space_available": not test_case.get("simulate_disk_full", False)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_expected_output(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate expected output for a test case."""
|
||||
expected_output = {
|
||||
"status": test_case.get("expected_result", "success"),
|
||||
"image_type": test_case.get("image_type", "qcow2"),
|
||||
"architecture": test_case.get("arch", "amd64"),
|
||||
"release": test_case.get("release", "trixie")
|
||||
}
|
||||
|
||||
if test_case.get("expected_result") == "success":
|
||||
expected_output["image_path"] = f"/tmp/test-output/debian-{test_case.get('release')}-{test_case.get('arch')}.{test_case.get('image_type')}"
|
||||
expected_output["build_log"] = "Build completed successfully"
|
||||
else:
|
||||
expected_output["error"] = test_case.get("expected_error", "Unknown error")
|
||||
expected_output["build_log"] = f"Build failed: {test_case.get('expected_error', 'Unknown error')}"
|
||||
|
||||
return expected_output
|
||||
|
||||
|
||||
def load_test_cases_from_file(file_path: str) -> List[Dict[str, Any]]:
|
||||
"""Load test cases from a file."""
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
if file_path.endswith('.json'):
|
||||
return json.load(f)
|
||||
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file format: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load test cases from {file_path}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def save_test_cases_to_file(test_cases: List[Dict[str, Any]], file_path: str) -> bool:
|
||||
"""Save test cases to a file."""
|
||||
try:
|
||||
with open(file_path, 'w') as f:
|
||||
if file_path.endswith('.json'):
|
||||
json.dump(test_cases, f, indent=2)
|
||||
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
||||
yaml.dump(test_cases, f, default_flow_style=False)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file format: {file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save test cases to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def validate_test_case(test_case: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Validate a test case definition."""
|
||||
validation_result = {
|
||||
"valid": True,
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
}
|
||||
|
||||
# Check required fields
|
||||
required_fields = ["name", "description", "container", "release", "arch", "image_type", "packages"]
|
||||
for field in required_fields:
|
||||
if field not in test_case:
|
||||
validation_result["valid"] = False
|
||||
validation_result["errors"].append(f"Missing required field: {field}")
|
||||
|
||||
# Check field types
|
||||
if "packages" in test_case and not isinstance(test_case["packages"], list):
|
||||
validation_result["valid"] = False
|
||||
validation_result["errors"].append("Packages field must be a list")
|
||||
|
||||
if "arch" in test_case and test_case["arch"] not in ["amd64", "arm64", "i386"]:
|
||||
validation_result["warnings"].append(f"Unsupported architecture: {test_case['arch']}")
|
||||
|
||||
if "release" in test_case and test_case["release"] not in ["trixie", "bookworm", "bullseye"]:
|
||||
validation_result["warnings"].append(f"Unsupported Debian release: {test_case['release']}")
|
||||
|
||||
return validation_result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test the test case definitions
|
||||
test_cases = TestCaseDefinitions.get_basic_functionality_tests()
|
||||
print(f"Generated {len(test_cases)} basic functionality test cases")
|
||||
|
||||
for test_case in test_cases:
|
||||
manifest = TestDataGenerator.generate_manifest(test_case)
|
||||
print(f"Generated manifest for {test_case['name']}: {len(manifest['pipeline']['stages'])} stages")
|
||||
|
||||
validation = validate_test_case(test_case)
|
||||
print(f"Validation for {test_case['name']}: {'Valid' if validation['valid'] else 'Invalid'}")
|
||||
if validation['errors']:
|
||||
print(f" Errors: {validation['errors']}")
|
||||
if validation['warnings']:
|
||||
print(f" Warnings: {validation['warnings']}")
|
||||
289
test/testutil.py
Normal file
289
test/testutil.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test utilities for deb-bootc-image-builder.
|
||||
|
||||
This module provides common utilities for testing, including:
|
||||
- Test data generation
|
||||
- Mock objects
|
||||
- Helper functions
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import yaml
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestDataGenerator:
|
||||
"""Generate test data for deb-bootc-image-builder tests."""
|
||||
|
||||
@staticmethod
|
||||
def create_debian_package_list() -> List[str]:
|
||||
"""Create a list of Debian packages for testing."""
|
||||
return [
|
||||
"linux-image-amd64",
|
||||
"linux-headers-amd64",
|
||||
"systemd",
|
||||
"systemd-sysv",
|
||||
"dbus",
|
||||
"ostree",
|
||||
"grub-efi-amd64",
|
||||
"initramfs-tools",
|
||||
"util-linux",
|
||||
"parted",
|
||||
"e2fsprogs",
|
||||
"dosfstools",
|
||||
"efibootmgr",
|
||||
"sudo",
|
||||
"network-manager",
|
||||
"curl",
|
||||
"wget",
|
||||
"nano",
|
||||
"vim-tiny"
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def create_debian_repository_config() -> Dict[str, Any]:
|
||||
"""Create Debian repository configuration for testing."""
|
||||
return {
|
||||
"release": "trixie",
|
||||
"arch": "amd64",
|
||||
"repos": [
|
||||
{
|
||||
"name": "debian",
|
||||
"baseurls": ["http://deb.debian.org/debian"],
|
||||
"enabled": True
|
||||
},
|
||||
{
|
||||
"name": "debian-security",
|
||||
"baseurls": ["http://deb.debian.org/debian-security"],
|
||||
"enabled": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_ostree_config() -> Dict[str, Any]:
|
||||
"""Create OSTree configuration for testing."""
|
||||
return {
|
||||
"mode": "bare-user-only",
|
||||
"repo": "/var/lib/ostree/repo",
|
||||
"bootable": True,
|
||||
"deployment": {
|
||||
"osname": "debian",
|
||||
"ref": "debian/trixie/amd64"
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_grub_config() -> Dict[str, Any]:
|
||||
"""Create GRUB configuration for testing."""
|
||||
return {
|
||||
"uefi": True,
|
||||
"secure_boot": False,
|
||||
"timeout": 5,
|
||||
"default_entry": 0,
|
||||
"kernel_path": "/boot/vmlinuz",
|
||||
"initramfs_path": "/boot/initrd.img"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_filesystem_config() -> Dict[str, Any]:
|
||||
"""Create filesystem configuration for testing."""
|
||||
return {
|
||||
"rootfs_type": "ext4",
|
||||
"ostree_integration": True,
|
||||
"home_symlink": True,
|
||||
"users": [
|
||||
{
|
||||
"name": "debian-user",
|
||||
"password": "debian",
|
||||
"groups": ["sudo", "users"]
|
||||
}
|
||||
],
|
||||
"permissions": {
|
||||
"/etc/ostree": "755",
|
||||
"/var/lib/ostree": "755"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockContainerImage:
|
||||
"""Mock container image for testing."""
|
||||
|
||||
def __init__(self, labels: Optional[Dict[str, str]] = None):
|
||||
"""Initialize mock container image."""
|
||||
self.labels = labels or {
|
||||
"com.debian.bootc": "true",
|
||||
"ostree.bootable": "true",
|
||||
"org.debian.version": "13",
|
||||
"version": "1.0"
|
||||
}
|
||||
self.ref = "debian:trixie"
|
||||
self.arch = "amd64"
|
||||
self.os = "linux"
|
||||
|
||||
def get_labels(self) -> Dict[str, str]:
|
||||
"""Get image labels."""
|
||||
return self.labels
|
||||
|
||||
def get_ref(self) -> str:
|
||||
"""Get image reference."""
|
||||
return self.ref
|
||||
|
||||
def get_arch(self) -> str:
|
||||
"""Get image architecture."""
|
||||
return self.arch
|
||||
|
||||
def get_os(self) -> str:
|
||||
"""Get image operating system."""
|
||||
return self.os
|
||||
|
||||
|
||||
class MockOSTreeRepo:
|
||||
"""Mock OSTree repository for testing."""
|
||||
|
||||
def __init__(self, path: str):
|
||||
"""Initialize mock OSTree repository."""
|
||||
self.path = path
|
||||
self.refs = ["debian/trixie/amd64"]
|
||||
self.deployments = []
|
||||
|
||||
def list_refs(self) -> List[str]:
|
||||
"""List repository references."""
|
||||
return self.refs
|
||||
|
||||
def list_deployments(self) -> List[Dict[str, Any]]:
|
||||
"""List repository deployments."""
|
||||
return self.deployments
|
||||
|
||||
def get_deployment_info(self, ref: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get deployment information."""
|
||||
if ref in self.refs:
|
||||
return {
|
||||
"ref": ref,
|
||||
"osname": "debian",
|
||||
"bootable": True,
|
||||
"version": "13"
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class TestEnvironment:
|
||||
"""Test environment setup and teardown."""
|
||||
|
||||
def __init__(self, work_dir: str):
|
||||
"""Initialize test environment."""
|
||||
self.work_dir = work_dir
|
||||
self.original_cwd = os.getcwd()
|
||||
|
||||
def setup(self):
|
||||
"""Set up test environment."""
|
||||
os.chdir(self.work_dir)
|
||||
|
||||
# Create basic directory structure
|
||||
dirs = [
|
||||
"etc", "var", "home", "boot", "usr",
|
||||
"usr/bin", "usr/lib", "usr/sbin",
|
||||
"var/lib", "var/lib/ostree", "var/home"
|
||||
]
|
||||
|
||||
for dir_path in dirs:
|
||||
full_path = os.path.join(self.work_dir, dir_path)
|
||||
os.makedirs(full_path, exist_ok=True)
|
||||
|
||||
# Create /home -> /var/home symlink
|
||||
var_home = os.path.join(self.work_dir, "var", "home")
|
||||
home_link = os.path.join(self.work_dir, "home")
|
||||
if os.path.exists(home_link):
|
||||
os.remove(home_link)
|
||||
os.symlink(var_home, home_link)
|
||||
|
||||
logger.info(f"Test environment set up in {self.work_dir}")
|
||||
|
||||
def teardown(self):
|
||||
"""Tear down test environment."""
|
||||
os.chdir(self.original_cwd)
|
||||
logger.info("Test environment torn down")
|
||||
|
||||
def create_test_file(self, path: str, content: str = ""):
|
||||
"""Create a test file with specified content."""
|
||||
full_path = os.path.join(self.work_dir, path.lstrip("/"))
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
|
||||
with open(full_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return full_path
|
||||
|
||||
def create_test_directory(self, path: str):
|
||||
"""Create a test directory."""
|
||||
full_path = os.path.join(self.work_dir, path.lstrip("/"))
|
||||
os.makedirs(full_path, exist_ok=True)
|
||||
return full_path
|
||||
|
||||
|
||||
def create_temp_manifest(manifest_data: Dict[str, Any]) -> str:
|
||||
"""Create a temporary manifest file for testing."""
|
||||
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
||||
|
||||
try:
|
||||
json.dump(manifest_data, temp_file, indent=2)
|
||||
temp_file.close()
|
||||
return temp_file.name
|
||||
except Exception as e:
|
||||
temp_file.close()
|
||||
os.unlink(temp_file.name)
|
||||
raise e
|
||||
|
||||
|
||||
def cleanup_temp_files(*file_paths: str):
|
||||
"""Clean up temporary files."""
|
||||
for file_path in file_paths:
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.unlink(file_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clean up {file_path}: {e}")
|
||||
|
||||
|
||||
def assert_filesystem_structure(work_dir: str, expected_dirs: List[str]):
|
||||
"""Assert that expected filesystem structure exists."""
|
||||
for expected_dir in expected_dirs:
|
||||
full_path = os.path.join(work_dir, expected_dir.lstrip("/"))
|
||||
assert os.path.exists(full_path), f"Directory {expected_dir} not found"
|
||||
assert os.path.isdir(full_path), f"{expected_dir} is not a directory"
|
||||
|
||||
|
||||
def assert_file_contents(file_path: str, expected_content: str):
|
||||
"""Assert that file contains expected content."""
|
||||
assert os.path.exists(file_path), f"File {file_path} not found"
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
actual_content = f.read()
|
||||
|
||||
assert actual_content == expected_content, \
|
||||
f"File content mismatch in {file_path}"
|
||||
|
||||
|
||||
def create_mock_context():
|
||||
"""Create a mock osbuild context for testing."""
|
||||
context = Mock()
|
||||
context.root = "/tmp/mock-root"
|
||||
|
||||
def mock_run(cmd):
|
||||
mock_result = Mock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = b"mock output"
|
||||
mock_result.stderr = b""
|
||||
return mock_result
|
||||
|
||||
context.run = mock_run
|
||||
return context
|
||||
274
test/testutil_test.py
Normal file
274
test/testutil_test.py
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test the test utilities for deb-bootc-image-builder.
|
||||
|
||||
This module tests the test utility functions and classes,
|
||||
including:
|
||||
- Test data generation
|
||||
- Mock objects
|
||||
- Helper functions
|
||||
- Debian-specific utilities
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import Mock, patch
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestTestUtilities:
|
||||
"""Test cases for test utility functions and classes."""
|
||||
|
||||
def test_test_data_generator(self, work_dir):
|
||||
"""Test the TestDataGenerator class."""
|
||||
from testutil import TestDataGenerator
|
||||
|
||||
# Test Debian package list generation
|
||||
packages = TestDataGenerator.create_debian_package_list()
|
||||
assert isinstance(packages, list)
|
||||
assert len(packages) > 0
|
||||
|
||||
# Check for essential Debian packages
|
||||
essential_packages = ["linux-image-amd64", "systemd", "ostree"]
|
||||
for pkg in essential_packages:
|
||||
assert pkg in packages, f"Essential package {pkg} missing"
|
||||
|
||||
# Test repository configuration
|
||||
repo_config = TestDataGenerator.create_debian_repository_config()
|
||||
assert "release" in repo_config
|
||||
assert "arch" in repo_config
|
||||
assert "repos" in repo_config
|
||||
assert repo_config["release"] == "trixie"
|
||||
assert repo_config["arch"] == "amd64"
|
||||
|
||||
# Test OSTree configuration
|
||||
ostree_config = TestDataGenerator.create_ostree_config()
|
||||
assert "mode" in ostree_config
|
||||
assert "repo" in ostree_config
|
||||
assert ostree_config["mode"] == "bare-user-only"
|
||||
|
||||
# Test GRUB configuration
|
||||
grub_config = TestDataGenerator.create_grub_config()
|
||||
assert "uefi" in grub_config
|
||||
assert "timeout" in grub_config
|
||||
assert grub_config["uefi"] is True
|
||||
|
||||
def test_mock_container_image(self, work_dir):
|
||||
"""Test the MockContainerImage class."""
|
||||
from testutil import MockContainerImage
|
||||
|
||||
# Test default labels
|
||||
image = MockContainerImage()
|
||||
labels = image.get_labels()
|
||||
|
||||
assert "com.debian.bootc" in labels
|
||||
assert "ostree.bootable" in labels
|
||||
assert labels["com.debian.bootc"] == "true"
|
||||
assert labels["ostree.bootable"] == "true"
|
||||
|
||||
# Test custom labels
|
||||
custom_labels = {
|
||||
"com.debian.bootc": "true",
|
||||
"version": "2.0",
|
||||
"custom.label": "value"
|
||||
}
|
||||
image = MockContainerImage(custom_labels)
|
||||
|
||||
assert image.get_labels() == custom_labels
|
||||
assert image.get_ref() == "debian:trixie"
|
||||
assert image.get_arch() == "amd64"
|
||||
assert image.get_os() == "linux"
|
||||
|
||||
def test_mock_ostree_repo(self, work_dir):
|
||||
"""Test the MockOSTreeRepo class."""
|
||||
from testutil import MockOSTreeRepo
|
||||
|
||||
# Test repository creation
|
||||
repo = MockOSTreeRepo("/tmp/test-repo")
|
||||
|
||||
assert repo.path == "/tmp/test-repo"
|
||||
assert len(repo.list_refs()) > 0
|
||||
assert "debian/trixie/amd64" in repo.list_refs()
|
||||
|
||||
# Test deployment info
|
||||
deployment_info = repo.get_deployment_info("debian/trixie/amd64")
|
||||
assert deployment_info is not None
|
||||
assert deployment_info["ref"] == "debian/trixie/amd64"
|
||||
assert deployment_info["osname"] == "debian"
|
||||
assert deployment_info["bootable"] is True
|
||||
|
||||
# Test non-existent deployment
|
||||
non_existent = repo.get_deployment_info("non-existent")
|
||||
assert non_existent is None
|
||||
|
||||
def test_test_environment(self, work_dir):
|
||||
"""Test the TestEnvironment class."""
|
||||
from testutil import TestEnvironment
|
||||
|
||||
# Create test environment
|
||||
env = TestEnvironment(work_dir)
|
||||
|
||||
# Test setup
|
||||
env.setup()
|
||||
|
||||
# Check that directories were created
|
||||
expected_dirs = ["etc", "var", "home", "boot", "usr", "var/lib", "var/lib/ostree", "var/home"]
|
||||
for expected_dir in expected_dirs:
|
||||
full_path = os.path.join(work_dir, expected_dir)
|
||||
assert os.path.exists(full_path), f"Directory {expected_dir} not created"
|
||||
|
||||
# Check /home symlink
|
||||
home_link = os.path.join(work_dir, "home")
|
||||
var_home = os.path.join(work_dir, "var", "home")
|
||||
assert os.path.islink(home_link), "/home symlink not created"
|
||||
assert os.path.realpath(home_link) == var_home
|
||||
|
||||
# Test file creation
|
||||
test_file = env.create_test_file("test.txt", "test content")
|
||||
assert os.path.exists(test_file)
|
||||
|
||||
with open(test_file, 'r') as f:
|
||||
content = f.read()
|
||||
assert content == "test content"
|
||||
|
||||
# Test directory creation
|
||||
test_dir = env.create_test_directory("test_dir")
|
||||
assert os.path.exists(test_dir)
|
||||
assert os.path.isdir(test_dir)
|
||||
|
||||
# Test teardown
|
||||
env.teardown()
|
||||
# Note: teardown only changes directory, doesn't remove files
|
||||
|
||||
def test_utility_functions(self, work_dir):
|
||||
"""Test utility functions."""
|
||||
from testutil import (
|
||||
create_temp_manifest,
|
||||
cleanup_temp_files,
|
||||
assert_filesystem_structure,
|
||||
assert_file_contents,
|
||||
create_mock_context
|
||||
)
|
||||
|
||||
# Test manifest creation
|
||||
manifest_data = {
|
||||
"pipeline": {
|
||||
"build": {"name": "test"},
|
||||
"stages": []
|
||||
}
|
||||
}
|
||||
|
||||
manifest_file = create_temp_manifest(manifest_data)
|
||||
assert os.path.exists(manifest_file)
|
||||
|
||||
# Test manifest loading
|
||||
with open(manifest_file, 'r') as f:
|
||||
loaded_data = json.load(f)
|
||||
assert loaded_data == manifest_data
|
||||
|
||||
# Test cleanup
|
||||
cleanup_temp_files(manifest_file)
|
||||
assert not os.path.exists(manifest_file)
|
||||
|
||||
# Test filesystem structure assertion
|
||||
test_dir = os.path.join(work_dir, "test_structure")
|
||||
os.makedirs(test_dir, exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "etc"), exist_ok=True)
|
||||
os.makedirs(os.path.join(test_dir, "var"), exist_ok=True)
|
||||
|
||||
assert_filesystem_structure(test_dir, ["/etc", "/var"])
|
||||
|
||||
# Test file contents assertion
|
||||
test_file = os.path.join(test_dir, "test.txt")
|
||||
with open(test_file, 'w') as f:
|
||||
f.write("test content")
|
||||
|
||||
assert_file_contents(test_file, "test content")
|
||||
|
||||
# Test mock context creation
|
||||
context = create_mock_context()
|
||||
assert context.root == "/tmp/mock-root"
|
||||
assert hasattr(context, 'run')
|
||||
|
||||
# Test mock context run method
|
||||
result = context.run("test command")
|
||||
assert result.returncode == 0
|
||||
assert result.stdout == b"mock output"
|
||||
|
||||
def test_debian_specific_utilities(self, work_dir):
|
||||
"""Test Debian-specific utility functions."""
|
||||
from testutil import TestDataGenerator
|
||||
|
||||
# Test Debian filesystem configuration
|
||||
fs_config = TestDataGenerator.create_filesystem_config()
|
||||
|
||||
assert "rootfs_type" in fs_config
|
||||
assert "ostree_integration" in fs_config
|
||||
assert "home_symlink" in fs_config
|
||||
assert "users" in fs_config
|
||||
assert "permissions" in fs_config
|
||||
|
||||
assert fs_config["rootfs_type"] == "ext4"
|
||||
assert fs_config["ostree_integration"] is True
|
||||
assert fs_config["home_symlink"] is True
|
||||
|
||||
# Test user configuration
|
||||
users = fs_config["users"]
|
||||
assert len(users) > 0
|
||||
assert "name" in users[0]
|
||||
assert "password" in users[0]
|
||||
assert "groups" in users[0]
|
||||
|
||||
# Test permission configuration
|
||||
permissions = fs_config["permissions"]
|
||||
assert "/etc/ostree" in permissions
|
||||
assert "/var/lib/ostree" in permissions
|
||||
assert permissions["/etc/ostree"] == "755"
|
||||
|
||||
def test_error_handling(self, work_dir):
|
||||
"""Test error handling in utility functions."""
|
||||
from testutil import cleanup_temp_files, assert_file_contents
|
||||
|
||||
# Test cleanup with non-existent file
|
||||
cleanup_temp_files("/non/existent/file")
|
||||
# Should not raise an exception
|
||||
|
||||
# Test file contents assertion with non-existent file
|
||||
with pytest.raises(AssertionError):
|
||||
assert_file_contents("/non/existent/file", "content")
|
||||
|
||||
def test_performance_utilities(self, work_dir):
|
||||
"""Test performance-related utilities."""
|
||||
from testutil import create_mock_context
|
||||
|
||||
# Test multiple context creation
|
||||
start_time = time.time()
|
||||
contexts = []
|
||||
|
||||
for i in range(100):
|
||||
context = create_mock_context()
|
||||
contexts.append(context)
|
||||
|
||||
end_time = time.time()
|
||||
creation_time = end_time - start_time
|
||||
|
||||
# Should be reasonably fast
|
||||
assert creation_time < 1.0, f"Context creation took too long: {creation_time}s"
|
||||
assert len(contexts) == 100
|
||||
|
||||
# Test context functionality
|
||||
for context in contexts:
|
||||
result = context.run("test")
|
||||
assert result.returncode == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
461
test/vm.py
Normal file
461
test/vm.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Virtual machine testing utilities for deb-bootc-image-builder.
|
||||
|
||||
This module provides utilities for testing built images in virtual machines,
|
||||
including:
|
||||
- VM creation and management
|
||||
- Image boot testing
|
||||
- Debian-specific VM configurations
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VirtualMachine:
|
||||
"""Virtual machine for testing Debian images."""
|
||||
|
||||
def __init__(self, name: str, image_path: str, vm_type: str = "qemu"):
|
||||
"""Initialize virtual machine."""
|
||||
self.name = name
|
||||
self.image_path = image_path
|
||||
self.vm_type = vm_type
|
||||
self.vm_process = None
|
||||
self.vm_pid = None
|
||||
self.network_config = None
|
||||
self.memory = "2G"
|
||||
self.cpus = 2
|
||||
self.disk_size = "10G"
|
||||
|
||||
# Debian-specific configurations
|
||||
self.debian_release = "trixie"
|
||||
self.architecture = "amd64"
|
||||
self.boot_timeout = 300 # 5 minutes
|
||||
|
||||
logger.info(f"Initialized VM {name} with image {image_path}")
|
||||
|
||||
def configure_network(self, network_type: str = "user", port_forward: Optional[Dict[str, int]] = None):
|
||||
"""Configure VM network."""
|
||||
self.network_config = {
|
||||
"type": network_type,
|
||||
"port_forward": port_forward or {}
|
||||
}
|
||||
logger.info(f"Configured network: {network_type}")
|
||||
|
||||
def set_resources(self, memory: str = "2G", cpus: int = 2, disk_size: str = "10G"):
|
||||
"""Set VM resource limits."""
|
||||
self.memory = memory
|
||||
self.cpus = cpus
|
||||
self.disk_size = disk_size
|
||||
logger.info(f"Set resources: {memory} RAM, {cpus} CPUs, {disk_size} disk")
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the virtual machine."""
|
||||
try:
|
||||
if self.vm_type == "qemu":
|
||||
return self._start_qemu()
|
||||
else:
|
||||
logger.error(f"Unsupported VM type: {self.vm_type}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start VM: {e}")
|
||||
return False
|
||||
|
||||
def _start_qemu(self) -> bool:
|
||||
"""Start QEMU virtual machine."""
|
||||
cmd = [
|
||||
"qemu-system-x86_64",
|
||||
"-name", self.name,
|
||||
"-m", self.memory,
|
||||
"-smp", str(self.cpus),
|
||||
"-drive", f"file={self.image_path},if=virtio,format=qcow2",
|
||||
"-enable-kvm",
|
||||
"-display", "none",
|
||||
"-serial", "stdio",
|
||||
"-monitor", "none"
|
||||
]
|
||||
|
||||
# Add network configuration
|
||||
if self.network_config:
|
||||
if self.network_config["type"] == "user":
|
||||
cmd.extend(["-net", "user"])
|
||||
if self.network_config["port_forward"]:
|
||||
for host_port, guest_port in self.network_config["port_forward"].items():
|
||||
cmd.extend(["-net", f"user,hostfwd=tcp::{host_port}-:{guest_port}"])
|
||||
elif self.network_config["type"] == "bridge":
|
||||
cmd.extend(["-net", "bridge,br=virbr0"])
|
||||
|
||||
# Add Debian-specific optimizations
|
||||
cmd.extend([
|
||||
"-cpu", "host",
|
||||
"-machine", "type=q35,accel=kvm",
|
||||
"-device", "virtio-net-pci,netdev=net0",
|
||||
"-netdev", "user,id=net0"
|
||||
])
|
||||
|
||||
logger.info(f"Starting QEMU VM with command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
self.vm_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
self.vm_pid = self.vm_process.pid
|
||||
logger.info(f"VM started with PID: {self.vm_pid}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start QEMU VM: {e}")
|
||||
return False
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""Stop the virtual machine."""
|
||||
try:
|
||||
if self.vm_process:
|
||||
self.vm_process.terminate()
|
||||
self.vm_process.wait(timeout=30)
|
||||
logger.info("VM stopped gracefully")
|
||||
return True
|
||||
else:
|
||||
logger.warning("No VM process to stop")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("VM did not stop gracefully, forcing termination")
|
||||
if self.vm_process:
|
||||
self.vm_process.kill()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop VM: {e}")
|
||||
return False
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if VM is running."""
|
||||
if self.vm_process:
|
||||
return self.vm_process.poll() is None
|
||||
return False
|
||||
|
||||
def wait_for_boot(self, timeout: Optional[int] = None) -> bool:
|
||||
"""Wait for VM to boot up."""
|
||||
if timeout is None:
|
||||
timeout = self.boot_timeout
|
||||
|
||||
logger.info(f"Waiting for VM to boot (timeout: {timeout}s)")
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
if self.is_running():
|
||||
# Check if VM is responsive (basic boot check)
|
||||
if self._check_boot_status():
|
||||
logger.info("VM booted successfully")
|
||||
return True
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
logger.error("VM boot timeout exceeded")
|
||||
return False
|
||||
|
||||
def _check_boot_status(self) -> bool:
|
||||
"""Check if VM has booted successfully."""
|
||||
# This is a simplified check - in practice, you'd want to:
|
||||
# 1. Check for specific boot messages
|
||||
# 2. Verify network connectivity
|
||||
# 3. Check for system services
|
||||
# 4. Verify Debian-specific indicators
|
||||
|
||||
try:
|
||||
# For now, just check if the process is still running
|
||||
return self.is_running()
|
||||
except Exception as e:
|
||||
logger.debug(f"Boot status check failed: {e}")
|
||||
return False
|
||||
|
||||
def execute_command(self, command: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""Execute a command in the VM (requires guest agent or SSH)."""
|
||||
# This is a placeholder - in practice, you'd use:
|
||||
# 1. QEMU guest agent
|
||||
# 2. SSH connection
|
||||
# 3. Serial console interaction
|
||||
|
||||
logger.info(f"Executing command in VM: {command}")
|
||||
|
||||
# For now, return a mock result
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": f"Mock output for: {command}",
|
||||
"stderr": "",
|
||||
"returncode": 0
|
||||
}
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
"""Get system information from the VM."""
|
||||
# This would typically use guest agent or SSH
|
||||
return {
|
||||
"os": "Debian GNU/Linux",
|
||||
"release": self.debian_release,
|
||||
"architecture": self.architecture,
|
||||
"kernel": "Linux",
|
||||
"uptime": "0:00:00",
|
||||
"memory": "0 MB",
|
||||
"disk": "0 MB"
|
||||
}
|
||||
|
||||
def check_debian_specific_features(self) -> Dict[str, bool]:
|
||||
"""Check Debian-specific features in the VM."""
|
||||
features = {
|
||||
"ostree_integration": False,
|
||||
"grub_efi": False,
|
||||
"initramfs_tools": False,
|
||||
"systemd": False,
|
||||
"apt_package_manager": False
|
||||
}
|
||||
|
||||
# This would check actual VM state
|
||||
# For now, return mock results
|
||||
features["ostree_integration"] = True
|
||||
features["grub_efi"] = True
|
||||
features["initramfs_tools"] = True
|
||||
features["systemd"] = True
|
||||
features["apt_package_manager"] = True
|
||||
|
||||
return features
|
||||
|
||||
|
||||
class VMTester:
|
||||
"""Test runner for virtual machines."""
|
||||
|
||||
def __init__(self, test_config: Dict[str, Any]):
|
||||
"""Initialize VM tester."""
|
||||
self.test_config = test_config
|
||||
self.vms: List[VirtualMachine] = []
|
||||
self.test_results: List[Dict[str, Any]] = []
|
||||
|
||||
def create_test_vm(self, image_path: str, vm_config: Dict[str, Any]) -> VirtualMachine:
|
||||
"""Create a test virtual machine."""
|
||||
vm_name = vm_config.get("name", f"test-vm-{len(self.vms)}")
|
||||
vm = VirtualMachine(vm_name, image_path)
|
||||
|
||||
# Apply configuration
|
||||
if "network" in vm_config:
|
||||
vm.configure_network(**vm_config["network"])
|
||||
|
||||
if "resources" in vm_config:
|
||||
vm.set_resources(**vm_config["resources"])
|
||||
|
||||
if "debian" in vm_config:
|
||||
debian_config = vm_config["debian"]
|
||||
if "release" in debian_config:
|
||||
vm.debian_release = debian_config["release"]
|
||||
if "architecture" in debian_config:
|
||||
vm.architecture = debian_config["architecture"]
|
||||
if "boot_timeout" in debian_config:
|
||||
vm.boot_timeout = debian_config["boot_timeout"]
|
||||
|
||||
self.vms.append(vm)
|
||||
return vm
|
||||
|
||||
def run_basic_boot_test(self, vm: VirtualMachine) -> Dict[str, Any]:
|
||||
"""Run basic boot test on VM."""
|
||||
test_result = {
|
||||
"test_name": "basic_boot_test",
|
||||
"vm_name": vm.name,
|
||||
"start_time": time.time(),
|
||||
"success": False,
|
||||
"error": None,
|
||||
"boot_time": None
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Starting basic boot test for VM: {vm.name}")
|
||||
|
||||
# Start VM
|
||||
if not vm.start():
|
||||
test_result["error"] = "Failed to start VM"
|
||||
return test_result
|
||||
|
||||
# Wait for boot
|
||||
start_time = time.time()
|
||||
if vm.wait_for_boot():
|
||||
boot_time = time.time() - start_time
|
||||
test_result["boot_time"] = boot_time
|
||||
test_result["success"] = True
|
||||
logger.info(f"Boot test passed for VM: {vm.name} (boot time: {boot_time:.2f}s)")
|
||||
else:
|
||||
test_result["error"] = "VM failed to boot within timeout"
|
||||
|
||||
except Exception as e:
|
||||
test_result["error"] = str(e)
|
||||
logger.error(f"Boot test failed for VM {vm.name}: {e}")
|
||||
|
||||
finally:
|
||||
test_result["end_time"] = time.time()
|
||||
if test_result["success"]:
|
||||
vm.stop()
|
||||
|
||||
return test_result
|
||||
|
||||
def run_debian_feature_test(self, vm: VirtualMachine) -> Dict[str, Any]:
|
||||
"""Run Debian-specific feature test on VM."""
|
||||
test_result = {
|
||||
"test_name": "debian_feature_test",
|
||||
"vm_name": vm.name,
|
||||
"start_time": time.time(),
|
||||
"success": False,
|
||||
"error": None,
|
||||
"features": {}
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Starting Debian feature test for VM: {vm.name}")
|
||||
|
||||
# Start VM
|
||||
if not vm.start():
|
||||
test_result["error"] = "Failed to start VM"
|
||||
return test_result
|
||||
|
||||
# Wait for boot
|
||||
if not vm.wait_for_boot():
|
||||
test_result["error"] = "VM failed to boot"
|
||||
return test_result
|
||||
|
||||
# Check Debian features
|
||||
features = vm.check_debian_specific_features()
|
||||
test_result["features"] = features
|
||||
|
||||
# Validate required features
|
||||
required_features = ["ostree_integration", "grub_efi", "systemd"]
|
||||
missing_features = [f for f in required_features if not features.get(f, False)]
|
||||
|
||||
if missing_features:
|
||||
test_result["error"] = f"Missing required features: {missing_features}"
|
||||
else:
|
||||
test_result["success"] = True
|
||||
logger.info(f"Debian feature test passed for VM: {vm.name}")
|
||||
|
||||
except Exception as e:
|
||||
test_result["error"] = str(e)
|
||||
logger.error(f"Debian feature test failed for VM {vm.name}: {e}")
|
||||
|
||||
finally:
|
||||
test_result["end_time"] = time.time()
|
||||
if vm.is_running():
|
||||
vm.stop()
|
||||
|
||||
return test_result
|
||||
|
||||
def run_all_tests(self) -> List[Dict[str, Any]]:
|
||||
"""Run all configured tests."""
|
||||
logger.info("Starting VM test suite")
|
||||
|
||||
for vm in self.vms:
|
||||
# Run basic boot test
|
||||
boot_result = self.run_basic_boot_test(vm)
|
||||
self.test_results.append(boot_result)
|
||||
|
||||
# Run Debian feature test
|
||||
feature_result = self.run_debian_feature_test(vm)
|
||||
self.test_results.append(feature_result)
|
||||
|
||||
# Generate summary
|
||||
total_tests = len(self.test_results)
|
||||
passed_tests = len([r for r in self.test_results if r["success"]])
|
||||
failed_tests = total_tests - passed_tests
|
||||
|
||||
logger.info(f"Test suite completed: {passed_tests}/{total_tests} tests passed")
|
||||
|
||||
if failed_tests > 0:
|
||||
logger.warning(f"{failed_tests} tests failed")
|
||||
for result in self.test_results:
|
||||
if not result["success"]:
|
||||
logger.error(f"Test {result['test_name']} failed: {result.get('error', 'Unknown error')}")
|
||||
|
||||
return self.test_results
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up all VMs and resources."""
|
||||
logger.info("Cleaning up VM resources")
|
||||
|
||||
for vm in self.vms:
|
||||
if vm.is_running():
|
||||
vm.stop()
|
||||
|
||||
self.vms.clear()
|
||||
self.test_results.clear()
|
||||
|
||||
|
||||
def create_test_vm_config(image_path: str,
|
||||
debian_release: str = "trixie",
|
||||
architecture: str = "amd64") -> Dict[str, Any]:
|
||||
"""Create a test VM configuration."""
|
||||
return {
|
||||
"name": f"debian-{debian_release}-{architecture}-test",
|
||||
"image_path": image_path,
|
||||
"network": {
|
||||
"type": "user",
|
||||
"port_forward": {"2222": 22} # SSH port forwarding
|
||||
},
|
||||
"resources": {
|
||||
"memory": "2G",
|
||||
"cpus": 2,
|
||||
"disk_size": "10G"
|
||||
},
|
||||
"debian": {
|
||||
"release": debian_release,
|
||||
"architecture": architecture,
|
||||
"boot_timeout": 300
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def run_vm_test_suite(image_path: str,
|
||||
debian_release: str = "trixie",
|
||||
architecture: str = "amd64") -> List[Dict[str, Any]]:
|
||||
"""Run a complete VM test suite."""
|
||||
# Create test configuration
|
||||
vm_config = create_test_vm_config(image_path, debian_release, architecture)
|
||||
|
||||
# Create tester
|
||||
tester = VMTester({})
|
||||
|
||||
try:
|
||||
# Create test VM
|
||||
vm = tester.create_test_vm(image_path, vm_config)
|
||||
|
||||
# Run tests
|
||||
results = tester.run_all_tests()
|
||||
|
||||
return results
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
tester.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
test_image = "/tmp/test-image.qcow2"
|
||||
|
||||
if os.path.exists(test_image):
|
||||
print("Running VM test suite...")
|
||||
results = run_vm_test_suite(test_image)
|
||||
|
||||
for result in results:
|
||||
status = "PASS" if result["success"] else "FAIL"
|
||||
print(f"{status}: {result['test_name']} - {result['vm_name']}")
|
||||
if not result["success"]:
|
||||
print(f" Error: {result.get('error', 'Unknown error')}")
|
||||
else:
|
||||
print(f"Test image not found: {test_image}")
|
||||
print("Create a test image first or update the path")
|
||||
Loading…
Add table
Add a link
Reference in a new issue