Files
announcement/scripts/mutation_report.py
T
swaits e8e55b7676 chore: initial release v0.1.0
Runtime-agnostic oneshot broadcast channel for Rust.
2025-10-13 19:36:37 -06:00

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()