#!/usr/bin/env python3 """ Standalone tests for xcache.py using clang compiler without requiring sccache server. This test suite verifies that xcache works correctly by testing the wrapper functionality and ensuring proper command construction and execution. """ import os import shutil import subprocess import sys import tempfile from dataclasses import dataclass from pathlib import Path from typing import Optional @dataclass class TestResults: """Test results container.""" passed: int = 0 failed: int = 0 errors: Optional[list[str]] = None def __post_init__(self) -> None: if self.errors is None: self.errors = [] def find_clang() -> Optional[str]: """Find clang compiler in PATH.""" clang_path = shutil.which("clang") if clang_path: return clang_path # Check common locations common_paths = [ "/usr/bin/clang", "/usr/local/bin/clang", "/opt/local/bin/clang", "C:/Program Files/LLVM/bin/clang.exe", "C:/msys64/mingw64/bin/clang.exe", ] for path in common_paths: if os.path.isfile(path) and os.access(path, os.X_OK): return path return None def create_test_source_file(temp_dir: Path) -> Path: """Create a simple C source file for testing.""" source_file = temp_dir / "test.c" source_content = """ #include int main() { printf("Hello from xcache test!\\n"); return 0; } """ source_file.write_text(source_content) return source_file def create_response_file(temp_dir: Path, args: list[str]) -> Path: """Create a response file with the given arguments.""" response_file = temp_dir / "compile_args.rsp" # Write arguments to response file (space-separated) # Properly quote arguments that contain spaces (Windows paths) # Use forward slashes for Windows paths to avoid backslash escape issues quoted_args: list[str] = [] for arg in args: # Convert Windows backslashes to forward slashes for response files normalized_arg = arg.replace("\\", "/") if " " in normalized_arg and not ( normalized_arg.startswith('"') and normalized_arg.endswith('"') ): quoted_args.append(f'"{normalized_arg}"') else: quoted_args.append(normalized_arg) content = " ".join(quoted_args) response_file.write_text(content) return response_file def test_clang_direct_compilation(results: TestResults) -> None: """Test that clang compilation works directly (baseline test).""" print("๐Ÿงช Testing direct clang compilation (baseline)...") clang_path = find_clang() if not clang_path: results.errors.append("clang compiler not found - skipping baseline test") return with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Create test source file source_file = create_test_source_file(temp_path) output_file = temp_path / "test_baseline.o" # Test direct clang compilation cmd = [clang_path, "-c", str(source_file), "-o", str(output_file)] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0 and output_file.exists(): print(" โœ… Direct clang compilation works") print(f" Output file created: {output_file.stat().st_size} bytes") results.passed += 1 else: print( f" โŒ Direct clang compilation failed (return code: {result.returncode})" ) print(f" Stderr: {result.stderr}") results.failed += 1 results.errors.append( f"Direct clang compilation failed: {result.stderr}" ) except subprocess.TimeoutExpired: print(" โŒ Direct clang compilation timed out") results.failed += 1 results.errors.append("Direct clang compilation timed out") except Exception as e: print(f" โŒ Direct clang compilation error: {e}") results.failed += 1 results.errors.append(f"Direct clang compilation error: {e}") def test_clang_response_file_compilation(results: TestResults) -> None: """Test that clang can handle response files directly.""" print("\n๐Ÿงช Testing clang response file compilation...") clang_path = find_clang() if not clang_path: results.errors.append("clang compiler not found - skipping response file test") return with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Create test source file source_file = create_test_source_file(temp_path) output_file = temp_path / "test_response.o" # Create response file with compilation arguments compile_args = ["-c", str(source_file), "-o", str(output_file)] response_file = create_response_file(temp_path, compile_args) # Test clang with response file cmd = [clang_path, f"@{response_file}"] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0 and output_file.exists(): print(" โœ… Clang response file compilation works") print(f" Output file created: {output_file.stat().st_size} bytes") print(f" Response file: @{response_file}") results.passed += 1 else: print( f" โŒ Clang response file compilation failed (return code: {result.returncode})" ) print(f" Stderr: {result.stderr}") results.failed += 1 results.errors.append( f"Clang response file compilation failed: {result.stderr}" ) except subprocess.TimeoutExpired: print(" โŒ Clang response file compilation timed out") results.failed += 1 results.errors.append("Clang response file compilation timed out") except Exception as e: print(f" โŒ Clang response file compilation error: {e}") results.failed += 1 results.errors.append(f"Clang response file compilation error: {e}") def test_xcache_wrapper_script_creation(results: TestResults) -> None: """Test that xcache creates wrapper scripts correctly.""" print("\n๐Ÿงช Testing xcache wrapper script creation...") clang_path = find_clang() if not clang_path: results.errors.append("clang compiler not found - skipping wrapper script test") return with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Create test source file and response file source_file = create_test_source_file(temp_path) compile_args = ["-c", str(source_file), "-o", "test.o"] response_file = create_response_file(temp_path, compile_args) # Create a mock sccache script that just echoes its arguments mock_sccache = ( temp_path / "sccache.bat" if os.name == "nt" else temp_path / "sccache" ) if os.name == "nt": mock_content = """@echo off echo MOCK_SCCACHE_CALLED: %* """ else: mock_content = """#!/bin/bash echo "MOCK_SCCACHE_CALLED: $@" """ mock_sccache.write_text(mock_content) if os.name != "nt": mock_sccache.chmod(0o755) # Set up environment to use mock sccache env = os.environ.copy() env["PATH"] = f"{temp_path}{os.pathsep}{env.get('PATH', '')}" env["XCACHE_DEBUG"] = "1" # Test xcache with response file (should create wrapper script) cmd = [sys.executable, "ci/util/xcache.py", clang_path, f"@{response_file}"] try: result = subprocess.run( cmd, capture_output=True, text=True, env=env, timeout=30 ) # Check if wrapper script was created and executed if "Created compiler wrapper:" in result.stderr: print(" โœ… Wrapper script creation detected") # Check if wrapper was executed if "Executing with wrapper:" in result.stderr: print(" โœ… Wrapper script execution detected") # Check if mock sccache was called if "MOCK_SCCACHE_CALLED:" in result.stdout: print(" โœ… Mock sccache was called through wrapper") print(f" Sccache called with: {result.stdout.strip()}") results.passed += 1 else: print(" โŒ Mock sccache was not called") results.failed += 1 results.errors.append( "Mock sccache was not called through wrapper" ) else: print(" โŒ Wrapper script was not executed") results.failed += 1 results.errors.append("Wrapper script was not executed") else: print(" โŒ Wrapper script was not created") print(f" Stderr: {result.stderr}") results.failed += 1 results.errors.append( f"Wrapper script creation failed: {result.stderr}" ) except subprocess.TimeoutExpired: print(" โŒ Wrapper script test timed out") results.failed += 1 results.errors.append("Wrapper script test timed out") except Exception as e: print(f" โŒ Wrapper script test error: {e}") results.failed += 1 results.errors.append(f"Wrapper script test error: {e}") def test_xcache_direct_mode(results: TestResults) -> None: """Test xcache direct mode (no response files).""" print("\n๐Ÿงช Testing xcache direct mode...") clang_path = find_clang() if not clang_path: results.errors.append("clang compiler not found - skipping direct mode test") return with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) # Create test source file source_file = create_test_source_file(temp_path) # Create a mock sccache script that just passes through to real compiler mock_sccache = ( temp_path / "sccache.bat" if os.name == "nt" else temp_path / "sccache" ) if os.name == "nt": mock_content = f'''@echo off echo MOCK_SCCACHE_DIRECT: %* "{clang_path}" %* ''' else: mock_content = f'''#!/bin/bash echo "MOCK_SCCACHE_DIRECT: $@" "{clang_path}" "$@" ''' mock_sccache.write_text(mock_content) if os.name != "nt": mock_sccache.chmod(0o755) # Set up environment to use mock sccache env = os.environ.copy() env["PATH"] = f"{temp_path}{os.pathsep}{env.get('PATH', '')}" env["XCACHE_DEBUG"] = "1" output_file = temp_path / "test_direct.o" # Test xcache without response files (direct mode) cmd = [ sys.executable, "ci/util/xcache.py", clang_path, "-c", str(source_file), "-o", str(output_file), ] try: result = subprocess.run( cmd, capture_output=True, text=True, env=env, timeout=30 ) if result.returncode == 0 and output_file.exists(): print(" โœ… Direct mode compilation succeeded") print(f" Output file created: {output_file.stat().st_size} bytes") # Check if direct execution was used (no wrapper) if "Direct execution:" in result.stderr: print(" โœ… Direct execution mode confirmed") results.passed += 1 else: print(" โš ๏ธ Direct execution mode not clearly indicated") results.passed += 1 # Still pass since compilation worked else: print( f" โŒ Direct mode compilation failed (return code: {result.returncode})" ) print(f" Stderr: {result.stderr}") results.failed += 1 results.errors.append( f"Direct mode compilation failed: {result.stderr}" ) except subprocess.TimeoutExpired: print(" โŒ Direct mode test timed out") results.failed += 1 results.errors.append("Direct mode test timed out") except Exception as e: print(f" โŒ Direct mode test error: {e}") results.failed += 1 results.errors.append(f"Direct mode test error: {e}") def run_all_tests() -> TestResults: """Run all xcache standalone tests.""" print("๐Ÿš€ XCACHE STANDALONE TESTS WITH CLANG") print("=" * 60) results = TestResults() # Check prerequisites clang_path = find_clang() print(f"๐Ÿ“‹ Test Environment:") print(f" Clang: {clang_path or 'NOT FOUND'}") print(f" xcache: ci/util/xcache.py") print(f" Note: Using mock sccache for testing wrapper functionality") print() if not clang_path: print("โŒ Clang compiler not found - install clang to run tests") results.errors.append("Clang compiler not available") return results # Run tests test_clang_direct_compilation(results) test_clang_response_file_compilation(results) test_xcache_wrapper_script_creation(results) test_xcache_direct_mode(results) return results def main() -> int: """Main test function.""" results = run_all_tests() print("\n" + "=" * 60) print("๐Ÿ“Š TEST RESULTS SUMMARY") print("=" * 60) total_tests = results.passed + results.failed if total_tests > 0: success_rate = (results.passed / total_tests) * 100 print(f" Tests passed: {results.passed}") print(f" Tests failed: {results.failed}") print(f" Success rate: {success_rate:.1f}%") else: print(" No tests were executed") if results.errors: print(f"\n๐Ÿšจ Errors encountered:") for i, error in enumerate(results.errors, 1): print(f" {i}. {error}") if results.failed == 0 and results.passed > 0: print( "\nโœ… All xcache standalone tests passed! Wrapper functionality verified." ) return 0 elif results.passed > 0: print( f"\nโš ๏ธ Some tests passed but {results.failed} failed. Check errors above." ) return 1 else: print("\nโŒ No tests passed. Check prerequisites and errors above.") return 1 if __name__ == "__main__": sys.exit(main())