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