Feature/annotate missing coverage lines by ideepu · Pull Request #6 · ideepu/python-coverage-comment · GitHub
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
2 changes: 2 additions & 0 deletions README.md
103 changes: 57 additions & 46 deletions codecov/github.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
import dataclasses
import json
import pathlib
import sys
from collections.abc import Iterable

from codecov import github_client, log, settings
from codecov import github_client, groups, log, settings

GITHUB_ACTIONS_LOGIN = 'CI-codecov[bot]'

Expand All @@ -24,6 +25,39 @@ class NoArtifact(Exception):
pass


@dataclasses.dataclass
class Annotation:
file: pathlib.Path
line_start: int
line_end: int
title: str
message_type: str
message: str

def __str__(self) -> str:
return f'{self.message_type} {self.message} in {self.file}:{self.line_start}-{self.line_end}'

def __repr__(self) -> str:
return f'{self.message_type} {self.message} in {self.file}:{self.line_start}-{self.line_end}'

def to_dict(self):
return {
'file': str(self.file),
'line_start': self.line_start,
'line_end': self.line_end,
'title': self.title,
'message_type': self.message_type,
'message': self.message,
}


class AnnotationEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Annotation):
return o.to_dict()
return super().default(o)


@dataclasses.dataclass
class RepositoryInfo:
default_branch: str
Expand Down Expand Up @@ -134,54 +168,31 @@ def post_comment( # pylint: disable=too-many-arguments
raise CannotPostComment from exc


def escape_property(s: str) -> str:
return s.replace('%', '%25').replace('\r', '%0D').replace('\n', '%0A').replace(':', '%3A').replace(',', '%2C')


def escape_data(s: str) -> str:
return s.replace('%', '%25').replace('\r', '%0D').replace('\n', '%0A')


def get_workflow_command(command: str, command_value: str, **kwargs: str) -> str:
"""
Returns a string that can be printed to send a workflow command
https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
"""
values_listed = [f'{key}={escape_property(value)}' for key, value in kwargs.items()]

context = f" {','.join(values_listed)}" if values_listed else ''
return f'::{command}{context}::{escape_data(command_value)}'


def send_workflow_command(command: str, command_value: str, **kwargs: str) -> None:
print(
get_workflow_command(command=command, command_value=command_value, **kwargs),
file=sys.stderr,
)


def create_missing_coverage_annotations(annotation_type: str, annotations: list[tuple[pathlib.Path, int, int]]):
def create_missing_coverage_annotations(
annotation_type: str,
annotations: Iterable[groups.Group],
) -> list[Annotation]:
"""
Create annotations for lines with missing coverage.

annotation_type: The type of annotation to create. Can be either "error" or "warning".
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)
"""
send_workflow_command(command='group', command_value='Annotations of lines with missing coverage')
for file, line_start, line_end in annotations:
if line_start == line_end:
message = f'Missing coverage on line {line_start}'
formatted_annotations: list[Annotation] = []
for group in annotations:
if group.line_start == group.line_end:
message = f'Missing coverage on line {group.line_start}'
else:
message = f'Missing coverage on lines {line_start}-{line_end}'

send_workflow_command(
command=annotation_type,
command_value=message,
# This will produce \ paths when running on windows.
# GHA doc is unclear whether this is right or not.
file=str(file),
line=str(line_start),
endLine=str(line_end),
title='Missing coverage',
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='Missing coverage',
message_type=annotation_type,
message=message,
)
)
send_workflow_command(command='endgroup', command_value='')
return formatted_annotations
8 changes: 8 additions & 0 deletions codecov/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@

def __getattr__(name):
return getattr(logger, name)


def setup(debug: bool = False):
logging.basicConfig(
level='DEBUG' if debug else 'INFO',
format='%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(module)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
27 changes: 0 additions & 27 deletions codecov/log_utils.py

This file was deleted.

46 changes: 28 additions & 18 deletions codecov/main.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
# -*- coding: utf-8 -*-
import logging
import json
import os
import sys

import httpx

from codecov import coverage as coverage_module, diff_grouper, github, github_client, log, log_utils, settings, template
from codecov import coverage as coverage_module, diff_grouper, github, github_client, log, settings, template


def main():
try:
config = settings.Config.from_environ(environ=os.environ)
log.setup(debug=config.DEBUG)

logging.basicConfig(level='DEBUG' if config.DEBUG else 'INFO')
logging.getLogger().handlers[0].formatter = (
log_utils.ConsoleFormatter() if config.DEBUG else log_utils.GitHubFormatter()
)
if config.SKIP_COVERAGE and not config.ANNOTATE_MISSING_LINES:
log.info('Nothing to do since both SKIP_COVERAGE and ANNOTATE_MISSING_LINES are set to False. Exiting.')
sys.exit(0)

log.info('Starting action')
log.info('Starting...')
github_session = httpx.Client(
base_url=github_client.BASE_URL,
follow_redirects=True,
headers={'Authorization': f'token {config.GITHUB_TOKEN}'},
)

exit_code = action(config=config, github_session=github_session)
log.info('Ending action')
log.info('Ending...')
sys.exit(exit_code)

except Exception: # pylint: disable=broad-except
Expand Down Expand Up @@ -66,14 +66,32 @@ def process_pr( # pylint: disable=too-many-locals
repo_info: github.RepositoryInfo,
pr_number: int,
) -> int:
log.info('Generating comment for PR')
_, coverage = coverage_module.get_coverage_info(coverage_path=config.COVERAGE_PATH)
base_ref = config.GITHUB_BASE_REF or repo_info.default_branch
pr_diff = github.get_pr_diff(github=gh, repository=config.GITHUB_REPOSITORY, pr_number=pr_number)
added_lines = coverage_module.parse_diff_output(diff=pr_diff)
diff_coverage = coverage_module.get_diff_coverage_info(coverage=coverage, added_lines=added_lines)
marker = template.get_marker(marker_id=config.SUBPROJECT_ID)

if config.ANNOTATE_MISSING_LINES:
log.info('Generating annotations for missing lines.')
annotations = diff_grouper.get_diff_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
formatted_annotations = github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=annotations,
)
print(*formatted_annotations, sep='\n')
if config.ANNOTATIONS_OUTPUT_PATH:
log.info('Writing annotations to file.')
with config.ANNOTATIONS_OUTPUT_PATH.open('w+') as annotations_file:
json.dump(formatted_annotations, annotations_file, cls=github.AnnotationEncoder)
log.info('Annotations generated.')

if config.SKIP_COVERAGE:
log.info('Skipping coverage report generation')
return 0

log.info('Generating comment for PR')
marker = template.get_marker(marker_id=config.SUBPROJECT_ID)
files_info, count_files, changed_files_info = template.select_changed_files(
coverage=coverage,
diff_coverage=diff_coverage,
Expand Down Expand Up @@ -121,14 +139,6 @@ def process_pr( # pylint: disable=too-many-locals
)
return 1

# TODO: Disable this for now now and make it work through Github APIs
if pr_number and config.ANNOTATE_MISSING_LINES:
annotations = diff_grouper.get_diff_missing_groups(coverage=coverage, diff_coverage=diff_coverage)
github.create_missing_coverage_annotations(
annotation_type=config.ANNOTATION_TYPE,
annotations=[(annotation.file, annotation.line_start, annotation.line_end) for annotation in annotations],
)

try:
github.post_comment(
github=gh,
Expand Down
10 changes: 10 additions & 0 deletions codecov/settings.py