Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ on:

permissions:
contents: read
pull-requests: read
pull-requests: write
checks: write

jobs:
test-coverage-mode:
Expand All @@ -35,8 +36,8 @@ jobs:
coverage-files: coverage.xml
compare-branch: origin/main
fail-under: '0'
post-comment: 'false'
create-annotations: 'false'
post-comment: 'true'
create-annotations: 'true'
create-badge: 'true'

test-quality-mode:
Expand Down
104 changes: 51 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,89 +60,87 @@ Works with **any language** that can produce Cobertura XML, lcov, or JaCoCo cove

## What You Get

### PR Comment (auto-posted, updates on re-run)
### PR Comment — Passing

> ## :white_check_mark: Diff Coverage: 82.0%
> <table><tr><td>
> <img src="https://img.shields.io/badge/diff_coverage-82.0%25-green?style=for-the-badge" alt="82.0%" />
> &ensp;<img src="https://img.shields.io/badge/threshold-passed-success?style=flat-square" alt="passed" />
>
> > :heavy_check_mark: Meets threshold of 80%
> `████████████████░░░░`&ensp;**82.0%** on changed lines
> </td></tr></table>
>
> | Metric | Value |
> |--------|------:|
> | **Coverage on diff lines** | **82.0%** |
> | Lines changed | 50 |
> | Lines uncovered | 9 |
> | Files changed | 3 |
> **50** lines changed &emsp; **9** uncovered &emsp; **3** files
>
> <details>
> <summary>File breakdown (3 files)</summary>
> <summary>&ensp;📂&ensp;<b>3 files changed</b></summary>
> <br>
>
> | File | Coverage | Uncovered Lines |
> |------|:--------:|:---------------:|
> | `src/bar.py` | 60.0% | 5, 6, 7, 8, 15, 22 |
> | `src/foo.py` | 85.0% | 13, 27, 42 |
> | `src/baz.py` | 100.0% | |
> | &ensp; | File | Coverage | Uncovered Lines |
> |:---:|:---|---:|:---|
> | 🔴 | `src/bar.py` | 60.0% | 5, 6, 7, 8, 15, 22 |
> | 🟡 | `src/foo.py` | 85.0% | 13, 27, 42 |
> | 🟢 | `src/baz.py` | 100.0% | |
>
> </details>
>
> ---
> <sub>Posted by <a href="https://github.com/Affanmir/diff-cover-action">diff-cover-action</a></sub>
> <sub>🛡️&ensp;<a href="https://github.com/marketplace/actions/diff-cover-pr-coverage-quality-reports">diff-cover-action</a></sub>

### PR Comment -- Below Threshold
### PR Comment — Failing

> ## :red_circle: Diff Coverage: 45.0%
> <table><tr><td>
> <img src="https://img.shields.io/badge/diff_coverage-45.0%25-orange?style=for-the-badge" alt="45.0%" />
> &ensp;<img src="https://img.shields.io/badge/threshold-failed-critical?style=flat-square" alt="failed" />
>
> > :x: Below threshold of 80% -- needs 35.0% more coverage
> `█████████░░░░░░░░░░░`&ensp;**45.0%** on changed lines
> </td></tr></table>
>
> | Metric | Value |
> |--------|------:|
> | **Coverage on diff lines** | **45.0%** |
> | Lines changed | 120 |
> | Lines uncovered | 66 |
> | Files changed | 8 |
> > **80%** required — missing **35.0%** more coverage
>
> **120** lines changed &emsp; **66** uncovered &emsp; **8** files
>
> <details>
> <summary>File breakdown (8 files)</summary>
> <summary>&ensp;📂&ensp;<b>8 files changed</b></summary>
> <br>
>
> | File | Coverage | Uncovered Lines |
> |------|:--------:|:---------------:|
> | `src/payments/stripe.py` | 0.0% | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (+12 more) |
> | `src/auth/login.py` | 25.0% | 15, 16, 17, 30, 31, 32 |
> | `src/api/routes.py` | 40.0% | 22, 23, 55, 56, 57 |
> | `src/models/user.py` | 50.0% | 8, 9, 44 |
> | `src/utils/cache.py` | 60.0% | 18, 19 |
> | `src/services/email.py` | 70.0% | 33 |
> | `src/config.py` | 80.0% | 5 |
> | `src/middleware.py` | 100.0% | |
> | &ensp; | File | Coverage | Uncovered Lines |
> |:---:|:---|---:|:---|
> | 🔴 | `src/payments/stripe.py` | 0.0% | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (+12 more) |
> | 🔴 | `src/auth/login.py` | 25.0% | 15, 16, 17, 30, 31, 32 |
> | 🔴 | `src/api/routes.py` | 40.0% | 22, 23, 55, 56, 57 |
> | 🔴 | `src/models/user.py` | 50.0% | 8, 9, 44 |
> | 🔴 | `src/utils/cache.py` | 60.0% | 18, 19 |
> | 🟡 | `src/services/email.py` | 70.0% | 33 |
> | 🟡 | `src/config.py` | 80.0% | 5 |
> | 🟢 | `src/middleware.py` | 100.0% | |
>
> </details>
>
> ---
> <sub>Posted by <a href="https://github.com/Affanmir/diff-cover-action">diff-cover-action</a></sub>
> <sub>🛡️&ensp;<a href="https://github.com/marketplace/actions/diff-cover-pr-coverage-quality-reports">diff-cover-action</a></sub>

