Skip to content
Open
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
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ Note: Either `GITHUB_PR_NUMBER` or `GITHUB_REF` is required. `GITHUB_PR_NUMBER`

- `MINIMUM_GREEN`: The minimum coverage percentage for green status. Default is 100.
- `MINIMUM_ORANGE`: The minimum coverage percentage for orange status. Default is 70.
- `BRANCH_COVERAGE`: Show branch coverage in the report. Default is False.
- `ANNOTATE_MISSING_LINES`: Whether to annotate missing lines in the coverage report. Default is False.
- `ANNOTATION_TYPE`: The type of annotation to use for missing lines. 'notice' or 'warning' or 'error'. Default is 'warning'.
- `MAX_FILES_IN_COMMENT`: The maximum number of files to include in the coverage report comment. Default is 25.
Expand All @@ -52,12 +51,9 @@ Note: Either `GITHUB_PR_NUMBER` or `GITHUB_REF` is required. `GITHUB_PR_NUMBER`

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
2. 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.
project coverage, this may be expected.

## Dev Setup

Expand Down
6 changes: 0 additions & 6 deletions codecov/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ class Config:
MINIMUM_GREEN: decimal.Decimal = decimal.Decimal('100')
MINIMUM_ORANGE: decimal.Decimal = decimal.Decimal('70')
TEST_FRAMEWORK: TestFramework = TestFramework.PYTEST
# TODO: Remove branch coverage and just use the report
BRANCH_COVERAGE: bool = False
ANNOTATE_MISSING_LINES: bool = False
ANNOTATION_TYPE: AnnotationType = AnnotationType.WARNING
MAX_FILES_IN_COMMENT: int = 25
Expand All @@ -78,10 +76,6 @@ def clean_minimum_orange(cls, value: str) -> decimal.Decimal:
def clean_annotate_missing_lines(cls, value: str) -> bool:
return str_to_bool(value)

@classmethod
def clean_branch_coverage(cls, value: str) -> bool:
return str_to_bool(value)

@classmethod
def clean_complete_project_report(cls, value: str) -> bool:
return str_to_bool(value)
Expand Down
131 changes: 62 additions & 69 deletions codecov/coverage/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,45 @@
import decimal
import pathlib

from codecov import diff_grouper
from codecov.config import Config, TestFramework
from codecov.coverage.base import BaseCoverage, BaseCoverageHandler, DiffCoverage, FileDiffCoverage


def _branch_missing_lines(missing_branches: list[list[int]] | None) -> list[int]:
if not missing_branches:
return []

lines: list[int] = []
for branch in missing_branches:
from_line = abs(branch[0])
if from_line > 0:
lines.append(from_line)
to_line = abs(branch[1])
if to_line > 0 and to_line != from_line:
lines.append(to_line)
return lines


def _incorporate_branch_missing(
covered_lines: list[int],
missing_lines: list[int],
missing_branches: list[list[int]] | None,
) -> tuple[list[int], list[int]]:
branch_lines = _branch_missing_lines(missing_branches)
if not branch_lines:
return covered_lines, missing_lines

missing = sorted(set(missing_lines) | set(branch_lines))
covered = sorted(set(covered_lines) - set(branch_lines))
return covered, missing


@dataclasses.dataclass
class PytestCoverageInfo: # pylint: disable=too-many-instance-attributes
covered_lines: int
num_statements: int
missing_lines: int
excluded_lines: int
num_branches: int | None
num_partial_branches: int | None # TODO: Removed this
covered_branches: int | None
missing_branches: int | None
percent_covered: decimal.Decimal
percent_covered_display: str

Expand All @@ -29,15 +53,12 @@ class PytestFileCoverage:
missing_lines: list[int]
excluded_lines: list[int]
info: PytestCoverageInfo
executed_branches: list[list[int]] | None
missing_branches: list[list[int]] | None


@dataclasses.dataclass
class PytestCoverageMetadata:
version: str
timestamp: datetime.datetime
branch_coverage: bool
show_contexts: bool


Expand All @@ -55,11 +76,9 @@ 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)
numerator = decimal.Decimal(num_covered)
denominator = decimal.Decimal(num_total)
if denominator == 0:
return decimal.Decimal('1')
return numerator / denominator
Expand All @@ -68,7 +87,6 @@ def extract_meta(self, data: dict) -> PytestCoverageMetadata:
return PytestCoverageMetadata(
version=data['meta']['version'],
timestamp=datetime.datetime.fromisoformat(data['meta']['timestamp']),
branch_coverage=data['meta']['branch_coverage'],
show_contexts=data['meta']['show_contexts'],
)

Expand All @@ -80,21 +98,25 @@ def extract_coverage_info(self, data: dict) -> PytestCoverageInfo:
percent_covered_display=data['percent_covered_display'],
missing_lines=data['missing_lines'],
excluded_lines=data['excluded_lines'],
num_branches=data.get('num_branches'),
num_partial_branches=data.get('num_partial_branches'),
covered_branches=data.get('covered_branches'),
missing_branches=data.get('missing_branches'),
)

def extract_file_coverage(self, path: str, file_data: dict) -> PytestFileCoverage:
covered_lines, missing_lines = _incorporate_branch_missing(
covered_lines=file_data['executed_lines'],
missing_lines=file_data['missing_lines'],
missing_branches=file_data.get('missing_branches'),
)
info = self.extract_coverage_info(file_data['summary'])
return PytestFileCoverage(
path=pathlib.Path(path),
excluded_lines=file_data['excluded_lines'],
missing_lines=file_data['missing_lines'],
covered_lines=file_data['executed_lines'],
executed_branches=file_data.get('executed_branches'),
missing_branches=file_data.get('missing_branches'),
info=self.extract_coverage_info(file_data['summary']),
missing_lines=missing_lines,
covered_lines=covered_lines,
info=dataclasses.replace(
info,
covered_lines=len(covered_lines),
missing_lines=len(missing_lines),
),
)

