Files
gh-cskiro-claudex-analysis-…/skills/codebase-auditor/scripts/analyzers/test_coverage.py
2025-11-29 18:16:43 +08:00

185 lines
6.8 KiB
Python

"""
Test Coverage Analyzer
Analyzes:
- Test coverage percentage
- Testing Trophy distribution
- Test quality
- Untested critical paths
"""
import json
from pathlib import Path
from typing import Dict, List
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
"""
Analyze test coverage and quality.
Args:
codebase_path: Path to codebase
metadata: Project metadata
Returns:
List of testing-related findings
"""
findings = []
# Check for test files existence
test_stats = analyze_test_presence(codebase_path, metadata)
if test_stats:
findings.extend(test_stats)
# Analyze coverage if coverage reports exist
coverage_findings = analyze_coverage_reports(codebase_path, metadata)
if coverage_findings:
findings.extend(coverage_findings)
return findings
def analyze_test_presence(codebase_path: Path, metadata: Dict) -> List[Dict]:
"""Check for test file presence and basic test hygiene."""
findings = []
# Count test files
test_extensions = {'.test.js', '.test.ts', '.test.jsx', '.test.tsx', '.spec.js', '.spec.ts'}
test_dirs = {'__tests__', 'tests', 'test', 'spec'}
test_file_count = 0
source_file_count = 0
exclude_dirs = {'node_modules', '.git', 'dist', 'build', '__pycache__'}
source_extensions = {'.js', '.jsx', '.ts', '.tsx', '.py'}
for file_path in codebase_path.rglob('*'):
if file_path.is_file() and not any(excluded in file_path.parts for excluded in exclude_dirs):
# Check if it's a test file
is_test = (
any(file_path.name.endswith(ext) for ext in test_extensions) or
any(test_dir in file_path.parts for test_dir in test_dirs)
)
if is_test:
test_file_count += 1
elif file_path.suffix in source_extensions:
source_file_count += 1
# Calculate test ratio
if source_file_count > 0:
test_ratio = (test_file_count / source_file_count) * 100
if test_ratio < 20:
findings.append({
'severity': 'high',
'category': 'testing',
'subcategory': 'test_coverage',
'title': f'Low test file ratio ({test_ratio:.1f}%)',
'description': f'Only {test_file_count} test files for {source_file_count} source files',
'file': None,
'line': None,
'code_snippet': None,
'impact': 'Insufficient testing leads to bugs and difficult refactoring',
'remediation': 'Add tests for untested modules, aim for at least 80% coverage',
'effort': 'high',
})
elif test_ratio < 50:
findings.append({
'severity': 'medium',
'category': 'testing',
'subcategory': 'test_coverage',
'title': f'Moderate test file ratio ({test_ratio:.1f}%)',
'description': f'{test_file_count} test files for {source_file_count} source files',
'file': None,
'line': None,
'code_snippet': None,
'impact': 'More tests needed to achieve recommended 80% coverage',
'remediation': 'Continue adding tests, focus on critical paths first',
'effort': 'medium',
})
return findings
def analyze_coverage_reports(codebase_path: Path, metadata: Dict) -> List[Dict]:
"""Analyze coverage reports if they exist."""
findings = []
# Look for coverage reports (Istanbul/c8 format)
coverage_files = [
codebase_path / 'coverage' / 'coverage-summary.json',
codebase_path / 'coverage' / 'coverage-final.json',
codebase_path / '.nyc_output' / 'coverage-summary.json',
]
for coverage_file in coverage_files:
if coverage_file.exists():
try:
with open(coverage_file, 'r') as f:
coverage_data = json.load(f)
# Extract total coverage
total = coverage_data.get('total', {})
line_coverage = total.get('lines', {}).get('pct', 0)
branch_coverage = total.get('branches', {}).get('pct', 0)
function_coverage = total.get('functions', {}).get('pct', 0)
statement_coverage = total.get('statements', {}).get('pct', 0)
# Check against 80% threshold
if line_coverage < 80:
severity = 'high' if line_coverage < 50 else 'medium'
findings.append({
'severity': severity,
'category': 'testing',
'subcategory': 'test_coverage',
'title': f'Line coverage below target ({line_coverage:.1f}%)',
'description': f'Current coverage is {line_coverage:.1f}%, target is 80%',
'file': 'coverage/coverage-summary.json',
'line': None,
'code_snippet': None,
'impact': 'Low coverage means untested code paths and higher bug risk',
'remediation': f'Add tests to increase coverage by {80 - line_coverage:.1f}%',
'effort': 'high',
})
if branch_coverage < 75:
findings.append({
'severity': 'medium',
'category': 'testing',
'subcategory': 'test_coverage',
'title': f'Branch coverage below target ({branch_coverage:.1f}%)',
'description': f'Current branch coverage is {branch_coverage:.1f}%, target is 75%',
'file': 'coverage/coverage-summary.json',
'line': None,
'code_snippet': None,
'impact': 'Untested branches can hide bugs in conditional logic',
'remediation': 'Add tests for edge cases and conditional branches',
'effort': 'medium',
})
break # Found coverage, don't check other files
except:
pass
# If no coverage report found
if not findings:
findings.append({
'severity': 'medium',
'category': 'testing',
'subcategory': 'test_infrastructure',
'title': 'No coverage report found',
'description': 'Could not find coverage-summary.json',
'file': None,
'line': None,
'code_snippet': None,
'impact': 'Cannot measure test effectiveness without coverage reports',
'remediation': 'Configure test runner to generate coverage reports (Jest: --coverage, Vitest: --coverage)',
'effort': 'low',
})
return findings