125 lines
3.6 KiB
Python
Executable File
125 lines
3.6 KiB
Python
Executable File
#!/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()
|