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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ Note: Either `GITHUB_PR_NUMBER` or `GITHUB_REF` is required. `GITHUB_PR_NUMBER`
- `COVERAGE_REPORT_URL`: URL of the full coverage report to mention in the comment.
- `DEBUG`: Whether to enable debug mode. Default is False.

## Notes

1. The coverage report displays only files that have missing coverage. If all files are fully covered, the
report will be empty.
2. When branch coverage is enabled, the coverage percentage is calculated based on the uncovered branches in
the affected files.
3. If the complete project report option is enabled, the report is included as-is in the comment, without any
modifications or recalculations. If you notice discrepancies between the PR coverage and the complete
project coverage, this may be expected. For consistent results, it is recommended to enable branch
coverage when your report includes it.

## Setting up Local Environment using Pipenv

To get started, follow these steps:
Expand Down
51 changes: 43 additions & 8 deletions codecov/coverage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,24 @@ class DiffCoverage:


class BaseCoverage(ABC):
def compute_coverage(self, num_covered: int, num_total: int) -> decimal.Decimal:
if num_total == 0:
def convert_to_decimal(self, value: float, precision: int = 2) -> decimal.Decimal:
return decimal.Decimal(str(float(value) / 100)).quantize(
exp=decimal.Decimal(10) ** -precision,
rounding=decimal.ROUND_DOWN,
)

def compute_coverage(
self,
num_covered: int,
num_total: int,
num_branches_covered: int = 0,
num_branches_total: int = 0,
) -> decimal.Decimal:
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(self, coverage_path: pathlib.Path) -> Coverage:
try:
Expand All @@ -99,10 +113,13 @@ def get_diff_coverage_info( # pylint: disable=too-many-locals
self,
added_lines: dict[pathlib.Path, list[int]],
coverage: Coverage,
branch_coverage: bool = False,
) -> DiffCoverage:
files = {}
total_num_lines = 0
total_num_violations = 0
total_num_branches_covered = 0
total_num_branches = 0
num_changed_lines = 0