### Diff Quality PR Comment

> ## :large_orange_diamond: Diff Quality: 75.0%
> <table><tr><td>
> <img src="https://img.shields.io/badge/diff_quality-75.0%25-yellowgreen?style=for-the-badge" alt="75.0%" />
> &ensp;<img src="https://img.shields.io/badge/threshold-failed-critical?style=flat-square" alt="failed" />
>
> `███████████████░░░░░`&ensp;**75.0%** on changed lines
> </td></tr></table>
>
> > :x: Below threshold of 90%
> > **90%** required — missing **15.0%** more quality coverage
>
> | Metric | Value |
> |--------|------:|
> | **Quality on diff lines** | **75.0%** |
> | Lines changed | 20 |
> | Lines with violations | 4 |
> | Files changed | 1 |
> **20** lines changed &emsp; **4** violations &emsp; **1** file
>
> <details>
> <summary>File breakdown (1 file)</summary>
> <summary>&ensp;📂&ensp;<b>1 file changed</b></summary>
> <br>
>
> | File | Quality | Violation Lines |
> |------|:-------:|:---------------:|
> | `src/module.py` | 75.0% | 10, 11, 12, 30 |
> | &ensp; | File | Quality | Violation Lines |
> |:---:|:---|---:|:---|
> | 🟡 | `src/module.py` | 75.0% | 10, 11, 12, 30 |
>
> </details>
>
> ---
> <sub>Posted by <a href="https://github.com/Affanmir/diff-cover-action">diff-cover-action</a></sub>
> <sub>🛡️&ensp;<a href="https://github.com/marketplace/actions/diff-cover-pr-coverage-quality-reports">diff-cover-action</a></sub>

### Inline Annotations (appear directly on the PR diff)

Expand Down
42 changes: 34 additions & 8 deletions src/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,37 @@
return None


def _progress_bar(percent: float, width: int = 20) -> str:
"""Generate a Unicode progress bar. Example: ████████████████░░░░ for 80%."""
filled = round(percent / 100 * width)
filled = max(0, min(width, filled))
return "\u2588" * filled + "\u2591" * (width - filled)


def _status_icon(percent: float) -> str:
"""Return a colored circle emoji based on coverage percentage."""
if percent >= 90:
return "\U0001f7e2" # 🟢

Check warning on line 113 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 113 is not covered by tests
if percent >= 70:
return "\U0001f7e1" # 🟡
return "\U0001f534" # 🔴

Check warning on line 116 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 116 is not covered by tests


def _badge_color(percent: float) -> str:
"""Map a coverage percentage to a shields.io color name."""
if percent >= 90:
return "brightgreen"
if percent >= 80:
return "green"

Check warning on line 124 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 124 is not covered by tests
if percent >= 70:
return "yellowgreen"

Check warning on line 126 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 126 is not covered by tests
if percent >= 60:
return "yellow"

Check warning on line 128 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 128 is not covered by tests
if percent >= 40:
return "orange"
return "red"

Check warning on line 131 in src/comment.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 131 is not covered by tests


def _render_comment_body(
*,
report: Report,
Expand All @@ -115,24 +146,19 @@
autoescape=False,
keep_trailing_newline=True,
)
env.filters["progress_bar"] = _progress_bar
env.filters["status_icon"] = _status_icon
env.filters["badge_color"] = _badge_color

template_name = f"comment_{mode}.md.j2"
template = env.get_template(template_name)

if report.total_percent_covered >= 90:
icon = ":white_check_mark:"
elif report.total_percent_covered >= 70:
icon = ":large_orange_diamond:"
else:
icon = ":red_circle:"

return template.render(
report=report,
mode=mode,
fail_under=fail_under,
threshold_met=threshold_met,
identifier=identifier,
icon=icon,
md_report_content=md_report_content,
)

Expand Down
42 changes: 34 additions & 8 deletions src/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@
_set_output("exit-code", str(exit_code))


