| |
| """ |
| Coverage Gap Analysis Script |
| |
| Analyzes pytest coverage reports to identify high-impact testing opportunities. |
| Categorizes files by size, calculates coverage gaps, and prioritizes files for |
| maximum coverage improvement. |
| |
| Usage: |
| python3 tests/scripts/analyze_coverage_gaps.py --format all --output-dir tests/coverage_reports/metrics/ |
| python3 tests/scripts/analyze_coverage_gaps.py --format json --output-dir /path/to/output |
| python3 tests/scripts/analyze_coverage_gaps.py --format markdown --output-dir reports/ |
| |
| Output Files: |
| - coverage_summary.json: Module-level aggregations and file tiers |
| - priority_files_for_phases_12_13.json: Ranked file list for test planning |
| - zero_coverage_analysis.json: Files with 0% coverage >100 lines |
| - PHASE_11_COVERAGE_ANALYSIS_REPORT.md: Comprehensive markdown report |
| |
| Requirements: |
| - Python 3.11+ |
| - coverage.json from pytest-cov |
| |
| Author: Phase 11 Coverage Analysis |
| Generated: 2026-02-15 |
| """ |
|
|
| import argparse |
| import json |
| import sys |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Any, Dict, List, Tuple |
|
|
|
|
| def load_coverage_data(coverage_path: str) -> Dict[str, Any]: |
| """ |
| Load coverage.json from pytest-cov. |
| |
| Args: |
| coverage_path: Path to coverage.json file |
| |
| Returns: |
| Coverage data dictionary |
| |
| Raises: |
| FileNotFoundError: If coverage.json doesn't exist |
| json.JSONDecodeError: If coverage.json is invalid |
| """ |
| path = Path(coverage_path) |
| if not path.exists(): |
| raise FileNotFoundError( |
| f"Coverage file not found: {coverage_path}\n" |
| f"Run tests with coverage first: pytest --cov=backend --cov-report=json" |
| ) |
|
|
| with open(path, 'r') as f: |
| return json.load(f) |
|
|
|
|
| def categorize_file_by_size(lines: int) -> str: |
| """ |
| Categorize file by total lines of code. |
| |
| Args: |
| lines: Total number of lines (statements) |
| |
| Returns: |
| Tier category (Tier 1-5) |
| """ |
| if lines >= 500: |
| return "Tier 1" |
| elif lines >= 300: |
| return "Tier 2" |
| elif lines >= 200: |
| return "Tier 3" |
| elif lines >= 100: |
| return "Tier 4" |
| else: |
| return "Tier 5" |
|
|
|
|
| def calculate_metrics(filedata: Dict[str, Any]) -> Dict[str, float]: |
| """ |
| Calculate coverage metrics for a single file. |
| |
| Args: |
| filedata: File coverage data from coverage.json |
| |
| Returns: |
| Dictionary with calculated metrics |
| """ |
| total_lines = filedata['summary']['num_statements'] |
| covered_lines = filedata['summary']['covered_lines'] |
| coverage_pct = filedata['summary']['percent_covered'] |
|
|
| |
| coverage_gap = total_lines - covered_lines |
|
|
| |
| potential_gain = coverage_gap * 0.5 |
|
|
| |
| priority_score = coverage_gap / total_lines if total_lines > 0 else 0 |
|
|
| return { |
| 'total_lines': total_lines, |
| 'covered_lines': covered_lines, |
| 'coverage_pct': coverage_pct, |
| 'coverage_gap': coverage_gap, |
| 'potential_gain': potential_gain, |
| 'priority_score': priority_score |
| } |
|
|
|
|
| def analyze_files(coverage_data: Dict[str, Any]) -> List[Dict[str, Any]]: |
| """ |
| Analyze all files in coverage report and calculate metrics. |
| |
| Args: |
| coverage_data: Full coverage data dictionary |
| |
| Returns: |
| List of file analysis results with metrics |
| """ |
| files_analysis = [] |
|
|
| for filepath, filedata in coverage_data['files'].items(): |
| metrics = calculate_metrics(filedata) |
| tier = categorize_file_by_size(metrics['total_lines']) |
|
|
| files_analysis.append({ |
| 'file': filepath, |
| 'tier': tier, |
| **metrics |
| }) |
|
|
| return files_analysis |
|
|
|
|
| def create_module_aggregation(files_analysis: List[Dict[str, Any]]) -> Dict[str, Any]: |
| """ |
| Aggregate metrics by module (core/, api/, tools/, etc.). |
| |
| Args: |
| files_analysis: List of file analysis results |
| |
| Returns: |
| Module-level aggregated metrics |
| """ |
| modules = {} |
|
|
| for file_data in files_analysis: |
| |
| parts = file_data['file'].split('/') |
| module = parts[0] if len(parts) > 1 else 'other' |
|
|
| if module not in modules: |
| modules[module] = { |
| 'total_lines': 0, |
| 'covered_lines': 0, |
| 'files': [] |
| } |
|
|
| modules[module]['total_lines'] += file_data['total_lines'] |
| modules[module]['covered_lines'] += file_data['covered_lines'] |
| modules[module]['files'].append(file_data) |
|
|
| |
| for module in modules.values(): |
| module['coverage_pct'] = ( |
| module['covered_lines'] / module['total_lines'] * 100 |
| if module['total_lines'] > 0 else 0 |
| ) |
| module['coverage_gap'] = module['total_lines'] - module['covered_lines'] |
|
|
| return modules |
|
|
|
|
| def get_high_priority_files(files_analysis: List[Dict[str, Any]]) -> List[Dict[str, Any]]: |
| """ |
| Get high-priority files for testing (Tier 1-3, sorted by coverage gap). |
| |
| Args: |
| files_analysis: List of file analysis results |
| |
| Returns: |
| Sorted list of high-priority files |
| """ |
| |
| high_priority = [f for f in files_analysis if f['tier'] in ['Tier 1', 'Tier 2', 'Tier 3']] |
|
|
| |
| high_priority.sort(key=lambda x: x['coverage_gap'], reverse=True) |
|
|
| return high_priority |
|
|
|
|
| def get_zero_coverage_files(files_analysis: List[Dict[str, Any]]) -> List[Dict[str, Any]]: |
| """ |
| Get files with 0% coverage and >100 lines. |
| |
| Args: |
| files_analysis: List of file analysis results |
| |
| Returns: |
| List of zero-coverage files |
| """ |
| zero_coverage = [ |
| f for f in files_analysis |
| if f['coverage_pct'] == 0 and f['total_lines'] >= 100 |
| ] |
|
|
| |
| zero_coverage.sort(key=lambda x: x['total_lines'], reverse=True) |
|
|
| return zero_coverage |
|
|
|
|
| def estimate_test_complexity(file_data: Dict[str, Any]) -> str: |
| """ |
| Estimate test complexity based on file size and coverage. |
| |
| Args: |
| file_data: File analysis data |
| |
| Returns: |
| Complexity level (low, medium, high) |
| """ |
| lines = file_data['total_lines'] |
|
|
| if lines < 150: |
| return "low" |
| elif lines < 400: |
| return "medium" |
| else: |
| return "high" |
|
|
|
|
| def recommend_test_type(filepath: str) -> str: |
| """ |
| Recommend test type based on file characteristics. |
| |
| Args: |
| filepath: File path |
| |
| Returns: |
| Recommended test type (unit, integration, property, e2e) |
| """ |
| |
| if any(keyword in filepath for keyword in [ |
| 'workflow', 'engine', 'handler', 'coordinator', 'executor' |
| ]): |
| return "property" |
|
|
| |
| if any(keyword in filepath for keyword in [ |
| 'endpoint', 'route', 'api' |
| ]): |
| return "integration" |
|
|
| |
| if any(keyword in filepath for keyword in [ |
| 'service', 'util', 'helper', 'manager' |
| ]): |
| return "unit" |
|
|
| |
| return "unit" |
|
|
|
|
| def create_coverage_summary( |
| files_analysis: List[Dict[str, Any]], |
| modules: Dict[str, Any] |
| ) -> Dict[str, Any]: |
| """ |
| Create coverage summary JSON output. |
| |
| Args: |
| files_analysis: List of file analysis results |
| modules: Module-level aggregated metrics |
| |
| Returns: |
| Coverage summary dictionary |
| """ |
| |
| total_lines = sum(f['total_lines'] for f in files_analysis) |
| total_covered = sum(f['covered_lines'] for f in files_analysis) |
| overall_pct = (total_covered / total_lines * 100) if total_lines > 0 else 0 |
|
|
| |
| high_priority = get_high_priority_files(files_analysis) |
| zero_coverage = get_zero_coverage_files(files_analysis) |
|
|
| |
| files_by_tier = { |
| 'Tier 1': [f for f in files_analysis if f['tier'] == 'Tier 1'], |
| 'Tier 2': [f for f in files_analysis if f['tier'] == 'Tier 2'], |
| 'Tier 3': [f for f in files_analysis if f['tier'] == 'Tier 3'], |
| 'Tier 4': [f for f in files_analysis if f['tier'] == 'Tier 4'], |
| 'Tier 5': [f for f in files_analysis if f['tier'] == 'Tier 5'], |
| } |
|
|
| return { |
| 'generated_at': datetime.utcnow().isoformat() + 'Z', |
| 'overall': { |
| 'percent_covered': round(overall_pct, 2), |
| 'covered_lines': total_covered, |
| 'total_lines': total_lines, |
| 'coverage_gap': total_lines - total_covered |
| }, |
| 'modules': { |
| module: { |
| 'percent_covered': round(data['coverage_pct'], 2), |
| 'covered_lines': data['covered_lines'], |
| 'total_lines': data['total_lines'], |
| 'coverage_gap': data['coverage_gap'], |
| 'file_count': len(data['files']) |
| } |
| for module, data in modules.items() |
| }, |
| 'files_by_size': { |
| tier: { |
| 'file_count': len(files), |
| 'total_lines': sum(f['total_lines'] for f in files), |
| 'covered_lines': sum(f['covered_lines'] for f in files), |
| 'avg_coverage_pct': round( |
| sum(f['covered_lines'] for f in files) / |
| sum(f['total_lines'] for f in files) * 100 |
| if sum(f['total_lines'] for f in files) > 0 else 0, |
| 2 |
| ) |
| } |
| for tier, files in files_by_tier.items() |
| }, |
| 'high_priority_files': [ |
| { |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'coverage_pct': round(f['coverage_pct'], 2), |
| 'uncovered_lines': f['coverage_gap'], |
| 'potential_gain': round(f['potential_gain']), |
| 'priority_score': round(f['priority_score'], 3) |
| } |
| for f in high_priority[:50] |
| ], |
| 'zero_coverage_files': [ |
| { |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'estimated_gain_lines': round(f['total_lines'] * 0.5) |
| } |
| for f in zero_coverage |
| ] |
| } |
|
|
|
|
| def create_priority_files_list( |
| high_priority: List[Dict[str, Any]], |
| zero_coverage: List[Dict[str, Any]] |
| ) -> Dict[str, Any]: |
| """ |
| Create prioritized file list for Phases 12-13. |
| |
| Args: |
| high_priority: List of high-priority files |
| zero_coverage: List of zero-coverage files |
| |
| Returns: |
| Priority files dictionary |
| """ |
| |
| total_lines = sum(f['total_lines'] for f in high_priority) |
| total_covered = sum(f['covered_lines'] for f in high_priority) |
| overall_pct = (total_covered / total_lines * 100) if total_lines > 0 else 0 |
|
|
| |
| phase_12_files = [ |
| f for f in high_priority |
| if f['tier'] == 'Tier 1' and f['coverage_pct'] < 20 |
| ][:20] |
|
|
| phase_13_files = [ |
| f for f in high_priority |
| if f not in phase_12_files and f['coverage_pct'] < 30 |
| ][:20] |
|
|
| |
| phase_13_files.extend(zero_coverage[:10]) |
|
|
| return { |
| 'generated_at': datetime.utcnow().isoformat() + 'Z', |
| 'current_coverage': { |
| 'percent': round(overall_pct, 2), |
| 'covered_lines': total_covered, |
| 'total_lines': total_lines |
| }, |
| 'target_coverage': { |
| 'percent': 80, |
| 'required_lines': int(total_lines * 0.8), |
| 'gap_lines': total_lines - int(total_lines * 0.8) |
| }, |
| 'phases': { |
| '12': { |
| 'target_percent': 28, |
| 'target_gain': 5.2, |
| 'files': [ |
| { |
| 'rank': i + 1, |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'current_percent': round(f['coverage_pct'], 2), |
| 'uncovered_lines': f['coverage_gap'], |
| 'estimated_tests_needed': max(30, int(f['total_lines'] / 20)), |
| 'recommended_test_type': recommend_test_type(f['file']), |
| 'coverage_complexity': estimate_test_complexity(f), |
| 'tier': f['tier'] |
| } |
| for i, f in enumerate(phase_12_files) |
| ] |
| }, |
| '13': { |
| 'target_percent': 35, |
| 'target_gain': 7.0, |
| 'files': [ |
| { |
| 'rank': i + 1, |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'current_percent': round(f['coverage_pct'], 2), |
| 'uncovered_lines': f['coverage_gap'], |
| 'estimated_tests_needed': max(30, int(f['total_lines'] / 20)), |
| 'recommended_test_type': recommend_test_type(f['file']), |
| 'coverage_complexity': estimate_test_complexity(f), |
| 'tier': f['tier'] |
| } |
| for i, f in enumerate(phase_13_files) |
| ] |
| } |
| }, |
| 'zero_coverage_quick_wins': [ |
| { |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'estimated_gain_lines': round(f['total_lines'] * 0.5), |
| 'recommended_test_type': recommend_test_type(f['file']) |
| } |
| for f in zero_coverage[:30] |
| ] |
| } |
|
|
|
|
| def create_zero_coverage_analysis(zero_coverage: List[Dict[str, Any]]) -> Dict[str, Any]: |
| """ |
| Create zero coverage analysis JSON. |
| |
| Args: |
| zero_coverage: List of zero-coverage files |
| |
| Returns: |
| Zero coverage analysis dictionary |
| """ |
| return { |
| 'generated_at': datetime.utcnow().isoformat() + 'Z', |
| 'total_zero_coverage_files': len(zero_coverage), |
| 'total_lines_uncovered': sum(f['total_lines'] for f in zero_coverage), |
| 'estimated_total_gain': sum(f['total_lines'] * 0.5 for f in zero_coverage), |
| 'files': [ |
| { |
| 'file': f['file'], |
| 'lines': f['total_lines'], |
| 'estimated_gain_lines': round(f['total_lines'] * 0.5), |
| 'recommended_test_type': recommend_test_type(f['file']), |
| 'complexity': estimate_test_complexity(f) |
| } |
| for f in zero_coverage |
| ] |
| } |
|
|
|
|
| def generate_markdown_report( |
| coverage_summary: Dict[str, Any], |
| priority_files: Dict[str, Any], |
| modules: Dict[str, Any] |
| ) -> str: |
| """ |
| Generate comprehensive markdown report. |
| |
| Args: |
| coverage_summary: Coverage summary data |
| priority_files: Priority files data |
| modules: Module aggregated data |
| |
| Returns: |
| Markdown report content |
| """ |
| overall = coverage_summary['overall'] |
|
|
| md = f"""# Phase 11 Coverage Analysis Report |
| |
| **Generated:** {coverage_summary['generated_at']} |
| **Purpose:** Identify highest-impact testing opportunities for Phases 12-13 |
| |
| --- |
| |
| ## Executive Summary |
| |
| ### Current Coverage Status |
| |
| | Metric | Value | |
| |--------|-------| |
| | Overall Coverage | {overall['percent_covered']}% | |
| | Covered Lines | {overall['covered_lines']:,} | |
| | Total Lines | {overall['total_lines']:,} | |
| | Coverage Gap | {overall['coverage_gap']:,} lines | |
| | Target Coverage (80%) | {int(overall['total_lines'] * 0.8):,} lines | |
| | Remaining Gap | {int(overall['total_lines'] * 0.8) - overall['covered_lines']:,} lines | |
| |
| ### File Distribution by Size |
| |
| | Tier | Size Range | File Count | Total Lines | Avg Coverage | |
| |------|------------|------------|-------------|--------------| |
| | Tier 1 | ≥500 lines | {coverage_summary['files_by_size']['Tier 1']['file_count']} | {coverage_summary['files_by_size']['Tier 1']['total_lines']:,} | {coverage_summary['files_by_size']['Tier 1']['avg_coverage_pct']}% | |
| | Tier 2 | 300-499 lines | {coverage_summary['files_by_size']['Tier 2']['file_count']} | {coverage_summary['files_by_size']['Tier 2']['total_lines']:,} | {coverage_summary['files_by_size']['Tier 2']['avg_coverage_pct']}% | |
| | Tier 3 | 200-299 lines | {coverage_summary['files_by_size']['Tier 3']['file_count']} | {coverage_summary['files_by_size']['Tier 3']['total_lines']:,} | {coverage_summary['files_by_size']['Tier 3']['avg_coverage_pct']}% | |
| | Tier 4 | 100-199 lines | {coverage_summary['files_by_size']['Tier 4']['file_count']} | {coverage_summary['files_by_size']['Tier 4']['total_lines']:,} | {coverage_summary['files_by_size']['Tier 4']['avg_coverage_pct']}% | |
| | Tier 5 | <100 lines | {coverage_summary['files_by_size']['Tier 5']['file_count']} | {coverage_summary['files_by_size']['Tier 5']['total_lines']:,} | {coverage_summary['files_by_size']['Tier 5']['avg_coverage_pct']}% | |
| |
| --- |
| |
| ## Top 20 High-Impact Files |
| |
| Ranked by coverage gap (uncovered lines). **Target: 50% coverage per file** (Phase 8.6 proven sustainable). |
| |
| | Rank | File | Lines | Current % | Uncovered | Priority | Tier | Test Type | Complexity | |
| |------|------|-------|-----------|-----------|----------|------|-----------|------------| |
| """ |
|
|
| for i, file_data in enumerate(coverage_summary['high_priority_files'][:20], 1): |
| filepath = file_data['file'] |
| lines = file_data['lines'] |
| pct = file_data['coverage_pct'] |
| uncovered = file_data['uncovered_lines'] |
| priority = file_data['priority_score'] |
|
|
| |
| tier = "Tier 1" if lines >= 500 else "Tier 2" if lines >= 300 else "Tier 3" |
| test_type = recommend_test_type(filepath) |
| complexity = "high" if lines >= 400 else "medium" if lines >= 150 else "low" |
|
|
| md += f"| {i} | {filepath} | {lines} | {pct}% | {uncovered} | {priority:.3f} | {tier} | {test_type} | {complexity} |\n" |
|
|
| md += f""" |
| |
| --- |
| |
| ## Zero Coverage Quick Wins |
| |
| Files with **0% coverage** and >100 lines. Testing these to 50% coverage provides fast wins. |
| |
| **Total zero-coverage files >100 lines:** {len(coverage_summary['zero_coverage_files'])} |
| |
| | File | Lines | Est. Gain (50%) | Test Type | Complexity | |
| |------|-------|-----------------|-----------|------------| |
| """ |
|
|
| for file_data in coverage_summary['zero_coverage_files'][:25]: |
| filepath = file_data['file'] |
| lines = file_data['lines'] |
| gain = file_data['estimated_gain_lines'] |
| test_type = recommend_test_type(filepath) |
| complexity = "high" if lines >= 400 else "medium" if lines >= 150 else "low" |
|
|
| md += f"| {filepath} | {lines} | {gain} | {test_type} | {complexity} |\n" |
|
|
| md += f""" |
| |
| --- |
| |
| ## Module Breakdown |
| |
| ### Core Module |
| |
| **Files:** {len(modules.get('core', {}).get('files', []))} |
| |
| | Metric | Value | |
| |--------|-------| |
| | Coverage | {modules.get('core', {}).get('coverage_pct', 0):.1f}% | |
| | Covered Lines | {modules.get('core', {}).get('covered_lines', 0):,} | |
| | Total Lines | {modules.get('core', {}).get('total_lines', 0):,} | |
| | Coverage Gap | {modules.get('core', {}).get('coverage_gap', 0):,} | |
| |
| **Top 5 Gaps in core/:** |
| """ |
|
|
| core_files = sorted( |
| modules.get('core', {}).get('files', []), |
| key=lambda x: x['coverage_gap'], |
| reverse=True |
| )[:5] |
|
|
| for i, f in enumerate(core_files, 1): |
| md += f"{i}. {f['file']}: {f['coverage_gap']} lines uncovered ({f['coverage_pct']:.1f}%)\n" |
|
|
| md += f""" |
| |
| ### API Module |
| |
| **Files:** {len(modules.get('api', {}).get('files', []))} |
| |
| | Metric | Value | |
| |--------|-------| |
| | Coverage | {modules.get('api', {}).get('coverage_pct', 0):.1f}% | |
| | Covered Lines | {modules.get('api', {}).get('covered_lines', 0):,} | |
| | Total Lines | {modules.get('api', {}).get('total_lines', 0):,} | |
| | Coverage Gap | {modules.get('api', {}).get('coverage_gap', 0):,} | |
| |
| **Top 5 Gaps in api/:** |
| """ |
|
|
| api_files = sorted( |
| modules.get('api', {}).get('files', []), |
| key=lambda x: x['coverage_gap'], |
| reverse=True |
| )[:5] |
|
|
| for i, f in enumerate(api_files, 1): |
| md += f"{i}. {f['file']}: {f['coverage_gap']} lines uncovered ({f['coverage_pct']:.1f}%)\n" |
|
|
| md += f""" |
| |
| ### Tools Module |
| |
| **Files:** {len(modules.get('tools', {}).get('files', []))} |
| |
| | Metric | Value | |
| |--------|-------| |
| | Coverage | {modules.get('tools', {}).get('coverage_pct', 0):.1f}% | |
| | Covered Lines | {modules.get('tools', {}).get('covered_lines', 0):,} | |
| | Total Lines | {modules.get('tools', {}).get('total_lines', 0):,} | |
| | Coverage Gap | {modules.get('tools', {}).get('coverage_gap', 0):,} | |
| |
| **Top 5 Gaps in tools/:** |
| """ |
|
|
| tools_files = sorted( |
| modules.get('tools', {}).get('files', []), |
| key=lambda x: x['coverage_gap'], |
| reverse=True |
| )[:5] |
|
|
| for i, f in enumerate(tools_files, 1): |
| md += f"{i}. {f['file']}: {f['coverage_gap']} lines uncovered ({f['coverage_pct']:.1f}%)\n" |
|
|
| md += f""" |
| |
| --- |
| |
| ## Phase 12-13 Testing Strategy |
| |
| ### Strategy Overview |
| |
| Based on Phase 8.6 validation (3.38x velocity acceleration), this analysis prioritizes **high-impact files** for maximum coverage gain. |
| |
| **Key Principles:** |
| - Target 50% average coverage per file (proven sustainable) |
| - Prioritize largest files first (Tier 1 > Tier 2 > Tier 3) |
| - Use appropriate test types (property, integration, unit) |
| - 3-4 files per plan for focused execution |
| |
| ### Phase 12: Tier 1 Files |
| |
| **Target:** {priority_files['phases']['12']['target_percent']}% coverage (+{priority_files['phases']['12']['target_gain']} percentage points) |
| **Focus:** Files ≥500 lines with <20% coverage |
| **Estimated Plans:** 4-5 plans |
| **Estimated Velocity:** +1.3-1.5% per plan |
| |
| **Files:** {len(priority_files['phases']['12']['files'])} files |
| |
| ### Phase 13: Tier 2-3 + Zero Coverage |
| |
| **Target:** {priority_files['phases']['13']['target_percent']}% coverage (+{priority_files['phases']['13']['target_gain']} percentage points) |
| **Focus:** Files 300-500 lines, <30% coverage + zero-coverage quick wins |
| **Estimated Plans:** 5-6 plans |
| **Estimated Velocity:** +1.2-1.4% per plan |
| |
| **Files:** {len(priority_files['phases']['13']['files'])} files |
| |
| ### Test Type Recommendations |
| |
| | Test Type | When to Use | Examples | |
| |-----------|-------------|----------| |
| | **Property Tests** | Stateful logic, workflows, handlers | workflow_engine, byok_handler, coordinators | |
| | **Integration Tests** | API endpoints, routes | atom_agent_endpoints, workflow_endpoints, API routes | |
| | **Unit Tests** | Isolated logic, services, utilities | lancedb_handler, auto_document_ingestion, services | |
| |
| ### Execution Plan |
| |
| **Files Per Plan:** 3-4 high-impact files |
| **Target Coverage Per File:** 50% (Phase 8.6 proven sustainable) |
| **Estimated Velocity:** +1.5% per plan (maintaining Phase 8.6 acceleration) |
| **Estimated Duration:** 4-6 hours per plan |
| |
| **Total Estimated Impact:** |
| - Phase 12: +{priority_files['phases']['12']['target_gain']} percentage points |
| - Phase 13: +{priority_files['phases']['13']['target_gain']} percentage points |
| - Combined: +{priority_files['phases']['12']['target_gain'] + priority_files['phases']['13']['target_gain']} percentage points |
| |
| --- |
| |
| ## Recommendations |
| |
| 1. **Start with Tier 1 files** (Phase 12) - Highest ROI with 3.38x velocity acceleration |
| 2. **Target 50% coverage per file** - Proven sustainable from Phase 8.6 |
| 3. **Use appropriate test types** - Property tests for stateful logic, integration for APIs |
| 4. **3-4 files per plan** - Focused execution without overwhelming complexity |
| 5. **Leverage existing test infrastructure** - Property test patterns, AsyncMock, FastAPI TestClient |
| |
| --- |
| |
| ## Appendix: Data Sources |
| |
| - **Coverage Data:** `tests/coverage_reports/metrics/coverage.json` |
| - **Analysis Script:** `tests/scripts/analyze_coverage_gaps.py` |
| - **Priority Files:** `tests/coverage_reports/priority_files_for_phases_12_13.json` |
| |
| *Generated by Phase 11 Coverage Analysis - {datetime.utcnow().strftime('%Y-%m-%d')}* |
| """ |
|
|
| return md |
|
|
|
|
| def main(): |
| """Main entry point for coverage analysis script.""" |
| parser = argparse.ArgumentParser( |
| description='Analyze coverage gaps and prioritize testing opportunities', |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=__doc__ |
| ) |
| parser.add_argument( |
| '--coverage-file', |
| default='backend/tests/coverage_reports/metrics/coverage.json', |
| help='Path to coverage.json (default: backend/tests/coverage_reports/metrics/coverage.json)' |
| ) |
| parser.add_argument( |
| '--format', |
| choices=['json', 'markdown', 'all'], |
| default='all', |
| help='Output format (default: all)' |
| ) |
| parser.add_argument( |
| '--output-dir', |
| default='backend/tests/coverage_reports/metrics/', |
| help='Output directory for generated files (default: backend/tests/coverage_reports/metrics/)' |
| ) |
|
|
| args = parser.parse_args() |
|
|
| try: |
| |
| print(f"Loading coverage data from {args.coverage_file}...") |
| coverage_data = load_coverage_data(args.coverage_file) |
| print(f"Found {len(coverage_data['files'])} files") |
|
|
| |
| print("Analyzing coverage gaps...") |
| files_analysis = analyze_files(coverage_data) |
| modules = create_module_aggregation(files_analysis) |
| high_priority = get_high_priority_files(files_analysis) |
| zero_coverage = get_zero_coverage_files(files_analysis) |
|
|
| |
| output_dir = Path(args.output_dir) |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| if args.format in ['json', 'all']: |
| print("Generating JSON files...") |
|
|
| |
| coverage_summary = create_coverage_summary(files_analysis, modules) |
| summary_path = output_dir / 'coverage_summary.json' |
| with open(summary_path, 'w') as f: |
| json.dump(coverage_summary, f, indent=2) |
| print(f" Created: {summary_path}") |
|
|
| |
| priority_files = create_priority_files_list(high_priority, zero_coverage) |
| priority_path = output_dir / 'priority_files_for_phases_12_13.json' |
| with open(priority_path, 'w') as f: |
| json.dump(priority_files, f, indent=2) |
| print(f" Created: {priority_path}") |
|
|
| |
| zero_analysis = create_zero_coverage_analysis(zero_coverage) |
| zero_path = output_dir / 'zero_coverage_analysis.json' |
| with open(zero_path, 'w') as f: |
| json.dump(zero_analysis, f, indent=2) |
| print(f" Created: {zero_path}") |
|
|
| if args.format in ['markdown', 'all']: |
| print("Generating markdown report...") |
|
|
| |
| markdown_report = generate_markdown_report( |
| coverage_summary if args.format in ['json', 'all'] else create_coverage_summary(files_analysis, modules), |
| priority_files if args.format in ['json', 'all'] else create_priority_files_list(high_priority, zero_coverage), |
| modules |
| ) |
|
|
| report_path = output_dir / 'PHASE_11_COVERAGE_ANALYSIS_REPORT.md' |
| with open(report_path, 'w') as f: |
| f.write(markdown_report) |
| print(f" Created: {report_path}") |
|
|
| print("\nAnalysis complete!") |
| print(f"High-priority files identified: {len(high_priority)}") |
| print(f"Zero-coverage files (>100 lines): {len(zero_coverage)}") |
|
|
| return 0 |
|
|
| except FileNotFoundError as e: |
| print(f"Error: {e}", file=sys.stderr) |
| return 1 |
| except json.JSONDecodeError as e: |
| print(f"Error: Invalid JSON in coverage file: {e}", file=sys.stderr) |
| return 1 |
| except Exception as e: |
| print(f"Unexpected error: {e}", file=sys.stderr) |
| import traceback |
| traceback.print_exc() |
| return 1 |
|
|
|
|
| if __name__ == '__main__': |
| sys.exit(main()) |
|
|