for path, added_lines_for_file in added_lines.items():
Expand All @@ -126,7 +143,17 @@ def get_diff_coverage_info( # pylint: disable=too-many-locals
total_num_lines += count_total
total_num_violations += count_missing

percent_covered = self.compute_coverage(num_covered=count_executed, num_total=count_total)
if branch_coverage:
total_num_branches_covered += file.info.covered_branches or 0
total_num_branches += file.info.num_branches or 0
percent_covered = self.compute_coverage(
num_covered=count_executed,
num_total=count_total,
num_branches_covered=file.info.covered_branches or 0,
num_branches_total=file.info.num_branches or 0,
)
else:
percent_covered = self.compute_coverage(num_covered=count_executed, num_total=count_total)

files[path] = FileDiffCoverage(
path=path,
Expand All @@ -136,10 +163,18 @@ def get_diff_coverage_info( # pylint: disable=too-many-locals
added_statements=sorted(added),
added_lines=added_lines_for_file,
)
final_percentage = self.compute_coverage(
num_covered=total_num_lines - total_num_violations,
num_total=total_num_lines,
)
if branch_coverage:
final_percentage = self.compute_coverage(
num_covered=total_num_lines - total_num_violations,
num_total=total_num_lines,
num_branches_covered=total_num_branches_covered,
num_branches_total=total_num_branches,
)
else:
final_percentage = self.compute_coverage(
num_covered=total_num_lines - total_num_violations,
num_total=total_num_lines,
)

return DiffCoverage(
total_num_lines=total_num_lines,
Expand Down
8 changes: 2 additions & 6 deletions codecov/coverage/pytest.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from __future__ import annotations

import datetime
import decimal
import pathlib

from codecov.coverage.base import BaseCoverage, Coverage, CoverageInfo, CoverageMetadata, FileCoverage


class PytestCoverage(BaseCoverage):
def _convert_to_decimal(self, value: float, precision: int = 2) -> decimal.Decimal:
return decimal.Decimal(str(float(value) / 100)).quantize(decimal.Decimal(10) ** -precision)

def extract_info(self, data: dict) -> Coverage:
"""
{
Expand Down Expand Up @@ -73,7 +69,7 @@ def extract_info(self, data: dict) -> Coverage:
info=CoverageInfo(
covered_lines=file_data['summary']['covered_lines'],
num_statements=file_data['summary']['num_statements'],
percent_covered=self._convert_to_decimal(file_data['summary']['percent_covered']),
percent_covered=self.convert_to_decimal(file_data['summary']['percent_covered']),
percent_covered_display=file_data['summary']['percent_covered_display'],
missing_lines=file_data['summary']['missing_lines'],
excluded_lines=file_data['summary']['excluded_lines'],
Expand All @@ -88,7 +84,7 @@ def extract_info(self, data: dict) -> Coverage:
info=CoverageInfo(
covered_lines=data['totals']['covered_lines'],
num_statements=data['totals']['num_statements'],
percent_covered=self._convert_to_decimal(data['totals']['percent_covered']),
percent_covered=self.convert_to_decimal(data['totals']['percent_covered']),
percent_covered_display=data['totals']['percent_covered_display'],
missing_lines=data['totals']['missing_lines'],
excluded_lines=data['totals']['excluded_lines'],
Expand Down
34 changes: 19 additions & 15 deletions codecov/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ def _process_coverage(self):
if self.config.BRANCH_COVERAGE:
coverage = diff_grouper.fill_branch_missing_groups(coverage=coverage)
added_lines = GithubDiffParser(diff=self.github.pr_diff).parse()
diff_coverage = self.coverage_module.get_diff_coverage_info(added_lines=added_lines, coverage=coverage)
diff_coverage = self.coverage_module.get_diff_coverage_info(
added_lines=added_lines,
coverage=coverage,
branch_coverage=self.config.BRANCH_COVERAGE,
)
self.coverage = coverage
self.diff_coverage = diff_coverage

Expand All @@ -86,24 +90,24 @@ def _process_pr(self):
)
try:
comment = template.get_comment_markdown(
coverage=self.coverage,
diff_coverage=self.diff_coverage,
files=files_info,
count_files=count_files,
coverage_files=coverage_files_info,
count_coverage_files=count_coverage_files,
max_files=self.config.MAX_FILES_IN_COMMENT,
minimum_green=self.config.MINIMUM_GREEN,
minimum_orange=self.config.MINIMUM_ORANGE,
repo_name=self.config.GITHUB_REPOSITORY,
pr_number=self.github.pr_number,
base_ref=self.github.base_ref,
base_template=template.read_template_file('comment.md.j2'),
marker=marker,
template.read_template_file('comment.md.j2'),
self.coverage,
self.diff_coverage,
self.config.MINIMUM_GREEN,
self.config.MINIMUM_ORANGE,
self.config.GITHUB_REPOSITORY,
self.github.pr_number,
self.github.base_ref,
marker,
subproject_id=self.config.SUBPROJECT_ID,
branch_coverage=self.config.BRANCH_COVERAGE,
complete_project_report=self.config.COMPLETE_PROJECT_REPORT,
coverage_report_url=self.config.COVERAGE_REPORT_URL,
max_files=self.config.MAX_FILES_IN_COMMENT,
files=files_info,
count_files=count_files,
coverage_files=coverage_files_info,
count_coverage_files=count_coverage_files,
)
except MissingMarker as e:
log.error(
Expand Down
24 changes: 5 additions & 19 deletions codecov/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import itertools
import pathlib
from importlib import resources
from typing import Any

import jinja2
from jinja2.sandbox import SandboxedEnvironment
Expand Down Expand Up @@ -61,24 +62,17 @@ class FileInfo:


def get_comment_markdown( # pylint: disable=too-many-arguments,too-many-locals,too-many-positional-arguments
base_template: str,
coverage: Coverage,
diff_coverage: DiffCoverage,
files: list[FileInfo],
count_files: int,
coverage_files: list[FileInfo],
count_coverage_files: int,
max_files: int | None,
minimum_green: decimal.Decimal,
minimum_orange: decimal.Decimal,
repo_name: str,
pr_number: int,
base_ref: str,
base_template: str,
marker: str,
subproject_id: str | None = None,
branch_coverage: bool = False,
complete_project_report: bool = False,
coverage_report_url: str | None = None,
/,
**kwargs: Any,
):
env = SandboxedEnvironment(loader=jinja2.FileSystemLoader('codecov/template_files/'))
env.filters['pct'] = pct
Expand Down Expand Up @@ -116,18 +110,10 @@ def get_comment_markdown( # pylint: disable=too-many-arguments,too-many-locals,
comment = env.from_string(base_template).render(
coverage=coverage,
diff_coverage=diff_coverage,
max_files=max_files,
files=files,
count_files=count_files,
coverage_files=coverage_files,
count_coverage_files=count_coverage_files,
missing_diff_lines=missing_diff_lines,
missing_lines_for_whole_project=missing_lines_for_whole_project,
subproject_id=subproject_id,
marker=marker,
branch_coverage=branch_coverage,
complete_project_report=complete_project_report,
coverage_report_url=coverage_report_url,
**kwargs,
)
except jinja2.exceptions.TemplateError as exc:
log.error('Template rendering error: %s', str(exc))
Expand Down
89 changes: 89 additions & 0 deletions tests/coverage/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,92 @@ def test_get_diff_coverage_info(self, make_coverage_obj, added_lines, update_obj
coverage=make_coverage_obj(**update_obj),
)
assert result == expected

@pytest.mark.parametrize(
'added_lines, update_obj, expected',
[
# A similar example to the previous one, but with branch coverage enabled.
# The statements are covered, but the branches are not.
(
{
pathlib.Path('codebase/code.py'): [4, 5, 6],
pathlib.Path('codebase/other.py'): [10, 13],
},
{},
DiffCoverage(
total_num_lines=1,
total_num_violations=1,
total_percent_covered=decimal.Decimal('0.25'),
num_changed_lines=5,
# Percent is due to the fact that the branches are not covered.
files={
pathlib.Path('codebase/code.py'): FileDiffCoverage(
path=pathlib.Path('codebase/code.py'),
percent_covered=decimal.Decimal('0.25'),
added_statements=[6],
covered_statements=[],
missing_statements=[6],
added_lines=[4, 5, 6],
),
pathlib.Path('codebase/other.py'): FileDiffCoverage(
path=pathlib.Path('codebase/other.py'),
percent_covered=decimal.Decimal('0.25'),
added_statements=[],
covered_statements=[],
missing_statements=[],
added_lines=[10, 13],
),
},
),
),
# Files with missing coverage and branches
(
{
pathlib.Path('codebase/code.py'): [2, 3, 4, 5, 6],
pathlib.Path('codebase/other.py'): [10, 11, 12, 13],
},
{
'codebase/code.py': {
'executed_lines': [1, 2, 3, 5, 6],
'missing_lines': [4, 5],
},
'codebase/other.py': {
'executed_lines': [10, 11, 12, 13],
'missing_lines': [10, 13],
},
},
DiffCoverage(
total_num_lines=9,
total_num_violations=4,
total_percent_covered=decimal.Decimal('0.4375'),
num_changed_lines=9,
# Percent is due to the fact that the branches are not covered.
files={
pathlib.Path('codebase/code.py'): FileDiffCoverage(
path=pathlib.Path('codebase/code.py'),
percent_covered=decimal.Decimal('0.625'),
added_statements=[2, 3, 4, 5, 6],
covered_statements=[2, 3, 5, 6],
missing_statements=[4, 5],
added_lines=[2, 3, 4, 5, 6],
),
pathlib.Path('codebase/other.py'): FileDiffCoverage(
path=pathlib.Path('codebase/other.py'),
percent_covered=decimal.Decimal('0.625'),
added_statements=[10, 11, 12, 13],
covered_statements=[10, 11, 12, 13],
missing_statements=[10, 13],
added_lines=[10, 11, 12, 13],
),
},
),
),
],
)
def test_get_diff_coverage_info_branch_coverage(self, make_coverage_obj, added_lines, update_obj, expected):
result = BaseCoverageDemo().get_diff_coverage_info(
added_lines=added_lines,
coverage=make_coverage_obj(**update_obj),
branch_coverage=True,
)
assert result == expected
4 changes: 2 additions & 2 deletions tests/coverage/test_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_extract_info(self, coverage_json):
info=CoverageInfo(
covered_lines=6,
num_statements=10,
percent_covered=PytestCoverage()._convert_to_decimal(60.0),
percent_covered=PytestCoverage().convert_to_decimal(60.0),
percent_covered_display='60%',
missing_lines=4,
excluded_lines=0,
Expand All @@ -40,7 +40,7 @@ def test_extract_info(self, coverage_json):
info=CoverageInfo(
covered_lines=6,
num_statements=10,
percent_covered=PytestCoverage()._convert_to_decimal(60.0),
percent_covered=PytestCoverage().convert_to_decimal(60.0),
percent_covered_display='60%',
missing_lines=4,
excluded_lines=0,
Expand Down
Loading