Skip to content

Commit b2eb38d

Browse files
authored
Merge pull request #466 from ferdnyc/branch-coverage
2 parents deb3b4c + a496858 commit b2eb38d

File tree

5 files changed

+179
-81
lines changed

5 files changed

+179
-81
lines changed

CONTRIBUTING.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,16 @@ To run the end-to-end tests, you'll need:
6464
- Please be aware that the tests will launch `gh auth setup-git` which might be
6565
surprising if you use `https` remotes (sadly, setting `GIT_CONFIG_GLOBAL`
6666
seems not to be enough to isolate tests.)
67+
68+
## Coverage labs
69+
70+
### Computing the coverage rate
71+
72+
The coverage rate is `covered_lines / total_lines` (as one would expect).
73+
In case "branch coverage" is enabled, the coverage rate is
74+
`(covered_lines + covered_branches) / (total_lines + total_branches)`.
75+
In order to display coverage rates, we need to round the values. Depending on
76+
the situation, we either round to 0 or 2 decimal places. Rounding rules are:
77+
- We always round down (truncate) the value.
78+
- We don't display the trailing zeros in the decimal part (nor the decimal point
79+
if the decimal part is 0).

coverage_comment/coverage.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,36 @@
1313
# The dataclasses in this module are accessible in the template, which is overridable by the user.
1414
# As a coutesy, we should do our best to keep the existing fields for backward compatibility,
1515
# and if we really can't and can't add properties, at least bump the major version.
16-
@dataclasses.dataclass
16+
@dataclasses.dataclass(kw_only=True)
1717
class CoverageMetadata:
1818
version: str
1919
timestamp: datetime.datetime
2020
branch_coverage: bool
2121
show_contexts: bool
2222

2323

24-
@dataclasses.dataclass
24+
@dataclasses.dataclass(kw_only=True)
2525
class CoverageInfo:
2626
covered_lines: int
2727
num_statements: int
2828
percent_covered: decimal.Decimal
2929
missing_lines: int
3030
excluded_lines: int
31-
num_branches: int | None
32-
num_partial_branches: int | None
33-
covered_branches: int | None
34-
missing_branches: int | None
31+
num_branches: int = 0
32+
num_partial_branches: int = 0
33+
covered_branches: int = 0
34+
missing_branches: int = 0
3535

3636

37-
@dataclasses.dataclass
37+
@dataclasses.dataclass(kw_only=True)
3838
class FileCoverage:
3939
path: pathlib.Path
4040
executed_lines: list[int]
4141
missing_lines: list[int]
4242
excluded_lines: list[int]
4343
info: CoverageInfo
44+
executed_branches: list[list[int]] | None = None
45+
missing_branches: list[list[int]] | None = None
4446

4547

4648
@dataclasses.dataclass
@@ -56,7 +58,7 @@ class Coverage:
5658
# Maybe in v4, we can change it to a simpler format.
5759

5860

59-
@dataclasses.dataclass
61+
@dataclasses.dataclass(kw_only=True)
6062
class FileDiffCoverage:
6163
path: pathlib.Path
6264
percent_covered: decimal.Decimal
@@ -73,7 +75,7 @@ def violation_lines(self) -> list[int]:
7375
return self.missing_statements
7476

7577

76-
@dataclasses.dataclass
78+
@dataclasses.dataclass(kw_only=True)
7779
class DiffCoverage:
7880
total_num_lines: int
7981
total_num_violations: int
@@ -82,10 +84,18 @@ class DiffCoverage:
8284
files: dict[pathlib.Path, FileDiffCoverage]
8385

8486

85-
def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal:
86-
if num_total == 0:
87+
def compute_coverage(
88+
num_covered: int,
89+
num_total: int,
90+
num_branches_covered: int = 0,
91+
num_branches_total: int = 0,
92+
) -> decimal.Decimal:
93+
"""Compute the coverage percentage, with or without branch coverage."""
94+
numerator = decimal.Decimal(num_covered + num_branches_covered)
95+
denominator = decimal.Decimal(num_total + num_branches_total)
96+
if denominator == 0:
8797
return decimal.Decimal("1")
88-
return decimal.Decimal(num_covered) / decimal.Decimal(num_total)
98+
return numerator / denominator
8999