def _progress_bar(percent: float, width: int = 20) -> str:
"""Generate a Unicode progress bar."""
filled = round(percent / 100 * width)
filled = max(0, min(width, filled))
return "\u2588" * filled + "\u2591" * (width - filled)


def _status_icon(percent: float) -> str:
"""Return a colored circle emoji based on coverage percentage."""
if percent >= 90:
return "\U0001f7e2" # 🟢
if percent >= 70:
return "\U0001f7e1" # 🟡
return "\U0001f534" # 🔴


def _badge_color(percent: float) -> str:
"""Map a coverage percentage to a shields.io color name."""
if percent >= 90:
return "brightgreen"

Check warning on line 70 in src/outputs.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Line 70 is not covered by tests
if percent >= 80:
return "green"
if percent >= 70:
return "yellowgreen"
if percent >= 60:
return "yellow"
if percent >= 40:
return "orange"
return "red"

Check warning on line 79 in src/outputs.py

View workflow job for this annotation

GitHub Actions / Test Coverage Mode

Uncovered Line

Lines 73-79 are not covered by tests


def write_step_summary(
*,
report: Report,
Expand All @@ -61,20 +92,15 @@
autoescape=False,
keep_trailing_newline=True,
)
env.filters["progress_bar"] = _progress_bar
env.filters["status_icon"] = _status_icon
env.filters["badge_color"] = _badge_color
template = env.get_template("step_summary.md.j2")

if report.total_percent_covered >= 90:
icon = "white_check_mark"
elif report.total_percent_covered >= 70:
icon = "large_orange_diamond"
else:
icon = "red_circle"

content = template.render(
report=report,
mode=mode,
fail_under=fail_under,
threshold_met=threshold_met,
icon=icon,
)
_append_to_github_file("GITHUB_STEP_SUMMARY", content)
38 changes: 22 additions & 16 deletions templates/comment_coverage.md.j2
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
<!-- diff-cover-action:{{ identifier }} -->
## {{ icon }} Diff Coverage: {{ "%.1f" | format(report.total_percent_covered) }}%
{% set pct = "%.1f" | format(report.total_percent_covered) %}
{% set color = report.total_percent_covered | badge_color %}
<table><tr><td>
<img src="https://img.shields.io/badge/diff_coverage-{{ pct }}%25-{{ color }}?style=for-the-badge" alt="{{ pct }}%" />
{% if fail_under > 0 %}
{% if threshold_met %}
> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}%
&ensp;<img src="https://img.shields.io/badge/threshold-passed-success?style=flat-square" alt="passed" />
{% else %}
> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% — needs {{ "%.1f" | format(fail_under - report.total_percent_covered) }}% more coverage
&ensp;<img src="https://img.shields.io/badge/threshold-failed-critical?style=flat-square" alt="failed" />
{% endif %}
{% endif %}

| Metric | Value |
|--------|------:|
| **Coverage on diff lines** | **{{ "%.1f" | format(report.total_percent_covered) }}%** |
| Lines changed | {{ report.total_num_lines }} |
| Lines uncovered | {{ report.total_num_violations }} |
| Files changed | {{ report.files | length }} |
`{{ report.total_percent_covered | progress_bar }}`&ensp;**{{ pct }}%** on changed lines
</td></tr></table>

{% if fail_under > 0 and not threshold_met %}
> **{{ "%.0f" | format(fail_under) }}%** required — missing **{{ "%.1f" | format(fail_under - report.total_percent_covered) }}%** more coverage
{% endif %}

**{{ report.total_num_lines }}** lines changed &emsp; **{{ report.total_num_violations }}** uncovered &emsp; **{{ report.files | length }}** file{{ "s" if report.files | length != 1 else "" }}
{% if report.files %}

<details>
<summary>File breakdown ({{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }})</summary>
<summary>&ensp;📂&ensp;<b>{{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }} changed</b></summary>
<br>

| File | Coverage | Uncovered Lines |
|------|:--------:|:---------------:|
| &ensp; | File | Coverage | Uncovered Lines |
|:---:|:---|---:|:---|
{% for f in report.files | sort(attribute='percent_covered') %}
| `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
| {{ f.percent_covered | status_icon }} | `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') if f.violation_lines else '—' }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
{% endfor %}

</details>
{% endif %}
{% if md_report_content %}

<details>
<summary>Full diff-cover report</summary>
<summary>&ensp;📋&ensp;<b>Full diff-cover report</b></summary>
<br>

{{ md_report_content }}

</details>
{% endif %}

---
<sub>Posted by <a href="https://github.com/marketplace/actions/diff-cover-action">diff-cover-action</a></sub>
<sub>🛡️&ensp;<a href="https://github.com/marketplace/actions/diff-cover-pr-coverage-quality-reports">diff-cover-action</a></sub>
Loading
Loading