#!/usr/bin/env python3 """ Integration test script for archbuild. This script creates a temporary repository, initializes it with a basic config, adds test packages, and verifies they build and are added correctly. Usage: python tests/integration_test.py [--keep-temp] Options: --keep-temp Don't delete temporary directory after test (for debugging) """ import asyncio import os import shutil import subprocess import sys import tempfile from pathlib import Path # Add src to path for development sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from archbuild.aur import AURClient from archbuild.builder import Builder, BuildStatus from archbuild.config import Config, RepositoryConfig, BuildingConfig, SigningConfig, PackageOverride from archbuild.logging import setup_logging, console from archbuild.repo import RepoManager # Test packages - real packages that exist in the AUR # Chosen for small size and fast build times TEST_PACKAGES = [ "neofetch-git", # Small bash script, very fast build "yay", # Popular AUR helper ] # Alternative packages if the above aren't available FALLBACK_PACKAGES = [ "gtk2", # Legacy GTK library "paru", # Another AUR helper ] class IntegrationTest: """Integration test runner.""" def __init__(self, keep_temp: bool = False, use_cli: bool = False, use_binary: bool = False): self.keep_temp = keep_temp self.use_cli = use_cli self.use_binary = use_binary self.temp_dir: Path | None = None self.config: Config | None = None self.config_path: Path | None = None self.passed = 0 self.failed = 0 def _run_cli(self, command: str, *args: str) -> subprocess.CompletedProcess: """Run archbuild CLI command via subprocess.""" if not self.config_path: raise RuntimeError("Config path not set") env = os.environ.copy() if self.use_binary: binary_path = Path(__file__).parent.parent / "dist" / "archbuild-bin" if not binary_path.exists(): raise RuntimeError(f"Binary not found at {binary_path}. Run scripts/build_binary.py first.") cmd = [str(binary_path), "-c", str(self.config_path), command] + list(args) # No PYTHONPATH needed for the standalone binary else: # Add src to PYTHONPATH so the CLI can find the package env["PYTHONPATH"] = str(Path(__file__).parent.parent / "src") cmd = [sys.executable, "-m", "archbuild.cli", "-c", str(self.config_path), command] + list(args) return subprocess.run(cmd, capture_output=True, text=True, env=env) def setup(self) -> None: """Set up temporary test environment.""" console.print("\n[bold blue]═══ Setting up test environment ═══[/]") # Create temp directory self.temp_dir = Path(tempfile.mkdtemp(prefix="archbuild_test_")) console.print(f" Created temp directory: {self.temp_dir}") # Create subdirectories repo_dir = self.temp_dir / "repo" build_dir = self.temp_dir / "build" repo_dir.mkdir() build_dir.mkdir() # Create custom makepkg.conf that disables debug packages # (avoids needing debugedit which may not be installed) makepkg_conf = self.temp_dir / "makepkg.conf" makepkg_conf.write_text(""" # Minimal makepkg.conf for testing CARCH="x86_64" CHOST="x86_64-pc-linux-gnu" CFLAGS="-O2 -pipe" CXXFLAGS="$CFLAGS" LDFLAGS="" MAKEFLAGS="-j$(nproc)" OPTIONS=(!debug !strip !staticlibs) PKGEXT='.pkg.tar.zst' SRCEXT='.src.tar.gz' PACKAGER="Integration Test " DLAGENTS=('file::/usr/bin/curl -qgC - -o %o %u' 'ftp::/usr/bin/curl -qgfC - --ftp-pasv --retry 3 --retry-delay 3 -o %o %u' 'http::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u' 'https::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u' 'rsync::/usr/bin/rsync --no-motd -z %u %o' 'scp::/usr/bin/scp -C %u %o') VCSCLIENTS=('git::git' 'hg::mercurial' 'svn::subversion') """) self.config = Config( repository=RepositoryConfig( name="testrepo", path=repo_dir, build_dir=build_dir, compression="zst", ), building=BuildingConfig( parallel=False, # Sequential for cleaner test output max_workers=1, clean=True, update_system=False, retry_attempts=2, ), signing=SigningConfig( enabled=False, ), log_level="INFO", # Apply package overrides to use our custom makepkg.conf package_overrides={ "_default": PackageOverride( extra_args=["--config", str(makepkg_conf)], ), }, ) # Save config to disk for CLI to use from archbuild.config import save_config self.config_path = self.temp_dir / "config.yaml" save_config(self.config, self.config_path) setup_logging(self.config.log_level) console.print(" [green]✓[/] Configuration created") def teardown(self) -> None: """Clean up test environment.""" if self.temp_dir and self.temp_dir.exists(): if self.keep_temp: console.print(f"\n[yellow]Keeping temp directory:[/] {self.temp_dir}") else: shutil.rmtree(self.temp_dir) console.print("\n[dim]Cleaned up temp directory[/]") def check(self, condition: bool, message: str) -> bool: """Check a condition and report pass/fail.""" if condition: console.print(f" [green]✓[/] {message}") self.passed += 1 return True else: console.print(f" [red]✗[/] {message}") self.failed += 1 return False async def test_init(self) -> bool: """Test repository initialization.""" console.print("\n[bold blue]═══ Test: Repository Initialization ═══[/]") if self.use_cli: result = self._run_cli("init") self.check(result.returncode == 0, "CLI: init command success") else: repo = RepoManager(self.config) repo.ensure_repo_exists() # Check directories exist self.check( self.config.repository.path.exists(), f"Repository directory exists: {self.config.repository.path}" ) self.check( self.config.repository.build_dir.exists(), f"Build directory exists: {self.config.repository.build_dir}" ) return self.failed == 0 async def test_aur_client(self) -> list[str]: """Test AUR client and find available test packages.""" console.print("\n[bold blue]═══ Test: AUR Client ═══[/]") available_packages = [] async with AURClient() as aur: # Test package lookup for pkg_name in TEST_PACKAGES + FALLBACK_PACKAGES: pkg = await aur.get_package(pkg_name) if pkg: self.check(True, f"Found package: {pkg_name} ({pkg.version})") available_packages.append(pkg_name) if len(available_packages) >= 2: break else: console.print(f" [yellow]⚠[/] Package not found: {pkg_name}") self.check( len(available_packages) >= 1, f"Found {len(available_packages)} test package(s)" ) return available_packages async def test_build_packages(self, packages: list[str]) -> dict[str, BuildStatus]: """Test building packages.""" console.print("\n[bold blue]═══ Test: Package Building ═══[/]") results: dict[str, BuildStatus] = {} if self.use_cli: for pkg_name in packages: console.print(f"\n Building {pkg_name} via CLI...") result = self._run_cli("build", pkg_name, "-f") if result.returncode == 0: results[pkg_name] = BuildStatus.SUCCESS self.check(True, f"CLI: Built {pkg_name} successfully") # Check for created artifacts pkg_dir = self.config.repository.build_dir / pkg_name artifacts = list(pkg_dir.glob("*.pkg.tar.*")) artifacts = [a for a in artifacts if not a.name.endswith(".sig")] self.check(len(artifacts) > 0, f" Created {len(artifacts)} artifact(s)") else: results[pkg_name] = BuildStatus.FAILED self.check(False, f"CLI: Failed to build {pkg_name}\nError: {result.stderr}") else: async with AURClient() as aur: async with Builder(self.config, aur) as builder: for pkg_name in packages: console.print(f"\n Building {pkg_name}...") result = await builder.build_package(pkg_name, force=True) results[pkg_name] = result.status if result.status == BuildStatus.SUCCESS: self.check(True, f"Built {pkg_name} successfully ({result.duration:.1f}s)") self.check( len(result.artifacts) > 0, f" Created {len(result.artifacts)} artifact(s)" ) for artifact in result.artifacts: console.print(f" → {artifact.name}") else: self.check(False, f"Failed to build {pkg_name}: {result.error}") return results async def test_repo_add(self, packages: list[str]) -> None: """Test adding packages to repository.""" console.print("\n[bold blue]═══ Test: Repository Management ═══[/]") if self.use_cli: # In CLI mode, 'add' or 'build' already adds to repo, but we can test 'remake' result = self._run_cli("remake") self.check(result.returncode == 0, "CLI: remake command success") else: repo = RepoManager(self.config) async with AURClient() as aur: async with Builder(self.config, aur) as builder: for pkg_name in packages: # Get the build result with artifacts pkg_dir = self.config.repository.build_dir / pkg_name if not pkg_dir.exists(): continue # Find artifacts artifacts = list(pkg_dir.glob("*.pkg.tar.*")) artifacts = [a for a in artifacts if not a.name.endswith(".sig")] if artifacts: # Create a mock build result for add_packages from archbuild.builder import BuildResult mock_result = BuildResult( package=pkg_name, status=BuildStatus.SUCCESS, artifacts=artifacts, ) success = repo.add_packages(mock_result) self.check(success, f"Added {pkg_name} to repository") # List packages in repo repo = RepoManager(self.config) pkg_list = repo.list_packages() self.check(len(pkg_list) > 0, f"Repository contains {len(pkg_list)} package(s)") for pkg in pkg_list: console.print(f" → {pkg.name} {pkg.version} ({pkg.size / 1024:.1f} KB)") async def test_repo_database(self) -> None: """Test repository database integrity.""" console.print("\n[bold blue]═══ Test: Database Integrity ═══[/]") if self.use_cli: # We already tested remake, let's just check the DB file pass db_path = self.config.repository.path / f"{self.config.repository.name}.db.tar.zst" self.check(db_path.exists(), f"Database file exists: {db_path.name}") if db_path.exists(): # Try to list contents with tar result = subprocess.run( ["tar", "-tf", str(db_path)], capture_output=True, text=True, ) if result.returncode == 0: entries = [e for e in result.stdout.strip().split("\n") if e] self.check( len(entries) > 0, f"Database contains {len(entries)} entries" ) # Check for integrity issues repo = RepoManager(self.config) issues = repo.check_integrity() self.check( len(issues) == 0, f"No integrity issues found" if not issues else f"Issues: {issues}" ) async def test_cleanup(self) -> None: """Test package cleanup functionality.""" console.print("\n[bold blue]═══ Test: Cleanup ═══[/]") if self.use_cli: result = self._run_cli("cleanup") self.check(result.returncode == 0, "CLI: cleanup command success") else: repo = RepoManager(self.config) removed = repo.cleanup() self.check(True, f"Cleanup removed {removed} old version(s)") async def run(self) -> bool: """Run all integration tests.""" mode_str = " (Python Mode)" if self.use_binary: mode_str = " (Binary Mode)" elif self.use_cli: mode_str = " (CLI Mode)" console.print("[bold magenta]╔════════════════════════════════════════════╗[/]") console.print(f"[bold magenta]║ Archbuild Integration Test Suite{mode_str:<10}║[/]") console.print("[bold magenta]╚════════════════════════════════════════════╝[/]") try: self.setup() # Run tests await self.test_init() packages = await self.test_aur_client() if not packages: console.print("[red]No test packages available, cannot continue[/]") return False build_results = await self.test_build_packages(packages) # Build all found packages successful = [p for p, s in build_results.items() if s == BuildStatus.SUCCESS] if successful: await self.test_repo_add(successful) await self.test_repo_database() await self.test_cleanup() else: console.print("[yellow]No successful builds to test repository with[/]") # Summary console.print("\n[bold blue]═══ Test Summary ═══[/]") total = self.passed + self.failed console.print(f" Total: {total}") console.print(f" [green]Passed: {self.passed}[/]") console.print(f" [red]Failed: {self.failed}[/]") if self.failed == 0: console.print("\n[bold green]✓ All tests passed![/]") return True else: console.print(f"\n[bold red]✗ {self.failed} test(s) failed[/]") return False except KeyboardInterrupt: console.print("\n[yellow]Test interrupted[/]") return False except Exception as e: console.print(f"\n[bold red]Test error:[/] {e}") import traceback traceback.print_exc() return False finally: self.teardown() async def main() -> int: """Main entry point.""" keep_temp = "--keep-temp" in sys.argv use_cli = "--use-cli" in sys.argv use_binary = "--use-binary" in sys.argv if use_binary: use_cli = True # Binary mode is a sub-mode of CLI mode # Check if running on Arch Linux if not Path("/etc/arch-release").exists(): console.print("[yellow]Warning: Not running on Arch Linux[/]") console.print("[yellow]Some tests may fail or be skipped[/]") # Check for required tools for tool in ["makepkg", "pacman", "git"]: result = subprocess.run(["which", tool], capture_output=True) if result.returncode != 0: console.print(f"[red]Required tool not found: {tool}[/]") return 1 test = IntegrationTest(keep_temp=keep_temp, use_cli=use_cli, use_binary=use_binary) success = await test.run() return 0 if success else 1 if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)