90100

91101
def get_coverage_info(
@@ -138,6 +148,26 @@ def generate_coverage_markdown(coverage_path: pathlib.Path) -> str:
138148
)
139149

140150

151+
def _make_coverage_info(data: dict) -> CoverageInfo:
152+
"""Build a CoverageInfo object from a "summary" or "totals" key."""
153+
return CoverageInfo(
154+
covered_lines=data["covered_lines"],
155+
num_statements=data["num_statements"],
156+
percent_covered=compute_coverage(
157+
num_covered=data["covered_lines"],
158+
num_total=data["num_statements"],
159+
num_branches_covered=data.get("covered_branches", 0),
160+
num_branches_total=data.get("num_branches", 0),
161+
),
162+
missing_lines=data["missing_lines"],
163+
excluded_lines=data["excluded_lines"],
164+
num_branches=data.get("num_branches", 0),
165+
num_partial_branches=data.get("num_partial_branches", 0),
166+
covered_branches=data.get("covered_branches", 0),
167+
missing_branches=data.get("missing_branches", 0),
168+
)
169+
170+
141171
def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
142172
"""
143173
{
@@ -191,39 +221,13 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
191221
excluded_lines=file_data["excluded_lines"],
192222
executed_lines=file_data["executed_lines"],
193223
missing_lines=file_data["missing_lines"],
194-
info=CoverageInfo(
195-
covered_lines=file_data["summary"]["covered_lines"],
196-
num_statements=file_data["summary"]["num_statements"],
197-
percent_covered=compute_coverage(
198-
file_data["summary"]["covered_lines"],
199-
file_data["summary"]["num_statements"],
200-
),
201-
missing_lines=file_data["summary"]["missing_lines"],
202-
excluded_lines=file_data["summary"]["excluded_lines"],
203-
num_branches=file_data["summary"].get("num_branches"),
204-
num_partial_branches=file_data["summary"].get(
205-
"num_partial_branches"
206-
),
207-
covered_branches=file_data["summary"].get("covered_branches"),
208-
missing_branches=file_data["summary"].get("missing_branches"),
209-
),
224+
executed_branches=file_data.get("executed_branches"),
225+
missing_branches=file_data.get("missing_branches"),
226+
info=_make_coverage_info(file_data["summary"]),
210227
)
211228
for path, file_data in data["files"].items()
212229
},
213-
info=CoverageInfo(
214-
covered_lines=data["totals"]["covered_lines"],
215-
num_statements=data["totals"]["num_statements"],
216-
percent_covered=compute_coverage(
217-
data["totals"]["covered_lines"],
218-
data["totals"]["num_statements"],
219-
),
220-
missing_lines=data["totals"]["missing_lines"],
221-
excluded_lines=data["totals"]["excluded_lines"],
222-
num_branches=data["totals"].get("num_branches"),
223-
num_partial_branches=data["totals"].get("num_partial_branches"),
224-
covered_branches=data["totals"].get("covered_branches"),
225-
missing_branches=data["totals"].get("missing_branches"),
226-
),
230+
info=_make_coverage_info(data["totals"]),
227231
)
228232

229233

@@ -256,7 +260,8 @@ def get_diff_coverage_info(
256260
total_num_violations += count_missing
257261

258262
percent_covered = compute_coverage(
259-
num_covered=count_executed, num_total=count_total
263+
num_covered=count_executed,
264+
num_total=count_total,
260265
)
261266

262267
files[path] = FileDiffCoverage(

tests/conftest.py

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
282282
percent_covered=decimal.Decimal("1.0"),
283283
missing_lines=0,
284284
excluded_lines=0,
285-
num_branches=0 if has_branches else None,
286-
num_partial_branches=0 if has_branches else None,
287-
covered_branches=0 if has_branches else None,
288-
missing_branches=0 if has_branches else None,
285+
num_branches=0,
286+
num_partial_branches=0,
287+
covered_branches=0,
288+
missing_branches=0,
289289
),
290290
files={},
291291
)
@@ -313,10 +313,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
313313
percent_covered=decimal.Decimal("1.0"),
314314
missing_lines=0,
315315
excluded_lines=0,
316-
num_branches=0 if has_branches else None,
317-
num_partial_branches=0 if has_branches else None,
318-
covered_branches=0 if has_branches else None,
319-
missing_branches=0 if has_branches else None,
316+
num_branches=0,
317+
num_partial_branches=0,
318+
covered_branches=0,
319+
missing_branches=0,
320320
),
321321
)
322322
if set(line.split()) & {
@@ -340,7 +340,6 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
340340
coverage_obj.files[current_file].excluded_lines.append(line_number)
341341
coverage_obj.files[current_file].info.excluded_lines += 1
342342
coverage_obj.info.excluded_lines += 1
343-
344343
if has_branches and "branch" in line:
345344
coverage_obj.files[current_file].info.num_branches += 1
346345
coverage_obj.info.num_branches += 1
@@ -353,21 +352,22 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
353352
elif "branch missing" in line:
354353
coverage_obj.files[current_file].info.missing_branches += 1
355354
coverage_obj.info.missing_branches += 1
356-
357355
info = coverage_obj.files[current_file].info
358356
coverage_obj.files[
359357
current_file
360358
].info.percent_covered = coverage_module.compute_coverage(
361359
num_covered=info.covered_lines,
362360
num_total=info.num_statements,
361+
num_branches_covered=info.covered_branches,
362+
num_branches_total=info.num_branches,
363363
)
364-
365364
info = coverage_obj.info
366365
coverage_obj.info.percent_covered = coverage_module.compute_coverage(
367366
num_covered=info.covered_lines,
368367
num_total=info.num_statements,
368+
num_branches_covered=info.covered_branches,
369+
num_branches_total=info.num_branches,
369370
)
370-
371371
return coverage_obj
372372

373373
return _
@@ -425,9 +425,19 @@ def coverage_code():
425425
9
426426
10 branch missing
427427
11 missing
428-
12
428+
12 covered
429429
13 branch covered
430430
14 covered
431+
15 branch partial
432+
16 branch covered
433+
17 branch missing
434+
18 covered
435+
19 covered
436+
20 branch partial
437+
21 branch missing
438+
22 branch covered
439+
23 branch covered
440+
24 branch covered
431441
"""
432442

