annator-command-center / tests /scripts /flaky_test_tracker.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 9)
81e3673 verified
Raw
History Blame Contribute Delete
22.1 kB
#!/usr/bin/env python
"""
Flaky Test Tracker - SQLite Quarantine Database
Tracks flaky tests with failure history, timestamps, and reliability scoring.
Provides persistent storage for flaky test detection across CI/CD runs.
Usage:
from backend.tests.scripts.flaky_test_tracker import FlakyTestTracker
tracker = FlakyTestTracker(Path('flaky_tests.db'))
tracker.record_flaky_test('tests/test_foo.py::test_bar', 'backend', 10, 3, 'flaky', [...])
quarantined = tracker.get_quarantined_tests('backend')
reliability = tracker.get_test_reliability_score('tests/test_foo.py::test_bar', 'backend')
Database Schema:
- flaky_tests table with indexed lookups on (test_path, platform)
- Tracks first_detected, last_detected, failure_history (JSON)
- Supports platform filtering and reliability scoring
"""
import json
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
class FlakyTestTracker:
"""Track flaky tests in SQLite database with failure history."""
def __init__(self, db_path: Path):
"""Initialize database and create schema if needed.
Args:
db_path: Path to SQLite database file
"""
self.db_path = db_path
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._create_schema()
def _create_schema(self):
"""Create flaky_tests table and indexes if not exists."""
# Create main table
self.conn.execute("""
CREATE TABLE IF NOT EXISTS flaky_tests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
test_path TEXT NOT NULL,
platform TEXT NOT NULL,
first_detected TEXT NOT NULL,
last_detected TEXT NOT NULL,
total_runs INTEGER NOT NULL DEFAULT 0,
failure_count INTEGER NOT NULL DEFAULT 0,
flaky_rate REAL NOT NULL DEFAULT 0.0,
avg_execution_time REAL DEFAULT 0.0,
max_execution_time REAL DEFAULT 0.0,
classification TEXT NOT NULL,
failure_history TEXT NOT NULL,
quarantine_reason TEXT,
issue_url TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
# Check if columns exist for migration pattern
cursor = self.conn.execute("PRAGMA table_info(flaky_tests)")
columns = [row[1] for row in cursor.fetchall()]
# Add execution time columns if they don't exist (migration)
if 'avg_execution_time' not in columns:
self.conn.execute("ALTER TABLE flaky_tests ADD COLUMN avg_execution_time REAL DEFAULT 0.0")
if 'max_execution_time' not in columns:
self.conn.execute("ALTER TABLE flaky_tests ADD COLUMN max_execution_time REAL DEFAULT 0.0")
# Create indexes for fast lookup
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_test_path_platform
ON flaky_tests(test_path, platform)
""")
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_flaky_rate
ON flaky_tests(flaky_rate DESC)
""")
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_classification
ON flaky_tests(classification)
""")
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_max_execution_time
ON flaky_tests(max_execution_time DESC)
""")
self.conn.commit()
def record_flaky_test(
self,
test_path: str,
platform: str,
total_runs: int,
failure_count: int,
classification: str,
failure_history: List[Dict],
execution_times: Optional[List[float]] = None,
quarantine_reason: Optional[str] = None
) -> int:
"""Record or update a flaky test in the database.
Args:
test_path: Full test identifier (e.g., tests/test_foo.py::test_bar)
platform: Platform name (backend/frontend/mobile/desktop)
total_runs: Total number of test runs executed
failure_count: Number of failures observed
classification: Test classification (stable/flaky/broken)
failure_history: List of failure details (JSON-serializable)
execution_times: Optional list of execution times per run (seconds)
quarantine_reason: Optional reason for quarantine
Returns:
test_id: Database ID of the inserted/updated record
"""
flaky_rate = failure_count / total_runs if total_runs > 0 else 0.0
# Calculate execution time statistics
if execution_times and len(execution_times) > 0:
avg_time = sum(execution_times) / len(execution_times)
max_time = max(execution_times)
else:
avg_time = 0.0
max_time = 0.0
now = datetime.utcnow().isoformat()
# Check if test already exists
cursor = self.conn.execute(
"SELECT id, failure_history, total_runs, failure_count, avg_execution_time, max_execution_time FROM flaky_tests "
"WHERE test_path = ? AND platform = ?",
(test_path, platform)
)
row = cursor.fetchone()
if row:
# Update existing record
test_id, existing_history_json, existing_runs, existing_failures, existing_avg_time, existing_max_time = row
existing_history = json.loads(existing_history_json)
merged_history = existing_history + failure_history
# Aggregate statistics
new_total_runs = existing_runs + total_runs
new_failure_count = existing_failures + failure_count
new_flaky_rate = new_failure_count / new_total_runs if new_total_runs > 0 else 0.0
# Update execution times with weighted average
if avg_time > 0:
new_avg_time = ((existing_avg_time * existing_runs) + (avg_time * total_runs)) / new_total_runs
new_max_time = max(existing_max_time, max_time)
else:
new_avg_time = existing_avg_time
new_max_time = existing_max_time
self.conn.execute("""
UPDATE flaky_tests
SET last_detected = ?,
total_runs = ?,
failure_count = ?,
flaky_rate = ?,
avg_execution_time = ?,
max_execution_time = ?,
classification = ?,
failure_history = ?,
quarantine_reason = COALESCE(?, quarantine_reason),
updated_at = ?
WHERE id = ?
""", (
now, new_total_runs, new_failure_count,
round(new_flaky_rate, 3), round(new_avg_time, 3), round(new_max_time, 3),
classification, json.dumps(merged_history), quarantine_reason, now, test_id
))
self.conn.commit()
return test_id
else:
# Insert new record
cursor = self.conn.execute("""
INSERT INTO flaky_tests (
test_path, platform, first_detected, last_detected,
total_runs, failure_count, flaky_rate, avg_execution_time, max_execution_time,
classification, failure_history, quarantine_reason
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
test_path, platform, now, now,
total_runs, failure_count, round(flaky_rate, 3),
round(avg_time, 3), round(max_time, 3),
classification, json.dumps(failure_history), quarantine_reason
))
self.conn.commit()
return cursor.lastrowid
def get_quarantined_tests(self, platform: Optional[str] = None) -> List[Dict]:
"""Get all quarantined tests, optionally filtered by platform.
Args:
platform: Optional platform filter (backend/frontend/mobile/desktop)
Returns:
List of flaky test records as dictionaries
"""
query = "SELECT * FROM flaky_tests WHERE classification = 'flaky'"
params = []
if platform:
query += " AND platform = ?"
params.append(platform)
query += " ORDER BY flaky_rate DESC"
cursor = self.conn.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_dict(row) for row in rows]
def get_test_reliability_score(self, test_path: str, platform: str) -> float:
"""Calculate reliability score for a test (0.0 to 1.0).
Reliability = 1.0 - flaky_rate
Returns 1.0 if test not found (no failures = perfect reliability)
Args:
test_path: Full test identifier
platform: Platform name
Returns:
Reliability score (0.0 = completely unreliable, 1.0 = perfect)
"""
cursor = self.conn.execute(
"SELECT flaky_rate FROM flaky_tests WHERE test_path = ? AND platform = ?",
(test_path, platform)
)
row = cursor.fetchone()
if not row:
return 1.0 # No failures recorded = perfect reliability
flaky_rate = row[0]
return max(0.0, round(1.0 - flaky_rate, 3))
def get_test_history(
self,
test_path: str,
platform: str
) -> Optional[Dict]:
"""Get full history for a specific test.
Args:
test_path: Full test identifier
platform: Platform name
Returns:
Test record as dictionary, or None if not found
"""
cursor = self.conn.execute(
"SELECT * FROM flaky_tests WHERE test_path = ? AND platform = ?",
(test_path, platform)
)
row = cursor.fetchone()
if not row:
return None
return self._row_to_dict(row)
def get_flaky_tests_by_rate(
self,
min_flaky_rate: float = 0.0,
max_flaky_rate: float = 1.0,
platform: Optional[str] = None,
limit: int = 100
) -> List[Dict]:
"""Get flaky tests within a flaky rate range.
Args:
min_flaky_rate: Minimum flaky rate (inclusive)
max_flaky_rate: Maximum flaky rate (inclusive)
platform: Optional platform filter
limit: Maximum number of records to return
Returns:
List of flaky test records sorted by flaky rate (descending)
"""
query = """
SELECT * FROM flaky_tests
WHERE flaky_rate >= ? AND flaky_rate <= ?
"""
params = [min_flaky_rate, max_flaky_rate]
if platform:
query += " AND platform = ?"
params.append(platform)
query += " ORDER BY flaky_rate DESC LIMIT ?"
params.append(limit)
cursor = self.conn.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_dict(row) for row in rows]
def mark_test_fixed(
self,
test_path: str,
platform: str,
fixed_note: Optional[str] = None
) -> bool:
"""Mark a flaky test as fixed (remove from quarantine).
Args:
test_path: Full test identifier
platform: Platform name
fixed_note: Optional note about how the test was fixed
Returns:
True if test was updated, False if not found
"""
now = datetime.utcnow().isoformat()
cursor = self.conn.execute(
"UPDATE flaky_tests "
"SET classification = 'stable', "
" updated_at = ?, "
" quarantine_reason = ? "
"WHERE test_path = ? AND platform = ?",
(now, fixed_note or "Marked as fixed", test_path, platform)
)
self.conn.commit()
return cursor.rowcount > 0
def update_execution_time(
self,
test_path: str,
platform: str,
execution_times: List[float]
) -> bool:
"""Update execution time metrics for a test.
Args:
test_path: Full test identifier
platform: Platform name
execution_times: List of execution times from recent runs
Returns:
True if test was updated, False if not found
"""
if not execution_times:
return False
# Calculate execution time statistics
avg_time = sum(execution_times) / len(execution_times)
max_time = max(execution_times)
# Get existing data to calculate weighted average
cursor = self.conn.execute(
"SELECT id, total_runs, avg_execution_time, max_execution_time FROM flaky_tests "
"WHERE test_path = ? AND platform = ?",
(test_path, platform)
)
row = cursor.fetchone()
if not row:
return False
test_id, existing_runs, existing_avg_time, existing_max_time = row
# Calculate weighted average
new_runs = existing_runs + len(execution_times)
new_avg_time = ((existing_avg_time * existing_runs) + sum(execution_times)) / new_runs
new_max_time = max(existing_max_time, max_time)
now = datetime.utcnow().isoformat()
cursor = self.conn.execute(
"UPDATE flaky_tests "
"SET avg_execution_time = ?, max_execution_time = ?, total_runs = ?, updated_at = ? "
"WHERE id = ?",
(round(new_avg_time, 3), round(new_max_time, 3), new_runs, now, test_id)
)
self.conn.commit()
return cursor.rowcount > 0
def get_slow_tests(
self,
min_time: float = 10.0,
platform: Optional[str] = None,
limit: int = 50
) -> List[Dict]:
"""Get slow tests exceeding execution time threshold.
Args:
min_time: Minimum execution time in seconds (default: 10s)
platform: Optional platform filter
limit: Maximum records to return
Returns:
List of slow test records sorted by max_execution_time (descending)
"""
query = """
SELECT * FROM flaky_tests
WHERE max_execution_time >= ?
"""
params = [min_time]
if platform:
query += " AND platform = ?"
params.append(platform)
query += " ORDER BY max_execution_time DESC LIMIT ?"
params.append(limit)
cursor = self.conn.execute(query, params)
rows = cursor.fetchall()
return [self._row_to_dict(row) for row in rows]
def get_statistics(self, platform: Optional[str] = None) -> Dict:
"""Get aggregate statistics about flaky tests.
Args:
platform: Optional platform filter
Returns:
Dictionary with statistics (total, flaky, broken, stable counts)
"""
where_clause = f"WHERE platform = '{platform}'" if platform else ""
params = []
# Get counts by classification
query = f"""
SELECT
COUNT(*) as total,
SUM(CASE WHEN classification = 'flaky' THEN 1 ELSE 0 END) as flaky,
SUM(CASE WHEN classification = 'broken' THEN 1 ELSE 0 END) as broken,
SUM(CASE WHEN classification = 'stable' THEN 1 ELSE 0 END) as stable,
AVG(flaky_rate) as avg_flaky_rate
FROM flaky_tests
{where_clause}
"""
cursor = self.conn.execute(query, params)
row = cursor.fetchone()
return {
"total": row[0] or 0,
"flaky": row[1] or 0,
"broken": row[2] or 0,
"stable": row[3] or 0,
"avg_flaky_rate": round(row[4] or 0.0, 3)
}
def _row_to_dict(self, row) -> Dict:
"""Convert database row to dictionary.
Args:
row: Database row tuple
Returns:
Dictionary mapping column names to values
"""
columns = [
'id', 'test_path', 'platform', 'first_detected', 'last_detected',
'total_runs', 'failure_count', 'flaky_rate', 'avg_execution_time', 'max_execution_time',
'classification', 'failure_history', 'quarantine_reason', 'issue_url',
'created_at', 'updated_at'
]
result = dict(zip(columns, row))
# Parse JSON fields
if result['failure_history']:
try:
result['failure_history'] = json.loads(result['failure_history'])
except json.JSONDecodeError:
result['failure_history'] = []
return result
def close(self):
"""Close database connection."""
if self.conn:
self.conn.close()
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close()
def main():
"""CLI for flaky test tracker operations."""
import argparse
parser = argparse.ArgumentParser(
description="Flaky Test Tracker - SQLite Quarantine Database",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Record a flaky test
python flaky_test_tracker.py --record \\
--test-path tests/test_foo.py::test_bar \\
--platform backend \\
--total-runs 10 \\
--failures 3 \\
--classification flaky
# Get quarantined tests
python flaky_test_tracker.py --quarantined --platform backend
# Get reliability score
python flaky_test_tracker.py --reliability \\
--test-path tests/test_foo.py::test_bar \\
--platform backend
# Get statistics
python flaky_test_tracker.py --stats
"""
)
parser.add_argument(
"--db-path",
type=str,
default="tests/coverage_reports/metrics/flaky_tests.db",
help="Path to SQLite database"
)
parser.add_argument(
"--record",
action="store_true",
help="Record a flaky test"
)
parser.add_argument(
"--test-path",
type=str,
help="Test identifier (e.g., tests/test_foo.py::test_bar)"
)
parser.add_argument(
"--platform",
type=str,
choices=["backend", "frontend", "mobile", "desktop"],
help="Platform name"
)
parser.add_argument(
"--total-runs",
type=int,
help="Total number of test runs"
)
parser.add_argument(
"--failures",
type=int,
help="Number of failures"
)
parser.add_argument(
"--classification",
type=str,
choices=["stable", "flaky", "broken"],
help="Test classification"
)
parser.add_argument(
"--quarantine-reason",
type=str,
help="Reason for quarantine"
)
parser.add_argument(
"--quarantined",
action="store_true",
help="Get quarantined tests"
)
parser.add_argument(
"--reliability",
action="store_true",
help="Get reliability score for a test"
)
parser.add_argument(
"--stats",
action="store_true",
help="Get aggregate statistics"
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON"
)
args = parser.parse_args()
tracker = FlakyTestTracker(Path(args.db_path))
try:
if args.record:
if not all([args.test_path, args.platform, args.total_runs is not None,
args.failures is not None, args.classification]):
print("ERROR: --record requires --test-path, --platform, --total-runs, "
"--failures, --classification")
return 2
failure_history = [
{"run": i, "failed": i < args.failures}
for i in range(args.total_runs)
]
test_id = tracker.record_flaky_test(
args.test_path,
args.platform,
args.total_runs,
args.failures,
args.classification,
failure_history,
args.quarantine_reason
)
if args.json:
print(json.dumps({"test_id": test_id}))
else:
print(f"Recorded test ID: {test_id}")
elif args.quarantined:
tests = tracker.get_quarantined_tests(args.platform)
if args.json:
print(json.dumps(tests, indent=2))
else:
print(f"Quarantined tests: {len(tests)}")
for test in tests:
print(f" - {test['test_path']} ({test['platform']}) "
f"flaky_rate={test['flaky_rate']}")
elif args.reliability:
if not all([args.test_path, args.platform]):
print("ERROR: --reliability requires --test-path and --platform")
return 2
score = tracker.get_test_reliability_score(args.test_path, args.platform)
if args.json:
print(json.dumps({"reliability_score": score}))
else:
print(f"Reliability score: {score}")
elif args.stats:
stats = tracker.get_statistics(args.platform)
if args.json:
print(json.dumps(stats, indent=2))
else:
print(f"Total tests: {stats['total']}")
print(f"Flaky: {stats['flaky']}")
print(f"Broken: {stats['broken']}")
print(f"Stable: {stats['stable']}")
print(f"Avg flaky rate: {stats['avg_flaky_rate']}")
else:
parser.print_help()
finally:
tracker.close()
if __name__ == "__main__":
main()