Skip to content

Commit bd1ef9f

Browse files
committed
Added DIY branch coverage tool
1 parent ee7caee commit bd1ef9f

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

BRANCH_COVERAGE_GUIDE.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
## Add Branch Coverage
2+
3+
### Step 1: Register in `mypy/branch_coverage.py`
4+
5+
```python
6+
BRANCH_COVERAGE = {
7+
'check_return_stmt': set(),
8+
'your_function_name': set(), # Add your function
9+
}
10+
11+
BRANCH_DESCRIPTIONS = {
12+
'your_function_name': {
13+
1: 'Function entry',
14+
2: 'if condition_x - TRUE',
15+
3: 'if condition_x - FALSE',
16+
4: 'elif condition_y - TRUE',
17+
5: 'else branch',
18+
}
19+
}
20+
```
21+
22+
### Step 2: Instrument Your Function
23+
24+
```python
25+
def your_function_name(self, param):
26+
from mypy.branch_coverage import record_branch
27+
record_branch('your_function_name', 1) # Function entry
28+
29+
if condition_x:
30+
record_branch('your_function_name', 2) # TRUE
31+
# code...
32+
elif condition_y:
33+
record_branch('your_function_name', 3) # FALSE from if
34+
record_branch('your_function_name', 4) # TRUE for elif
35+
# code...
36+
else:
37+
record_branch('your_function_name', 3) # FALSE from if
38+
record_branch('your_function_name', 5) # else
39+
# code...
40+
```
41+
42+
**Important:** Import `record_branch` inside the function to avoid circular imports.
43+
44+
## Run Tests
45+
46+
**CRITICAL**: Must use `-n0` to disable parallel execution, or coverage data will not be collected!
47+
48+
```bash
49+
# Activate virtual environment first
50+
source venv/bin/activate
51+
52+
# Run all tests
53+
pytest mypy/test/testcheck.py -n0
54+
55+
# Run specific test file
56+
pytest mypy/test/testcheck.py::TypeCheckSuite::::check-basic.test::testInvalidReturn -n0
57+
```
58+
59+
## View Reports
60+
61+
Reports are automatically saved in the project root directory:`branch_coverage_report.txt`

