#!/usr/bin/env python3 """ McRogueFace Test Runner Runs all headless tests and reports results. Usage: python3 tests/run_tests.py # Run all tests python3 tests/run_tests.py unit # Run only unit tests python3 tests/run_tests.py -v # Verbose output """ import os import subprocess import sys import time import hashlib from pathlib import Path # Configuration TESTS_DIR = Path(__file__).parent BUILD_DIR = TESTS_DIR.parent / "build" MCROGUEFACE = BUILD_DIR / "mcrogueface" TIMEOUT = 10 # seconds per test # Test directories to run (in order) TEST_DIRS = ['unit', 'integration', 'regression'] # ANSI colors GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' RESET = '\033[0m' BOLD = '\033[1m' def get_screenshot_checksum(test_dir): """Get checksums of any PNG files in build directory.""" checksums = {} for png in BUILD_DIR.glob("*.png"): with open(png, 'rb') as f: checksums[png.name] = hashlib.md5(f.read()).hexdigest()[:8] return checksums def run_test(test_path, verbose=False): """Run a single test and return (passed, duration, output).""" start = time.time() # Clean any existing screenshots for png in BUILD_DIR.glob("test_*.png"): png.unlink() try: result = subprocess.run( [str(MCROGUEFACE), '--headless', '--exec', str(test_path)], capture_output=True, text=True, timeout=TIMEOUT, cwd=str(BUILD_DIR) ) duration = time.time() - start passed = result.returncode == 0 output = result.stdout + result.stderr # Check for PASS/FAIL in output if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]: passed = False return passed, duration, output except subprocess.TimeoutExpired: return False, TIMEOUT, "TIMEOUT" except Exception as e: return False, 0, str(e) def find_tests(directory): """Find all test files in a directory.""" test_dir = TESTS_DIR / directory if not test_dir.exists(): return [] return sorted(test_dir.glob("*.py")) def main(): verbose = '-v' in sys.argv or '--verbose' in sys.argv # Determine which directories to test dirs_to_test = [] for arg in sys.argv[1:]: if arg in TEST_DIRS: dirs_to_test.append(arg) if not dirs_to_test: dirs_to_test = TEST_DIRS print(f"{BOLD}McRogueFace Test Runner{RESET}") print(f"Testing: {', '.join(dirs_to_test)}") print("=" * 60) results = {'pass': 0, 'fail': 0, 'total_time': 0} failures = [] for test_dir in dirs_to_test: tests = find_tests(test_dir) if not tests: continue print(f"\n{BOLD}{test_dir}/{RESET} ({len(tests)} tests)") for test_path in tests: test_name = test_path.name passed, duration, output = run_test(test_path, verbose) results['total_time'] += duration if passed: results['pass'] += 1 status = f"{GREEN}PASS{RESET}" else: results['fail'] += 1 status = f"{RED}FAIL{RESET}" failures.append((test_dir, test_name, output)) # Get screenshot checksums if any were generated checksums = get_screenshot_checksum(BUILD_DIR) checksum_str = "" if checksums: checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]" print(f" {status} {test_name} ({duration:.2f}s){checksum_str}") if verbose and not passed: print(f" Output: {output[:200]}...") # Summary print("\n" + "=" * 60) total = results['pass'] + results['fail'] pass_rate = (results['pass'] / total * 100) if total > 0 else 0 print(f"{BOLD}Results:{RESET} {results['pass']}/{total} passed ({pass_rate:.1f}%)") print(f"{BOLD}Time:{RESET} {results['total_time']:.2f}s") if failures: print(f"\n{RED}{BOLD}Failures:{RESET}") for test_dir, test_name, output in failures: print(f" {test_dir}/{test_name}") if verbose: # Show last few lines of output lines = output.strip().split('\n')[-5:] for line in lines: print(f" {line}") sys.exit(0 if results['fail'] == 0 else 1) if __name__ == '__main__': main()