Skip to content

Commit 79c2d6d

Browse files
committed
Add an option to generate a plain-text markdown table for PR coverage
1 parent 7188638 commit 79c2d6d

File tree

6 files changed

+319
-0
lines changed

6 files changed

+319
-0
lines changed

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ inputs:
9696
description: >
9797
If true, will create an annotation on every line with missing coverage on a pull request.
9898
default: false
99+
INCLUDE_PLAIN_TEXT_REPORT:
100+
description: >
101+
If true, will include a plain-text markdown coverage table in the job summary,
102+
suitable for pasting into text-based tools. Only applies in PR mode.
103+
default: false
99104
ANNOTATION_TYPE:
100105
description: >
101106
Only relevant if ANNOTATE_MISSING_LINES is set to true. This parameter allows you to choose between
@@ -182,4 +187,5 @@ runs:
182187
MERGE_COVERAGE_FILES: ${{ inputs.MERGE_COVERAGE_FILES }}
183188
ANNOTATE_MISSING_LINES: ${{ inputs.ANNOTATE_MISSING_LINES }}
184189
ANNOTATION_TYPE: ${{ inputs.ANNOTATION_TYPE }}
190+
INCLUDE_PLAIN_TEXT_REPORT: ${{ inputs.INCLUDE_PLAIN_TEXT_REPORT }}
185191
VERBOSE: ${{ inputs.VERBOSE }}

coverage_comment/main.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,29 @@ def process_pr(
270270
content=summary_comment, github_step_summary=config.GITHUB_STEP_SUMMARY
271271
)
272272

