#!/usr/bin/env python3 """Integration test runner script. This script: 1. Starts fresh Docker containers using compose.yml 2. Waits for services to be ready 3. Runs integration tests 4. Cleans up containers afterward """ import asyncio import os import subprocess import sys import time from pathlib import Path import httpx from rich.console import Console PROJECT_ROOT = Path(__file__).parent.parent COMPOSE_FILE = PROJECT_ROOT / "compose.testing.yml" console = Console() def log(message: str, style: str = "") -> None: """Print styled log message.""" console.print(message, style=style) def run_command( cmd: list[str], check: bool = True, capture_output: bool = False ) -> subprocess.CompletedProcess: """Run a command and return the result.""" log(f"Running: {' '.join(cmd)}", "cyan") return subprocess.run( cmd, check=check, capture_output=capture_output, text=True, cwd=PROJECT_ROOT ) async def wait_for_service( url: str, service_name: str, endpoint: str = "", timeout: int = 60 ) -> bool: """Wait for a service to be ready.""" log(f"Waiting for {service_name} at {url}...", "yellow") start_time = time.time() async with httpx.AsyncClient() as client: while time.time() - start_time < timeout: try: full_url = f"{url}{endpoint}" if endpoint else url response = await client.get(full_url, timeout=5.0) if response.status_code == 200: log(f"โœ… {service_name} ready", "green") return True except Exception: pass await asyncio.sleep(2) log(f"โŒ {service_name} at {url} not ready after {timeout}s", "red") return False async def wait_for_mint(url: str, timeout: int = 60) -> bool: """Wait for mint to be ready.""" return await wait_for_service(url, "Cashu Mint", "/v1/info", timeout) def cleanup_docker() -> None: """Clean up Docker containers and volumes.""" log("๐Ÿงน Cleaning up Docker containers and volumes...", "yellow") try: # Stop and remove containers run_command( ["docker-compose", "-f", str(COMPOSE_FILE), "down", "-v"], check=False ) # Remove any orphaned containers run_command(["docker", "container", "prune", "-f"], check=False) # Remove unused volumes (be careful with this) run_command(["docker", "volume", "prune", "-f"], check=False) log("โœ… Docker cleanup completed", "green") except Exception as e: log(f"โš ๏ธ Docker cleanup failed: {e}", "yellow") def start_services() -> None: """Start Docker services with fresh state.""" log("๐Ÿš€ Starting Docker services...", "blue") # Ensure we start with clean state cleanup_docker() # Start services run_command( [ "docker-compose", "-f", str(COMPOSE_FILE), "up", "-d", "--force-recreate", # Recreate containers even if config hasn't changed "--renew-anon-volumes", # Recreate anonymous volumes ] ) log("โœ… Docker services started", "green") def run_tests() -> bool: """Run the integration tests.""" log("๐Ÿงช Running integration tests...", "blue") env = os.environ.copy() env["RUN_INTEGRATION_TESTS"] = "1" env["USE_LOCAL_SERVICES"] = "1" # Use local Docker services # Run only integration tests cmd = [ sys.executable, "-m", "pytest", "tests/integration/", "-v", "--tb=short", "--color=yes", ] try: result = subprocess.run(cmd, env=env, cwd=PROJECT_ROOT) if result.returncode == 0: log("โœ… Integration tests passed", "green") return True else: log("โŒ Integration tests failed", "red") return False except Exception as e: log(f"โŒ Failed to run tests: {e}", "red") return False def check_dependencies() -> bool: """Check that required dependencies are available.""" log("๐Ÿ” Checking dependencies...", "blue") # Check Docker try: run_command(["docker", "--version"], capture_output=True) log("โœ… Docker found", "green") except (subprocess.CalledProcessError, FileNotFoundError): log("โŒ Docker not found. Please install Docker.", "red") return False # Check Docker Compose try: run_command(["docker-compose", "--version"], capture_output=True) log("โœ… Docker Compose found", "green") except (subprocess.CalledProcessError, FileNotFoundError): log("โŒ Docker Compose not found. Please install Docker Compose.", "red") return False # Check pytest try: run_command([sys.executable, "-m", "pytest", "--version"], capture_output=True) log("โœ… pytest found", "green") except (subprocess.CalledProcessError, FileNotFoundError): log("โŒ pytest not found. Please install pytest.", "red") return False # Check compose file exists if not COMPOSE_FILE.exists(): log(f"โŒ Compose file not found: {COMPOSE_FILE}", "red") return False else: log("โœ… Compose file found", "green") return True async def main() -> int: """Main function.""" log("๐ŸŽฏ Starting integration test runner", "bold blue") try: # Check dependencies if not check_dependencies(): sys.exit(1) # Start services start_services() # Wait for services to be ready services_ready = await asyncio.gather( wait_for_mint("http://localhost:3338"), wait_for_service("http://localhost:3000", "Mock OpenAI", "/"), wait_for_service("http://localhost:8000", "Router", "/"), return_exceptions=True, ) if not all(services_ready): failed_services = [ service for service, ready in zip( ["Mint", "Mock OpenAI", "Router"], services_ready ) if not ready ] raise RuntimeError( f"Services failed to start: {', '.join(failed_services)}" ) # Run tests success = run_tests() if success: log( "๐ŸŽ‰ Integration tests completed successfully!", "bold green", ) return 0 else: log("๐Ÿ’ฅ Integration tests failed!", "bold red") return 1 except KeyboardInterrupt: log("โน๏ธ Interrupted by user", "yellow") return 1 except Exception as e: log(f"๐Ÿ’ฅ Unexpected error: {e}", "red") return 1 finally: # Always cleanup cleanup_docker() if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)