diff --git a/README.md b/README.md index cee4c3c..7023147 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Note: Either `GITHUB_PR_NUMBER` or `GITHUB_REF` is required. - `SUBPROJECT_ID`: The ID or URL of the subproject or report. - `MINIMUM_GREEN`: The minimum coverage percentage for green status. Default is 100. - `MINIMUM_ORANGE`: The minimum coverage percentage for orange status. Default is 70. +- `BRANCH_COVERAGE`: Show branch coverage in the report. Default is False. - `SKIP_COVERAGE`: Skip coverage reporting as github comment and generate only annotaions. Default is False. - `ANNOTATIONS_OUTPUT_PATH`: The path where the annotaions should be stored. Should be a .json file. - `ANNOTATE_MISSING_LINES`: Whether to annotate missing lines in the coverage report. Default is False. diff --git a/codecov/coverage.py b/codecov/coverage.py index e6c3cda..5d3ab77 100644 --- a/codecov/coverage.py +++ b/codecov/coverage.py @@ -12,9 +12,6 @@ from codecov import log -# The dataclasses in this module are accessible in the template, which is overridable by the user. -# As a coutesy, we should do our best to keep the existing fields for backward compatibility, -# and if we really can't and can't add properties, at least bump the major version. @dataclasses.dataclass class CoverageMetadata: version: str @@ -43,6 +40,8 @@ class FileCoverage: executed_lines: list[int] missing_lines: list[int] excluded_lines: list[int] + executed_branches: list[list[int]] | None + missing_branches: list[list[int]] | None info: CoverageInfo @@ -53,10 +52,6 @@ class Coverage: files: dict[pathlib.Path, FileCoverage] -# The format for Diff Coverage objects may seem a little weird, because it -# was originally copied from diff-cover schema. - - @dataclasses.dataclass class FileDiffCoverage: path: pathlib.Path @@ -89,7 +84,7 @@ def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal: return decimal.Decimal(num_covered) / decimal.Decimal(num_total) -def get_coverage_info(coverage_path: pathlib.Path) -> tuple[dict, Coverage]: +def get_coverage_info(coverage_path: pathlib.Path) -> Coverage: try: with coverage_path.open() as coverage_data: json_coverage = json.loads(coverage_data.read()) @@ -100,7 +95,7 @@ def get_coverage_info(coverage_path: pathlib.Path) -> tuple[dict, Coverage]: log.error('Invalid JSON format in coverage report file: %s', coverage_path) raise - return json_coverage, extract_info(data=json_coverage) + return extract_info(data=json_coverage) def extract_info(data: dict) -> Coverage: @@ -129,6 +124,8 @@ def extract_info(data: dict) -> Coverage: }, "missing_lines": [7], "excluded_lines": [], + "executed_branches": [], + "missing_branches": [], } }, "totals": { @@ -156,8 +153,10 @@ def extract_info(data: dict) -> Coverage: pathlib.Path(path): FileCoverage( path=pathlib.Path(path), excluded_lines=file_data['excluded_lines'], - executed_lines=file_data['executed_lines'], missing_lines=file_data['missing_lines'], + executed_lines=file_data['executed_lines'], + executed_branches=file_data.get('executed_branches'), + missing_branches=file_data.get('missing_branches'), info=CoverageInfo( covered_lines=file_data['summary']['covered_lines'], num_statements=file_data['summary']['num_statements'], diff --git a/codecov/main.py b/codecov/main.py index 19228d3..9823d64 100644 --- a/codecov/main.py +++ b/codecov/main.py @@ -66,11 +66,11 @@ def process_pr( # pylint: disable=too-many-locals repo_info: github.RepositoryInfo, pr_number: int, ) -> int: - _, coverage = coverage_module.get_coverage_info(coverage_path=config.COVERAGE_PATH) + coverage = coverage_module.get_coverage_info(coverage_path=config.COVERAGE_PATH) base_ref = config.GITHUB_BASE_REF or repo_info.default_branch pr_diff = github.get_pr_diff(github=gh, repository=config.GITHUB_REPOSITORY, pr_number=pr_number) added_lines = coverage_module.parse_diff_output(diff=pr_diff) - diff_coverage = coverage_module.get_diff_coverage_info(coverage=coverage, added_lines=added_lines) + 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.') @@ -119,6 +119,7 @@ def process_pr( # pylint: disable=too-many-locals base_template=template.read_template_file('comment.md.j2'), marker=marker, subproject_id=config.SUBPROJECT_ID, + branch_coverage=config.BRANCH_COVERAGE, complete_project_report=config.COMPLETE_PROJECT_REPORT, coverage_report_url=config.COVERAGE_REPORT_URL, ) diff --git a/codecov/settings.py b/codecov/settings.py index 17f029b..ec17495 100644 --- a/codecov/settings.py +++ b/codecov/settings.py @@ -47,6 +47,7 @@ class Config: SUBPROJECT_ID: str | None = None MINIMUM_GREEN: decimal.Decimal = decimal.Decimal('100') MINIMUM_ORANGE: decimal.Decimal = decimal.Decimal('70') + BRANCH_COVERAGE: bool = False SKIP_COVERAGE: bool = False ANNOTATE_MISSING_LINES: bool = False ANNOTATION_TYPE: str = 'warning' @@ -78,6 +79,10 @@ def clean_annotate_missing_lines(cls, value: str) -> bool: def clean_skip_coverage(cls, value: str) -> bool: return str_to_bool(value) + @classmethod + def clean_branch_coverage(cls, value: str) -> bool: + return str_to_bool(value) + @classmethod def clean_complete_project_report(cls, value: str) -> bool: return str_to_bool(value) diff --git a/codecov/template.py b/codecov/template.py index 6a635e4..35c0da3 100644 --- a/codecov/template.py +++ b/codecov/template.py @@ -83,6 +83,7 @@ def get_comment_markdown( # pylint: disable=too-many-arguments,too-many-locals base_template: str, marker: str, subproject_id: str | None = None, + branch_coverage: bool = False, complete_project_report: bool = False, coverage_report_url: str | None = None, ): @@ -128,6 +129,7 @@ def get_comment_markdown( # pylint: disable=too-many-arguments,too-many-locals missing_lines_for_whole_project=missing_lines_for_whole_project, subproject_id=subproject_id, marker=marker, + branch_coverage=branch_coverage, complete_project_report=complete_project_report, coverage_report_url=coverage_report_url, ) diff --git a/codecov/template_files/comment.md.j2 b/codecov/template_files/comment.md.j2 index 5b043bb..357a78d 100644 --- a/codecov/template_files/comment.md.j2 +++ b/codecov/template_files/comment.md.j2 @@ -4,9 +4,9 @@ {% block coverage_badges -%} {%- block coverage_evolution_badge -%} {%- if coverage %} -{%- set text = "Coverage of the whole project for this PR is" ~ coverage.info.percent_covered_display ~ "." -%} +{%- set text = "Coverage of the whole project for this PR is " ~ coverage.info.percent_covered_display ~ "%." -%} {%- set color = coverage.info.percent_covered | get_badge_color -%} - + {%- endif -%} {%- endblock coverage_evolution_badge -%} @@ -27,6 +27,22 @@ {%- endmacro -%} +{%- macro branches_badge(path, branches_count, base=false) -%} +{% set text = "The " ~ path ~ " contains " ~ branches_count ~ " branch" ~ (branches_count | pluralize(plural='es')) ~"." -%} +{% set color = "008080" -%} + + +{%- endmacro -%} + +{%- macro missing_branches_badge(path, missing_branches_count, base=false) -%} +{%- set text = missing_branches_count ~ " branch" ~ (missing_branches_count | pluralize(plural='es')) ~ " missing the coverage in " ~ path ~ "." -%} +{% if missing_branches_count == 0 -%} +{%- set color = "brightgreen" -%} +{% else -%} +{%- set color = "red" -%} +{% endif -%} + +{%- endmacro -%} {%- macro missing_lines_badge(path, missing_lines_count, base=false) -%} {%- set text = missing_lines_count ~ " statement" ~ (statements_count | pluralize) ~ " missing the coverage in " ~ path ~ "." -%} @@ -36,20 +52,20 @@ {%- set color = "red" -%} {% endif -%} - {%- endmacro -%} {%- macro coverage_rate_badge(path, percent_covered, percent_covered_display, covered_statements_count, statements_count, base=false) -%} -{%- set text = "The coverage rate of " ~ path ~ " is " ~ percent_covered_display ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%} +{%- set text = "The coverage rate of " ~ path ~ " is " ~ percent_covered_display ~ "% (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%} +{%- set label = percent_covered_display ~ "%" -%} {%- set message = "(" ~ covered_statements_count ~ "/" ~ statements_count ~ ")" -%} {%- set color = percent_covered | get_badge_color -%} - + {%- endmacro -%} {%- macro diff_coverage_rate_badge(path, added_statements_count, covered_statements_count, percent_covered) -%} {% if added_statements_count -%} -{% set text = "In this PR, " ~ (added_statements_count) ~ " new statements are added to " ~ path ~ ", " ~ covered_statements_count ~ " of which are covered (" ~ (percent_covered | pct) ~ ")." -%} +{% set text = "In this PR, " ~ (added_statements_count) ~ " new statement" ~ (added_statements_count | pluralize) ~ " " ~ (added_statements_count | pluralize(singular='is', plural='are')) ~ " added to " ~ path ~ ", and " ~ covered_statements_count ~ " statement" ~ (covered_statements_count | pluralize) ~ " "~ (covered_statements_count | pluralize(singular='is', plural='are')) ~ " covered (" ~ (percent_covered | pct) ~ ")." -%} {% set label = (percent_covered | pct(precision=0)) -%} {% set message = "(" ~ covered_statements_count ~ "/" ~ added_statements_count ~ ")" -%} {%- set color = (percent_covered | x100 | get_badge_color()) -%} @@ -70,14 +86,20 @@ _This PR does not seem to contain any modification to coverable code._ {%- else -%} -
Click to see coverage of changed files - +
Click to see coverage of changed files +
FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
+ + {% if branch_coverage %}{% endif %} + + + {% if branch_coverage %}{% endif %} {%- for parent, files_in_folder in files|groupby(attribute="path.parent") -%} - + + {%- for file in files_in_folder -%} {%- set path = file.coverage.path -%} @@ -100,6 +122,24 @@ _This PR does not seem to contain any modification to coverable code._ ) -}} {%- endblock missing_lines_badge_cell -%} +{% if branch_coverage %} +{#- Branches cell -#} +{%- block branches_badge_cell scoped -%} +{{- branches_badge( + path=path, + branches_count=file.coverage.info.num_branches, +) -}} +{%- endblock branches_badge_cell -%} + +{#- Missing cell -#} +{%- block missing_branches_badge_cell scoped -%} +{{- missing_branches_badge( + path=path, + missing_branches_count=file.coverage.info.missing_branches, +) -}} +{%- endblock missing_branches_badge_cell -%} +{% endif %} + {#- Coverage rate -#} {%- block coverage_rate_badge_cell scoped -%} {{- coverage_rate_badge( @@ -111,7 +151,7 @@ _This PR does not seem to contain any modification to coverable code._ ) -}} {%- endblock coverage_rate_badge_cell -%} -{#- Coverage of added lines -#} +{#- Coverage of added lines (new stmts) -#} {%- block diff_coverage_rate_badge_cell scoped -%} {{- diff_coverage_rate_badge( path=path, @@ -121,7 +161,7 @@ _This PR does not seem to contain any modification to coverable code._ ) -}} {%- endblock diff_coverage_rate_badge_cell -%} -{#- Link to missing lines -#} +{#- Link to lines missing -#} {%- block link_to_missing_diff_lines_cell scoped -%} {%- endblock link_to_missing_diff_lines_cell -%} + +{#- Link to branch missing lines -#} +{%- if branch_coverage -%} +{%- block link_to_branches_missing_lines_cell scoped -%} + +{%- endblock link_to_branches_missing_lines_cell -%} +{%- endif -%} + + {%- endfor -%} {%- endfor -%} @@ -165,6 +220,24 @@ _This PR does not seem to contain any modification to coverable code._ ) -}} {%- endblock missing_lines_badge_total_cell -%} +{% if branch_coverage %} +{#- Branches cell -#} +{%- block branches_badge_total_cell scoped -%} +{{- branches_badge( + path="the whole project", + branches_count=coverage.info.num_branches, +) -}} +{%- endblock branches_badge_total_cell -%} + +{#- Missing cell -#} +{%- block missing_branches_badge_total_cell scoped -%} +{{- missing_branches_badge( + path="the whole project", + missing_branches_count=coverage.info.missing_branches, +) -}} +{%- endblock missing_branches_badge_total_cell -%} +{% endif %} + {#- Coverage rate -#} {%- block coverage_rate_badge_total_cell scoped -%} {{- coverage_rate_badge( @@ -187,27 +260,18 @@ _This PR does not seem to contain any modification to coverable code._ {%- endblock diff_coverage_rate_badge_total_cell -%} +{% if branch_coverage %} + +{% endif %}
FileStatementsMissingBranchesMissing
Coverage         
Coverage         
(new stmts)

Lines missing              

Branches missing              
  {{ parent }}  {{ parent }}
@@ -141,6 +181,21 @@ _This PR does not seem to contain any modification to coverable code._ +{%- set comma = joiner() -%} +{%- for branch in file.coverage.missing_branches -%} +{{- comma() -}} +{{- branch[0] | abs -}} -> {{- branch[1] | abs -}} +{%- endfor -%} +
  
{%- if max_files and count_files > max_files %} - _The report is truncated to {{ max_files }} files out of {{ count_files }}. - {% endif %} -{%- block footer %} - - - -This report was generated by [CI-codecov] - -
- -{% endblock footer -%} - {%- endif -%} {%- endblock coverage_by_file %} @@ -220,8 +284,15 @@ This report was generated by {{- branch[0] | abs -}} -> {{- branch[1] | abs -}} +{%- endfor -%} + +{%- endblock project_link_to_branches_missing_lines_cell -%} +{%- endif -%} + + {%- endfor -%} @@ -302,32 +408,43 @@ _No additional project files to report the coverage._ ) -}} {%- endblock project_missing_lines_badge_total_cell -%} +{% if branch_coverage %} +{#- Branches cell -#} +{%- block project_branches_badge_total_cell scoped -%} +{{- branches_badge( + path="the whole project", + branches_count=coverage.info.num_branches, +) -}} +{%- endblock project_branches_badge_total_cell -%} + +{#- Missing cell -#} +{%- block project_missing_branches_badge_total_cell scoped -%} +{{- missing_branches_badge( + path="the whole project", + missing_branches_count=coverage.info.missing_branches, +) -}} +{%- endblock project_missing_branches_badge_total_cell -%} +{% endif %} + {#- Coverage rate -#} {%- block project_coverage_rate_badge_total_cell scoped -%} {{- coverage_rate_badge( path="the whole project", percent_covered=coverage.info.percent_covered, + percent_covered_display=coverage.info.percent_covered_display, covered_statements_count=coverage.info.covered_lines, statements_count=coverage.info.num_statements, ) -}} {%- endblock project_coverage_rate_badge_total_cell -%}   +{% if branch_coverage %} +  +{% endif %} - -{%- block project_footer %} - - - -This report was generated by [CI-codecov] - - - -{% endblock project_footer -%} - {%- endif -%} {%- endif -%} {%- endblock project_coverage_by_file %} @@ -337,4 +454,10 @@ This report was generated by here. {%- endif -%} {%- endblock full_coverage_report %} + +{%- block footer %} + +This report was generated by [CI-codecov] + +{% endblock footer -%} {{ marker -}} diff --git a/tests/conftest.py b/tests/conftest.py index a581423..908499c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,6 +85,8 @@ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage: covered_branches=0 if has_branches else None, missing_branches=0 if has_branches else None, ), + executed_branches=[], + missing_branches=[], ) if set(line.split()) & { 'covered', @@ -206,6 +208,8 @@ def coverage_json(): }, 'missing_lines': [6, 8, 10, 11], 'excluded_lines': [], + 'executed_branches': [[1, 0], [2, 1], [3, 0], [5, 1], [13, 0], [14, 0]], + 'missing_branches': [[6, 0], [8, 1], [10, 0], [11, 0]], } }, 'totals': { diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 6654165..9a06c8a 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -316,6 +316,8 @@ def test_extract_info(coverage_json): covered_branches=1, missing_branches=1, ), + executed_branches=[[1, 0], [2, 1], [3, 0], [5, 1], [13, 0], [14, 0]], + missing_branches=[[6, 0], [8, 1], [10, 0], [11, 0]], ) }, info=coverage.CoverageInfo( @@ -338,7 +340,7 @@ def test_extract_info(coverage_json): def test_get_coverage_info(coverage_json): with patch('pathlib.Path.open') as mock_open: mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(coverage_json) - _, result = coverage.get_coverage_info(pathlib.Path('path/to/file.json')) + result = coverage.get_coverage_info(pathlib.Path('path/to/file.json')) assert result == coverage.extract_info(coverage_json) diff --git a/tests/test_settings.py b/tests/test_settings.py index ac454ea..3d290dc 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -138,6 +138,11 @@ def test_config_clean_skip_coverage(): assert value is False +def test_config_clean_branch_coverage(): + value = settings.Config.clean_branch_coverage('False') + assert value is False + + def test_config_clean_complete_project_report(): value = settings.Config.clean_complete_project_report('True') assert value is True diff --git a/tests/test_template.py b/tests/test_template.py index b9057d1..af31fa1 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -168,8 +168,38 @@ def test_comment_template(coverage_obj, diff_coverage_obj): pr_number=1, base_template=template.read_template_file('comment.md.j2'), ) - expected = '## Coverage report\n\n\n
Click to see coverage of changed files\n \n\n\n\n\n\n\n\n\n\n
FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  codebase
  code.py6-8
Project Total 
\n\n\n\nThis report was generated by [CI-codecov]\n\n\n
\n\n\n\n\n\n\n' - assert result == expected + assert result.startswith('## Coverage report') + assert '' in result + + +def test_comment_template_branch_coverage(coverage_obj, diff_coverage_obj): + chaned_files, total, files = template.select_changed_files( + coverage=coverage_obj, + diff_coverage=diff_coverage_obj, + max_files=25, + ) + result = template.get_comment_markdown( + coverage=coverage_obj, + diff_coverage=diff_coverage_obj, + coverage_files=chaned_files, + count_coverage_files=total, + files=files, + count_files=total, + max_files=25, + minimum_green=decimal.Decimal('100'), + minimum_orange=decimal.Decimal('70'), + base_ref='main', + marker='', + repo_name='org/repo', + pr_number=1, + base_template=template.read_template_file('comment.md.j2'), + branch_coverage=True, + ) + assert result.startswith('## Coverage report') + assert '' in result + assert 'BranchesMissing' in result + assert 'Branches missing' in result + assert 'colspan="9"' in result def test_template_no_files(coverage_obj):