diff --git a/README.md b/README.md index 7023147..eefe48d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ Permissions needed for the Github Token: `Pull requests:read` `Pull requests:write` +If you have given `ANNOTATIONS_DATA_BRANCH` branch then Github Token also requires content write permissions. +Read more on how to use this here. + +`Contents:write` + **install:** ```bash diff --git a/codecov/github.py b/codecov/github.py index 006d6fe..4159f1e 100644 --- a/codecov/github.py +++ b/codecov/github.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import base64 import dataclasses import json import pathlib @@ -7,9 +8,10 @@ from codecov import github_client, groups, log, settings GITHUB_CODECOV_LOGIN = 'CI-codecov[bot]' +COMMIT_MESSAGE = 'Update annotations data' -class CannotDeterminePR(Exception): +class CannotGetBranch(Exception): pass @@ -58,6 +60,13 @@ def default(self, o): return super().default(o) +@dataclasses.dataclass +class User: + name: str + email: str + login: str + + @dataclasses.dataclass class RepositoryInfo: default_branch: str @@ -76,16 +85,21 @@ def get_repository_info(github: github_client.GitHub, repository: str) -> Reposi return RepositoryInfo(default_branch=response.default_branch, visibility=response.visibility) -def get_my_login(github: github_client.GitHub) -> str: +def get_my_login(github: github_client.GitHub) -> User: try: response = github.user.get() + user = User( + name=response.name, + email=response.email or f'{response.id}+{response.login}@users.noreply.github.com', + login=response.login, + ) except github_client.Forbidden: # The GitHub actions user cannot access its own details # and I'm not sure there's a way to see that we're using # the GitHub actions user except noting that it fails - return GITHUB_CODECOV_LOGIN + return User(name=GITHUB_CODECOV_LOGIN, email='', login=GITHUB_CODECOV_LOGIN) - return response.login + return user def get_pr_number(github: github_client.GitHub, config: settings.Config) -> int: @@ -138,7 +152,7 @@ def get_pr_diff(github: github_client.GitHub, repository: str, pr_number: int) - def post_comment( # pylint: disable=too-many-arguments github: github_client.GitHub, - me: str, + user: User, repository: str, pr_number: int, contents: str, @@ -151,7 +165,7 @@ def post_comment( # pylint: disable=too-many-arguments comments_path = github.repos(repository).issues.comments for comment in issue_comments_path.get(): - if comment.user.login == me and marker in comment.body: + if comment.user.login == user.login and marker in comment.body: log.info('Update previous comment') try: comments_path(comment.id).patch(body=contents) @@ -198,3 +212,55 @@ def create_missing_coverage_annotations( ) ) return formatted_annotations + + +def write_annotations_to_branch( + github: github_client.GitHub, user: User, pr_number: int, config: settings.Config, annotations: list[Annotation] +) -> None: + log.info('Getting the annotations data branch.') + try: + data_branch = github.repos(config.GITHUB_REPOSITORY).branches(config.ANNOTATIONS_DATA_BRANCH).get() + if data_branch.protected: + raise github_client.NotFound + except github_client.Forbidden as exc: + raise CannotGetBranch from exc + except github_client.NotFound as exc: + log.warning(f'Branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}" does not exist.') + raise CannotGetBranch from exc + + log.info('Writing annotations to branch.') + file_name = f'{pr_number}-annotations.json' + file_sha: str | None = None + try: + file = github.repos(config.GITHUB_REPOSITORY).contents(file_name).get(ref=config.ANNOTATIONS_DATA_BRANCH) + file_sha = file.sha + except github_client.NotFound: + pass + except github_client.Forbidden as exc: + log.error(f'Forbidden access to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".') + raise CannotGetBranch from exc + + try: + encoded_content = base64.b64encode(json.dumps(annotations, cls=AnnotationEncoder).encode()).decode() + github.repos(config.GITHUB_REPOSITORY).contents(file_name).put( + message=COMMIT_MESSAGE, + branch=config.ANNOTATIONS_DATA_BRANCH, + sha=file_sha, + committer={ + 'name': user.name, + 'email': user.email, + }, + content=encoded_content, + ) + except github_client.NotFound as exc: + log.error(f'Branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}" does not exist.') + raise CannotGetBranch from exc + except github_client.Forbidden as exc: + log.error(f'Forbidden access to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".') + raise CannotGetBranch from exc + except github_client.Conflict as exc: + log.error(f'Conflict writing to branch "{config.GITHUB_REPOSITORY}/{config.ANNOTATIONS_DATA_BRANCH}".') + raise CannotGetBranch from exc + except github_client.ValidationFailed as exc: + log.error('Validation failed on committer name or email.') + raise CannotGetBranch from exc diff --git a/codecov/github_client.py b/codecov/github_client.py index d6ce5ae..e059a13 100644 --- a/codecov/github_client.py +++ b/codecov/github_client.py @@ -63,6 +63,7 @@ def _http(self, method: str, path: str, *, use_bytes: bool = False, use_text: bo headers=headers, **requests_kwargs, ) + contents: str | bytes | JsonObject if use_bytes: contents = response.content elif use_text: @@ -76,6 +77,8 @@ def _http(self, method: str, path: str, *, use_bytes: bool = False, use_text: bo cls: type[ApiError] = { 403: Forbidden, 404: NotFound, + 409: Conflict, + 422: ValidationFailed, }.get(exc.response.status_code, ApiError) raise cls(str(contents)) from exc @@ -113,3 +116,11 @@ class NotFound(ApiError): class Forbidden(ApiError): pass + + +class Conflict(ApiError): + pass + + +class ValidationFailed(ApiError): + pass diff --git a/codecov/main.py b/codecov/main.py index 3857045..373fe76 100644 --- a/codecov/main.py +++ b/codecov/main.py @@ -41,8 +41,8 @@ def action(config: settings.Config, github_session: httpx.Client) -> int: try: pr_number = github.get_pr_number(github=gh, config=config) except github.CannotGetPullRequest: - log.debug('Cannot get pull request number. Exiting.', exc_info=True) - log.info( + log.error('Cannot get pull request number. Exiting.', exc_info=True) + log.error( 'This worflow is not triggered on a pull_request event, ' "nor on a push event on a branch. Consequently, there's nothing to do. " 'Exiting.' @@ -74,37 +74,18 @@ def process_pr( # pylint: disable=too-many-locals added_lines = coverage_module.parse_diff_output(diff=pr_diff) diff_coverage = coverage_module.get_diff_coverage_info(added_lines=added_lines, coverage=coverage) - 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, + user: github.User = github.get_my_login(github=gh) + try: + generate_annotations( + config=config, user=user, pr_number=pr_number, gh=gh, coverage=coverage, diff_coverage=diff_coverage ) - - if config.BRANCH_COVERAGE: - branch_annotations = diff_grouper.get_branch_missing_groups(coverage=coverage, diff_coverage=diff_coverage) - formatted_annotations.extend( - github.create_missing_coverage_annotations( - annotation_type=config.ANNOTATION_TYPE, - annotations=branch_annotations, - branch=True, - ) - ) - - # Print to console - yellow = '\033[93m' - reset = '\033[0m' - print(yellow, end='') - print(*formatted_annotations, sep='\n') - print(reset, end='') - - # Save to file - 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.') + except github.CannotGetBranch: + log.error( + 'Cannot retrieve the annotation data branch.' + 'Please ensure it exists and that you have sufficient permissions and branch protection is disabled. Exiting.', + exc_info=True, + ) + return 1 if config.SKIP_COVERAGE: log.info('Skipping coverage report generation') @@ -163,7 +144,7 @@ def process_pr( # pylint: disable=too-many-locals try: github.post_comment( github=gh, - me=github.get_my_login(github=gh), + user=user, repository=config.GITHUB_REPOSITORY, pr_number=pr_number, contents=comment, @@ -178,3 +159,55 @@ def process_pr( # pylint: disable=too-many-locals log.debug('Comment created on PR') return 0 + + +def generate_annotations( # pylint: disable=too-many-arguments + config: settings.Config, user: github.User, pr_number: int, gh: github_client.GitHub, coverage, diff_coverage +): + if not config.ANNOTATE_MISSING_LINES: + return + + 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, + ) + + if config.BRANCH_COVERAGE: + branch_annotations = diff_grouper.get_branch_missing_groups(coverage=coverage, diff_coverage=diff_coverage) + formatted_annotations.extend( + github.create_missing_coverage_annotations( + annotation_type=config.ANNOTATION_TYPE, + annotations=branch_annotations, + branch=True, + ) + ) + + if not formatted_annotations: + log.info('No annotations to generate. Exiting.') + return + + # Print to console + yellow = '\033[93m' + reset = '\033[0m' + print(yellow, end='') + print(*formatted_annotations, sep='\n') + print(reset, end='') + + # Save to file + 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) + + if config.ANNOTATIONS_DATA_BRANCH: + log.info('Writing annotations to branch.') + github.write_annotations_to_branch( + github=gh, + user=user, + pr_number=pr_number, + config=config, + annotations=formatted_annotations, + ) + log.info('Annotations generated.') diff --git a/codecov/settings.py b/codecov/settings.py index ec17495..036bf13 100644 --- a/codecov/settings.py +++ b/codecov/settings.py @@ -52,6 +52,7 @@ class Config: ANNOTATE_MISSING_LINES: bool = False ANNOTATION_TYPE: str = 'warning' ANNOTATIONS_OUTPUT_PATH: pathlib.Path | None = None + ANNOTATIONS_DATA_BRANCH: str | None = None MAX_FILES_IN_COMMENT: int = 25 COMPLETE_PROJECT_REPORT: bool = False COVERAGE_REPORT_URL: str | None = None diff --git a/tests/test_github.py b/tests/test_github.py index c29eb81..1a4f247 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import annotations +import base64 import json import pathlib @@ -126,15 +127,15 @@ def test_get_pr_diff_not_found(gh, session, base_config): def test_get_my_login(gh, session): - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) result = github.get_my_login(github=gh) - assert result == 'foo' + assert result == github.User(name='bar', email='baz', login='foo') def test_get_my_login_github_bot(gh, session): session.register('GET', '/user')(status_code=403) result = github.get_my_login(github=gh) - assert result == github.GITHUB_CODECOV_LOGIN + assert result == github.User(name=github.GITHUB_CODECOV_LOGIN, email='', login=github.GITHUB_CODECOV_LOGIN) @pytest.mark.parametrize( @@ -151,7 +152,7 @@ def test_post_comment_create(gh, session, existing_comments): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='hi!', @@ -166,7 +167,7 @@ def test_post_comment_content_too_long_error(gh, session): with pytest.raises(github.CannotPostComment): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='a' * 65537, @@ -181,7 +182,7 @@ def test_post_comment_create_error(gh, session): with pytest.raises(github.CannotPostComment): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='hi!', @@ -200,7 +201,7 @@ def test_post_comment_update(gh, session): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='hi!', @@ -220,7 +221,7 @@ def test_post_comment_update_error(gh, session): with pytest.raises(github.CannotPostComment): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='hi!', @@ -240,7 +241,7 @@ def test_post_comment_server_error(gh, session): with pytest.raises(github.CannotPostComment): github.post_comment( github=gh, - me='foo', + user=github.User(name='foo', email='bar', login='foo'), repository='foo/bar', pr_number=123, contents='hi!', @@ -393,3 +394,289 @@ def test_non_annotation_encoder(): ) def test_create_missing_coverage_annotations(annotation_type, annotations, expected_annotations): assert github.create_missing_coverage_annotations(annotation_type, annotations) == expected_annotations + + +def test_write_annotations_to_branch_protected_branch(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': True} + ) + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=[], + ) + + +def test_write_annotations_to_branch_forbidden(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + status_code=403 + ) + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=[], + ) + + +def test_write_annotations_to_branch_get_annotations_forbidden(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': 'annotations'} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(status_code=403) + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=[], + ) + + +def test_write_annotations_to_branch_annotations_create(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(status_code=404) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': None, + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(json={'content': {'sha': 'abc'}}) + + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) + + +def test_write_annotations_to_branch_annotations_update(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(json={'sha': 'abc'}) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': 'abc', + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(json={'content': {'sha': 'abc'}}) + + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) + + +def test_write_annotations_to_branch_annotations_update_not_found(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(json={'sha': 'abc'}) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': 'abc', + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(status_code=404) + + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) + + +def test_write_annotations_to_branch_annotations_update_forbidden(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(json={'sha': 'abc'}) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': 'abc', + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(status_code=403) + + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) + + +def test_write_annotations_to_branch_annotations_update_conflict(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(json={'sha': 'abc'}) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': 'abc', + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(status_code=409) + + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) + + +def test_write_annotations_to_branch_annotations_update_validation_failed(gh, session, base_config): + config = base_config(ANNOTATIONS_DATA_BRANCH='annotations', ANNOTATE_MISSING_LINES=True) + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/branches/{config.ANNOTATIONS_DATA_BRANCH}')( + json={'protected': False, 'name': config.ANNOTATIONS_DATA_BRANCH} + ) + session.register( + 'GET', f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', params={'ref': 'annotations'} + )(json={'sha': 'abc'}) + session.register( + 'PUT', + f'/repos/{config.GITHUB_REPOSITORY}/contents/123-annotations.json', + json={ + 'message': github.COMMIT_MESSAGE, + 'branch': config.ANNOTATIONS_DATA_BRANCH, + 'sha': 'abc', + 'committer': {'name': 'foo', 'email': 'bar'}, + 'content': base64.b64encode(json.dumps(annotations, cls=github.AnnotationEncoder).encode()).decode(), + }, + )(status_code=422) + + with pytest.raises(github.CannotGetBranch): + github.write_annotations_to_branch( + github=gh, + user=github.User(name='foo', email='bar', login='foo'), + pr_number=123, + config=config, + annotations=annotations, + ) diff --git a/tests/test_main.py b/tests/test_main.py index bd6811d..1b0712e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ import pytest -from codecov import github, main +from codecov import diff_grouper, github, main @mock.patch('pathlib.Path.open') @@ -23,6 +23,7 @@ def test_process_pr_skip_coverage( mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -48,6 +49,7 @@ def test_process_pr_skip_coverage_with_annotations( mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -73,15 +75,26 @@ def test_process_branch_coverage_in_annotations( BRANCH_COVERAGE=True, ) caplog.set_level('INFO') + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] mock_read_template_file.return_value = """ {% block foo %}foo{% endblock foo %} {{ marker }} """ mock_post_comment.return_value = None mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) + github.create_missing_coverage_annotations = mock.Mock(return_value=annotations) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -104,7 +117,7 @@ def test_process_pr_with_annotations_missing_marker_error( mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -135,7 +148,7 @@ def test_process_pr_with_annotations_template_error( mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -168,7 +181,7 @@ def test_process_pr_with_annotations_cannot_post( mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -196,15 +209,26 @@ def test_process_pr_with_annotations( SUBPROJECT_ID='sub_project', ) caplog.set_level('DEBUG') + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] mock_read_template_file.return_value = """ {% block foo %}foo{% endblock foo %} {{ marker }} """ mock_post_comment.return_value = None mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) + github.create_missing_coverage_annotations = mock.Mock(return_value=annotations) diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) - session.register('GET', '/user')(json={'login': 'foo'}) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) repo_info = github.RepositoryInfo(default_branch='main', visibility='public') result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) @@ -213,6 +237,74 @@ def test_process_pr_with_annotations( assert caplog.records[-1].message == 'Comment created on PR' +@mock.patch('pathlib.Path.open') +@mock.patch('codecov.main.template.read_template_file') +@mock.patch('codecov.main.github.post_comment') +def test_process_pr_with_annotations_to_branch( + mock_post_comment: mock.Mock, + mock_read_template_file: mock.Mock, + mock_open: mock.Mock, + base_config, + gh, + coverage_json, + session, + caplog, +): + config = base_config( + ANNOTATE_MISSING_LINES=True, + ANNOTATIONS_OUTPUT_PATH=pathlib.Path(tempfile.mkstemp(suffix='.json')[1]), + SUBPROJECT_ID='sub_project', + ANNOTATIONS_DATA_BRANCH='annotations-data', + ) + caplog.set_level('DEBUG') + annotations = [ + github.Annotation( + file=pathlib.Path('file.py'), + line_start=10, + line_end=10, + title='Error', + message_type='warning', + message='Error', + ) + ] + mock_read_template_file.return_value = """ + {% block foo %}foo{% endblock foo %} + {{ marker }} + """ + mock_post_comment.return_value = None + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) + github.create_missing_coverage_annotations = mock.Mock(return_value=annotations) + github.write_annotations_to_branch = mock.Mock(return_value=None) + diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) + + repo_info = github.RepositoryInfo(default_branch='main', visibility='public') + result = main.process_pr(config, gh, repo_info, config.GITHUB_PR_NUMBER) + + assert result == 0 + assert caplog.records[-1].message == 'Comment created on PR' + + +@mock.patch('pathlib.Path.open') +def test_process_pr_generate_annotations(mock_open: mock.Mock, gh, base_config, coverage_obj, session, coverage_json): + config = base_config(BRANCH_COVERAGE=True) + main.generate_annotations = mock.Mock(side_effect=github.CannotGetBranch) + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) + diff_grouper.group_branches = mock.Mock(return_value=coverage_obj) + diff_data = 'diff --git a/file.py b/file.py\nindex 1234567..abcdefg 100644\n--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n+bar\n-baz\n+qux\n' + session.register('GET', f'/repos/{config.GITHUB_REPOSITORY}/pulls/{config.GITHUB_PR_NUMBER}')(text=diff_data) + session.register('GET', '/user')(json={'login': 'foo', 'id': 123, 'name': 'bar', 'email': 'baz'}) + + result = main.process_pr( + config=config, + repo_info=github.RepositoryInfo(default_branch='main', visibility='public'), + pr_number=123, + gh=gh, + ) + assert result == 1 + + @mock.patch('codecov.main.settings.Config.from_environ') @mock.patch('codecov.main.log.setup') @mock.patch('codecov.main.sys.exit')