| |
| """ |
| Mutation Testing Runner |
| |
| This script runs mutation testing using mutmut and generates reports. |
| |
| Usage: |
| python run_mutation_tests.py --target priority_p0_financial |
| python run_mutation_tests.py --all |
| python run_mutation_tests.py --quick |
| |
| Options: |
| --target: Run mutation tests for specific target |
| --all: Run all mutation tests (all targets) |
| --quick: Quick smoke test (fewer mutations) |
| --report: Generate HTML report |
| --threshold: Override score threshold |
| """ |
|
|
| import argparse |
| import subprocess |
| import sys |
| import os |
| from pathlib import Path |
| import configparser |
| from datetime import datetime |
|
|
|
|
| def load_targets_config(): |
| """Load mutation testing targets configuration.""" |
| config_path = Path(__file__).parent / "targets" / "TARGETS.ini" |
| config = configparser.ConfigParser() |
| config.read(config_path) |
| return config |
|
|
|
|
| def run_mutmut_for_target(target_name, config, quick=False, threshold=None): |
| """Run mutmut for a specific target.""" |
| if target_name not in config: |
| print(f"Error: Target '{target_name}' not found in configuration") |
| return False |
|
|
| target_config = config[target_name] |
| modules = target_config.get('modules', '').split('\n') |
| modules = [m.strip() for m in modules if m.strip()] |
|
|
| if not modules: |
| print(f"Warning: No modules defined for target '{target_name}'") |
| return False |
|
|
| |
| score_threshold = float(target_config.get('mutation_score_threshold', 80.0)) |
| if threshold is not None: |
| score_threshold = float(threshold) |
|
|
| print(f"\n{'='*60}") |
| print(f"Running mutation tests for: {target_name}") |
| print(f"Modules: {', '.join(modules)}") |
| print(f"Score threshold: {score_threshold}%") |
| print(f"{'='*60}\n") |
|
|
| |
| cmd = [ |
| "mutmut", "run", |
| "--paths-to-mutate", ",".join(modules), |
| "--runner", "pytest tests/ -x -q --tb=line" |
| ] |
|
|
| if quick: |
| cmd.extend(["--max-examples", "10"]) |
|
|
| |
| try: |
| result = subprocess.run( |
| cmd, |
| cwd=Path(__file__).parent.parent.parent, |
| capture_output=True, |
| text=True, |
| timeout=3600 |
| ) |
|
|
| print(result.stdout) |
| if result.stderr: |
| print("STDERR:", result.stderr, file=sys.stderr) |
|
|
| |
| subprocess.run( |
| ["mutmut", "html"], |
| cwd=Path(__file__).parent.parent.parent, |
| capture_output=False |
| ) |
|
|
| |
| score = parse_mutation_score(result.stdout) |
|
|
| if score is not None: |
| print(f"\n{'='*60}") |
| print(f"Mutation Score: {score:.2f}%") |
| print(f"Threshold: {score_threshold}%") |
| print(f"{'='*60}\n") |
|
|
| if score >= score_threshold: |
| print(f"✅ PASSED: Score {score:.2f}% >= {score_threshold}%") |
| return True |
| else: |
| print(f"❌ FAILED: Score {score:.2f}% < {score_threshold}%") |
| return False |
| else: |
| print("Warning: Could not parse mutation score") |
| return False |
|
|
| except subprocess.TimeoutExpired: |
| print(f"Error: Mutation testing timed out for target '{target_name}'") |
| return False |
| except Exception as e: |
| print(f"Error running mutation tests: {e}") |
| return False |
|
|
|
|
| def parse_mutation_score(output): |
| """Parse mutation score from mutmut output.""" |
| import re |
|
|
| |
| match = re.search(r'Mutation score:\s+(\d+\.\d+)%', output) |
| if match: |
| return float(match.group(1)) |
| return None |
|
|
|
|
| def run_all_targets(config, quick=False, threshold=None): |
| """Run mutation tests for all targets.""" |
| targets = [s for s in config.sections() if s.startswith('priority_')] |
|
|
| results = {} |
| for target in targets: |
| results[target] = run_mutmut_for_target( |
| target, |
| config, |
| quick=quick, |
| threshold=threshold |
| ) |
|
|
| |
| print(f"\n{'='*60}") |
| print("MUTATION TESTING SUMMARY") |
| print(f"{'='*60}\n") |
|
|
| for target, passed in results.items(): |
| status = "✅ PASSED" if passed else "❌ FAILED" |
| print(f"{target}: {status}") |
|
|
| total = len(results) |
| passed = sum(1 for r in results.values() if r) |
|
|
| print(f"\nTotal: {passed}/{total} targets passed") |
|
|
| return passed == total |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Run mutation testing for Atom platform" |
| ) |
| parser.add_argument( |
| "--target", |
| help="Specific target to test (e.g., priority_p0_financial)" |
| ) |
| parser.add_argument( |
| "--all", |
| action="store_true", |
| help="Run all mutation tests" |
| ) |
| parser.add_argument( |
| "--quick", |
| action="store_true", |
| help="Quick smoke test (fewer mutations)" |
| ) |
| parser.add_argument( |
| "--threshold", |
| type=float, |
| help="Override score threshold" |
| ) |
|
|
| args = parser.parse_args() |
|
|
| |
| config = load_targets_config() |
|
|
| |
| if args.target: |
| success = run_mutmut_for_target( |
| args.target, |
| config, |
| quick=args.quick, |
| threshold=args.threshold |
| ) |
| sys.exit(0 if success else 1) |
| elif args.all: |
| success = run_all_targets( |
| config, |
| quick=args.quick, |
| threshold=args.threshold |
| ) |
| sys.exit(0 if success else 1) |
| else: |
| parser.print_help() |
| sys.exit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|