#!/usr/bin/env python3 """ Test Runner - Execute and format test results Supports: - pytest (Python) - unittest (Python) - jest (JavaScript) - JUnit (Java) Usage: python run_tests.py python run_tests.py python run_tests.py --framework pytest """ import argparse import json import os import subprocess import sys from pathlib import Path class TestResult: """Represents test execution results.""" def __init__(self): self.passed = 0 self.failed = 0 self.errors = 0 self.skipped = 0 self.total = 0 self.duration = 0.0 self.failures = [] def to_dict(self): return { 'passed': self.passed, 'failed': self.failed, 'errors': self.errors, 'skipped': self.skipped, 'total': self.total, 'duration': self.duration, 'failures': self.failures } class TestRunner: """Base class for test runners.""" def __init__(self, target): self.target = target def run(self) -> TestResult: raise NotImplementedError class PytestRunner(TestRunner): """Run pytest tests.""" def run(self) -> TestResult: result = TestResult() try: # Run pytest with verbose output and JSON report cmd = [ 'python', '-m', 'pytest', self.target, '-v', '--tb=short' ] process = subprocess.run( cmd, capture_output=True, text=True, timeout=60 ) # Parse output output = process.stdout + process.stderr lines = output.split('\n') for line in lines: if ' PASSED' in line: result.passed += 1 elif ' FAILED' in line: result.failed += 1 # Extract test name and failure info test_name = line.split('::')[1].split(' ')[0] if '::' in line else 'unknown' result.failures.append({ 'test': test_name, 'message': 'See output for details' }) elif ' ERROR' in line: result.errors += 1 elif ' SKIPPED' in line: result.skipped += 1 # Extract duration if 'passed in' in line or 'failed in' in line: try: duration_str = line.split(' in ')[1].split('s')[0] result.duration = float(duration_str) except: pass result.total = result.passed + result.failed + result.errors + result.skipped return result except FileNotFoundError: print("Error: pytest not found. Install with: pip install pytest", file=sys.stderr) sys.exit(1) except subprocess.TimeoutExpired: print("Error: Tests timed out after 60 seconds", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error running tests: {e}", file=sys.stderr) sys.exit(1) class UnittestRunner(TestRunner): """Run unittest tests.""" def run(self) -> TestResult: result = TestResult() try: cmd = [ 'python', '-m', 'unittest', 'discover', '-s', self.target, '-v' ] process = subprocess.run( cmd, capture_output=True, text=True, timeout=60 ) output = process.stdout + process.stderr lines = output.split('\n') for line in lines: if ' ... ok' in line: result.passed += 1 elif 'FAIL:' in line: result.failed += 1 test_name = line.replace('FAIL:', '').strip() result.failures.append({ 'test': test_name, 'message': 'See output for details' }) elif 'ERROR:' in line: result.errors += 1 # Parse summary line for line in reversed(lines): if 'Ran ' in line and ' test' in line: try: result.total = int(line.split('Ran ')[1].split(' test')[0]) except: pass break return result except FileNotFoundError: print("Error: unittest module not found", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error running tests: {e}", file=sys.stderr) sys.exit(1) class JestRunner(TestRunner): """Run Jest tests.""" def run(self) -> TestResult: result = TestResult() try: cmd = ['npx', 'jest', self.target, '--verbose'] process = subprocess.run( cmd, capture_output=True, text=True, timeout=60 ) output = process.stdout + process.stderr lines = output.split('\n') for line in lines: if '✓' in line or 'PASS' in line: result.passed += 1 elif '✕' in line or 'FAIL' in line: result.failed += 1 # Parse summary for line in lines: if 'Tests:' in line: parts = line.split(',') for part in parts: if 'passed' in part: try: result.passed = int(part.split()[0]) except: pass elif 'failed' in part: try: result.failed = int(part.split()[0]) except: pass result.total = result.passed + result.failed return result except FileNotFoundError: print("Error: Jest not found. Install with: npm install --save-dev jest", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error running tests: {e}", file=sys.stderr) sys.exit(1) def detect_framework(target): """Detect testing framework to use.""" # Check if it's a Python file if target.endswith('.py') or os.path.isdir(target): # Check for pytest markers if os.path.isfile(target): with open(target, 'r') as f: content = f.read() if 'import pytest' in content or '@pytest' in content: return 'pytest' elif 'import unittest' in content or 'class Test' in content: return 'unittest' else: # Check for pytest.ini or setup.cfg if os.path.exists('pytest.ini') or os.path.exists('setup.cfg'): return 'pytest' return 'unittest' # Check if it's JavaScript elif target.endswith('.js') or target.endswith('.test.js'): return 'jest' return 'pytest' # Default def run_tests(target, framework=None, output_format='text'): """Run tests and format output.""" if not os.path.exists(target): print(f"Error: Target '{target}' not found", file=sys.stderr) sys.exit(1) # Detect framework if not specified if framework is None: framework = detect_framework(target) # Create appropriate runner if framework == 'pytest': runner = PytestRunner(target) elif framework == 'unittest': runner = UnittestRunner(target) elif framework == 'jest': runner = JestRunner(target) else: print(f"Error: Unsupported framework '{framework}'", file=sys.stderr) sys.exit(1) print(f"\nRunning tests with {framework}...") print(f"Target: {target}\n") # Run tests result = runner.run() # Output results if output_format == 'json': print(json.dumps(result.to_dict(), indent=2)) else: print("=" * 60) print("TEST RESULTS") print("=" * 60) print(f"Total: {result.total}") print(f"Passed: {result.passed} ✓") print(f"Failed: {result.failed} ✗") if result.errors > 0: print(f"Errors: {result.errors}") if result.skipped > 0: print(f"Skipped: {result.skipped}") if result.duration > 0: print(f"Duration: {result.duration:.2f}s") print() if result.failures: print("FAILURES:") for failure in result.failures: print(f" - {failure['test']}") if failure.get('message'): print(f" {failure['message']}") print() # Summary if result.failed == 0 and result.errors == 0: print("✓ All tests passed!") else: print(f"✗ {result.failed + result.errors} test(s) failed") print() def main(): parser = argparse.ArgumentParser(description='Run and format test results') parser.add_argument('target', help='Test file or directory to run') parser.add_argument('--framework', choices=['pytest', 'unittest', 'jest'], help='Testing framework to use (auto-detected if not specified)') parser.add_argument('--format', choices=['text', 'json'], default='text', help='Output format (default: text)') args = parser.parse_args() run_tests(args.target, args.framework, args.format) if __name__ == '__main__': main()