File size: 5,113 Bytes
673435a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"""
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))