From f7c40d48c0727a96843c85990cc36ae5a9ac6888 Mon Sep 17 00:00:00 2001
From: Joel Grunbaum <joelgrun@gmail.com>
Date: Sat, 07 Feb 2026 23:42:43 +0000
Subject: [PATCH] Add integration test for binary
---
tests/integration_test.py | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 444 insertions(+), 0 deletions(-)
diff --git a/tests/integration_test.py b/tests/integration_test.py
new file mode 100644
index 0000000..31e2ca0
--- /dev/null
+++ b/tests/integration_test.py
@@ -0,0 +1,444 @@
+#!/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 <test@test.local>"
+
+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)
--
Gitblit v1.10.0