Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e573d4
Add GitHub annotation generator
timkrins Jun 12, 2025
a839043
Rename to github-annotations
timkrins Jun 12, 2025
a4ed797
Add (unused) annotations type
timkrins Jun 12, 2025
7356129
Formatting
timkrins Jun 12, 2025
7824b0b
Merge remote-tracking branch 'remote/main' into github-warning-annota…
kingbuzzman Jun 14, 2025
d9cbac8
Adds open_file() and updates tests
kingbuzzman Jun 14, 2025
8b01b15
Adds ability to use stdout and stderr in --format
kingbuzzman Jun 14, 2025
452f2d6
Adds missing tests
kingbuzzman Jun 14, 2025
5c97184
Merge branch 'dev/create-open-file' into github-warning-annotations
kingbuzzman Jun 14, 2025
c0a5ab1
Updates test_report_generator.py to be more pytest-ic
kingbuzzman Jun 15, 2025
d107e59
Merge branch 'dev/update-report-tests' into github-warning-annotations
kingbuzzman Jun 15, 2025
ab4bff4
Expose the issues to the PR
kingbuzzman Jun 15, 2025
13e64c8
Fully tested
kingbuzzman Jun 15, 2025
70c9e08
Adds more tests
kingbuzzman Jun 15, 2025
7836cb0
Merge branch 'main' into github-warning-annotations
kingbuzzman Jun 15, 2025
d45ebb1
Merge branch 'main' into github-warning-annotations
kingbuzzman Jun 15, 2025
d7bf437
Merge branch 'Bachmann1234:main' into github-warning-annotations
kingbuzzman Jun 16, 2025
cc9c5b0
Merge branch 'main' into github-warning-annotations
kingbuzzman Jun 23, 2025
7a1e06f
chore: rename format argument to github-annotations
timkrins Jul 15, 2025
38ac10f
Rename another instance of github to github-annotations
timkrins Jul 15, 2025
97d77be
Rename file to remove reference to warning, update doc string
timkrins Jul 15, 2025
30bbc71
Merge remote-tracking branch 'upstream/main' into github-annotation
timkrins Jul 15, 2025
1271b58
Undo change to open_file
timkrins Jul 15, 2025
b78f0f6
Remove github-annotations error type for CI coverage job
timkrins Jul 15, 2025
392dc17
Remove default as it does not work
timkrins Jul 15, 2025
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: 1 addition & 1 deletion .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
cp .coverage htmlcov/.coverage
- name: Complete coverage
run: |
poetry run diff-cover coverage.xml --include-untracked
poetry run diff-cover coverage.xml --include-untracked --format github-annotations:error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with adding error. But i didn't want to impose it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bachmann1234 what do you think this should be?

Copy link
Contributor Author

@timkrins timkrins Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will change it to use the default, which will be warning.

Copy link
Contributor

@kingbuzzman kingbuzzman Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no "default" it will crash. This is a bigger bug that has little to do with your PR. For now, why dont we use warning and wait for @Bachmann1234

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - I see. I thought it would be a good test of the default, and in a way it was haha.

