Skip to content
Closed
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
31 changes: 28 additions & 3 deletions diff_cover/diff_cover_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
TOTAL_PERCENT_FLOAT_HELP = (
"Show total coverage/quality as a float rounded to 2 decimal places"
)
TREAT_UNMEASURED_AS_UNCOVERED_HELP = (
"Treat lines in the diff that are absent from the coverage report as uncovered"
)

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -190,6 +193,12 @@ def parse_coverage_args(argv):
default=None,
help=TOTAL_PERCENT_FLOAT_HELP,
)
parser.add_argument(
"--treat-unmeasured-as-uncovered",
action="store_true",
default=None,
help=TREAT_UNMEASURED_AS_UNCOVERED_HELP,
)

defaults = {
"show_uncovered": False,
Expand All @@ -204,6 +213,7 @@ def parse_coverage_args(argv):
"quiet": False,
"expand_coverage_report": False,
"total_percent_float": False,
"treat_unmeasured_as_uncovered": False,
}

return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
Expand All @@ -225,6 +235,7 @@ def generate_coverage_report(
show_uncovered=False,
expand_coverage_report=False,
total_percent_float=False,
treat_unmeasured_as_uncovered=False,
):
"""
Generate the diff coverage report, using kwargs from `parse_args()`.
Expand All @@ -237,6 +248,7 @@ def generate_coverage_report(
include_untracked=include_untracked,
exclude=exclude,
include=include,
filter_blank_lines=treat_unmeasured_as_uncovered,
)

xml_roots = [
Expand All @@ -263,7 +275,11 @@ def generate_coverage_report(
if css_url is not None:
css_url = os.path.relpath(css_file, os.path.dirname(html_report))
reporter = HtmlReportGenerator(
coverage, diff, css_url=css_url, total_percent_float=total_percent_float
coverage,
diff,
css_url=css_url,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
with open_file(html_report, "wb") as output_file:
reporter.generate_report(output_file)
Expand All @@ -274,15 +290,21 @@ def generate_coverage_report(
if "json" in report_formats:
json_report = report_formats["json"] or JSON_REPORT_DEFAULT_PATH
reporter = JsonReportGenerator(
coverage, diff, total_percent_float=total_percent_float
coverage,
diff,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
with open_file(json_report, "wb") as output_file:
reporter.generate_report(output_file)

if "markdown" in report_formats:
markdown_report = report_formats["markdown"] or MARKDOWN_REPORT_DEFAULT_PATH
reporter = MarkdownReportGenerator(
coverage, diff, total_percent_float=total_percent_float
coverage,
diff,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
with open_file(markdown_report, "wb") as output_file:
reporter.generate_report(output_file)
Expand All @@ -294,6 +316,7 @@ def generate_coverage_report(
diff,
report_formats["github-annotations"],
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
reporter.generate_report(sys.stdout.buffer)

Expand All @@ -303,6 +326,7 @@ def generate_coverage_report(
diff,
show_uncovered,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
output_file = io.BytesIO() if quiet else sys.stdout.buffer

Expand Down Expand Up @@ -400,6 +424,7 @@ def main(argv=None, directory=None):
show_uncovered=arg_dict["show_uncovered"],
expand_coverage_report=arg_dict["expand_coverage_report"],
total_percent_float=arg_dict["total_percent_float"],
treat_unmeasured_as_uncovered=arg_dict["treat_unmeasured_as_uncovered"],
)

if percent_covered >= fail_under:
Expand Down
11 changes: 9 additions & 2 deletions diff_cover/diff_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def __init__(
supported_extensions=None,
exclude=None,
include=None,
filter_blank_lines=False,
):
"""
Configure the reporter to use `git_diff` as the wrapper
Expand Down Expand Up @@ -148,6 +149,7 @@ def __init__(
self._ignore_unstaged = ignore_unstaged
self._include_untracked = include_untracked
self._supported_extensions = supported_extensions
self._filter_blank_lines = filter_blank_lines

# Cache diff information as a dictionary
# with file path keys and line number list values
Expand Down Expand Up @@ -377,6 +379,9 @@ def _parse_lines(self, diff_lines):
where `ADDED_LINES` and `DELETED_LINES` are lists of line
numbers added/deleted respectively.

If `self._filter_blank_lines` is True, blank/whitespace-only
added lines are excluded from `ADDED_LINES`.

Raises a `GitDiffError` if the diff lines are in an invalid format.
"""

Expand All @@ -399,8 +404,10 @@ def _parse_lines(self, diff_lines):
# calling this method, we're guaranteed to have a source
# file specified. We check anyway just to be safe.
if current_line_new is not None:
# Store the added line
added_lines.append(current_line_new)
# Skip blank/whitespace-only added lines when filtering
if not self._filter_blank_lines or line[1:].strip():
# Store the added line
added_lines.append(current_line_new)

# Increment the line number in the file
current_line_new += 1
Expand Down
35 changes: 33 additions & 2 deletions diff_cover/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@

from diff_cover.snippets import Snippet
from diff_cover.util import to_unix_path
from diff_cover.violationsreporters.base import Violation


class DiffViolations:
"""
Class to capture violations generated by a particular diff
"""

def __init__(self, violations, measured_lines, diff_lines):
def __init__(
self,
violations,
measured_lines,
diff_lines,
treat_unmeasured_as_uncovered=False,
):
self.lines = {violation.line for violation in violations}.intersection(
diff_lines
)
Expand All @@ -36,13 +43,28 @@ def __init__(self, violations, measured_lines, diff_lines):
else:
self.measured_lines = set(measured_lines).intersection(diff_lines)

# When enabled and coverage data exists for this file,
# treat diff lines absent from the coverage report as uncovered.
if treat_unmeasured_as_uncovered and self.measured_lines:
unmeasured_diff_lines = set(diff_lines) - set(measured_lines)
for line_num in unmeasured_diff_lines:
self.measured_lines.add(line_num)
self.lines.add(line_num)
self.violations.add(Violation(line_num, None))


class BaseReportGenerator(ABC):
"""
Generate a diff coverage report.
"""

def __init__(self, violations_reporter, diff_reporter, total_percent_float=False):
def __init__(
self,
violations_reporter,
diff_reporter,
total_percent_float=False,
treat_unmeasured_as_uncovered=False,
):
"""
Configure the report generator to build a report
from `violations_reporter` (of type BaseViolationReporter)
Expand All @@ -51,6 +73,7 @@ def __init__(self, violations_reporter, diff_reporter, total_percent_float=False
self._violations = violations_reporter
self._diff = diff_reporter
self._total_percent_float = total_percent_float
self._treat_unmeasured_as_uncovered = treat_unmeasured_as_uncovered
self._diff_violations_dict = None

self._cache_violations = None
Expand Down Expand Up @@ -204,6 +227,7 @@ def _diff_violations(self):
violations.get(to_unix_path(src_path), []),
self._violations.measured_lines(src_path),
self._diff.lines_changed(src_path),
treat_unmeasured_as_uncovered=self._treat_unmeasured_as_uncovered,
)
for src_path in src_paths_changed
}
Expand All @@ -213,6 +237,7 @@ def _diff_violations(self):
self._violations.violations(src_path),
self._violations.measured_lines(src_path),
self._diff.lines_changed(src_path),
treat_unmeasured_as_uncovered=self._treat_unmeasured_as_uncovered,
)
for src_path in src_paths_changed
}
Expand Down Expand Up @@ -295,11 +320,13 @@ def __init__(
diff_reporter,
css_url=None,
total_percent_float=False,
treat_unmeasured_as_uncovered=False,
):
super().__init__(
violations_reporter,
diff_reporter,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
self.css_url = css_url

Expand Down Expand Up @@ -438,11 +465,13 @@ def __init__(
diff_reporter,
show_uncovered=False,
total_percent_float=False,
treat_unmeasured_as_uncovered=False,
):
super().__init__(
violations_reporter,
diff_reporter,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
self.include_snippets = show_uncovered

Expand All @@ -464,11 +493,13 @@ def __init__(
diff_reporter,
annotations_type,
total_percent_float=False,
treat_unmeasured_as_uncovered=False,
):
super().__init__(
violations_reporter,
diff_reporter,
total_percent_float=total_percent_float,
treat_unmeasured_as_uncovered=treat_unmeasured_as_uncovered,
)
self.annotations_type = annotations_type

Expand Down
57 changes: 57 additions & 0 deletions tests/test_diff_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,63 @@ def test_git_diff_error(
diff.lines_changed("subdir/file1.py")


def test_blank_lines_included_by_default(diff, git_diff):
"""Without filter_blank_lines, blank added lines are included in lines_changed()."""
diff_str = dedent("""
diff --git a/file.py b/file.py
@@ -1,3 +1,6 @@
existing line
+
+def something():
+ print("hello")
+
+
""")

_set_git_diff_output(diff, git_diff, diff_str, "", "")

# All added lines are included (default behavior preserved)
assert diff.lines_changed("file.py") == [2, 3, 4, 5, 6]


def test_blank_lines_filtered_when_flag_set(git_diff):
"""With filter_blank_lines=True, blank added lines are excluded from lines_changed()."""
diff = GitDiffReporter(git_diff=git_diff, filter_blank_lines=True)
diff_str = dedent("""
diff --git a/file.py b/file.py
@@ -1,3 +1,6 @@
existing line
+
+def something():
+ print("hello")
+
+
""")

_set_git_diff_output(diff, git_diff, diff_str, "", "")

# Only non-blank added lines are included
assert diff.lines_changed("file.py") == [3, 4]


def test_whitespace_only_lines_filtered_when_flag_set(git_diff):
"""With filter_blank_lines=True, whitespace-only added lines are excluded."""
diff = GitDiffReporter(git_diff=git_diff, filter_blank_lines=True)
diff_str = dedent("""
diff --git a/file.py b/file.py
@@ -1,1 +1,4 @@
existing line
+ \t
+code_line
+
""")

_set_git_diff_output(diff, git_diff, diff_str, "", "")

# Only non-blank added line is included
assert diff.lines_changed("file.py") == [3]


def test_plus_sign_in_hunk_bug(diff, git_diff):
# This was a bug that caused a parse error
diff_str = dedent("""
Expand Down
Loading
Loading