def extract_info(self, data: dict) -> PytestCoverage:
Expand All @@ -116,14 +138,9 @@ def extract_info(self, data: dict) -> PytestCoverage:
"percent_covered_display": "88",
"missing_lines": 4,
"excluded_lines": 0,
"num_branches": 22,
"num_partial_branches": 4,
"covered_branches": 18,
"missing_branches": 4
},
"missing_lines": [7],
"excluded_lines": [],
"executed_branches": [],
"missing_branches": [],
}
},
Expand All @@ -134,20 +151,23 @@ def extract_info(self, data: dict) -> PytestCoverage:
"percent_covered_display": "75",
"missing_lines": 1,
"excluded_lines": 0,
"num_branches": 2,
"num_partial_branches": 1,
"covered_branches": 1,
"missing_branches": 1,
},
}
"""
files = {
pathlib.Path(path): self.extract_file_coverage(path, file_data) for path, file_data in data['files'].items()
}
total_covered_lines = sum(file.info.covered_lines for file in files.values())
total_missing_lines = sum(file.info.missing_lines for file in files.values())
info = self.extract_coverage_info(data['totals'])
return PytestCoverage(
meta=self.extract_meta(data),
files={
pathlib.Path(path): self.extract_file_coverage(path, file_data)
for path, file_data in data['files'].items()
},
info=self.extract_coverage_info(data['totals']),
files=files,
info=dataclasses.replace(
info,
covered_lines=total_covered_lines,
missing_lines=total_missing_lines,
),
)

def get_diff_coverage( # pylint: disable=too-many-locals
Expand All @@ -159,8 +179,6 @@ def get_diff_coverage( # pylint: disable=too-many-locals
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 @@ -184,17 +202,7 @@ def get_diff_coverage( # pylint: disable=too-many-locals
total_num_lines += count_total
total_num_violations += count_missing

if config.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)
percent_covered = self.compute_coverage(num_covered=count_executed, num_total=count_total)

files[path] = FileDiffCoverage(
path=path,
Expand All @@ -204,18 +212,11 @@ def get_diff_coverage( # pylint: disable=too-many-locals
added_statements=sorted(added),
added_lines=added_lines_for_file,
)
if config.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,
)

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 All @@ -224,11 +225,3 @@ def get_diff_coverage( # pylint: disable=too-many-locals
num_changed_lines=num_changed_lines,
files=files,
)

def get_coverage(self, config: Config) -> PytestCoverage:
coverage = super().get_coverage(config=config)

if config.BRANCH_COVERAGE:
coverage = diff_grouper.fill_branch_missing_groups(coverage=coverage)

return coverage
48 changes: 0 additions & 48 deletions codecov/diff_grouper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,6 @@
MAX_ANNOTATION_GAP = 3


def _flatten_branches(branches: list[list[int]] | None) -> list[int]:
flattened_branches: list[int] = []
if not branches:
return flattened_branches

for branch in branches:
start, end = abs(branch[0]), abs(branch[1])
if start == end:
flattened_branches.append(start)
else:
flattened_branches.extend(range(min(start, end), max(start, end) + 1))
return flattened_branches


def get_missing_groups(
coverage: 'PytestCoverage | JestCoverage',
) -> Iterable[groups.Group]:
Expand Down Expand Up @@ -77,37 +63,3 @@ def get_diff_missing_groups(
line_start=start,
line_end=end,
)


def fill_branch_missing_groups(coverage: 'PytestCoverage') -> 'PytestCoverage':
for file_coverage in coverage.files.values():
separators = {
*_flatten_branches(file_coverage.executed_branches),
*file_coverage.excluded_lines,
}
joiners = set(range(1, file_coverage.info.num_statements)) - separators

file_coverage.missing_branches = [
[start, end]
for start, end in groups.compute_contiguous_groups(
values=_flatten_branches(branches=file_coverage.missing_branches),
separators=separators,
joiners=joiners,
max_gap=MAX_ANNOTATION_GAP,
)
]
return coverage


def get_diff_branch_missing_groups(
coverage: 'PytestCoverage',
diff_coverage: 'DiffCoverage',
) -> Iterable[groups.Group]:
for path, _ in diff_coverage.files.items():
coverage_file = coverage.files[path]
for start, end in coverage_file.missing_branches or []:
yield groups.Group(
file=path,
line_start=start,
line_end=end,
)
8 changes: 3 additions & 5 deletions codecov/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,26 @@ def to_dict(self):
def create_missing_coverage_annotations(
annotation_type: str,
annotations: Iterable[Group],
branch: bool = False,
) -> list[Annotation]:
"""
Create annotations for lines with missing coverage.

annotation_type: The type of annotation to create. Can be either "error" or "warning" or "notice".
annotations: A list of tuples of the form (file, line_start, line_end)
branch: Whether to create branch coverage annotations or not
"""
formatted_annotations: list[Annotation] = []
for group in annotations:
if group.line_start == group.line_end:
message = f'Missing {"branch " if branch else ""}coverage on line {group.line_start}'
message = f'Missing coverage on line {group.line_start}'
else:
message = f'Missing {"branch " if branch else ""}coverage on lines {group.line_start}-{group.line_end}'
message = f'Missing coverage on lines {group.line_start}-{group.line_end}'

formatted_annotations.append(
Annotation(
file=group.file,
line_start=group.line_start,
line_end=group.line_end,
title=f'Missing {"branch " if branch else ""}coverage',
title='Missing coverage',
message_type=annotation_type,
message=message,
)
Expand Down
Loading
Loading