Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ To run the end-to-end tests, you'll need:
- Please be aware that the tests will launch `gh auth setup-git` which might be
surprising if you use `https` remotes (sadly, setting `GIT_CONFIG_GLOBAL`
seems not to be enough to isolate tests.)

## Coverage labs

### Computing the coverage rate

The coverage rate is `covered_lines / total_lines` (as one would expect).
In case "branch coverage" is enabled, the coverage rate is
`(covered_lines + covered_branches) / (total_lines + total_branches)`.
In order to display coverage rates, we need to round the values. Depending on
the situation, we either round to 0 or 2 decimal places. Rounding rules are:
- We always round down (truncate) the value.
- We don't display the trailing zeros in the decimal part (nor the decimal point
if the decimal part is 0).
91 changes: 48 additions & 43 deletions coverage_comment/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,36 @@
# The dataclasses in this module are accessible in the template, which is overridable by the user.
# As a coutesy, we should do our best to keep the existing fields for backward compatibility,
# and if we really can't and can't add properties, at least bump the major version.
@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class CoverageMetadata:
version: str
timestamp: datetime.datetime
branch_coverage: bool
show_contexts: bool


@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class CoverageInfo:
covered_lines: int
num_statements: int
percent_covered: decimal.Decimal
missing_lines: int
excluded_lines: int
num_branches: int | None
num_partial_branches: int | None
covered_branches: int | None
missing_branches: int | None
num_branches: int = 0
num_partial_branches: int = 0
covered_branches: int = 0
missing_branches: int = 0


@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class FileCoverage:
path: pathlib.Path
executed_lines: list[int]
missing_lines: list[int]
excluded_lines: list[int]
info: CoverageInfo
executed_branches: list[list[int]] | None = None
missing_branches: list[list[int]] | None = None
Comment thread
ferdnyc marked this conversation as resolved.


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


@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class FileDiffCoverage:
path: pathlib.Path
percent_covered: decimal.Decimal
Expand All @@ -73,7 +75,7 @@ def violation_lines(self) -> list[int]:
return self.missing_statements


@dataclasses.dataclass
@dataclasses.dataclass(kw_only=True)
class DiffCoverage:
total_num_lines: int
total_num_violations: int
Expand All @@ -82,10 +84,18 @@ class DiffCoverage:
files: dict[pathlib.Path, FileDiffCoverage]


def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal:
if num_total == 0:
def compute_coverage(
num_covered: int,
num_total: int,
num_branches_covered: int = 0,
num_branches_total: int = 0,
) -> decimal.Decimal:
"""Compute the coverage percentage, with or without branch coverage."""
numerator = decimal.Decimal(num_covered + num_branches_covered)
denominator = decimal.Decimal(num_total + num_branches_total)
if denominator == 0:
return decimal.Decimal("1")
return decimal.Decimal(num_covered) / decimal.Decimal(num_total)
return numerator / denominator


def get_coverage_info(
Expand Down Expand Up @@ -138,6 +148,26 @@ def generate_coverage_markdown(coverage_path: pathlib.Path) -> str:
)


def _make_coverage_info(data: dict) -> CoverageInfo:
"""Build a CoverageInfo object from a "summary" or "totals" key."""
return CoverageInfo(
covered_lines=data["covered_lines"],
num_statements=data["num_statements"],
percent_covered=compute_coverage(
num_covered=data["covered_lines"],
num_total=data["num_statements"],
num_branches_covered=data.get("covered_branches", 0),
num_branches_total=data.get("num_branches", 0),
),
missing_lines=data["missing_lines"],
excluded_lines=data["excluded_lines"],
num_branches=data.get("num_branches", 0),
num_partial_branches=data.get("num_partial_branches", 0),
covered_branches=data.get("covered_branches", 0),
missing_branches=data.get("missing_branches", 0),
)


