diff --git a/action.yml b/action.yml
index 42249d1b..82bf5d62 100644
--- a/action.yml
+++ b/action.yml
@@ -70,6 +70,15 @@ inputs:
the badge will be orange. Otherwise it will be red.
default: 70
required: false
+ MAX_FILES_IN_COMMENT:
+ description: >
+ Maximum number of files to display in the comment. If there are more
+ files than this number, they will only appear in the workflow summary.
+ The selected files are the ones with the most new uncovered lines. The
+ closer this number gets to 35, the higher the risk that it reaches
+ GitHub's maximum comment size limit of 65536 characters.
+ default: 25
+ required: false
MERGE_COVERAGE_FILES:
description: >
If true, will run `coverage combine` before reading the `.coverage` file.
diff --git a/coverage_comment/badge.py b/coverage_comment/badge.py
index 1824d940..9871932d 100644
--- a/coverage_comment/badge.py
+++ b/coverage_comment/badge.py
@@ -26,8 +26,8 @@ def get_badge_color(
def get_evolution_badge_color(
delta: decimal.Decimal | int,
- up_is_good: bool,
- neutral_color: str = "grey",
+ up_is_good: bool = True,
+ neutral_color: str = "lightgrey",
) -> str:
if delta == 0:
return neutral_color
@@ -67,8 +67,10 @@ def compute_badge_image(
def get_static_badge_url(label: str, message: str, color: str) -> str:
+ if not color or not message:
+ raise ValueError("color and message are required")
code = "-".join(
- e.replace("_", "__").replace("-", "--") for e in (label, message, color)
+ e.replace("_", "__").replace("-", "--") for e in (label, message, color) if e
)
return "https://img.shields.io/badge/" + urllib.parse.quote(f"{code}.svg")
diff --git a/coverage_comment/coverage.py b/coverage_comment/coverage.py
index 51956c23..3e023fdf 100644
--- a/coverage_comment/coverage.py
+++ b/coverage_comment/coverage.py
@@ -3,11 +3,9 @@
import dataclasses
import datetime
import decimal
-import functools
-import itertools
import json
import pathlib
-from collections.abc import Iterable, Sequence
+from collections.abc import Sequence
from coverage_comment import log, subprocess
@@ -74,10 +72,6 @@ class FileDiffCoverage:
def violation_lines(self) -> list[int]:
return self.missing_statements
- @functools.cached_property
- def violation_lines_collapsed(self):
- return list(collapse_lines(self.violation_lines))
-
@dataclasses.dataclass
class DiffCoverage:
@@ -201,10 +195,8 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
covered_lines=file_data["summary"]["covered_lines"],
num_statements=file_data["summary"]["num_statements"],
percent_covered=compute_coverage(
- file_data["summary"]["covered_lines"]
- + file_data["summary"].get("covered_branches", 0),
- file_data["summary"]["num_statements"]
- + file_data["summary"].get("num_branches", 0),
+ file_data["summary"]["covered_lines"],
+ file_data["summary"]["num_statements"],
),
missing_lines=file_data["summary"]["missing_lines"],
excluded_lines=file_data["summary"]["excluded_lines"],
@@ -222,10 +214,8 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
covered_lines=data["totals"]["covered_lines"],
num_statements=data["totals"]["num_statements"],
percent_covered=compute_coverage(
- data["totals"]["covered_lines"]
- + data["totals"].get("covered_branches", 0),
- data["totals"]["num_statements"]
- + data["totals"].get("num_branches", 0),
+ data["totals"]["covered_lines"],
+ data["totals"]["num_statements"],
),
missing_lines=data["totals"]["missing_lines"],
excluded_lines=data["totals"]["excluded_lines"],
@@ -328,11 +318,3 @@ def parse_line_number_diff_line(line: str) -> Sequence[int]:
"""
start, length = (int(i) for i in (line.split()[2][1:] + ",1").split(",")[:2])
return range(start, start + length)
-
-
-def collapse_lines(lines: list[int]) -> Iterable[tuple[int, int]]:
- # All consecutive line numbers have the same difference between their list index and their value.
- # Grouping by this difference therefore leads to buckets of consecutive numbers.
- for _, it in itertools.groupby(enumerate(lines), lambda x: x[1] - x[0]):
- t = list(it)
- yield t[0][1], t[-1][1]
diff --git a/coverage_comment/diff_grouper.py b/coverage_comment/diff_grouper.py
index d48043b4..bc9a54ca 100644
--- a/coverage_comment/diff_grouper.py
+++ b/coverage_comment/diff_grouper.py
@@ -1,77 +1,17 @@
from __future__ import annotations
-import dataclasses
-import functools
-import itertools
-import pathlib
from collections.abc import Iterable
from coverage_comment import coverage as coverage_module
+from coverage_comment import groups
MAX_ANNOTATION_GAP = 3
-@dataclasses.dataclass(frozen=True)
-class Group:
- file: pathlib.Path
- line_start: int
- line_end: int
-
-
-def compute_contiguous_groups(
- values: list[int], separators: set[int], joiners: set[int]
-) -> list[tuple[int, int]]:
- """
- Given a list of (sorted) values, a list of separators and a list of
- joiners, return a list of ranges (start, included end) describing groups of
- values.
-
- Groups are created by joining contiguous values together, and in some cases
- by merging groups, enclosing a gap of values between them. Gaps that may be
- enclosed are small gaps (<= MAX_ANNOTATION_GAP values after removing all
- joiners) where no line is a "separator"
- """
- contiguous_groups: list[tuple[int, int]] = []
- for _, contiguous_group in itertools.groupby(
- zip(values, itertools.count(1)), lambda x: x[1] - x[0]
- ):
- grouped_values = (e[0] for e in contiguous_group)
- first = next(grouped_values)
- try:
- *_, last = grouped_values
- except ValueError:
- last = first
- contiguous_groups.append((first, last))
-
- def reducer(
- acc: list[tuple[int, int]], group: tuple[int, int]
- ) -> list[tuple[int, int]]:
- if not acc:
- return [group]
-
- last_group = acc[-1]
- last_start, last_end = last_group
- next_start, next_end = group
-
- gap = set(range(last_end + 1, next_start)) - joiners
-
- gap_is_small = len(gap) <= MAX_ANNOTATION_GAP
- gap_contains_separators = gap & separators
-
- if gap_is_small and not gap_contains_separators:
- acc[-1] = (last_start, next_end)
- return acc
-
- acc.append(group)
- return acc
-
- return functools.reduce(reducer, contiguous_groups, [])
-
-
def get_diff_missing_groups(
coverage: coverage_module.Coverage,
diff_coverage: coverage_module.DiffCoverage,
-) -> Iterable[Group]:
+) -> Iterable[groups.Group]:
for path, diff_file in diff_coverage.files.items():
coverage_file = coverage.files[path]
@@ -87,12 +27,13 @@ def get_diff_missing_groups(
# they are separators.
joiners = set(diff_file.added_lines) - separators
- for start, end in compute_contiguous_groups(
+ for start, end in groups.compute_contiguous_groups(
values=diff_file.missing_statements,
separators=separators,
joiners=joiners,
+ max_gap=MAX_ANNOTATION_GAP,
):
- yield Group(
+ yield groups.Group(
file=path,
line_start=start,
line_end=end,
diff --git a/coverage_comment/groups.py b/coverage_comment/groups.py
new file mode 100644
index 00000000..7f44f164
--- /dev/null
+++ b/coverage_comment/groups.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import dataclasses
+import functools
+import itertools
+import pathlib
+
+
+@dataclasses.dataclass(frozen=True)
+class Group:
+ file: pathlib.Path
+ line_start: int
+ line_end: int
+
+
+def compute_contiguous_groups(
+ values: list[int], separators: set[int], joiners: set[int], max_gap: int
+) -> list[tuple[int, int]]:
+ """
+ Given a list of (sorted) values, a list of separators and a list of
+ joiners, return a list of ranges (start, included end) describing groups of
+ values.
+
+ Groups are created by joining contiguous values together, and in some cases
+ by merging groups, enclosing a gap of values between them. Gaps that may be
+ enclosed are small gaps (<= max_gap values after removing all joiners)
+ where no line is a "separator"
+ """
+ contiguous_groups: list[tuple[int, int]] = []
+ for _, contiguous_group in itertools.groupby(
+ zip(values, itertools.count(1)), lambda x: x[1] - x[0]
+ ):
+ grouped_values = (e[0] for e in contiguous_group)
+ first = next(grouped_values)
+ try:
+ *_, last = grouped_values
+ except ValueError:
+ last = first
+ contiguous_groups.append((first, last))
+
+ def reducer(
+ acc: list[tuple[int, int]], group: tuple[int, int]
+ ) -> list[tuple[int, int]]:
+ if not acc:
+ return [group]
+
+ last_group = acc[-1]
+ last_start, last_end = last_group
+ next_start, next_end = group
+
+ gap = set(range(last_end + 1, next_start)) - joiners
+
+ gap_is_small = len(gap) <= max_gap
+ gap_contains_separators = gap & separators
+
+ if gap_is_small and not gap_contains_separators:
+ acc[-1] = (last_start, next_end)
+ return acc
+
+ acc.append(group)
+ return acc
+
+ return functools.reduce(reducer, contiguous_groups, [])
diff --git a/coverage_comment/main.py b/coverage_comment/main.py
index 5fc6181f..701a1c14 100644
--- a/coverage_comment/main.py
+++ b/coverage_comment/main.py
@@ -156,12 +156,22 @@ def process_pr(
)
marker = template.get_marker(marker_id=config.SUBPROJECT_ID)
+
+ files_info, count_files = template.select_files(
+ coverage=coverage,
+ diff_coverage=diff_coverage,
+ previous_coverage=previous_coverage,
+ max_files=config.MAX_FILES_IN_COMMENT,
+ )
try:
comment = template.get_comment_markdown(
coverage=coverage,
diff_coverage=diff_coverage,
previous_coverage=previous_coverage,
previous_coverage_rate=previous_coverage_rate,
+ files=files_info,
+ count_files=count_files,
+ max_files=config.MAX_FILES_IN_COMMENT,
minimum_green=config.MINIMUM_GREEN,
minimum_orange=config.MINIMUM_ORANGE,
repo_name=config.GITHUB_REPOSITORY,
diff --git a/coverage_comment/settings.py b/coverage_comment/settings.py
index acc09c5e..ed0c27c1 100644
--- a/coverage_comment/settings.py
+++ b/coverage_comment/settings.py
@@ -60,6 +60,7 @@ class Config:
MERGE_COVERAGE_FILES: bool = False
ANNOTATE_MISSING_LINES: bool = False
ANNOTATION_TYPE: str = "warning"
+ MAX_FILES_IN_COMMENT: int = 25
VERBOSE: bool = False
# Only for debugging, not exposed in the action:
FORCE_WORKFLOW_RUN: bool = False
diff --git a/coverage_comment/template.py b/coverage_comment/template.py
index c22e4cac..7a397878 100644
--- a/coverage_comment/template.py
+++ b/coverage_comment/template.py
@@ -18,7 +18,6 @@
MARKER = (
""""""
)
-MAX_FILES = 100
def uptodate():
@@ -26,7 +25,9 @@ def uptodate():
class CommentLoader(jinja2.BaseLoader):
- def __init__(self, base_template: str, custom_template: str | None):
+ def __init__(
+ self, base_template: str, custom_template: str | None, debug: bool = False
+ ):
self.base_template = base_template
self.custom_template = custom_template
@@ -34,7 +35,11 @@ def get_source(
self, environment: jinja2.Environment, template: str
) -> tuple[str, str | None, Callable[..., bool]]:
if template == "base":
- return self.base_template, None, uptodate
+ return (
+ self.base_template,
+ "coverage_comment/template_files/comment.md.j2",
+ uptodate,
+ )
if self.custom_template and template == "custom":
return self.custom_template, None, uptodate
@@ -62,33 +67,38 @@ def pluralize(number, singular="", plural="s"):
def sign(val: int | decimal.Decimal) -> str:
- return "+" if val >= 0 else "" if val < 0 else "±"
+ return "+" if val > 0 else "" if val < 0 else "±"
def delta(val: int) -> str:
return f"({sign(val)}{val})"
+def remove_exponent(val: decimal.Decimal) -> decimal.Decimal:
+ # From https://docs.python.org/3/library/decimal.html#decimal-faq
+ return (
+ val.quantize(decimal.Decimal(1))
+ if val == val.to_integral()
+ else val.normalize()
+ )
+
+
def percentage_value(val: decimal.Decimal, precision: int = 2) -> decimal.Decimal:
- return val.quantize(
- decimal.Decimal("1." + ("0" * precision)), rounding=decimal.ROUND_DOWN
- ).normalize()
+ return remove_exponent(
+ (decimal.Decimal("100") * val).quantize(
+ decimal.Decimal("1." + ("0" * precision)),
+ rounding=decimal.ROUND_DOWN,
+ )
+ )
-def pct(val: decimal.Decimal | float, precision: int = 2) -> str:
- if isinstance(val, decimal.Decimal):
- val *= decimal.Decimal("100")
- rounded = percentage_value(val=val, precision=precision)
- return f"{rounded:f}%"
- else:
- return f"{val:.{precision}%}"
+def pct(val: decimal.Decimal, precision: int = 2) -> str:
+ rounded = percentage_value(val=val, precision=precision)
+ return f"{rounded:f}%"
-def pct_delta(val: decimal.Decimal, precision: int = 2) -> str:
- """
- Used for percentage point deltas
- """
- return f"({sign(val=val)}{percentage_value(val=val, precision=precision)})"
+def x100(val: decimal.Decimal):
+ return val * 100
@dataclasses.dataclass
@@ -113,6 +123,9 @@ def get_comment_markdown(
diff_coverage: coverage_module.DiffCoverage,
previous_coverage_rate: decimal.Decimal | None,
previous_coverage: coverage_module.Coverage | None,
+ files: list[FileInfo],
+ max_files: int | None,
+ count_files: int,
minimum_green: decimal.Decimal,
minimum_orange: decimal.Decimal,
repo_name: str,
@@ -127,27 +140,19 @@ def get_comment_markdown(
env = SandboxedEnvironment(loader=loader)
env.filters["pct"] = pct
env.filters["delta"] = delta
- env.filters["pct_delta"] = pct_delta
+ env.filters["x100"] = x100
+ env.filters["get_evolution_color"] = badge.get_evolution_badge_color
+ env.filters["generate_badge"] = badge.get_static_badge_url
+ env.filters["pluralize"] = pluralize
env.filters["file_url"] = functools.partial(
get_file_url, repo_name=repo_name, pr_number=pr_number
)
- env.filters["get_badge_color"] = lambda r: badge.get_badge_color(
- decimal.Decimal(r) * decimal.Decimal("100"),
+ env.filters["get_badge_color"] = functools.partial(
+ badge.get_badge_color,
minimum_green=minimum_green,
minimum_orange=minimum_orange,
)
- env.filters["get_evolution_color"] = badge.get_evolution_badge_color
- env.filters["generate_badge"] = badge.get_static_badge_url
- env.filters["pluralize"] = pluralize
- previous_coverage_files = previous_coverage.files if previous_coverage else {}
-
- # files contain the MAX_FILES files with the most new missing lines sorted by path
- files = select_files(
- coverage_files=coverage.files,
- diff_coverage_files=diff_coverage.files,
- previous_coverage_files=previous_coverage_files,
- )
missing_diff_lines = {
key: list(value)
for key, value in itertools.groupby(
@@ -157,13 +162,14 @@ def get_comment_markdown(
lambda x: x.file,
)
}
-
try:
comment = env.get_template("custom" if custom_template else "base").render(
previous_coverage_rate=previous_coverage_rate,
coverage=coverage,
diff_coverage=diff_coverage,
previous_coverage=previous_coverage,
+ count_files=count_files,
+ max_files=max_files,
files=files,
missing_diff_lines=missing_diff_lines,
subproject_id=subproject_id,
@@ -180,18 +186,21 @@ def get_comment_markdown(
def select_files(
- coverage_files: dict[pathlib.Path, coverage_module.FileCoverage],
- diff_coverage_files: dict[pathlib.Path, coverage_module.FileDiffCoverage],
- previous_coverage_files: dict[pathlib.Path, coverage_module.FileCoverage],
- max_files: int = MAX_FILES,
-) -> list[FileInfo]:
+ *,
+ coverage: coverage_module.Coverage,
+ diff_coverage: coverage_module.DiffCoverage,
+ previous_coverage: coverage_module.Coverage | None = None,
+ max_files: int | None,
+) -> tuple[list[FileInfo], int]:
"""
Selects the MAX_FILES files with the most new missing lines sorted by path
"""
+ previous_coverage_files = previous_coverage.files if previous_coverage else {}
+
files = []
- for path, coverage_file in coverage_files.items():
- diff_coverage_file = diff_coverage_files.get(path)
+ for path, coverage_file in coverage.files.items():
+ diff_coverage_file = diff_coverage.files.get(path)
previous_coverage_file = previous_coverage_files.get(path)
file_info = FileInfo(
@@ -210,8 +219,11 @@ def select_files(
if has_diff or has_evolution_from_previous:
files.append(file_info)
- files = sorted(files, key=lambda x: x.new_missing_lines)[:max_files]
- return sorted(files, key=lambda x: x.path)
+ count_files = len(files)
+ files = sorted(files, key=lambda x: len(x.new_missing_lines), reverse=True)
+ if max_files is not None:
+ files = files[:max_files]
+ return sorted(files, key=lambda x: x.path), count_files
def get_readme_markdown(
@@ -267,10 +279,11 @@ def read_template_file(template: str) -> str:
def get_file_url(
- repo_name: str,
- pr_number: int,
filename: pathlib.Path,
lines: tuple[int, int] | None = None,
+ *,
+ repo_name: str,
+ pr_number: int,
) -> str:
# To link to a file in a PR, GitHub uses the link to the file overview combined with a SHA256 hash of the file path
s = f"https://github.com/{repo_name}/pull/{pr_number}/files#diff-{hashlib.sha256(str(filename).encode('utf-8')).hexdigest()}"
diff --git a/coverage_comment/template_files/comment.md.j2 b/coverage_comment/template_files/comment.md.j2
index 28ca57fe..650ade22 100644
--- a/coverage_comment/template_files/comment.md.j2
+++ b/coverage_comment/template_files/comment.md.j2
@@ -1,85 +1,181 @@
-{%- block title -%}## Coverage report{%- if subproject_id -%} ({{ subproject_id }}){%- endif -%}{%- endblock title -%}
+{%- block title -%}## Coverage report{%- if subproject_id %} ({{ subproject_id }}){%- endif -%}{%- endblock title%}
{# Coverage evolution badge #}
-{%- block coverage_badges -%}
+{% block coverage_badges -%}
{%- block coverage_evolution_badge -%}
{%- if previous_coverage_rate %}
{%- set text = "Coverage for the whole project went from " ~ (previous_coverage_rate | pct) ~ " to " ~ (coverage.info.percent_covered | pct) -%}
{%- set color = (coverage.info.percent_covered - previous_coverage_rate) | get_evolution_color(neutral_color='blue') -%}
-
+
{%- else -%}
{%- set text = "Coverage for the whole project is " ~ (coverage.info.percent_covered | pct) ~ ". Previous coverage rate is not available, cannot report on evolution." -%}
-{%- set color = coverage.info.percent_covered | get_badge_color -%}
-
+{%- set color = coverage.info.percent_covered | x100 | get_badge_color -%}
+
{%- endif -%}
{%- endblock coverage_evolution_badge -%}
-{# Coverage diff badge #}
+{#- Coverage diff badge -#}
+{#- space #} {# space -#}
{%- block diff_coverage_badge -%}
{%- set text = (diff_coverage.total_percent_covered | pct) ~ " of the statement lines added by this PR are covered" -%}
-
+
{%- endblock diff_coverage_badge -%}
{%- endblock coverage_badges -%}
+
+{%- macro statements_badge(path, statements_count, previous_statements_count) -%}
+{% if previous_statements_count is not none -%}
+{% set statements_diff = statements_count - previous_statements_count %}
+{% if statements_diff > 0 -%}
+{% set text = "This PR adds " ~ statements_diff ~ " to the number of statements in " ~ path ~ ", taking it from " ~ previous_statements_count ~ " to " ~ statements_count ~"." -%}
+{% set color = "007ec6" -%}
+{% elif statements_diff < 0 -%}
+{% set text = "This PR removes " ~ (-statements_diff) ~ " from the number of statements in " ~ path ~ ", taking it from " ~ previous_statements_count ~ " to " ~ statements_count ~"." -%}
+{% set color = "49c2ee" -%}
+{% else -%}
+{% set text = "This PR doesn't change the number of statements in " ~ path ~ ", which is " ~ statements_count ~ "." -%}
+{% set color = "5d89ba" -%}
+{% endif -%}
+{% set message = statements_diff %}
+{% else -%}
+{% set text = "This PR adds " ~ statements_count ~ " statement" ~ (statements_count | pluralize) ~ " to " ~ path ~ ". The file did not seem to exist on the base branch." -%}
+{% set color = "007ec6" -%}
+{% set message = statements_count %}
+{% endif -%}
+
|
+
+{%- endmacro -%}
+
+{%- macro missing_lines_badge(path, missing_lines_count, previous_missing_lines_count) -%}
+{%- if previous_missing_lines_count is not none -%}
+{%- set missing_diff = missing_lines_count - previous_missing_lines_count %}
+{%- if missing_diff > 0 -%}
+{%- set text = "This PR adds " ~ missing_diff ~ " to the number of statements missing coverage in " ~ path ~ ", taking it from " ~ previous_missing_lines_count ~ " to " ~ missing_lines_count ~ "." -%}
+{%- elif missing_diff < 0 -%}
+{%- set text = "This PR removes " ~ (-missing_diff) ~ " from the number of statements missing coverage in " ~ path ~ ", taking it from " ~ previous_missing_lines_count ~ " to " ~ missing_lines_count ~ "." -%}
+{%- else -%}
+{%- set text = "This PR doesn't change the number of statements missing coverage in " ~ path ~ ", which is " ~ missing_lines_count ~ "." -%}
+{%- endif -%}
+{%- set message = missing_diff -%}
+{%- else -%}
+{%- set text = "This PR adds " ~ missing_lines_count ~ " statement" ~ (statements_count | pluralize) ~ " missing coverage to " ~ path ~ ". The file did not seem to exist on the base branch." -%}
+{%- set message = missing_lines_count -%}
+{%- endif -%}
+{%- set color = message | get_evolution_color(up_is_good=false) -%}
+ |
+
+{%- endmacro -%}
+
+{%- macro coverage_rate_badge(path, previous_percent_covered, previous_covered_statements_count, previous_statements_count, percent_covered, covered_statements_count, statements_count) -%}
+{%- if previous_percent_covered is not none -%}
+{%- set coverage_diff = percent_covered - previous_percent_covered -%}
+{%- if coverage_diff > 0 -%}
+{%- set text = "This PR adds " ~ ("{:.02f}".format(coverage_diff * 100)) ~ " percentage points to the coverage rate in " ~ path ~ ", taking it from " ~ previous_percent_covered | pct ~ " (" ~ previous_covered_statements_count ~ "/" ~ previous_statements_count ~ ") to " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%}
+{%- elif coverage_diff < 0 -%}
+{%- set text = "This PR removes " ~ ("{:.02f}".format(-coverage_diff * 100)) ~ " percentage points from the coverage rate in " ~ path ~ ", taking it from " ~ previous_percent_covered | pct ~ " (" ~ previous_covered_statements_count ~ "/" ~ previous_statements_count ~ ") to " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%}
+{%- else -%}
+{%- set text = "This PR doesn't change the coverage rate in " ~ path ~ ", which is " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%}
+{%- endif -%}
+{%- set color = coverage_diff | get_evolution_color() -%}
+{%- set message = "(" ~ previous_covered_statements_count ~ "/" ~ previous_statements_count ~ " > " ~ covered_statements_count ~ "/" ~ statements_count ~ ")" -%}
+{%- else -%}
+{%- set text = "The coverage rate of " ~ path ~ " is " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ "). The file did not seem to exist on the base branch." -%}
+{%- set message = "(" ~ covered_statements_count ~ "/" ~ statements_count ~ ")" -%}
+{%- set color = percent_covered | x100 | get_badge_color -%}
+{%- endif -%}
+ |
+
+{%- 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 label = (percent_covered | pct(precision=0)) -%}
+{% set message = "(" ~ covered_statements_count ~ "/" ~ added_statements_count ~ ")" -%}
+{%- set color = (percent_covered | x100 | get_badge_color()) -%}
+{% else -%}
+{% set text = "This PR does not seem to add statements to " ~ path ~ "." -%}
+{% set label = "" -%}
+{%- set color = "grey" -%}
+{% set message = "N/A" -%}
+{% endif -%}
+ |
+
+{%- endmacro -%}
+
+
{# Individual file report #}
{%- block coverage_by_file -%}
{%- if not files -%}
_This PR does not seem to contain any modification to coverable code._
{%- else -%}
Click to see where and how coverage changed
- File | Statements | Missing | Coverage | Coverage (new lines) | Lines missing |
+ File | Statements | Missing | Coverage | Coverage (new stmts) | Lines missing |
-{%- for parent, files_in_folder in groupby(attribute="path.parent") -%}
+{%- for parent, files_in_folder in files|groupby(attribute="path.parent") -%}
- {{ filename }} |
+ {{ parent }} |
{%- for file in files_in_folder -%}
{%- set path = file.coverage.path -%}
- {{ diff_file.path.name }} |
+ {{ path.name }} |
{#- Statements cell -#}
+{%- block statements_badge_cell scoped -%}
{{- statements_badge(
path=path,
statements_count=file.coverage.info.num_statements,
- previous_statements_count=(file.previous.coverage.info.num_statements if file.previous else None),
+ previous_statements_count=(file.previous.info.num_statements if file.previous else none),
) -}}
+{%- endblock statements_badge_cell-%}
{#- Missing cell -#}
+{%- block missing_lines_badge_cell scoped -%}
{{- missing_lines_badge(
path=path,
- statements_count=file.coverage.info.missing_lines,
- previous_statements_count=(file.previous.coverage.info.missing_lines if file.previous else None),
+ missing_lines_count=file.coverage.info.missing_lines,
+ previous_missing_lines_count=(file.previous.info.missing_lines if file.previous else none),
) -}}
+{%- endblock missing_lines_badge_cell -%}
{#- Coverage rate -#}
+{%- block coverage_rate_badge_cell scoped -%}
{{- coverage_rate_badge(
path=path,
- coverage_rate=file.coverage.info.percent_covered,
- previous_coverage_rate=(file.previous.coverage.info.percent_covered if file.previous else None),
+ previous_percent_covered=(file.previous.info.percent_covered if file.previous else none),
+ previous_covered_statements_count=(file.previous.info.covered_lines if file.previous else none),
+ previous_statements_count=(file.previous.info.num_statements if file.previous else none),
+ percent_covered=file.coverage.info.percent_covered,
+ covered_statements_count=file.coverage.info.covered_lines,
+ statements_count=file.coverage.info.num_statements,
) -}}
+{%- endblock coverage_rate_badge_cell -%}
-{# Coverage of added lines #}
+{#- Coverage of added lines -#}
+{%- block diff_coverage_rate_badge_cell scoped -%}
{{- diff_coverage_rate_badge(
path=path,
- added_statements_count=(file.diff.added_statements | length if file.diff else None),
- covered_statements_count=(file.diff.covered_statements | length if file.diff else None),
- percent_covered=(file.diff.percent_covered if file.diff else None)
+ added_statements_count=((file.diff.added_statements | length) if file.diff else none),
+ covered_statements_count=((file.diff.covered_statements | length) if file.diff else none),
+ percent_covered=(file.diff.percent_covered if file.diff else none)
) -}}
+{%- endblock diff_coverage_rate_badge_cell -%}
-{# Link to missing lines #}
+{#- Link to missing lines -#}
+{%- block link_to_missing_diff_lines_cell scoped -%}
{%- set comma = joiner() -%}
{%- for group in missing_diff_lines.get(path, []) -%}
-
-
{{- comma() -}}
+
+
{{- group.line_start -}}
{%- if group.line_start != group.line_end -%}
-
@@ -90,6 +186,7 @@ _This PR does not seem to contain any modification to coverable code._
{%- endfor -%}
|
+{%- endblock link_to_missing_diff_lines_cell -%}
{%- endfor -%}
{%- endfor -%}
@@ -97,124 +194,71 @@ _This PR does not seem to contain any modification to coverable code._
Project Total |
+
{#- Statements cell -#}
+{%- block statements_badge_total_cell scoped -%}
{{- statements_badge(
path="the whole project",
statements_count=coverage.info.num_statements,
- previous_statements_count=(previous_coverage.info.num_statements if previous_coverage else None),
+ previous_statements_count=(previous_coverage.info.num_statements if previous_coverage else none),
) -}}
+{%- endblock statements_badge_total_cell -%}
{#- Missing cell -#}
+{%- block missing_lines_badge_total_cell scoped -%}
{{- missing_lines_badge(
path="the whole project",
- statements_count=coverage.info.missing_lines,
- previous_statements_count=(previous_coverage.info.missing_lines if previous_coverage else None),
+ missing_lines_count=coverage.info.missing_lines,
+ previous_missing_lines_count=(previous_coverage.info.missing_lines if previous_coverage else none),
) -}}
+{%- endblock missing_lines_badge_total_cell -%}
{#- Coverage rate -#}
+{%- block coverage_rate_badge_total_cell scoped -%}
{{- coverage_rate_badge(
path="the whole project",
- coverage_rate=coverage.info.percent_covered,
- previous_coverage_rate=(previous_coverage.info.percent_covered if previous_coverage else None),
+ previous_percent_covered=(previous_coverage.info.percent_covered if previous_coverage else none),
+ previous_covered_statements_count=(previous_coverage.info.covered_lines if previous_coverage else none),
+ previous_statements_count=(previous_coverage.info.num_statements if previous_coverage else none),
+ percent_covered=coverage.info.percent_covered,
+ covered_statements_count=coverage.info.covered_lines,
+ statements_count=coverage.info.num_statements,
) -}}
+{%- endblock coverage_rate_badge_total_cell -%}
{# Coverage of added lines #}
+{%- block diff_coverage_rate_badge_total_cell scoped -%}
{{- diff_coverage_rate_badge(
path="the whole project",
- added_statements_count=(file.diff.added_statements | length if file.diff else None),
- covered_statements_count=(file.diff.covered_statements | length if file.diff else None),
- percent_covered=(file.diff.percent_covered if file.diff else None)
+ added_statements_count=diff_coverage.total_num_lines,
+ covered_statements_count=(diff_coverage.total_num_lines-diff_coverage.total_num_violations),
+ percent_covered=diff_coverage.total_percent_covered,
) -}}
+{%- endblock diff_coverage_rate_badge_total_cell -%}
|
-{%- endblock coverage_by_file -%}
+{%- if max_files and count_files > max_files %}
-This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)
-
+> [!NOTE]
+> The report is truncated to {{ max_files }} files out of {{ count_files }}. To see the full report, please visit the workflow summary page.
-{{ marker -}}
-
-
-{%- macro statements_badge(path, statements_count, previous_statements_count) -%}
-{% if previous_statements_count is not None -%}
-{% set statements_diff = statements_countprevious_statements_count - previous_statements_count %}
-{% if statements_diff > 0 -%}
-{% set text = "This PR adds " ~ statements_diff ~ " to the number of statements in " ~ path ~ ", taking it from " ~ previous_statements_count ~ " to " ~ statements_countprevious_statements_count ~". The number of new statements is at least " ~ statements_diff ~ " but it may be higher if other statements were simultaneously removed." -%}
-{% set color = "007ec6" -%}
-{% elif statements_diff < 0 -%}
-{% set text = "This PR removes " ~ (-statements_diff) ~ " from the number of statements in " ~ path ~ ", taking it from " ~ previous_statements_count ~ " to " ~ statements_countprevious_statements_count ~". The number of deleted statements is at least " ~ (-statements_diff) ~ " but it may be higher if other statements were simultaneously added." -%}
-{% set color = "49c2ee" -%}
-{% else -%}
-{% set text = "This PR doesn't change the number of statements in " ~ path ~ ", which is " ~ statements_countprevious_statements_count ~ ". Either the file was mostly unmodified, or the same number of statements was simultenousely added to and removed from the file." -%}
-{% set color = "5d89ba" -%}
-{% endif -%}
-{% set message = statements_diff %}
-{% else -%}
-{% set text = "This PR adds " ~ statements_countprevious_statements_count ~ (" statements" | pluralize) ~ " to " ~ path ~ ". The file did not seem to exist on the base branch." -%}
-{% set color = "007ec6" -%}
-{% set message = statements_countprevious_statements_count %}
{% endif -%}
- |
-{%- endmacro -%}
-
-{%- macro missing_lines_badge(path, missing_lines_count, previous_missing_lines_count) -%}
-{%- if previous_missing_lines_count is not None -%}
-{%- set missing_diff = missing_lines_count - previous_missing_lines_count %}
-{%- if missing_diff > 0 -%}
-{%- set text = "This PR adds " ~ missing_diff ~ " to the number of statements missing coverage in " ~ path ~ ", taking it from " ~ previous_missing_lines_count ~ " to " ~ missing_lines_count ~ ". This may be the result of adding untested statements and/or removing the tests that were covering existing statements. Also, it's possible that more non-covered statements were added, while other non-covered statements were removed elsewhere in the file." -%}
-{%- elif missing_diff < 0 -%}
-{%- set text = "This PR removes " ~ (-missing_diff) ~ " from the number of statements missing coverage in " ~ path ~ ", taking it from " ~ previous_missing_lines_count ~ " to " ~ missing_lines_count ~ ". This may be the result of removing untested statements and/or adding tests to cover existing non-covered statements. Also, it's possible that more non-covered statements were removed, while other non-covered statements were added elsewhere in the file." -%}
-{%- else -%}
-{%- set text = "This PR doesn't change the number of statements missing coverage in " ~ path ~ ", which is " ~ missing_lines_count ~ ". Either the modifications in the file were only to covered lines, or the same number of statements missing coverage was simultenousely added to and removed from the file." -%}
{%- endif -%}
-{%- set message = missing_diff -%}
-{%- else -%}
-{%- set text = "This PR adds " ~ missing_lines_count ~ (" statements" | pluralize) ~ " missing coverage to " ~ path ~ ". The file did not seem to exist on the base branch." -%}
-{%- set message = missing_lines_count -%}
-{%- endif -%}
-{%- set color = message | get_evolution_color(up_is_good=False) -%}
- |
+{%- endblock coverage_by_file -%}
-{%- endmacro -%}
+{%- block footer -%}
+
-{%- macro coverage_rate_badge(path, coverage_rate, previous_coverage_rate) -%}
-{%- if previous_coverage_rate is not None -%}
-{%- set coverage_diff = coverage_rate - previous_coverage_rate -%}
-{%- if coverage_diff > 0 -%}
-{%- set text = "This PR adds " ~ (coverage_diff * 100) ~ " percentage points to the coverage rate (number of statements covered by the tests / total number of statements) in " ~ path ~ ", taking it from " ~ previous_coverage_rate | pct ~ " to " ~ coverage_rate | pct ~ "." -%}
-{%- elif coverage_diff < 0 -%}
-{%- set text = "This PR removes " ~ (-coverage_diff * 100) ~ " percentage points from the coverage rate (number of statements covered by the tests / total number of statements) in " ~ path ~ ", taking it from " ~ previous_coverage_rate | pct ~ " to " ~ coverage_rate | pct ~ "." -%}
-{%- else -%}
-{%- set text = "This PR doesn't change the coverage rate (number of statements covered by the tests / total number of statements) in " ~ path ~ ", which is " ~ coverage_rate | pct ~ "." -%}
-{%- endif -%}
-{%- set color = coverage_diff | get_evolution_color(up_is_good=False) -%}
-{%- set message = (coverage_diff | pct_delta(precision=0)) -%}
-{%- else -%}
-{%- set text = "The coverage rate (number of statements covered by the tests / total number of statements) of " ~ path ~ " is " ~ coverage_rate | pct ~ ". The file did not seem to exist on the base branch." -%}
-{%- set message = "-" -%}
-{%- set color = "grey" -%}
-{%- endif -%}
- |
+This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)
-{%- endmacro -%}
+
-{%- macro diff_coverage_rate_badge(path, added_statements_count, covered_statements_count, percent_covered) -%}
-{% if added_statements_count -%}
-{% set text = "This PR adds " ~ (added_statements_count) ~ " statements to " ~ path ~ ", " ~ (percent_covered) ~ " of which are covered, the coverage rate on the diff is " ~ percent_covered | pct ~ "." -%}
-{% set label = (percent_covered | pct(precision=0)) -%}
-{% set message = "(" ~ percent_covered ~ "/" ~ added_statements_count ~ ")" -%}
-{%- set color = (percent_covered | get_badge_color()) -%}
-{% else -%}
-{% set text = "This PR does not seem to add statements to " ~ path ~ "." -%}
-{% set label = "-" -%}
-{%- set color = "grey" -%}
-{% set message = "-" -%}
-{% endif -%}
- |
+{%- endblock footer -%}
+
-{%- endmacro -%}
+{{ marker -}}
diff --git a/coverage_comment/template_files/log.txt.j2 b/coverage_comment/template_files/log.txt.j2
index b8d252c2..affa2fae 100644
--- a/coverage_comment/template_files/log.txt.j2
+++ b/coverage_comment/template_files/log.txt.j2
@@ -1,4 +1,4 @@
-{% if subproject_id %} Coverage info for {{ subproject_id }}:
+{% if subproject_id %}Coverage info for {{ subproject_id }}:
{% endif -%}
{% if is_public -%}
diff --git a/pyproject.toml b/pyproject.toml
index 978ddc30..57cfedcb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,7 +34,7 @@ build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
addopts = """
--cov-report term-missing --cov-branch --cov-report html --cov-report term
- --cov=coverage_comment -vv --strict-markers -rfE
+ --cov=coverage_comment --cov-context=test -vv --strict-markers -rfE
--ignore=tests/end_to_end/repo
"""
testpaths = ["tests/unit", "tests/integration", "tests/end_to_end"]
@@ -53,6 +53,9 @@ relative_files = true
[tool.coverage.report]
exclude_also = ["\\.\\.\\."]
+[tool.coverage.html]
+show_contexts = true
+
[tool.mypy]
no_implicit_optional = true
diff --git a/tests/conftest.py b/tests/conftest.py
index e7700c96..c7f44601 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -74,208 +74,6 @@ def _(**kwargs):
return _
-@pytest.fixture
-def coverage_json():
- return {
- "meta": {
- "version": "1.2.3",
- "timestamp": "2000-01-01T00:00:00",
- "branch_coverage": True,
- "show_contexts": False,
- },
- "files": {
- "codebase/code.py": {
- "executed_lines": [1, 2, 5, 6, 9],
- "summary": {
- "covered_lines": 5,
- "num_statements": 6,
- "percent_covered": 75.0,
- "missing_lines": 1,
- "excluded_lines": 0,
- "num_branches": 2,
- "num_partial_branches": 1,
- "covered_branches": 1,
- "missing_branches": 1,
- },
- "missing_lines": [7, 9],
- "excluded_lines": [],
- }
- },
- "totals": {
- "covered_lines": 5,
- "num_statements": 6,
- "percent_covered": 75.0,
- "missing_lines": 1,
- "excluded_lines": 0,
- "num_branches": 2,
- "num_partial_branches": 1,
- "covered_branches": 1,
- "missing_branches": 1,
- },
- }
-
-
-@pytest.fixture
-def coverage_obj():
- return coverage_module.Coverage(
- meta=coverage_module.CoverageMetadata(
- version="1.2.3",
- timestamp=datetime.datetime(2000, 1, 1),
- branch_coverage=True,
- show_contexts=False,
- ),
- info=coverage_module.CoverageInfo(
- covered_lines=5,
- num_statements=6,
- percent_covered=decimal.Decimal("0.75"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=2,
- num_partial_branches=1,
- covered_branches=1,
- missing_branches=1,
- ),
- files={
- pathlib.Path("codebase/code.py"): coverage_module.FileCoverage(
- path=pathlib.Path("codebase/code.py"),
- executed_lines=[1, 2, 5, 6, 9],
- missing_lines=[7, 9],
- excluded_lines=[],
- info=coverage_module.CoverageInfo(
- covered_lines=5,
- num_statements=6,
- percent_covered=decimal.Decimal("0.75"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=2,
- num_partial_branches=1,
- covered_branches=1,
- missing_branches=1,
- ),
- )
- },
- )
-
-
-@pytest.fixture
-def coverage_obj_no_branch():
- return coverage_module.Coverage(
- meta=coverage_module.CoverageMetadata(
- version="1.2.3",
- timestamp=datetime.datetime(2000, 1, 1),
- branch_coverage=False,
- show_contexts=False,
- ),
- info=coverage_module.CoverageInfo(
- covered_lines=5,
- num_statements=6,
- percent_covered=decimal.Decimal("0.75"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=None,
- num_partial_branches=None,
- covered_branches=None,
- missing_branches=None,
- ),
- files={
- pathlib.Path("codebase/code.py"): coverage_module.FileCoverage(
- path=pathlib.Path("codebase/code.py"),
- executed_lines=[1, 2, 5, 6, 9],
- missing_lines=[7],
- excluded_lines=[],
- info=coverage_module.CoverageInfo(
- covered_lines=5,
- num_statements=6,
- percent_covered=decimal.Decimal("0.8333"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=None,
- num_partial_branches=None,
- covered_branches=None,
- missing_branches=None,
- ),
- )
- },
- )
-
-
-@pytest.fixture
-def coverage_obj_more_files(coverage_obj_no_branch):
- coverage_obj_no_branch.files[
- pathlib.Path("codebase/other.py")
- ] = coverage_module.FileCoverage(
- path=pathlib.Path("codebase/other.py"),
- executed_lines=[10, 11, 12],
- missing_lines=[13],
- excluded_lines=[],
- info=coverage_module.CoverageInfo(
- covered_lines=3,
- num_statements=4,
- percent_covered=decimal.Decimal("0.75"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=None,
- num_partial_branches=None,
- covered_branches=None,
- missing_branches=None,
- ),
- )
- return coverage_obj_no_branch
-
-
-@pytest.fixture
-def make_coverage_obj(coverage_obj_more_files):
- def f(**kwargs):
- obj = coverage_obj_more_files
- for key, value in kwargs.items():
- vars(obj.files[pathlib.Path(key)]).update(value)
- return obj
-
- return f
-
-
-@pytest.fixture
-def diff_coverage_obj():
- return coverage_module.DiffCoverage(
- total_num_lines=5,
- total_num_violations=1,
- total_percent_covered=decimal.Decimal("0.8"),
- num_changed_lines=39,
- files={
- pathlib.Path("codebase/code.py"): coverage_module.FileDiffCoverage(
- path=pathlib.Path("codebase/code.py"),
- percent_covered=decimal.Decimal("0.8"),
- missing_statements=[3, 7, 8, 9, 12],
- added_lines=[3, 4, 5, 6, 7, 8, 9, 12],
- )
- },
- )
-
-
-@pytest.fixture
-def diff_coverage_obj_many_missing_lines():
- return coverage_module.DiffCoverage(
- total_num_lines=5,
- total_num_violations=1,
- total_percent_covered=decimal.Decimal("0.8"),
- num_changed_lines=39,
- files={
- pathlib.Path("codebase/code.py"): coverage_module.FileDiffCoverage(
- path=pathlib.Path("codebase/code.py"),
- percent_covered=decimal.Decimal("0.8"),
- missing_statements=[7, 9],
- added_lines=[7, 8, 9],
- ),
- pathlib.Path("codebase/main.py"): coverage_module.FileDiffCoverage(
- path=pathlib.Path("codebase/code.py"),
- percent_covered=decimal.Decimal("0.8"),
- missing_statements=[1, 2, 8, 17],
- added_lines=[1, 2, 3, 4, 5, 6, 7, 8, 17],
- ),
- },
- )
-
-
@pytest.fixture
def session(is_failed):
"""
@@ -465,3 +263,296 @@ def f():
yield f
_is_failed.clear()
+
+
+@pytest.fixture
+def make_coverage():
+ def _(code: str, has_branches: bool = True) -> coverage_module.Coverage:
+ current_file = None
+ coverage_obj = coverage_module.Coverage(
+ meta=coverage_module.CoverageMetadata(
+ version="1.2.3",
+ timestamp=datetime.datetime(2000, 1, 1),
+ branch_coverage=True,
+ show_contexts=False,
+ ),
+ info=coverage_module.CoverageInfo(
+ covered_lines=0,
+ num_statements=0,
+ percent_covered=decimal.Decimal("1.0"),
+ missing_lines=0,
+ excluded_lines=0,
+ num_branches=0 if has_branches else None,
+ num_partial_branches=0 if has_branches else None,
+ covered_branches=0 if has_branches else None,
+ missing_branches=0 if has_branches else None,
+ ),
+ files={},
+ )
+ line_number = 0
+ # (we start at 0 because the first line will be empty for readabilty)
+ for line in code.splitlines()[1:]:
+ line = line.strip()
+ if line.startswith("# file: "):
+ current_file = pathlib.Path(line.split("# file: ")[1])
+ continue
+ assert current_file, (line, current_file, code)
+ line_number += 1
+ if coverage_obj.files.get(current_file) is None:
+ coverage_obj.files[current_file] = coverage_module.FileCoverage(
+ path=current_file,
+ executed_lines=[],
+ missing_lines=[],
+ excluded_lines=[],
+ info=coverage_module.CoverageInfo(
+ covered_lines=0,
+ num_statements=0,
+ percent_covered=decimal.Decimal("1.0"),
+ missing_lines=0,
+ excluded_lines=0,
+ num_branches=0 if has_branches else None,
+ num_partial_branches=0 if has_branches else None,
+ covered_branches=0 if has_branches else None,
+ missing_branches=0 if has_branches else None,
+ ),
+ )
+ if set(line.split()) & {
+ "covered",
+ "missing",
+ "excluded",
+ "partial",
+ "branch",
+ }:
+ coverage_obj.files[current_file].info.num_statements += 1
+ coverage_obj.info.num_statements += 1
+ if "covered" in line or "partial" in line:
+ coverage_obj.files[current_file].executed_lines.append(line_number)
+ coverage_obj.files[current_file].info.covered_lines += 1
+ coverage_obj.info.covered_lines += 1
+ elif "missing" in line:
+ coverage_obj.files[current_file].missing_lines.append(line_number)
+ coverage_obj.files[current_file].info.missing_lines += 1
+ coverage_obj.info.missing_lines += 1
+ elif "excluded" in line:
+ coverage_obj.files[current_file].excluded_lines.append(line_number)
+ coverage_obj.files[current_file].info.excluded_lines += 1
+ coverage_obj.info.excluded_lines += 1
+
+ if has_branches and "branch" in line:
+ coverage_obj.files[current_file].info.num_branches += 1
+ coverage_obj.info.num_branches += 1
+ if "branch partial" in line:
+ coverage_obj.files[current_file].info.num_partial_branches += 1
+ coverage_obj.info.num_partial_branches += 1
+ elif "branch covered" in line:
+ coverage_obj.files[current_file].info.covered_branches += 1
+ coverage_obj.info.covered_branches += 1
+ elif "branch missing" in line:
+ coverage_obj.files[current_file].info.missing_branches += 1
+ coverage_obj.info.missing_branches += 1
+
+ info = coverage_obj.files[current_file].info
+ coverage_obj.files[
+ current_file
+ ].info.percent_covered = coverage_module.compute_coverage(
+ num_covered=info.covered_lines,
+ num_total=info.num_statements,
+ )
+
+ info = coverage_obj.info
+ coverage_obj.info.percent_covered = coverage_module.compute_coverage(
+ num_covered=info.covered_lines,
+ num_total=info.num_statements,
+ )
+
+ return coverage_obj
+
+ return _
+
+
+@pytest.fixture
+def make_diff_coverage():
+ return coverage_module.get_diff_coverage_info
+
+
+@pytest.fixture
+def make_coverage_and_diff(make_coverage, make_diff_coverage):
+ def _(code: str) -> tuple[coverage_module.Coverage, coverage_module.DiffCoverage]:
+ added_lines: dict[pathlib.Path, list[int]] = {}
+ new_code = ""
+ current_file = None
+ # (we start at 0 because the first line will be empty for readabilty)
+ line_number = 0
+ for line in code.splitlines()[1:]:
+ line = line.strip()
+ if line.startswith("# file: "):
+ new_code += line + "\n"
+ current_file = pathlib.Path(line.split("# file: ")[1])
+ line_number = 0
+ continue
+ assert current_file
+ line_number += 1
+
+ if line.startswith("+ "):
+ added_lines.setdefault(current_file, []).append(line_number)
+ new_code += line[2:] + "\n"
+ else:
+ new_code += line + "\n"
+
+ coverage = make_coverage("\n" + new_code)
+ return coverage, make_diff_coverage(added_lines=added_lines, coverage=coverage)
+
+ return _
+
+
+@pytest.fixture
+def coverage_code():
+ return """
+ # file: codebase/code.py
+ 1 covered
+ 2 covered
+ 3 covered
+ 4
+ 5 branch partial
+ 6 missing
+ 7
+ 8 missing
+ 9
+ 10 branch missing
+ 11 missing
+ 12
+ 13 branch covered
+ 14 covered
+ """
+
+
+@pytest.fixture
+def coverage_json():
+ return {
+ "meta": {
+ "version": "1.2.3",
+ "timestamp": "2000-01-01T00:00:00",
+ "branch_coverage": True,
+ "show_contexts": False,
+ },
+ "files": {
+ "codebase/code.py": {
+ "executed_lines": [1, 2, 3, 5, 13, 14],
+ "summary": {
+ "covered_lines": 6,
+ "num_statements": 10,
+ "percent_covered": 60.0,
+ "missing_lines": 4,
+ "excluded_lines": 0,
+ "num_branches": 3,
+ "num_partial_branches": 1,
+ "covered_branches": 1,
+ "missing_branches": 1,
+ },
+ "missing_lines": [6, 8, 10, 11],
+ "excluded_lines": [],
+ }
+ },
+ "totals": {
+ "covered_lines": 6,
+ "num_statements": 10,
+ "percent_covered": 60.0,
+ "missing_lines": 4,
+ "excluded_lines": 0,
+ "num_branches": 3,
+ "num_partial_branches": 1,
+ "covered_branches": 1,
+ "missing_branches": 1,
+ },
+ }
+
+
+@pytest.fixture
+def coverage_obj(make_coverage, coverage_code):
+ return make_coverage(coverage_code)
+
+
+@pytest.fixture
+def coverage_obj_no_branch_code():
+ return """
+ # file: codebase/code.py
+ covered
+ covered
+ missing
+
+ covered
+ missing
+
+ missing
+ missing
+ covered
+ """
+
+
+@pytest.fixture
+def coverage_obj_no_branch(make_coverage, coverage_obj_no_branch_code):
+ return make_coverage(coverage_obj_no_branch_code, has_branches=False)
+
+
+@pytest.fixture
+def coverage_obj_more_files(make_coverage):
+ return make_coverage(
+ """
+ # file: codebase/code.py
+ covered
+ covered
+ covered
+
+ branch partial
+ missing
+
+ missing
+
+ branch missing
+ missing
+
+ branch covered
+ covered
+ # file: codebase/other.py
+
+
+ missing
+ covered
+ missing
+ missing
+
+ missing
+ covered
+ covered
+ """
+ )
+
+
+@pytest.fixture
+def make_coverage_obj(coverage_obj_more_files):
+ def f(**kwargs):
+ obj = coverage_obj_more_files
+ for key, value in kwargs.items():
+ vars(obj.files[pathlib.Path(key)]).update(value)
+ return obj
+
+ return f
+
+
+@pytest.fixture
+def diff_coverage_obj(coverage_obj, make_diff_coverage):
+ return make_diff_coverage(
+ added_lines={pathlib.Path("codebase/code.py"): [3, 4, 5, 6, 7, 8, 9, 12]},
+ coverage=coverage_obj,
+ )
+
+
+@pytest.fixture
+def diff_coverage_obj_more_files(coverage_obj_more_files, make_diff_coverage):
+ return make_diff_coverage(
+ added_lines={
+ pathlib.Path("codebase/code.py"): [3, 4, 5, 6, 7, 8, 9, 12],
+ pathlib.Path("codebase/other.py"): [1, 2, 3, 4, 5, 6, 7, 8, 17],
+ },
+ coverage=coverage_obj_more_files,
+ )
diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py
index 066a9656..ca2d5bad 100644
--- a/tests/integration/test_github.py
+++ b/tests/integration/test_github.py
@@ -346,13 +346,13 @@ def test_annotations(capsys):
annotation_type="warning",
annotations=[
(pathlib.Path("codebase/code.py"), 1, 3),
- (pathlib.Path("codebase/main.py"), 5, 5),
+ (pathlib.Path("codebase/other.py"), 5, 5),
],
)
expected = """::group::Annotations of lines with missing coverage
::warning file=codebase/code.py,line=1,endLine=3,title=Missing coverage::Missing coverage on lines 1-3
-::warning file=codebase/main.py,line=5,endLine=5,title=Missing coverage::Missing coverage on line 5
+::warning file=codebase/other.py,line=5,endLine=5,title=Missing coverage::Missing coverage on line 5
::endgroup::"""
output = capsys.readouterr()
assert output.err.strip() == expected
diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py
index 72fd079b..a2419e90 100644
--- a/tests/integration/test_main.py
+++ b/tests/integration/test_main.py
@@ -192,10 +192,16 @@ def checker(payload):
comment_file = pathlib.Path("python-coverage-comment-action.txt").read_text()
assert comment == comment_file
assert comment == summary_file.read_text()
- assert "Coverage for the whole project went from unknown to 77.77%" in comment
- assert "75% of the code lines added by this PR are covered" in comment
assert (
- "### [foo.py](https://github.com/py-cov-action/foobar/pull/2/files#diff-b08fd7a517303ab07cfa211f74d03c1a4c2e64b3b0656d84ff32ecb449b785d2)\n`75%` of new lines are covered (`77.77%` of the complete file)"
+ "Coverage for the whole project is 77.77%. Previous coverage rate is not available"
+ in comment
+ )
+ assert (
+ "In this PR, 4 new statements are added to the whole project, 3 of which are covered (75%)."
+ in comment
+ )
+ assert (
+ "https://github.com/py-cov-action/foobar/pull/2/files#diff-b08fd7a517303ab07cfa211f74d03c1a4c2e64b3b0656d84ff32ecb449b785d2"
in comment
)
assert (
@@ -269,7 +275,10 @@ def checker(payload):
comment_file = pathlib.Path("python-coverage-comment-action.txt").read_text()
assert comment == comment_file
assert comment == summary_file.read_text()
- assert "Coverage evolution disabled because this PR targets" in comment
+ assert (
+ "Previous coverage rate is not available, cannot report on evolution."
+ in comment
+ )
def test_action__pull_request__post_comment(
@@ -324,8 +333,7 @@ def checker(payload):
assert not pathlib.Path("python-coverage-comment-action.txt").exists()
assert "Coverage for the whole project went from 30% to 77.77%" in comment
- assert comment.count(" 99%",
+ "",
+ ),
+ ],
+)
+def test_get_static_badge_url__error(label, message, color):
+ with pytest.raises(ValueError):
+ badge.get_static_badge_url(label=label, message=message, color=color)
def test_get_endpoint_url():
diff --git a/tests/unit/test_coverage.py b/tests/unit/test_coverage.py
index 53fe6000..1a1ad8d3 100644
--- a/tests/unit/test_coverage.py
+++ b/tests/unit/test_coverage.py
@@ -9,12 +9,6 @@
from coverage_comment import coverage, subprocess
-def test_collapse_lines():
- assert list(
- coverage.collapse_lines([1, 2, 3, 5, 6, 8, 11, 12, 17, 18, 19, 20, 21, 99])
- ) == [(1, 3), (5, 6), (8, 8), (11, 12), (17, 21), (99, 99)]
-
-
@pytest.mark.parametrize(
"num_covered, num_total, expected_coverage",
[
@@ -143,6 +137,8 @@ def test_generate_coverage_markdown(mocker):
pathlib.Path("codebase/code.py"): coverage.FileDiffCoverage(
path=pathlib.Path("codebase/code.py"),
percent_covered=decimal.Decimal("0.5"),
+ added_statements=[1, 3],
+ covered_statements=[1],
missing_statements=[3],
added_lines=[1, 3],
)
@@ -180,6 +176,8 @@ def test_generate_coverage_markdown(mocker):
pathlib.Path("codebase/code.py"): coverage.FileDiffCoverage(
path=pathlib.Path("codebase/code.py"),
percent_covered=decimal.Decimal("1"),
+ added_statements=[],
+ covered_statements=[],
missing_statements=[],
added_lines=[4, 5, 6],
)
@@ -213,12 +211,16 @@ def test_generate_coverage_markdown(mocker):
pathlib.Path("codebase/code.py"): coverage.FileDiffCoverage(
path=pathlib.Path("codebase/code.py"),
percent_covered=decimal.Decimal("1"),
+ added_statements=[5, 6],
+ covered_statements=[5, 6],
missing_statements=[],
added_lines=[4, 5, 6],
),
pathlib.Path("codebase/other.py"): coverage.FileDiffCoverage(
path=pathlib.Path("codebase/other.py"),
percent_covered=decimal.Decimal("0.5"),
+ added_statements=[10, 13],
+ covered_statements=[10],
missing_statements=[13],
added_lines=[10, 13],
),
diff --git a/tests/unit/test_diff_grouper.py b/tests/unit/test_diff_grouper.py
index 23d6f296..e20a647f 100644
--- a/tests/unit/test_diff_grouper.py
+++ b/tests/unit/test_diff_grouper.py
@@ -62,20 +62,24 @@ def test_group_annotations(coverage_obj, diff_coverage_obj):
assert list(result) == [
diff_grouper.Group(
- file=pathlib.Path("codebase/code.py"), line_start=7, line_end=9
- )
+ file=pathlib.Path("codebase/code.py"), line_start=6, line_end=8
+ ),
]
def test_group_annotations_more_files(
- coverage_obj, diff_coverage_obj_many_missing_lines
+ coverage_obj_more_files, diff_coverage_obj_more_files
):
result = diff_grouper.get_diff_missing_groups(
- coverage=coverage_obj, diff_coverage=diff_coverage_obj_many_missing_lines
+ coverage=coverage_obj_more_files,
+ diff_coverage=diff_coverage_obj_more_files,
)
assert list(result) == [
diff_grouper.Group(
- file=pathlib.Path("codebase/code.py"), line_start=7, line_end=9
- )
+ file=pathlib.Path("codebase/code.py"), line_start=6, line_end=8
+ ),
+ diff_grouper.Group(
+ file=pathlib.Path("codebase/other.py"), line_start=17, line_end=17
+ ),
]
diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py
index e0bb191d..64582619 100644
--- a/tests/unit/test_files.py
+++ b/tests/unit/test_files.py
@@ -65,8 +65,9 @@ def test_compute_datafile():
def test_parse_datafile():
- assert files.parse_datafile(contents="""{"coverage": 12.34}""") == decimal.Decimal(
- "0.1234"
+ assert files.parse_datafile(contents="""{"coverage": 12.34}""") == (
+ None,
+ decimal.Decimal("0.1234"),
)
diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py
index 0aed8149..c9ebd89d 100644
--- a/tests/unit/test_template.py
+++ b/tests/unit/test_template.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import datetime
import decimal
import pathlib
@@ -10,10 +9,20 @@
def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
+ files, total = template.select_files(
+ coverage=coverage_obj,
+ diff_coverage=diff_coverage_obj,
+ previous_coverage=coverage_obj,
+ max_files=25,
+ )
result = (
template.get_comment_markdown(
coverage=coverage_obj,
+ previous_coverage=coverage_obj,
diff_coverage=diff_coverage_obj,
+ files=files,
+ count_files=total,
+ max_files=25,
previous_coverage_rate=decimal.Decimal("0.92"),
minimum_green=decimal.Decimal("100"),
minimum_orange=decimal.Decimal("70"),
@@ -35,24 +44,28 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
.split(maxsplit=4)
)
- expected = [
- "92%",
- "75%",
- "80%",
- "bar",
- "",
- ]
+ expected = ["92%", "60%", "50%", "bar", ""]
assert result == expected
def test_template(coverage_obj, diff_coverage_obj):
+ files, total = template.select_files(
+ coverage=coverage_obj,
+ diff_coverage=diff_coverage_obj,
+ previous_coverage=None,
+ max_files=25,
+ )
result = template.get_comment_markdown(
coverage=coverage_obj,
diff_coverage=diff_coverage_obj,
+ previous_coverage=None,
previous_coverage_rate=decimal.Decimal("0.92"),
minimum_green=decimal.Decimal("79"),
minimum_orange=decimal.Decimal("40"),
+ files=files,
+ count_files=total,
+ max_files=25,
repo_name="org/repo",
pr_number=5,
base_template=template.read_template_file("comment.md.j2"),
@@ -62,107 +75,118 @@ def test_template(coverage_obj, diff_coverage_obj):
{% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
""",
)
+ print(result)
expected = """## Coverage report (foo)
-
-
-
-
-
-
-Diff Coverage details (click to unfold)
-### [codebase/code.py](https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3)
-`80%` of new lines are covered (`75%` of the complete file).
-Missing lines: [`3`](https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R3-R3), [`7-9`](https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R7-R9), [`12`](https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R12-R12)
+ Click to see where and how coverage changed
+ File | Statements | Missing | Coverage | Coverage (new stmts) | Lines missing |
+
+
+ codebase |
+ code.py |
+ | | | | 6-8 |
+
+
+Project Total |
+ | | | | |
+
+
+
+
+This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)
+
+
-
"""
assert result == expected
-def test_template_full():
- cov = coverage.Coverage(
- meta=coverage.CoverageMetadata(
- version="1.2.3",
- timestamp=datetime.datetime(2000, 1, 1),
- branch_coverage=True,
- show_contexts=False,
- ),
- info=coverage.CoverageInfo(
- covered_lines=6,
- num_statements=6,
- percent_covered=decimal.Decimal("1"),
- missing_lines=0,
- excluded_lines=0,
- num_branches=2,
- num_partial_branches=0,
- covered_branches=2,
- missing_branches=0,
- ),
- files={
- pathlib.Path("codebase/code.py"): coverage.FileCoverage(
- path=pathlib.Path("codebase/code.py"),
- executed_lines=[1, 2, 5, 6, 9],
- missing_lines=[],
- excluded_lines=[],
- info=coverage.CoverageInfo(
- covered_lines=5,
- num_statements=6,
- percent_covered=decimal.Decimal("5") / decimal.Decimal("6"),
- missing_lines=1,
- excluded_lines=0,
- num_branches=2,
- num_partial_branches=0,
- covered_branches=2,
- missing_branches=0,
- ),
- ),
- pathlib.Path("codebase/other.py"): coverage.FileCoverage(
- path=pathlib.Path("codebase/other.py"),
- executed_lines=[1, 2, 3],
- missing_lines=[],
- excluded_lines=[],
- info=coverage.CoverageInfo(
- covered_lines=6,
- num_statements=6,
- percent_covered=decimal.Decimal("1"),
- missing_lines=0,
- excluded_lines=0,
- num_branches=2,
- num_partial_branches=0,
- covered_branches=2,
- missing_branches=0,
- ),
- ),
- },
+def test_template_full(make_coverage, make_coverage_and_diff):
+ previous_cov = make_coverage(
+ """
+ # file: codebase/code.py
+ 1 covered
+ 2 covered
+ 3
+ 4 missing
+ 5 covered
+ 6 covered
+ 7
+ 8
+ 9 covered
+ # file: codebase/other.py
+ 1 covered
+ 2 covered
+ 3 covered
+ 4 covered
+ 5 covered
+ 6 covered
+ # file: codebase/third.py
+ 1 covered
+ 2 covered
+ 3 covered
+ 4 covered
+ 5 covered
+ 6 missing
+ 7 missing
+ """
+ )
+ cov, diff_cov = make_coverage_and_diff(
+ """
+ # file: codebase/code.py
+ 1 covered
+ 2 covered
+ 3
+ 4
+ 5 covered
+ 6 covered
+ 7
+ 8
+ 9 covered
+ 10
+ 11
+ + 12 missing
+ + 13 missing
+ + 14 missing
+ + 15 covered
+ + 16 covered
+ + 17
+ + 18
+ + 19
+ + 20
+ + 21
+ + 22 missing
+ # file: codebase/other.py
+ 1 covered
+ 2 covered
+ 3 covered
+ # file: codebase/third.py
+ 1 covered
+ 2 covered
+ 3 covered
+ 4 covered
+ 5 covered
+ 6 covered
+ 7 covered
+ """
)
- diff_cov = coverage.DiffCoverage(
- total_num_lines=6,
- total_num_violations=0,
- total_percent_covered=decimal.Decimal("1"),
- num_changed_lines=39,
- files={
- pathlib.Path("codebase/code.py"): coverage.FileDiffCoverage(
- path=pathlib.Path("codebase/code.py"),
- percent_covered=decimal.Decimal("0.5"),
- missing_statements=[12, 13, 14, 22],
- added_lines=[12, 13, 14, 15, 16, 22],
- ),
- pathlib.Path("codebase/other.py"): coverage.FileDiffCoverage(
- path=pathlib.Path("codebase/other.py"),
- percent_covered=decimal.Decimal("1"),
- missing_statements=[],
- added_lines=[4, 5, 6],
- ),
- },
+ files, total = template.select_files(
+ coverage=cov,
+ diff_coverage=diff_cov,
+ previous_coverage=previous_cov,
+ max_files=25,
)
result = template.get_comment_markdown(
coverage=cov,
diff_coverage=diff_cov,
- previous_coverage_rate=decimal.Decimal("1.0"),
+ previous_coverage=previous_cov,
+ files=files,
+ count_files=total,
+ max_files=25,
+ previous_coverage_rate=decimal.Decimal("11") / decimal.Decimal("12"),
minimum_green=decimal.Decimal("100"),
minimum_orange=decimal.Decimal("70"),
marker="",
@@ -171,93 +195,193 @@ def test_template_full():
base_template=template.read_template_file("comment.md.j2"),
)
expected = """## Coverage report
-
-
-
-
-
-
-Diff Coverage details (click to unfold)
-### [codebase/code.py](https://github.com/org/repo/pull/12/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3)
-`50%` of new lines are covered (`83.33%` of the complete file).
-Missing lines: [`12-14`](https://github.com/org/repo/pull/12/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R12-R14), [`22`](https://github.com/org/repo/pull/12/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R22-R22)
+ Click to see where and how coverage changed
+ File | Statements | Missing | Coverage | Coverage (new stmts) | Lines missing |
+
+
+ codebase |
+ code.py |
+
+ | | | | 12-14, 22 |
+ other.py |
+
+ | | | | |
+ third.py |
-### [codebase/other.py](https://github.com/org/repo/pull/12/files#diff-30cad827f61772ec66bb9ef8887058e6d8443a2afedb331d800feaa60228a26e)
-`100%` of new lines are covered (`100%` of the complete file).
+ | | | | |
+
+
+Project Total |
+
+ | | | | |
+
+
+
+
+This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)
+
+
-
"""
+ print(result)
assert result == expected
-def test_template__no_new_lines_with_coverage(coverage_obj):
- diff_cov = coverage.DiffCoverage(
- total_num_lines=0,
- total_num_violations=0,
- total_percent_covered=decimal.Decimal("1"),
- num_changed_lines=39,
- files={},
+def test_template__no_previous(coverage_obj_no_branch, diff_coverage_obj):
+ files, total = template.select_files(
+ coverage=coverage_obj_no_branch,
+ diff_coverage=diff_coverage_obj,
+ previous_coverage=None,
+ max_files=25,
)
-
result = template.get_comment_markdown(
- coverage=coverage_obj,
- diff_coverage=diff_cov,
- previous_coverage_rate=decimal.Decimal("1.0"),
+ coverage=coverage_obj_no_branch,
+ diff_coverage=diff_coverage_obj,
+ previous_coverage_rate=None,
+ previous_coverage=None,
+ files=files,
+ count_files=total,
+ max_files=25,
minimum_green=decimal.Decimal("100"),
minimum_orange=decimal.Decimal("70"),
marker="",
repo_name="org/repo",
- pr_number=1,
+ pr_number=3,
base_template=template.read_template_file("comment.md.j2"),
)
expected = """## Coverage report
-
-
-
-
-
+ Click to see where and how coverage changed
+ File | Statements | Missing | Coverage | Coverage (new stmts) | Lines missing |
+
+
+ codebase |
+ code.py |
+ | | | | 6-8 |
+
+
+Project Total |
+ | | | | |
+
+
+
+
+This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)
+
+
+
"""
+ print(result)
assert result == expected
-def test_template__no_branch_no_previous(coverage_obj_no_branch, diff_coverage_obj):
+def test_template__max_files(coverage_obj_more_files, diff_coverage_obj_more_files):
+ files, total = template.select_files(
+ coverage=coverage_obj_more_files,
+ diff_coverage=diff_coverage_obj_more_files,
+ previous_coverage=None,
+ max_files=25,
+ )
result = template.get_comment_markdown(
- coverage=coverage_obj_no_branch,
- diff_coverage=diff_coverage_obj,
- previous_coverage_rate=None,
- minimum_green=decimal.Decimal("100"),
- minimum_orange=decimal.Decimal("70"),
+ coverage=coverage_obj_more_files,
+ diff_coverage=diff_coverage_obj_more_files,
+ previous_coverage=None,
+ files=files,
+ count_files=total,
+ previous_coverage_rate=decimal.Decimal("0.92"),
+ minimum_green=decimal.Decimal("79"),
+ minimum_orange=decimal.Decimal("40"),
+ repo_name="org/repo",
+ pr_number=5,
+ max_files=1,
+ base_template=template.read_template_file("comment.md.j2"),
marker="",
+ subproject_id="foo",
+ custom_template="""{% extends "base" %}
+ {% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
+ """,
+ )
+ print(result)
+
+ assert "The report is truncated to 1 files out of 2." in result
+
+
+def test_template__no_max_files(coverage_obj_more_files, diff_coverage_obj_more_files):
+ files, total = template.select_files(
+ coverage=coverage_obj_more_files,
+ diff_coverage=diff_coverage_obj_more_files,
+ previous_coverage=None,
+ max_files=25,
+ )
+ result = template.get_comment_markdown(
+ coverage=coverage_obj_more_files,
+ diff_coverage=diff_coverage_obj_more_files,
+ previous_coverage=None,
+ files=files,
+ count_files=total,
+ previous_coverage_rate=decimal.Decimal("0.92"),
+ minimum_green=decimal.Decimal("79"),
+ minimum_orange=decimal.Decimal("40"),
repo_name="org/repo",
- pr_number=3,
+ pr_number=5,
+ max_files=None,
base_template=template.read_template_file("comment.md.j2"),
+ marker="",
+ subproject_id="foo",
+ custom_template="""{% extends "base" %}
+ {% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
+ """,
)
- expected = """## Coverage report
-
-
-
-
+ print(result)
-
-Diff Coverage details (click to unfold)
+ assert "The report is truncated" not in result
+ assert "code.py" in result
+ assert "other.py" in result
-### [codebase/code.py](https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3)
-`80%` of new lines are covered (`75%` of the complete file).
-Missing lines: [`3`](https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R3-R3), [`7-9`](https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R7-R9), [`12`](https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R12-R12)
-Missing lines: `3`, `7-9`, `12`
-
-"""
- assert result == expected
+def test_template__no_files(coverage_obj, diff_coverage_obj_more_files):
+ diff_coverage = coverage.DiffCoverage(
+ total_num_lines=0,
+ total_num_violations=0,
+ total_percent_covered=decimal.Decimal("1"),
+ num_changed_lines=0,
+ files={},
+ )
+ result = template.get_comment_markdown(
+ coverage=coverage_obj,
+ diff_coverage=diff_coverage,
+ previous_coverage=coverage_obj,
+ files=[],
+ count_files=0,
+ previous_coverage_rate=decimal.Decimal("0.92"),
+ minimum_green=decimal.Decimal("79"),
+ minimum_orange=decimal.Decimal("40"),
+ repo_name="org/repo",
+ pr_number=5,
+ max_files=25,
+ base_template=template.read_template_file("comment.md.j2"),
+ marker="",
+ subproject_id="foo",
+ custom_template="""{% extends "base" %}
+ {% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
+ """,
+ )
+ print(result)
+
+ assert (
+ "_This PR does not seem to contain any modification to coverable code."
+ in result
+ )
+ assert "code.py" not in result
+ assert "other.py" not in result
def test_read_template_file():
assert template.read_template_file("comment.md.j2").startswith(
- "{% block title %}## Coverage report{% if subproject_id %}"
+ "{%- block title -%}## Coverage report{%- if subproject_id %}"
)
@@ -265,6 +389,10 @@ def test_template__no_marker(coverage_obj, diff_coverage_obj):
with pytest.raises(template.MissingMarker):
template.get_comment_markdown(
coverage=coverage_obj,
+ previous_coverage=None,
+ files=[],
+ count_files=0,
+ max_files=25,
diff_coverage=diff_coverage_obj,
previous_coverage_rate=decimal.Decimal("0.92"),
minimum_green=decimal.Decimal("100"),
@@ -281,7 +409,11 @@ def test_template__broken_template(coverage_obj, diff_coverage_obj):
with pytest.raises(template.TemplateError):
template.get_comment_markdown(
coverage=coverage_obj,
+ previous_coverage=None,
diff_coverage=diff_coverage_obj,
+ files=[],
+ count_files=0,
+ max_files=25,
previous_coverage_rate=decimal.Decimal("0.92"),
minimum_green=decimal.Decimal("100"),
minimum_orange=decimal.Decimal("70"),
@@ -296,20 +428,37 @@ def test_template__broken_template(coverage_obj, diff_coverage_obj):
@pytest.mark.parametrize(
"value, displayed_coverage",
[
- ("0.83", "83%"),
- ("0.99999", "99.99%"),
- ("0.00001", "0%"),
- ("0.0501", "5.01%"),
- ("1", "100%"),
- ("0.8392", "83.92%"),
+ (decimal.Decimal("0.83"), "83%"),
+ (decimal.Decimal("0.99999"), "99.99%"),
+ (decimal.Decimal("0.00001"), "0%"),
+ (decimal.Decimal("0.0501"), "5.01%"),
+ (decimal.Decimal("1"), "100%"),
+ (decimal.Decimal("0.2"), "20%"),
+ (decimal.Decimal("0.8392"), "83.92%"),
],
)
def test_pct(value, displayed_coverage):
- assert template.pct(decimal.Decimal(value)) == displayed_coverage
+ assert template.pct(value) == displayed_coverage
+
+
+@pytest.mark.parametrize(
+ "number, singular, plural, expected",
+ [
+ (1, "", "s", ""),
+ (2, "", "s", "s"),
+ (0, "", "s", "s"),
+ (1, "y", "ies", "y"),
+ (2, "y", "ies", "ies"),
+ ],
+)
+def test_pluralize(number, singular, plural, expected):
+ assert (
+ template.pluralize(number=number, singular=singular, plural=plural) == expected
+ )
@pytest.mark.parametrize(
- "filepath, lines, result",
+ "filepath, lines, expected",
[
(
pathlib.Path("tests/conftest.py"),
@@ -322,17 +471,20 @@ def test_pct(value, displayed_coverage):
"https://github.com/py-cov-action/python-coverage-comment-action/pull/33/files#diff-b10564ab7d2c520cdd0243874879fb0a782862c3c902ab535faabe57d5a505e1R12-R15",
),
(
- pathlib.Path("codebase/main.py"),
+ pathlib.Path("codebase/other.py"),
(22, 22),
- "https://github.com/py-cov-action/python-coverage-comment-action/pull/33/files#diff-78013e21ec15af196dec6bfa8fd19ba3f6be7d390545d0cff142e47d803316faR22-R22",
+ "https://github.com/py-cov-action/python-coverage-comment-action/pull/33/files#diff-30cad827f61772ec66bb9ef8887058e6d8443a2afedb331d800feaa60228a26eR22-R22",
),
],
)
-def test_get_file_url_function(filepath, lines, result):
- file_url = template.get_file_url_function(
- "py-cov-action/python-coverage-comment-action", 33
+def test_get_file_url(filepath, lines, expected):
+ result = template.get_file_url(
+ filename=filepath,
+ lines=lines,
+ repo_name="py-cov-action/python-coverage-comment-action",
+ pr_number=33,
)
- assert file_url(pathlib.Path(filepath), lines) == result
+ assert result == expected
def test_uptodate():
@@ -351,3 +503,213 @@ def test_uptodate():
)
def test_get_marker(marker_id, result):
assert template.get_marker(marker_id=marker_id) == result
+
+
+@pytest.mark.parametrize(
+ "previous_code, current_code_and_diff, max_files, expected_files, expected_total",
+ [
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ 1 covered
+ """,
+ 2,
+ [],
+ 0,
+ id="unmodified",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ 1
+ 2 covered
+ """,
+ 2,
+ [],
+ 0,
+ id="info_did_not_change",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ 1 missing
+ """,
+ 2,
+ ["a.py"],
+ 1,
+ id="info_did_change",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ + 1 covered
+ """,
+ 2,
+ ["a.py"],
+ 1,
+ id="with_diff",
+ ),
+ pytest.param(
+ """
+ # file: b.py
+ 1 covered
+ # file: a.py
+ 1 covered
+ """,
+ """
+ # file: b.py
+ + 1 covered
+ # file: a.py
+ + 1 covered
+ """,
+ 2,
+ ["a.py", "b.py"],
+ 2,
+ id="ordered",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ # file: b.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ 1 covered
+ 2 covered
+ # file: b.py
+ 1 missing
+ """,
+ 1,
+ ["b.py"],
+ 2,
+ id="truncated",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ # file: c.py
+ 1 covered
+ # file: b.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ + 1 covered
+ # file: c.py
+ 1 missing
+ # file: b.py
+ 1 missing
+ """,
+ 2,
+ ["b.py", "c.py"],
+ 3,
+ id="truncated_and_ordered",
+ ),
+ pytest.param(
+ """
+ # file: a.py
+ 1 covered
+ # file: b.py
+ 1 covered
+ """,
+ """
+ # file: a.py
+ 1 covered
+ 2 covered
+ # file: b.py
+ 1 missing
+ """,
+ None,
+ ["a.py", "b.py"],
+ 2,
+ id="max_none",
+ ),
+ ],
+)
+def test_select_files(
+ make_coverage,
+ make_coverage_and_diff,
+ previous_code,
+ current_code_and_diff,
+ max_files,
+ expected_files,
+ expected_total,
+):
+ previous_cov = make_coverage(previous_code)
+ cov, diff_cov = make_coverage_and_diff(current_code_and_diff)
+
+ files, total = template.select_files(
+ coverage=cov,
+ diff_coverage=diff_cov,
+ previous_coverage=previous_cov,
+ max_files=max_files,
+ )
+ assert [str(e.path) for e in files] == expected_files
+ assert total == expected_total
+
+
+def test_select_files__no_previous(
+ make_coverage_and_diff,
+):
+ cov, diff_cov = make_coverage_and_diff(
+ """
+ # file: a.py
+ 1 covered
+ + 1 missing
+ """
+ )
+
+ files, total = template.select_files(
+ coverage=cov,
+ diff_coverage=diff_cov,
+ previous_coverage=None,
+ max_files=1,
+ )
+ assert [str(e.path) for e in files] == ["a.py"]
+ assert total == 1
+
+
+def test_get_readme_markdown():
+ result = template.get_readme_markdown(
+ is_public=True,
+ readme_url="https://example.com",
+ markdown_report="...markdown report...",
+ direct_image_url="https://example.com/direct.png",
+ html_report_url="https://example.com/report.html",
+ dynamic_image_url="https://example.com/dynamic.png",
+ endpoint_image_url="https://example.com/endpoint.png",
+ subproject_id="foo",
+ )
+ assert result.startswith("# Repository Coverage (foo)")
+
+
+def test_get_log_message():
+ result = template.get_log_message(
+ is_public=True,
+ readme_url="https://example.com",
+ direct_image_url="https://example.com/direct.png",
+ html_report_url="https://example.com/report.html",
+ dynamic_image_url="https://example.com/dynamic.png",
+ endpoint_image_url="https://example.com/endpoint.png",
+ subproject_id="foo",
+ )
+ assert result.startswith("Coverage info for foo:")