diff --git a/diff_cover/diff_cover_tool.py b/diff_cover/diff_cover_tool.py index 97b4204f..8422ad9b 100644 --- a/diff_cover/diff_cover_tool.py +++ b/diff_cover/diff_cover_tool.py @@ -15,6 +15,7 @@ JsonReportGenerator, MarkdownReportGenerator, StringReportGenerator, + GitHubWarningAnnotationsReportGenerator, ) from diff_cover.violationsreporters.violations_reporter import ( LcovCoverageReporter, @@ -24,6 +25,7 @@ HTML_REPORT_HELP = "Diff coverage HTML output" JSON_REPORT_HELP = "Diff coverage JSON output" MARKDOWN_REPORT_HELP = "Diff coverage Markdown output" +GITHUB_WARNING_ANNOTATIONS_HELP = "Print diff coverage GitHub warning annotations to the console" COMPARE_BRANCH_HELP = "Branch to compare" CSS_FILE_HELP = "Write CSS into an external file" FAIL_UNDER_HELP = ( @@ -92,6 +94,13 @@ def parse_coverage_args(argv): help=MARKDOWN_REPORT_HELP, ) + parser.add_argument( + "--github-warning-annotations", + action="store_true", + default=None, + help=GITHUB_WARNING_ANNOTATIONS_HELP, + ) + parser.add_argument( "--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED ) @@ -207,6 +216,7 @@ def generate_coverage_report( css_file=None, json_report=None, markdown_report=None, + github_warning_annotations=False, ignore_staged=False, ignore_unstaged=False, include_untracked=False, @@ -269,6 +279,10 @@ def generate_coverage_report( with open(markdown_report, "wb") as output_file: reporter.generate_report(output_file) + if github_warning_annotations: + reporter = GitHubWarningAnnotationsReportGenerator(coverage, diff) + 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 @@ -311,6 +325,7 @@ def main(argv=None, directory=None): html_report=arg_dict["html_report"], json_report=arg_dict["json_report"], markdown_report=arg_dict["markdown_report"], + github_warning_annotations=arg_dict["github_warning_annotations"], css_file=arg_dict["external_css_file"], ignore_staged=arg_dict["ignore_staged"], ignore_unstaged=arg_dict["ignore_unstaged"], diff --git a/diff_cover/report_generator.py b/diff_cover/report_generator.py index 132f9d93..1f20b143 100644 --- a/diff_cover/report_generator.py +++ b/diff_cover/report_generator.py @@ -419,6 +419,14 @@ def __init__(self, violations_reporter, diff_reporter, show_uncovered=False): super().__init__(violations_reporter, diff_reporter) self.include_snippets = show_uncovered +class GitHubWarningAnnotationsReportGenerator(TemplateReportGenerator): + """ + Generate a diff coverage report for GitHub warning annotations. + https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message + """ + + template_path = "github_coverage_warning_annotations.txt" + class HtmlReportGenerator(TemplateReportGenerator): """ diff --git a/diff_cover/templates/github_coverage_warning_annotations.txt b/diff_cover/templates/github_coverage_warning_annotations.txt new file mode 100644 index 00000000..13c0646b --- /dev/null +++ b/diff_cover/templates/github_coverage_warning_annotations.txt @@ -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("-") %} +::warning file={{ src_path }},line={{ splitLines[0] }}{% if splitLines[1] %},endLine={{ splitLines[1] }}{% endif %},title=Missing Coverage::Line {{ line }} missing coverage +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} diff --git a/tests/test_report_generator.py b/tests/test_report_generator.py index 926d82bb..b8c1e1b7 100644 --- a/tests/test_report_generator.py +++ b/tests/test_report_generator.py @@ -15,6 +15,7 @@ MarkdownReportGenerator, StringReportGenerator, TemplateReportGenerator, + GitHubWarningAnnotationsReportGenerator, ) from diff_cover.violationsreporters.violations_reporter import ( BaseViolationReporter, @@ -415,6 +416,57 @@ def test_empty_report(self): self.assert_report(expected) +class TestGitHubWarningAnnotationsReportGenerator(BaseReportGeneratorTest): + REPORT_GENERATOR_CLASS = GitHubWarningAnnotationsReportGenerator + + def test_generate_report(self): + # Generate a default report + self.use_default_values() + + # 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): + self.set_src_paths_changed(["file.py"]) + self.set_lines_changed("file.py", list(range(0, 100))) + self.set_violations("file.py", [Violation(10, None)]) + self.set_measured("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): + # Have the dependencies return an empty report + self.set_src_paths_changed(["file.py"]) + self.set_lines_changed("file.py", list(range(0, 100))) + self.set_violations("file.py", []) + self.set_measured("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): REPORT_GENERATOR_CLASS = HtmlReportGenerator