diff --git a/.gitignore b/.gitignore index 1c6ff859..873c347a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .DS_Store .coverage dev-env-vars +*.pyc +.venv +.vscode/settings.json diff --git a/coverage_comment/annotations.py b/coverage_comment/annotations.py index 9b06524c..6f714ce1 100644 --- a/coverage_comment/annotations.py +++ b/coverage_comment/annotations.py @@ -4,15 +4,17 @@ MISSING_LINES_GROUP_TITLE = "Annotations of lines with missing coverage" -def create_pr_annotations(annotation_type: str, coverage: coverage_module.Coverage): +def create_pr_annotations(annotation_type: str, coverage: coverage_module.DiffCoverage): github.send_workflow_command( command="group", command_value=MISSING_LINES_GROUP_TITLE ) - for filename, file_coverage in coverage.files.items(): - for missing_line in file_coverage.missing_lines: + for filepath, file_coverage in coverage.files.items(): + for missing_line in file_coverage.violation_lines: github.create_missing_coverage_annotation( - annotation_type=annotation_type, file=filename, line=missing_line + annotation_type=annotation_type, + file=str(filepath), + line=missing_line, ) github.send_workflow_command(command="endgroup", command_value="") diff --git a/coverage_comment/coverage.py b/coverage_comment/coverage.py index 25c13847..c99db530 100644 --- a/coverage_comment/coverage.py +++ b/coverage_comment/coverage.py @@ -58,7 +58,7 @@ class DiffCoverage: total_num_violations: int total_percent_covered: decimal.Decimal num_changed_lines: int - files: dict[pathlib.Path, FileDiffCoverage] + files: dict[str, FileDiffCoverage] def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal: @@ -191,14 +191,17 @@ def extract_info(data) -> Coverage: ) -def get_diff_coverage_info(base_ref: str) -> DiffCoverage: +def get_diff_coverage_info( + base_ref: str, compare_to_origin: bool = True +) -> DiffCoverage: subprocess.run("git", "fetch", "--depth=1000") subprocess.run("coverage", "xml") with tempfile.NamedTemporaryFile("r") as f: + ref_prefix = "origin/" if compare_to_origin else "" subprocess.run( "diff-cover", "coverage.xml", - f"--compare-branch=origin/{base_ref}", + f"--compare-branch={ref_prefix}{base_ref}", f"--json-report={f.name}", "--diff-range-notation=..", "--quiet", diff --git a/coverage_comment/main.py b/coverage_comment/main.py index e9f481b7..6995d83e 100644 --- a/coverage_comment/main.py +++ b/coverage_comment/main.py @@ -73,15 +73,22 @@ def action( if event_name in {"pull_request", "push"}: coverage = coverage_module.get_coverage_info(merge=config.MERGE_COVERAGE_FILES) + if event_name == "pull_request": + diff_coverage = coverage_module.get_diff_coverage_info( + base_ref=config.GITHUB_BASE_REF, + compare_to_origin=config.COV_DIFF_TO_ORIGIN, + ) + if config.ANNOTATE_MISSING_LINES: annotations.create_pr_annotations( - annotation_type=config.ANNOTATION_TYPE, coverage=coverage + annotation_type=config.ANNOTATION_TYPE, coverage=diff_coverage ) return generate_comment( config=config, coverage=coverage, + diff_coverage=diff_coverage, github_session=github_session, ) else: @@ -105,15 +112,13 @@ def action( def generate_comment( config: settings.Config, coverage: coverage_module.Coverage, + diff_coverage: coverage_module.DiffCoverage, github_session: httpx.Client, ) -> int: log.info("Generating comment for PR") gh = github_client.GitHub(session=github_session) - diff_coverage = coverage_module.get_diff_coverage_info( - base_ref=config.GITHUB_BASE_REF - ) previous_coverage_data_file = storage.get_datafile_contents( github=gh, repository=config.GITHUB_REPOSITORY, diff --git a/coverage_comment/settings.py b/coverage_comment/settings.py index 5f4e882a..1b92a69a 100644 --- a/coverage_comment/settings.py +++ b/coverage_comment/settings.py @@ -48,6 +48,7 @@ class Config: MERGE_COVERAGE_FILES: bool = False ANNOTATE_MISSING_LINES: bool = False ANNOTATION_TYPE: str = "warning" + COV_DIFF_TO_ORIGIN: bool = False VERBOSE: bool = False # Only for debugging, not exposed in the action: FORCE_WORKFLOW_RUN: bool = False diff --git a/tests/conftest.py b/tests/conftest.py index e616776f..b2870129 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import io import os import zipfile +from collections.abc import Callable import httpx import pytest @@ -43,7 +44,7 @@ def _(**kwargs): @pytest.fixture -def pull_request_config(base_config): +def pull_request_config(base_config) -> Callable: def _(**kwargs): defaults = { # GitHub stuff @@ -214,65 +215,6 @@ def coverage_obj_no_branch(): ) -@pytest.fixture -def coverage_obj_many_missing_lines(): - return coverage_module.Coverage( - meta=coverage_module.CoverageMetadata( - version="1.2.3", - timestamp=datetime.datetime(2000, 1, 1), - branch_coverage=True, - show_contexts=False, - ), - info=coverage_module.CoverageInfo( - covered_lines=7, - num_statements=10, - percent_covered=decimal.Decimal("0.8"), - missing_lines=12, - excluded_lines=0, - num_branches=2, - num_partial_branches=1, - covered_branches=1, - missing_branches=1, - ), - files={ - "codebase/main.py": coverage_module.FileCoverage( - path="codebase/main.py", - executed_lines=[1, 2, 5, 6, 9], - missing_lines=[3, 7, 13, 21, 123], - excluded_lines=[], - info=coverage_module.CoverageInfo( - covered_lines=5, - num_statements=10, - percent_covered=decimal.Decimal("0.5"), - missing_lines=5, - excluded_lines=0, - num_branches=2, - num_partial_branches=1, - covered_branches=1, - missing_branches=1, - ), - ), - "codebase/caller.py": coverage_module.FileCoverage( - path="codebase/caller.py", - executed_lines=[1, 2, 5], - missing_lines=[13, 21, 212], - excluded_lines=[], - info=coverage_module.CoverageInfo( - covered_lines=3, - num_statements=6, - percent_covered=decimal.Decimal("0.5"), - missing_lines=3, - excluded_lines=0, - num_branches=2, - num_partial_branches=1, - covered_branches=1, - missing_branches=1, - ), - ), - }, - ) - - @pytest.fixture def diff_coverage_obj(): return coverage_module.DiffCoverage( @@ -290,6 +232,38 @@ def diff_coverage_obj(): ) +@pytest.fixture +def diff_coverage_obj_many_missing_lines(): + return coverage_module.DiffCoverage( + total_num_lines=20, + total_num_violations=1, + total_percent_covered=decimal.Decimal("0.7"), + num_changed_lines=52, + files={ + "codebase/code.py": coverage_module.FileDiffCoverage( + path="codebase/code.py", + percent_covered=decimal.Decimal("0.8"), + violation_lines=[3, 5, 21, 111], + ), + "codebase/helper.py": coverage_module.FileDiffCoverage( + path="codebase/helper.py", + percent_covered=decimal.Decimal("0.6"), + violation_lines=[19, 22], + ), + "codebase/log.py": coverage_module.FileDiffCoverage( + path="codebase/log.py", + percent_covered=decimal.Decimal("0.9"), + violation_lines=[], + ), + "codebase/files.py": coverage_module.FileDiffCoverage( + path="codebase/files.py", + percent_covered=decimal.Decimal("0.8"), + violation_lines=[120, 121, 122], + ), + }, + ) + + @pytest.fixture def session(is_failed): """ diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index a4231862..89094402 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -3,6 +3,7 @@ import os import pathlib import subprocess +from collections.abc import Callable import pytest @@ -28,7 +29,7 @@ def file_path(integration_dir): @pytest.fixture -def write_file(file_path): +def write_file(file_path) -> Callable: def _(*variables): content = "import os" for i, var in enumerate(variables): @@ -255,13 +256,14 @@ def test_action__pull_request__annotations( )(status_code=200) result = main.action( - config=pull_request_config(ANNOTATE_MISSING_LINES=True), + config=pull_request_config( + ANNOTATE_MISSING_LINES=True, COV_DIFF_TO_ORIGIN=False + ), github_session=session, http_session=session, git=None, ) expected = """::group::Annotations of lines with missing coverage -::warning file=foo.py,line=6::This line has no coverage ::endgroup::""" output = capsys.readouterr() diff --git a/tests/unit/test_annotations.py b/tests/unit/test_annotations.py index 306bdcab..3b88d9fb 100644 --- a/tests/unit/test_annotations.py +++ b/tests/unit/test_annotations.py @@ -1,8 +1,10 @@ from coverage_comment import annotations -def test_annotations(coverage_obj, capsys): - annotations.create_pr_annotations(annotation_type="warning", coverage=coverage_obj) +def test_annotations(diff_coverage_obj, capsys): + annotations.create_pr_annotations( + annotation_type="warning", coverage=diff_coverage_obj + ) expected = """::group::Annotations of lines with missing coverage ::warning file=codebase/code.py,line=7::This line has no coverage @@ -12,20 +14,21 @@ def test_annotations(coverage_obj, capsys): assert output.out.strip() == expected -def test_annotations_several_files(coverage_obj_many_missing_lines, capsys): +def test_annotations_several_files(diff_coverage_obj_many_missing_lines, capsys): annotations.create_pr_annotations( - annotation_type="notice", coverage=coverage_obj_many_missing_lines + annotation_type="notice", coverage=diff_coverage_obj_many_missing_lines ) expected = """::group::Annotations of lines with missing coverage -::notice file=codebase/main.py,line=3::This line has no coverage -::notice file=codebase/main.py,line=7::This line has no coverage -::notice file=codebase/main.py,line=13::This line has no coverage -::notice file=codebase/main.py,line=21::This line has no coverage -::notice file=codebase/main.py,line=123::This line has no coverage -::notice file=codebase/caller.py,line=13::This line has no coverage -::notice file=codebase/caller.py,line=21::This line has no coverage -::notice file=codebase/caller.py,line=212::This line has no coverage +::notice file=codebase/code.py,line=3::This line has no coverage +::notice file=codebase/code.py,line=5::This line has no coverage +::notice file=codebase/code.py,line=21::This line has no coverage +::notice file=codebase/code.py,line=111::This line has no coverage +::notice file=codebase/helper.py,line=19::This line has no coverage +::notice file=codebase/helper.py,line=22::This line has no coverage +::notice file=codebase/files.py,line=120::This line has no coverage +::notice file=codebase/files.py,line=121::This line has no coverage +::notice file=codebase/files.py,line=122::This line has no coverage ::endgroup::""" output = capsys.readouterr() assert output.out.strip() == expected