""" VoiceForge - Coverage & Function Tracker ----------------------------------------- Tracks test coverage and identifies untested functions: - Collects all public functions in codebase - Matches against existing tests - Generates coverage report """ import ast import sys from pathlib import Path from collections import defaultdict def collect_functions(root_dir: Path) -> dict[str, list[str]]: """Collect all public functions from Python files""" functions = defaultdict(list) for py_file in root_dir.rglob("*.py"): if '__pycache__' in str(py_file) or 'test_' in py_file.name: continue try: with open(py_file, 'r', encoding='utf-8', errors='ignore') as f: source = f.read() tree = ast.parse(source) module_name = py_file.stem for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): # Skip private functions if not node.name.startswith('_'): functions[module_name].append(node.name) except Exception: pass return functions def collect_tested_functions(test_dir: Path) -> set[str]: """Extract function names that are being tested""" tested = set() for test_file in test_dir.rglob("test_*.py"): try: with open(test_file, 'r', encoding='utf-8', errors='ignore') as f: source = f.read() tree = ast.parse(source) for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): # Extract tested function name from test name test_name = node.name if test_name.startswith('test_'): # e.g., test_transcribe_audio -> transcribe_audio func_name = test_name[5:] tested.add(func_name) # Also check for mocked functions for child in ast.walk(node): if isinstance(child, ast.Attribute): tested.add(child.attr) except Exception: pass return tested def run_coverage_analysis(app_dir: str = "app", test_dir: str = "tests"): """Run coverage analysis and report untested functions""" print("=" * 60) print("๐Ÿ“Š VoiceForge Function Coverage Tracker") print("=" * 60) app_path = Path(app_dir) test_path = Path(test_dir) if not app_path.exists(): print(f"โŒ App directory not found: {app_dir}") sys.exit(1) # Collect all functions all_functions = collect_functions(app_path) total_functions = sum(len(funcs) for funcs in all_functions.values()) # Collect tested functions tested_functions = collect_tested_functions(test_path) print(f"\n๐Ÿ“ Scanned: {len(all_functions)} modules, {total_functions} functions") print(f"๐Ÿงช Tests cover: {len(tested_functions)} function patterns\n") # Find untested untested = defaultdict(list) tested_count = 0 for module, funcs in all_functions.items(): for func in funcs: if func in tested_functions or any(func in t for t in tested_functions): tested_count += 1 else: untested[module].append(func) coverage = (tested_count / total_functions * 100) if total_functions > 0 else 0 print("๐Ÿ“ˆ COVERAGE SUMMARY") print("-" * 40) print(f" Total Functions: {total_functions}") print(f" Tested: {tested_count}") print(f" Untested: {total_functions - tested_count}") print(f" Coverage: {coverage:.1f}%") # Coverage bar bar_length = int(coverage / 5) bar = "โ–ˆ" * bar_length + "โ–‘" * (20 - bar_length) print(f"\n [{bar}] {coverage:.1f}%") # Untested by module print("\nโš ๏ธ UNTESTED FUNCTIONS (by module)") print("-" * 40) for module, funcs in sorted(untested.items())[:10]: print(f"\n ๐Ÿ“ฆ {module}:") for func in funcs[:5]: print(f" โ€ข {func}()") if len(funcs) > 5: print(f" ... and {len(funcs) - 5} more") print("\n" + "=" * 60) if coverage >= 70: print("โœ… Coverage: GOOD") return 0 elif coverage >= 40: print("โš ๏ธ Coverage: NEEDS IMPROVEMENT") return 1 else: print("โŒ Coverage: LOW") return 2 if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Track VoiceForge function coverage") parser.add_argument("--app", default="app", help="App source directory") parser.add_argument("--tests", default="tests", help="Tests directory") args = parser.parse_args() sys.exit(run_coverage_analysis(args.app, args.tests))