433443

@@ -442,32 +452,48 @@ def coverage_json():
442452
},
443453
"files": {
444454
"codebase/code.py": {
445-
"executed_lines": [1, 2, 3, 5, 13, 14],
455+
"executed_lines": [
456+
1,
457+
2,
458+
3,
459+
5,
460+
12,
461+
13,
462+
14,
463+
15,
464+
16,
465+
18,
466+
19,
467+
20,
468+
22,
469+
23,
470+
24,
471+
],
446472
"summary": {
447-
"covered_lines": 6,
448-
"num_statements": 10,
449-
"percent_covered": 60.0,
450-
"missing_lines": 4,
473+
"covered_lines": 15,
474+
"num_statements": 21,
475+
"percent_covered": 0.625,
476+
"missing_lines": 6,
451477
"excluded_lines": 0,
452-
"num_branches": 3,
453-
"num_partial_branches": 1,
454-
"covered_branches": 1,
455-
"missing_branches": 1,
478+
"num_branches": 11,
479+
"num_partial_branches": 3,
480+
"covered_branches": 5,
481+
"missing_branches": 3,
456482
},
457-
"missing_lines": [6, 8, 10, 11],
483+
"missing_lines": [6, 8, 10, 11, 17, 21],
458484
"excluded_lines": [],
459485
}
460486
},
461487
"totals": {
462-
"covered_lines": 6,
463-
"num_statements": 10,
464-
"percent_covered": 60.0,
465-
"missing_lines": 4,
488+
"covered_lines": 15,
489+
"num_statements": 21,
490+
"percent_covered": 0.625,
491+
"missing_lines": 6,
466492
"excluded_lines": 0,
467-
"num_branches": 3,
468-
"num_partial_branches": 1,
469-
"covered_branches": 1,
470-
"missing_branches": 1,
493+
"num_branches": 11,
494+
"num_partial_branches": 3,
495+
"covered_branches": 5,
496+
"missing_branches": 3,
471497
},
472498
}
473499

0 commit comments

Comments
 (0)