Repository: py-cov-action/python-coverage-comment-action
Pull request #188 (2023-06-12 15:00:05)

feat: create annotate only from diff coverage
Tried my hand here! A few challenges remain, mainly that I'm not used to Jinja and doing web-mocking in Python, so unsure how to fix those test errors :-)


Patch:
diff --git a/.gitignore b/.gitignore
index 1c6ff85..873c347 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 9b06524..6f714ce 100644
--- a/coverage_comment/annotations.py
+++ b/coverage_comment/annotations.py
@@ -4,15 +4,17 @@ from coverage_comment import github
 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 25c1384..c99db53 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 e9f481b..6995d83 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 5f4e882..1b92a69 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


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index e616776..b287012 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,6 +4,7 @@ import functools
 import io
 import os
 import zipfile
+from collections.abc import Callable
 
 import httpx
 import pytest
@@ -43,7 +44,7 @@ def push_config(base_config):
 
 
 @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 a423186..8909440 100644
--- a/tests/integration/test_main.py
+++ b/tests/integration/test_main.py
@@ -3,6 +3,7 @@ import json
 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 306bdca..3b88d9f 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