mypy/branch_coverage.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Branch Coverage Tracking Module"""
2+
3+
import json
4+
from pathlib import Path
5+
6+
BRANCH_COVERAGE = {
7+
'check_return_stmt': set()
8+
}
9+
10+
BRANCH_DESCRIPTIONS = {
11+
'check_return_stmt': {
12+
1: 'Function entry',
13+
2: 'defn is not None - TRUE',
14+
3: 'defn is not None - FALSE',
15+
4: 'defn.is_generator - TRUE',
16+
5: 'defn.is_generator - FALSE (else/elif)',
17+
6: 'defn.is_coroutine - TRUE',
18+
7: 'defn.is_coroutine - FALSE (else)',
19+
8: 'isinstance(return_type, UninhabitedType) - TRUE',
20+
9: 'isinstance(return_type, UninhabitedType) - FALSE',
21+
10: 'not is_lambda and not return_type.ambiguous - TRUE',
22+
11: 'not is_lambda and not return_type.ambiguous - FALSE',
23+
12: 's.expr - TRUE (has return value)',
24+
13: 's.expr - FALSE (empty return)',
25+
14: 'isinstance(s.expr, (CallExpr, ...)) or isinstance(s.expr, AwaitExpr) - TRUE',
26+
15: 'isinstance(s.expr, (CallExpr, ...)) or isinstance(s.expr, AwaitExpr) - FALSE',
27+
16: 'isinstance(typ, Instance) and typ.type.fullname in NOT_IMPLEMENTED - TRUE',
28+
17: 'isinstance(typ, Instance) and typ.type.fullname in NOT_IMPLEMENTED - FALSE',
29+
18: 'defn.is_async_generator - TRUE',
30+
19: 'defn.is_async_generator - FALSE',
31+
20: 'isinstance(typ, AnyType) - TRUE',
32+
21: 'isinstance(typ, AnyType) - FALSE',
33+
22: 'warn_return_any conditions - TRUE (all conditions met)',
34+
23: 'warn_return_any conditions - FALSE (at least one condition not met)',
35+
24: 'declared_none_return - TRUE',
36+
25: 'declared_none_return - FALSE',
37+
26: 'is_lambda or isinstance(typ, NoneType) - TRUE',
38+
27: 'is_lambda or isinstance(typ, NoneType) - FALSE',
39+
28: 'defn.is_generator and not defn.is_coroutine and isinstance(return_type, AnyType) - TRUE',
40+
29: 'defn.is_generator and not defn.is_coroutine and isinstance(return_type, AnyType) - FALSE',
41+
30: 'isinstance(return_type, (NoneType, AnyType)) - TRUE',
42+
31: 'isinstance(return_type, (NoneType, AnyType)) - FALSE',
43+
32: 'self.in_checked_function() - TRUE',
44+
33: 'self.in_checked_function() - FALSE',
45+
}
46+
}
47+
48+
49+
def record_branch(function_name, branch_id):
50+
51+
if function_name in BRANCH_COVERAGE:
52+
BRANCH_COVERAGE[function_name].add(branch_id)
53+
54+
55+
def get_coverage_report():
56+
57+
report = []
58+
report.append("=" * 80)
59+
report.append("BRANCH COVERAGE REPORT")
60+
report.append("=" * 80)
61+
62+
for func_name, covered_branches in BRANCH_COVERAGE.items():
63+
report.append(f"\n{'=' * 80}")
64+
report.append(f"Function: {func_name}")
65+
report.append(f"{'=' * 80}")
66+
67+
descriptions = BRANCH_DESCRIPTIONS.get(func_name, {})
68+
total_branches = len(descriptions)
69+
covered_count = len(covered_branches)
70+
71+
report.append(f"Coverage: {covered_count}/{total_branches} branches ({covered_count/total_branches*100:.1f}%)")
72+
report.append("")
73+
74+
75+
for branch_id in sorted(descriptions.keys()):
76+
status = "COVERED" if branch_id in covered_branches else "NOT COVERED"
77+
desc = descriptions[branch_id]
78+
report.append(f" Branch {branch_id:2d}: {status:15s} | {desc}")
79+
80+
81+
uncovered = set(descriptions.keys()) - covered_branches
82+
if uncovered:
83+
report.append("\n" + "=" * 80)
84+
report.append("UNCOVERED BRANCHES:")
85+
report.append("=" * 80)
86+
for branch_id in sorted(uncovered):
87+
report.append(f" Branch {branch_id:2d}: {descriptions[branch_id]}")
88+
89+
report.append("\n" + "=" * 80)
90+
return "\n".join(report)
91+
92+
93+
def save_coverage_report(filename="branch_coverage_report.txt"):
94+
95+
report = get_coverage_report()
96+
output_path = Path.cwd() / filename
97+
with open(output_path, 'w', encoding='utf-8') as f:
98+
f.write(report)
99+
print(f"\nCoverage report saved to: {output_path}")
100+
101+

mypy/test/conftest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Pytest configuration for branch coverage collection
3+
"""
4+
5+
import pytest
6+
7+
8+
def pytest_sessionfinish(session, exitstatus):
9+
"""
10+
Hook that runs after all tests complete
11+
"""
12+
try:
13+
from mypy.branch_coverage import (
14+
save_coverage_report,
15+
get_coverage_report,
16+
BRANCH_COVERAGE
17+
)
18+
19+
total_covered = sum(len(branches) for branches in BRANCH_COVERAGE.values())
20+
21+
if total_covered > 0:
22+
print("\n" + "=" * 80)
23+
print("BRANCH COVERAGE COLLECTION COMPLETED")
24+
print("=" * 80)
25+
print(f"Total branches covered: {total_covered}")
26+
27+
save_coverage_report()
28+
29+
print("\n" + get_coverage_report())
30+
31+
print("\n" + "=" * 80)
32+
print("Coverage reports saved!")
33+
print("=" * 80)
34+
else:
35+
print("\nWarning: No branch coverage data collected")
36+
37+
except ImportError:
38+
print("\nBranch coverage module not found - skipping coverage report")
39+
except Exception as e:
40+
print(f"\nError saving coverage report: {e}")
41+
import traceback
42+
traceback.print_exc()

0 commit comments

Comments
 (0)