Skip to content

Commit 24c4537

Browse files
authored
[Issue #6][FEAT]: Add a diy branch coverage tool (#7)
* Added DIY branch coverage tool
1 parent ee7caee commit 24c4537

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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Branch Coverage Tracking Module"""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import Final
7+
8+
BRANCH_COVERAGE: dict[str, set[int]] = {"check_return_stmt": set()}
9+
10+
BRANCH_DESCRIPTIONS: Final[dict[str, dict[int, str]]] = {
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: str, branch_id: int) -> None:
50+
"""Record that a branch has been executed."""
51+
if function_name in BRANCH_COVERAGE:
52+
BRANCH_COVERAGE[function_name].add(branch_id)
53+
54+
55+
def get_coverage_report() -> str:
56+
"""Generate a coverage report as a string."""
57+
report: list[str] = []
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(
72+
f"Coverage: {covered_count}/{total_branches} branches ({covered_count / total_branches * 100:.1f}%)"
73+
)
74+
report.append("")
75+
76+
for branch_id in sorted(descriptions.keys()):
77+
status = "COVERED" if branch_id in covered_branches else "NOT COVERED"
78+
desc = descriptions[branch_id]
79+
report.append(f" Branch {branch_id:2d}: {status:15s} | {desc}")
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: str = "branch_coverage_report.txt") -> None:
94+
"""Save the coverage report to a file."""
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}")

mypy/test/conftest.py

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

0 commit comments

Comments
 (0)