273+
if config.INCLUDE_PLAIN_TEXT_REPORT:
274+
try:
275+
plain_text_report = template.get_plain_text_markdown(
276+
coverage=coverage,
277+
diff_coverage=diff_coverage,
278+
previous_coverage=previous_coverage,
279+
previous_coverage_rate=previous_coverage_rate,
280+
files=files_info,
281+
count_files=count_files,
282+
max_files=None,
283+
subproject_id=config.SUBPROJECT_ID,
284+
failure_msg=failure_msg,
285+
)
286+
github.add_job_summary(
287+
content=plain_text_report,
288+
github_step_summary=config.GITHUB_STEP_SUMMARY,
289+
)
290+
except template.TemplateError:
291+
log.warning(
292+
"There was a rendering error when computing the plain-text report. "
293+
"The plain-text output will not be available."
294+
)
295+
273296
if pr_number is not None and config.ANNOTATE_MISSING_LINES:
274297
annotations = diff_grouper.get_diff_missing_groups(
275298
coverage=coverage, diff_coverage=diff_coverage

coverage_comment/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class Config:
6464
MERGE_COVERAGE_FILES: bool = False
6565
ANNOTATE_MISSING_LINES: bool = False
6666
ANNOTATION_TYPE: str = "warning"
67+
INCLUDE_PLAIN_TEXT_REPORT: bool = False
6768
MAX_FILES_IN_COMMENT: int = 25
6869
USE_GH_PAGES_HTML_URL: bool = False
6970
VERBOSE: bool = False
@@ -95,6 +96,10 @@ def clean_merge_coverage_files(cls, value: str) -> bool:
9596
def clean_annotate_missing_lines(cls, value: str) -> bool:
9697
return str_to_bool(value)
9798

99+
@classmethod
100+
def clean_include_plain_text_report(cls, value: str) -> bool:
101+
return str_to_bool(value)
102+
98103
@classmethod
99104
def clean_annotation_type(cls, value: str) -> str:
100105
if value not in {"notice", "warning", "error"}:

coverage_comment/template.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,48 @@ def get_comment_markdown(
200200
return comment
201201

202202

203+
def get_plain_text_markdown(
204+
*,
205+
coverage: coverage_module.Coverage,
206+
diff_coverage: coverage_module.DiffCoverage,
207+
previous_coverage_rate: decimal.Decimal | None,
208+
previous_coverage: coverage_module.Coverage | None,
209+
files: list[FileInfo],
210+
max_files: int | None,
211+
count_files: int,
212+
subproject_id: str | None = None,
213+
failure_msg: str | None = None,
214+
) -> str:
215+
env = SandboxedEnvironment()
216+
env.filters["pct"] = pct
217+
218+
missing_diff_lines = {
219+
key: list(value)
220+
for key, value in itertools.groupby(
221+
diff_grouper.get_diff_missing_groups(
222+
coverage=coverage, diff_coverage=diff_coverage
223+
),
224+
lambda x: x.file,
225+
)
226+
}
227+
228+
try:
229+
return env.from_string(read_template_file("comment_plain.md.j2")).render(
230+
previous_coverage_rate=previous_coverage_rate,
231+
coverage=coverage,
232+
diff_coverage=diff_coverage,
233+
previous_coverage=previous_coverage,
234+
count_files=count_files,
235+
max_files=max_files,
236+
files=files,
237+
missing_diff_lines=missing_diff_lines,
238+
subproject_id=subproject_id,
239+
failure_msg=failure_msg,
240+
)
241+
except jinja2.exceptions.TemplateError as exc:
242+
raise TemplateError from exc
243+
244+
203245
def select_files(
204246
*,
205247
coverage: coverage_module.Coverage,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{%- if subproject_id %}## Plain-text coverage report ({{ subproject_id }}){% else %}## Plain-text coverage report{% endif %}
2+
3+
**Overall coverage:** {{ coverage.info.percent_covered | pct }}{% if previous_coverage_rate %} (was {{ previous_coverage_rate | pct }}){% endif %}
4+
5+
{% if failure_msg -%}
6+
> WARNING: {{ failure_msg }}
7+
8+
**Diff coverage:** N/A
9+
{%- else -%}
10+
**Diff coverage:** {{ diff_coverage.total_percent_covered | pct }} ({{ diff_coverage.total_num_lines - diff_coverage.total_num_violations }}/{{ diff_coverage.total_num_lines }} new statements covered)
11+
{%- endif %}
12+
{% if not files %}
13+
14+
_This PR does not seem to contain any modification to coverable code._
15+
{%- else %}
16+
17+
| File | Stmts | Missing | Coverage | New stmts coverage | Missing lines |
18+
| ---- | ----: | ------: | -------: | -----------------: | ------------- |
19+
{%- for parent, files_in_folder in files|groupby(attribute="path.parent") %}
20+
{%- for file in files_in_folder %}
21+
{%- set path = file.coverage.path %}
22+
{%- if file.previous %}
23+
{%- set cov_str = (file.previous.info.percent_covered | pct) ~ " -> " ~ (file.coverage.info.percent_covered | pct) %}
24+
{%- else %}
25+
{%- set cov_str = file.coverage.info.percent_covered | pct %}
26+
{%- endif %}
27+
{%- if file.diff and file.diff.added_statements %}
28+
{%- set diff_str = (file.diff.percent_covered | pct) ~ " (" ~ (file.diff.covered_statements | length) ~ "/" ~ (file.diff.added_statements | length) ~ ")" %}
29+
{%- else %}
30+
{%- set diff_str = "N/A" %}
31+
{%- endif %}
32+
{%- set comma = joiner(", ") %}
33+
{%- set ns = namespace(missing="") %}
34+
{%- for group in missing_diff_lines.get(path, []) %}
35+
{%- if group.line_start == group.line_end %}
36+
{%- set ns.missing = ns.missing ~ comma() ~ (group.line_start | string) %}
37+
{%- else %}
38+
{%- set ns.missing = ns.missing ~ comma() ~ (group.line_start | string) ~ "-" ~ (group.line_end | string) %}
39+
{%- endif %}
40+
{%- endfor %}
41+
| {{ path }} | {{ file.coverage.info.num_statements }} | {{ file.coverage.info.missing_lines }} | {{ cov_str }} | {{ diff_str }} | {{ ns.missing }} |
42+
{%- endfor %}
43+
{%- endfor %}
44+
| **Total** | {{ coverage.info.num_statements }} | {{ coverage.info.missing_lines }} | {% if previous_coverage %}{{ previous_coverage.info.percent_covered | pct }} -> {% endif %}{{ coverage.info.percent_covered | pct }} | {{ diff_coverage.total_percent_covered | pct }} ({{ diff_coverage.total_num_lines - diff_coverage.total_num_violations }}/{{ diff_coverage.total_num_lines }}) | |
45+
{%- if max_files and count_files > max_files %}
46+
47+
_Report truncated to {{ max_files }} files out of {{ count_files }}. See the workflow summary for the full report._
48+
{%- endif %}
49+
{%- endif %}

tests/unit/test_template.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,200 @@ def test_template__no_files(coverage_obj, diff_coverage_obj_more_files):
408408
assert "other.py" not in result
409409

410410

411+
def test_get_plain_text_markdown(make_coverage, make_coverage_and_diff):
412+
previous_cov = make_coverage(
413+
"""
414+
# file: codebase/code.py
415+
1 covered
416+
2 covered
417+
3
418+
4 missing
419+
5 covered
420+
6 covered
421+
7
422+
8
423+
9 covered
424+
# file: codebase/other.py
425+
1 covered
426+
2 covered
427+
3 covered
428+
4 covered
429+
5 covered
430+
6 covered
431+
# file: codebase/third.py
432+
1 covered
433+
2 covered
434+
3 covered
435+
4 covered
436+
5 covered
437+
6 missing
438+
7 missing
439+
"""
440+
)
441+
cov, diff_cov = make_coverage_and_diff(
442+
"""
443+
# file: codebase/code.py
444+
1 covered
445+
2 covered
446+
3
447+
4
448+
5 covered
449+
6 covered
450+
7
451+
8
452+
9 covered
453+
10
454+
11
455+
+ 12 missing
456+
+ 13 missing
457+
+ 14 missing
458+
+ 15 covered
459+
+ 16 covered
460+
+ 17
461+
+ 18
462+
+ 19
463+
+ 20
464+
+ 21
465+
+ 22 missing
466+
# file: codebase/other.py
467+
1 covered
468+
2 covered
469+
3 covered
470+
# file: codebase/third.py
471+
1 covered
472+
2 covered
473+
3 covered
474+
4 covered
475+
5 covered
476+
6 covered
477+
7 covered
478+
"""
479+
)
480+
481+
files, total = template.select_files(
482+
coverage=cov,
483+
diff_coverage=diff_cov,
484+
previous_coverage=previous_cov,
485+
max_files=25,
486+
)
487+
488+
result = template.get_plain_text_markdown(
489+
coverage=cov,
490+
diff_coverage=diff_cov,
491+
previous_coverage=previous_cov,
492+
previous_coverage_rate=decimal.Decimal("11") / decimal.Decimal("12"),
493+
files=files,
494+
count_files=total,
495+
max_files=25,
496+
)
497+
print(result)
498+
# No HTML anywhere
499+
assert "<img" not in result
500+
assert "<table" not in result
501+
assert "<td" not in result
502+
assert "<tr" not in result
503+
assert "<a " not in result
504+
# Has markdown table
505+
assert "| File" in result or "| ---- |" in result
506+
# Contains key data
507+
assert "code.py" in result
508+
assert "other.py" in result
509+
assert "third.py" in result
510+
assert "80.95%" in result # overall coverage
511+
assert "91.66%" in result # previous coverage rate
512+
assert "33.33%" in result # diff coverage
513+
assert "12-14" in result # missing line range
514+
assert "22" in result # missing single line
515+
assert "**Total**" in result
516+
517+
518+
def test_get_plain_text_markdown__no_files(coverage_obj):
519+
diff_cov = coverage.DiffCoverage(
520+
total_num_lines=0,
521+
total_num_violations=0,
522+
total_percent_covered=decimal.Decimal("1"),
523+
num_changed_lines=0,
524+
files={},
525+
)
526+
result = template.get_plain_text_markdown(
527+
coverage=coverage_obj,
528+
diff_coverage=diff_cov,
529+
previous_coverage=None,
530+
previous_coverage_rate=None,
531+
files=[],
532+
count_files=0,
533+
max_files=25,
534+
)
535+
print(result)
536+
assert "does not seem to contain any modification" in result
537+
assert "<img" not in result
538+
assert "| File" not in result
539+
540+
541+
def test_get_plain_text_markdown__with_failure_msg(coverage_obj, diff_coverage_obj):
542+
files, total = template.select_files(
543+
coverage=coverage_obj,
544+
diff_coverage=diff_coverage_obj,
545+
previous_coverage=None,
546+
max_files=25,
547+
)
548+
result = template.get_plain_text_markdown(
549+
coverage=coverage_obj,
550+
diff_coverage=diff_coverage_obj,
551+
previous_coverage=None,
552+
previous_coverage_rate=None,
553+
files=files,
554+
count_files=total,
555+
max_files=25,
556+
failure_msg="The diff is too large to process",
557+
)
558+
print(result)
559+
assert "WARNING: The diff is too large to process" in result
560+
assert "N/A" in result # diff coverage should show N/A
561+
562+
563+
def test_get_plain_text_markdown__no_previous(coverage_obj, diff_coverage_obj):
564+
files, total = template.select_files(
565+
coverage=coverage_obj,
566+
diff_coverage=diff_coverage_obj,
567+
previous_coverage=None,
568+
max_files=25,
569+
)
570+
result = template.get_plain_text_markdown(
571+
coverage=coverage_obj,
572+
diff_coverage=diff_coverage_obj,
573+
previous_coverage=None,
574+
previous_coverage_rate=None,
575+
files=files,
576+
count_files=total,
577+
max_files=25,
578+
)
579+
print(result)
580+
# Should not contain "was" or "->" for coverage evolution
581+
assert "(was " not in result
582+
assert "-> " not in result
583+
584+
585+
def test_get_plain_text_markdown__subproject(coverage_obj, diff_coverage_obj):
586+
files, total = template.select_files(
587+
coverage=coverage_obj,
588+
diff_coverage=diff_coverage_obj,
589+
previous_coverage=None,
590+
max_files=25,
591+
)
592+
result = template.get_plain_text_markdown(
593+
coverage=coverage_obj,
594+
diff_coverage=diff_coverage_obj,
595+
previous_coverage=None,
596+
previous_coverage_rate=None,
597+
files=files,
598+
count_files=total,
599+
max_files=25,
600+
subproject_id="mylib",
601+
)
602+
assert "(mylib)" in result
603+
604+
411605
def test_read_template_file():
412606
assert template.read_template_file("comment.md.j2").startswith(
413607
"{%- block title -%}## Coverage report{%- if subproject_id %}"

0 commit comments

Comments
 (0)