| """ |
| Flaky Test Tracker - Tracks test results across multiple runs to identify unstable tests. |
| |
| A test is considered flaky if: |
| - It fails in at least 1 of the last N runs (default: 5) |
| - Its pass rate is below threshold (default: 80%) |
| - It has been run at least minimum times (default: 3) |
| |
| Usage: |
| from scripts.flaky_test_tracker import FlakyTestTracker |
| |
| tracker = FlakyTestTracker() |
| tracker.update_from_pytest_report("pytest_report.json") |
| flaky_tests = tracker.get_flaky_tests(threshold=0.8) |
| """ |
| import json |
| import os |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Dict, List, Optional |
|
|
|
|
| class FlakyTestTracker: |
| """Tracks test results across runs to identify flaky tests.""" |
|
|
| def __init__(self, data_file: str = "backend/tests/e2e_ui/data/flaky_tests.json"): |
| """Initialize tracker with data file path.""" |
| self.data_file = Path(data_file) |
| self.data: Dict = self._load_data() |
|
|
| def _load_data(self) -> Dict: |
| """Load historical test data from JSON file.""" |
| if self.data_file.exists(): |
| with open(self.data_file, 'r') as f: |
| return json.load(f) |
| return { |
| "tests": {}, |
| "last_updated": None, |
| "total_runs": 0 |
| } |
|
|
| def _save_data(self) -> None: |
| """Save test data to JSON file.""" |
| self.data["last_updated"] = datetime.now().isoformat() |
| self.data_file.parent.mkdir(parents=True, exist_ok=True) |
| with open(self.data_file, 'w') as f: |
| json.dump(self.data, f, indent=2) |
|
|
| def update_from_pytest_report(self, report_file: str) -> None: |
| """Update tracker data from pytest JSON report.""" |
| with open(report_file, 'r') as f: |
| report = json.load(f) |
|
|
| summary = report.get("summary", {}) |
| self.data["total_runs"] += 1 |
|
|
| |
| for test_name, test_result in report.get("tests", {}).items(): |
| if test_name not in self.data["tests"]: |
| self.data["tests"][test_name] = { |
| "runs": [], |
| "total_runs": 0, |
| "passed": 0, |
| "failed": 0, |
| "skipped": 0, |
| "errors": 0 |
| } |
|
|
| test_data = self.data["tests"][test_name] |
| test_data["total_runs"] += 1 |
|
|
| |
| outcome = test_result.get("outcome", "unknown") |
| test_data["runs"].append({ |
| "run": self.data["total_runs"], |
| "outcome": outcome, |
| "timestamp": datetime.now().isoformat() |
| }) |
|
|
| if outcome == "passed": |
| test_data["passed"] += 1 |
| elif outcome == "failed": |
| test_data["failed"] += 1 |
| elif outcome == "skipped": |
| test_data["skipped"] += 1 |
| else: |
| test_data["errors"] += 1 |
|
|
| |
| test_data["runs"] = test_data["runs"][-10:] |
|
|
| self._save_data() |
|
|
| def get_flaky_tests( |
| self, |
| pass_threshold: float = 0.8, |
| min_runs: int = 3, |
| last_n_runs: Optional[int] = None |
| ) -> List[Dict]: |
| """ |
| Get list of flaky tests. |
| |
| Args: |
| pass_threshold: Minimum pass rate (0.0-1.0) |
| min_runs: Minimum runs before considering test |
| last_n_runs: Only consider last N runs (None = all runs) |
| |
| Returns: |
| List of flaky test info dicts |
| """ |
| flaky = [] |
|
|
| for test_name, test_data in self.data["tests"].items(): |
| |
| if test_data["total_runs"] < min_runs: |
| continue |
|
|
| |
| runs_to_consider = test_data["runs"][-last_n_runs:] if last_n_runs else test_data["runs"] |
| if not runs_to_consider: |
| continue |
|
|
| passed = sum(1 for r in runs_to_consider if r["outcome"] == "passed") |
| total = len(runs_to_consider) |
| pass_rate = passed / total if total > 0 else 0 |
|
|
| |
| has_failures = any(r["outcome"] in ["failed", "error"] for r in runs_to_consider) |
| if pass_rate < pass_threshold and has_failures: |
| flaky.append({ |
| "test": test_name, |
| "pass_rate": pass_rate, |
| "total_runs": total, |
| "passed": passed, |
| "failed": total - passed, |
| "last_outcome": runs_to_consider[-1]["outcome"] |
| }) |
|
|
| |
| flaky.sort(key=lambda x: x["pass_rate"]) |
| return flaky |
|
|
| def get_test_history(self, test_name: str) -> Dict: |
| """Get historical data for a specific test.""" |
| return self.data["tests"].get(test_name, {}) |
|
|
| def get_summary(self) -> Dict: |
| """Get summary of tracked test data.""" |
| total_tests = len(self.data["tests"]) |
| flaky = self.get_flaky_tests() |
|
|
| return { |
| "total_tests": total_tests, |
| "total_runs": self.data["total_runs"], |
| "flaky_count": len(flaky), |
| "flaky_tests": [t["test"] for t in flaky[:10]], |
| "last_updated": self.data["last_updated"] |
| } |
|
|
|
|
| if __name__ == "__main__": |
| |
| import sys |
|
|
| tracker = FlakyTestTracker() |
|
|
| if len(sys.argv) > 1: |
| |
| report_file = sys.argv[1] |
| tracker.update_from_pytest_report(report_file) |
| print(f"Updated tracker from {report_file}") |
|
|
| |
| flaky = tracker.get_flaky_tests() |
| print(f"\nFound {len(flaky)} flaky tests:") |
| for test in flaky: |
| print(f" - {test['test']}: {test['pass_rate']:.1%} pass rate ({test['passed']}/{test['total_runs']} runs)") |
|
|