poetry run diff-quality --violations flake8 --include-untracked
poetry run diff-quality --violations pylint --include-untracked
- name: Upload single coverage artifact
Expand Down
13 changes: 12 additions & 1 deletion diff_cover/diff_cover_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from diff_cover.git_diff import GitDiffFileTool, GitDiffTool
from diff_cover.git_path import GitPathTool
from diff_cover.report_generator import (
GitHubAnnotationsReportGenerator,
HtmlReportGenerator,
JsonReportGenerator,
MarkdownReportGenerator,
Expand All @@ -27,6 +28,7 @@
HTML_REPORT_DEFAULT_PATH = "diff-cover.html"
JSON_REPORT_DEFAULT_PATH = "diff-cover.json"
MARKDOWN_REPORT_DEFAULT_PATH = "diff-cover.md"
GITHUB_ANNOTATIONS_DEFAULT_TYPE = "warning"
COMPARE_BRANCH_HELP = "Branch to compare"
CSS_FILE_HELP = "Write CSS into an external file"
FAIL_UNDER_HELP = (
Expand Down Expand Up @@ -266,9 +268,18 @@ def generate_coverage_report(
if "markdown" in report_formats:
markdown_report = report_formats["markdown"] or MARKDOWN_REPORT_DEFAULT_PATH
reporter = MarkdownReportGenerator(coverage, diff)
with open(markdown_report, "wb") as output_file:
with open_file(markdown_report, "wb") as output_file:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find, but should really be a separate PR; this should have a test associated with it.

Suggested change
with open_file(markdown_report, "wb") as output_file:
with open(markdown_report, "wb") as output_file:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - I merged in your branch - it was your change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here: #516

reporter.generate_report(output_file)

if "github-annotations" in report_formats:
# Github annotations are always written to stdout, but we can use different types
reporter = GitHubAnnotationsReportGenerator(
coverage,
diff,
report_formats["github-annotations"] or GITHUB_ANNOTATIONS_DEFAULT_TYPE,
)
reporter.generate_report(sys.stdout.buffer)

# Generate the report for stdout
reporter = StringReportGenerator(coverage, diff, show_uncovered)
output_file = io.BytesIO() if quiet else sys.stdout.buffer
Expand Down
21 changes: 21 additions & 0 deletions diff_cover/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,27 @@ def __init__(self, violations_reporter, diff_reporter, show_uncovered=False):
self.include_snippets = show_uncovered


class GitHubAnnotationsReportGenerator(TemplateReportGenerator):
"""
Generate a diff coverage report for GitHub annotations.
https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-notice-message
https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
"""

template_path = "github_coverage_annotations.txt"

def __init__(self, violations_reporter, diff_reporter, annotations_type):
super().__init__(violations_reporter, diff_reporter)
self.annotations_type = annotations_type

def _context(self):
context = super().report_dict()
context.update({"annotations_type": self.annotations_type})
return context


class HtmlReportGenerator(TemplateReportGenerator):
"""
Generate an HTML formatted diff coverage report.
Expand Down
10 changes: 10 additions & 0 deletions diff_cover/templates/github_coverage_annotations.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% if src_stats %}
{% for src_path, stats in src_stats|dictsort %}
{% if stats.percent_covered < 100 %}
{% for line in stats.violation_lines %}
{% set splitLines = line.split("-") %}
::{{ annotations_type }} file={{ src_path }},line={{ splitLines[0] }}{% if splitLines[1] %},endLine={{ splitLines[1] }}{% endif %},title=Missing Coverage::Line {{ line }} missing coverage
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
50 changes: 49 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# pylint: disable=use-implicit-booleaness-not-comparison

"""High-level integration tests of diff-cover tool."""

import json
import os
import os.path
import re
import shutil
import textwrap
from collections import defaultdict
from pathlib import Path
from subprocess import Popen
Expand Down Expand Up @@ -397,6 +397,54 @@ def test_expand_coverage_report_uncomplete_report(
assert runbin(["coverage_missing_lines.xml", "--expand-coverage-report"]) == 0
compare_console("expand_console_report.txt", capsys.readouterr().out)

def test_github_silent(self, runbin, patch_git_command, capsys):
patch_git_command.set_stdout("git_diff_add.txt")
assert (
runbin(["coverage.xml", "--format", "github-annotations:notice", "-q"]) == 0
)
expected = textwrap.dedent(
"""\
::notice file=test_src.txt,line=2,title=Missing Coverage::Line 2 missing coverage
::notice file=test_src.txt,line=4,title=Missing Coverage::Line 4 missing coverage
::notice file=test_src.txt,line=6,title=Missing Coverage::Line 6 missing coverage
::notice file=test_src.txt,line=8,title=Missing Coverage::Line 8 missing coverage
::notice file=test_src.txt,line=10,title=Missing Coverage::Line 10 missing coverage
"""
)
assert capsys.readouterr().out == expected

@pytest.mark.usefixtures("patch_git_command")
def test_github_fully_covered(self, runbin, capsys):
assert runbin(["coverage2.xml", "--format", "github-annotations:notice"]) == 0
expected = textwrap.dedent(
"""\
-------------
Diff Coverage
Diff: origin/main...HEAD, staged and unstaged changes
-------------
No lines with coverage information in this diff.
-------------

"""
)
assert capsys.readouterr().out == expected

def test_github_empty_diff(self, runbin, patch_git_command, capsys):
patch_git_command.set_stdout("")
assert runbin(["coverage.xml", "--format", "github-annotations:notice"]) == 0
expected = textwrap.dedent(
"""\
-------------
Diff Coverage
Diff: origin/main...HEAD, staged and unstaged changes
-------------
No lines with coverage information in this diff.
-------------

"""
)
assert capsys.readouterr().out == expected

def test_real_world_cpp_lcov_coverage(self, runbin, patch_git_command, capsys):
"""Test with real C++ LCOV coverage data"""
# Create git diff for C++ files
Expand Down
59 changes: 59 additions & 0 deletions tests/test_report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from diff_cover.diff_reporter import BaseDiffReporter
from diff_cover.report_generator import (
BaseReportGenerator,
GitHubAnnotationsReportGenerator,
HtmlReportGenerator,
JsonReportGenerator,
MarkdownReportGenerator,
Expand Down Expand Up @@ -407,6 +408,64 @@ def test_empty_report(self):
self.assert_report(expected)


class TestGitHubAnnotationsReportGenerator(BaseReportGeneratorTest):

@pytest.fixture
def report(self, coverage, diff):
# Create a concrete instance of a report generator
return GitHubAnnotationsReportGenerator(coverage, diff, "warning")

@pytest.mark.usefixtures("use_default_values")
def test_generate_report(self):
# Verify that we got the expected string
expected = dedent(
"""
::warning file=file1.py,line=10,endLine=11,title=Missing Coverage::Line 10-11 missing coverage
::warning file=subdir/file2.py,line=10,endLine=11,title=Missing Coverage::Line 10-11 missing coverage
"""
).strip()

self.assert_report(expected)

def test_single_line(
self, diff, diff_lines_changed, coverage_violations, coverage_measured_lines
):
diff.src_paths_changed.return_value = ["file.py"]
diff_lines_changed.update({"file.py": list(range(100))})
coverage_violations.update({"file.py": [Violation(10, None)]})
coverage_measured_lines.update({"file.py": [2]})

# Verify that we got the expected string
expected = dedent(
"""
::warning file=file.py,line=10,title=Missing Coverage::Line 10 missing coverage
"""
).strip()

self.assert_report(expected)

def test_hundred_percent(
self, diff, diff_lines_changed, coverage_violations, coverage_measured_lines
):
# Have the dependencies return an empty report
diff.src_paths_changed.return_value = ["file.py"]
diff_lines_changed.update({"file.py": list(range(100))})
coverage_violations.update({"file.py": []})
coverage_measured_lines.update({"file.py": [2]})

expected = ""

self.assert_report(expected)

def test_empty_report(self):
# Have the dependencies return an empty report
# (this is the default)

expected = ""

self.assert_report(expected)


class TestHtmlReportGenerator(BaseReportGeneratorTest):

@pytest.fixture
Expand Down