def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
"""
{
Expand Down Expand Up @@ -191,39 +221,13 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
excluded_lines=file_data["excluded_lines"],
executed_lines=file_data["executed_lines"],
missing_lines=file_data["missing_lines"],
info=CoverageInfo(
covered_lines=file_data["summary"]["covered_lines"],
num_statements=file_data["summary"]["num_statements"],
percent_covered=compute_coverage(
file_data["summary"]["covered_lines"],
file_data["summary"]["num_statements"],
),
missing_lines=file_data["summary"]["missing_lines"],
excluded_lines=file_data["summary"]["excluded_lines"],
num_branches=file_data["summary"].get("num_branches"),
num_partial_branches=file_data["summary"].get(
"num_partial_branches"
),
covered_branches=file_data["summary"].get("covered_branches"),
missing_branches=file_data["summary"].get("missing_branches"),
),
executed_branches=file_data.get("executed_branches"),
missing_branches=file_data.get("missing_branches"),
info=_make_coverage_info(file_data["summary"]),
)
for path, file_data in data["files"].items()
},
info=CoverageInfo(
covered_lines=data["totals"]["covered_lines"],
num_statements=data["totals"]["num_statements"],
percent_covered=compute_coverage(
data["totals"]["covered_lines"],
data["totals"]["num_statements"],
),
missing_lines=data["totals"]["missing_lines"],
excluded_lines=data["totals"]["excluded_lines"],
num_branches=data["totals"].get("num_branches"),
num_partial_branches=data["totals"].get("num_partial_branches"),
covered_branches=data["totals"].get("covered_branches"),
missing_branches=data["totals"].get("missing_branches"),
),
info=_make_coverage_info(data["totals"]),
)


Expand Down Expand Up @@ -256,7 +260,8 @@ def get_diff_coverage_info(
total_num_violations += count_missing

percent_covered = compute_coverage(
num_covered=count_executed, num_total=count_total
num_covered=count_executed,
num_total=count_total,
)

files[path] = FileDiffCoverage(
Expand Down
88 changes: 57 additions & 31 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
percent_covered=decimal.Decimal("1.0"),
missing_lines=0,
excluded_lines=0,
num_branches=0 if has_branches else None,
num_partial_branches=0 if has_branches else None,
covered_branches=0 if has_branches else None,
missing_branches=0 if has_branches else None,
num_branches=0,
num_partial_branches=0,
covered_branches=0,
missing_branches=0,
Comment thread
ferdnyc marked this conversation as resolved.
),
files={},
)
Expand Down Expand Up @@ -313,10 +313,10 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
percent_covered=decimal.Decimal("1.0"),
missing_lines=0,
excluded_lines=0,
num_branches=0 if has_branches else None,
num_partial_branches=0 if has_branches else None,
covered_branches=0 if has_branches else None,
missing_branches=0 if has_branches else None,
num_branches=0,
num_partial_branches=0,
covered_branches=0,
missing_branches=0,
),
)
if set(line.split()) & {
Expand All @@ -340,7 +340,6 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
coverage_obj.files[current_file].excluded_lines.append(line_number)
coverage_obj.files[current_file].info.excluded_lines += 1
coverage_obj.info.excluded_lines += 1

if has_branches and "branch" in line:
coverage_obj.files[current_file].info.num_branches += 1
coverage_obj.info.num_branches += 1
Expand All @@ -353,21 +352,22 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
elif "branch missing" in line:
coverage_obj.files[current_file].info.missing_branches += 1
coverage_obj.info.missing_branches += 1

info = coverage_obj.files[current_file].info
coverage_obj.files[
current_file
].info.percent_covered = coverage_module.compute_coverage(
num_covered=info.covered_lines,
num_total=info.num_statements,
num_branches_covered=info.covered_branches,
num_branches_total=info.num_branches,
)

info = coverage_obj.info
coverage_obj.info.percent_covered = coverage_module.compute_coverage(
num_covered=info.covered_lines,
num_total=info.num_statements,
num_branches_covered=info.covered_branches,
num_branches_total=info.num_branches,
)

return coverage_obj

return _
Expand Down Expand Up @@ -425,9 +425,19 @@ def coverage_code():
9
10 branch missing
11 missing
12
12 covered
13 branch covered
14 covered
15 branch partial
16 branch covered
17 branch missing
18 covered
19 covered
20 branch partial
21 branch missing
22 branch covered
23 branch covered
24 branch covered
"""


Expand All @@ -442,32 +452,48 @@ def coverage_json():
},
"files": {
"codebase/code.py": {
"executed_lines": [1, 2, 3, 5, 13, 14],
"executed_lines": [
1,
2,
3,
5,
12,
13,
14,
15,
16,
18,
19,
20,
22,
23,
24,
],
"summary": {
"covered_lines": 6,
"num_statements": 10,
"percent_covered": 60.0,
"missing_lines": 4,
"covered_lines": 15,
"num_statements": 21,
"percent_covered": 0.625,
"missing_lines": 6,
"excluded_lines": 0,
"num_branches": 3,
"num_partial_branches": 1,
"covered_branches": 1,
"missing_branches": 1,
"num_branches": 11,
"num_partial_branches": 3,
"covered_branches": 5,
"missing_branches": 3,
},
"missing_lines": [6, 8, 10, 11],
"missing_lines": [6, 8, 10, 11, 17, 21],
"excluded_lines": [],
}
},
"totals": {
"covered_lines": 6,
"num_statements": 10,
"percent_covered": 60.0,
"missing_lines": 4,
"covered_lines": 15,
"num_statements": 21,
"percent_covered": 0.625,
"missing_lines": 6,
"excluded_lines": 0,
"num_branches": 3,
"num_partial_branches": 1,
"covered_branches": 1,
"missing_branches": 1,
"num_branches": 11,
"num_partial_branches": 3,
"covered_branches": 5,
"missing_branches": 3,
},
}

Expand Down
Loading