#!/usr/bin/env python3 """ Generate mutation testing trend report from mutation_history.json """ import json import sys from datetime import datetime from pathlib import Path def load_history(history_file: Path) -> list: """Load mutation testing history from JSON file.""" if not history_file.exists(): print(f"āŒ History file not found: {history_file}") sys.exit(1) with open(history_file) as f: return json.load(f) def format_timestamp(ts: str) -> str: """Format ISO timestamp to readable date.""" dt = datetime.fromisoformat(ts.replace('Z', '+00:00')) return dt.strftime('%Y-%m-%d %H:%M') def print_summary(history: list) -> None: """Print summary statistics.""" if not history: print("No mutation testing history found.") return latest = history[-1] stats = latest['stats'] print("\n" + "=" * 70) print(" MUTATION TESTING SUMMARY") print("=" * 70) print(f"\nLatest Run: {format_timestamp(latest['timestamp'])}") print(f"Git Branch: {latest['git_branch']}") print(f"Git Commit: {latest['git_commit'][:8]}") print(f"\nCurrent Score: {stats['score']}%") print(f" Total Mutants: {stats['total_mutants']}") print(f" Viable: {stats['viable']}") print(f" Caught: {stats['caught']}") print(f" Missed: {stats['missed']}") print(f" Timeout: {stats['timeout']}") print(f" Unviable: {stats['unviable']}") def print_trend(history: list, limit: int = 10) -> None: """Print mutation score trend.""" if len(history) < 2: print("\nšŸ“Š Not enough history for trend analysis (need at least 2 runs)") return print(f"\nšŸ“ˆ TREND (last {min(limit, len(history))} runs):") print("-" * 70) print(f"{'Date':<17} {'Branch':<15} {'Score':<8} {'Caught/Viable':<15} {'Change'}") print("-" * 70) recent = history[-limit:] if len(history) > limit else history prev_score = None for entry in recent: stats = entry['stats'] score = stats['score'] date = format_timestamp(entry['timestamp']) branch = entry['git_branch'][:14] caught_viable = f"{stats['caught']}/{stats['viable']}" # Calculate change if prev_score is not None: change = score - prev_score if change > 0: change_str = f"+{change:.1f}% ⬆" elif change < 0: change_str = f"{change:.1f}% ⬇" else: change_str = "0.0% āž”" else: change_str = "-" print(f"{date:<17} {branch:<15} {score:>6.1f}% {caught_viable:<15} {change_str}") prev_score = score def print_statistics(history: list) -> None: """Print overall statistics.""" if not history: return scores = [entry['stats']['score'] for entry in history] avg_score = sum(scores) / len(scores) min_score = min(scores) max_score = max(scores) print("\nšŸ“Š STATISTICS:") print("-" * 70) print(f"Total Runs: {len(history)}") print(f"Average Score: {avg_score:.2f}%") print(f"Min Score: {min_score:.2f}%") print(f"Max Score: {max_score:.2f}%") print(f"Score Range: {max_score - min_score:.2f}%") def main(): """Main entry point.""" history_file = Path("mutation_history.json") if len(sys.argv) > 1: history_file = Path(sys.argv[1]) history = load_history(history_file) print_summary(history) print_trend(history, limit=10) print_statistics(history) print("\n" + "=" * 70 + "\n") if __name__ == "__main__": main()