diff-cover 9.1.0__tar.gz → 9.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {diff_cover-9.1.0 → diff_cover-9.2.0}/PKG-INFO +13 -1
  2. {diff_cover-9.1.0 → diff_cover-9.2.0}/README.rst +12 -0
  3. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/diff_cover_tool.py +14 -1
  4. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/diff_reporter.py +48 -19
  5. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/violationsreporters/violations_reporter.py +26 -2
  6. {diff_cover-9.1.0 → diff_cover-9.2.0}/pyproject.toml +3 -3
  7. diff_cover-9.2.0/tests/fixtures/coverage_missing_lines.xml +24 -0
  8. diff_cover-9.2.0/tests/fixtures/expand_console_report.txt +10 -0
  9. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_diff_reporter.py +51 -4
  10. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_integration.py +14 -0
  11. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_violations_reporter.py +61 -0
  12. {diff_cover-9.1.0 → diff_cover-9.2.0}/LICENSE +0 -0
  13. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/__init__.py +0 -0
  14. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/command_runner.py +0 -0
  15. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/config_parser.py +0 -0
  16. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/diff_quality_tool.py +0 -0
  17. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/git_diff.py +0 -0
  18. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/git_path.py +0 -0
  19. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/hook.py +0 -0
  20. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/hookspecs.py +0 -0
  21. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/report_generator.py +0 -0
  22. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/snippets.py +0 -0
  23. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/console_coverage_report.txt +0 -0
  24. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/console_quality_report.txt +0 -0
  25. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/external_style.css +0 -0
  26. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/html_coverage_report.html +0 -0
  27. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/html_quality_report.html +0 -0
  28. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/markdown_coverage_report.md +0 -0
  29. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/markdown_quality_report.md +0 -0
  30. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/snippet_content.html +0 -0
  31. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/snippet_content.md +0 -0
  32. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/snippet_content.txt +0 -0
  33. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/templates/snippet_style.html +0 -0
  34. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/util.py +0 -0
  35. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/violationsreporters/__init__.py +0 -0
  36. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/violationsreporters/base.py +0 -0
  37. {diff_cover-9.1.0 → diff_cover-9.2.0}/diff_cover/violationsreporters/java_violations_reporter.py +0 -0
  38. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/__init__.py +0 -0
  39. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/add_console_report.txt +0 -0
  40. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/add_html_report.html +0 -0
  41. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/changed_console_report.txt +0 -0
  42. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/changed_html_report.html +0 -0
  43. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/coverage.xml +0 -0
  44. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/coverage1.xml +0 -0
  45. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/coverage2.xml +0 -0
  46. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/delete_console_report.txt +0 -0
  47. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/delete_html_report.html +0 -0
  48. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/dotnet_coverage.xml +0 -0
  49. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/dotnet_coverage_console_report.txt +0 -0
  50. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/empty.txt +0 -0
  51. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/empty_pycodestyle_violations.txt +0 -0
  52. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/external_css_html_report.html +0 -0
  53. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/external_style.css +0 -0
  54. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_add.txt +0 -0
  55. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_changed.txt +0 -0
  56. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_code_dupe.txt +0 -0
  57. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_delete.txt +0 -0
  58. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_dotnet.txt +0 -0
  59. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_external_css.txt +0 -0
  60. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_lua.txt +0 -0
  61. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_moved.txt +0 -0
  62. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_mult.txt +0 -0
  63. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_subdir.txt +0 -0
  64. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_unicode.txt +0 -0
  65. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_violations.txt +0 -0
  66. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/git_diff_violations_two_files.txt +0 -0
  67. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/hello.py +0 -0
  68. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/hi.py +0 -0
  69. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/html_report.html +0 -0
  70. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/html_report_empty.html +0 -0
  71. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/html_report_one_snippet.html +0 -0
  72. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/html_report_two_snippets.html +0 -0
  73. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/lcov.info +0 -0
  74. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/lua_console_report.txt +0 -0
  75. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/luacoverage.xml +0 -0
  76. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/markdown_report_one_snippet.md +0 -0
  77. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/markdown_report_two_snippets.md +0 -0
  78. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/moved_console_report.txt +0 -0
  79. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/moved_coverage.xml +0 -0
  80. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/moved_html_report.html +0 -0
  81. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/mult_inputs_console_report.txt +0 -0
  82. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/mult_inputs_html_report.html +0 -0
  83. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pycodestyle_report.txt +0 -0
  84. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pycodestyle_violations_report.html +0 -0
  85. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pycodestyle_violations_report.txt +0 -0
  86. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pycodestyle_violations_report_external_css.html +0 -0
  87. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pyflakes_two_files.txt +0 -0
  88. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pyflakes_violations_report.html +0 -0
  89. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pyflakes_violations_report.txt +0 -0
  90. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_dupe.txt +0 -0
  91. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_dupe_violations_report.txt +0 -0
  92. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_report.txt +0 -0
  93. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_violations_console_report.txt +0 -0
  94. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_violations_report.html +0 -0
  95. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylint_violations_report.txt +0 -0
  96. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/pylintrc +0 -0
  97. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/show_uncovered_lines_console.txt +0 -0
  98. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet.css +0 -0
  99. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_8859.py +0 -0
  100. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_arabic_output.html +0 -0
  101. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_default.html +0 -0
  102. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_invalid_violations.html +0 -0
  103. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_list.html +0 -0
  104. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_list.md +0 -0
  105. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_list2.md +0 -0
  106. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_list3.md +0 -0
  107. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_no_filename_ext.html +0 -0
  108. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_src.py +0 -0
  109. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_src2.cpp +0 -0
  110. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_src3.cpp +0 -0
  111. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_unicode.html +0 -0
  112. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_unicode.py +0 -0
  113. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/snippet_unicode_html_output.html +0 -0
  114. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/subdir_coverage_console_report.txt +0 -0
  115. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/subdir_coverage_html_report.html +0 -0
  116. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/test_src.txt +0 -0
  117. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/unicode_console_report.txt +0 -0
  118. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/unicode_coverage.xml +0 -0
  119. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/unicode_html_report.html +0 -0
  120. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/unicode_test_src.txt +0 -0
  121. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/fixtures/violations_test_file.py +0 -0
  122. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/helpers.py +0 -0
  123. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/snippet_list_unicode.html +0 -0
  124. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_clover_violations_reporter/test.xml +0 -0
  125. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_clover_violations_reporter.py +0 -0
  126. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_config_parser.py +0 -0
  127. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_diff_cover_main.py +0 -0
  128. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_diff_cover_tool.py +0 -0
  129. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_diff_quality_main.py +0 -0
  130. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_git_diff.py +0 -0
  131. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_git_diff_file.py +0 -0
  132. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_git_path.py +0 -0
  133. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_java_violations_reporter.py +0 -0
  134. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_report_generator.py +0 -0
  135. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_snippets.py +0 -0
  136. {diff_cover-9.1.0 → diff_cover-9.2.0}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: diff_cover
3
- Version: 9.1.0
3
+ Version: 9.2.0
4
4
  Summary: Run coverage and linting reports on diffs
5
5
  Home-page: https://github.com/Bachmann1234/diff-cover
6
6
  License: Apache-2.0
@@ -307,6 +307,18 @@ It can be enabled by using the ``-q``/``--quiet`` flag:
307
307
 
308
308
  If enabled, the tool will only print errors and failures but no information or warning messages.
309
309
 
310
+ Compatibility with multi-line statements
311
+ ----------------------------------------
312
+ ``diff-cover`` relies on the comparison of diff reports and coverage reports, and does not report
313
+ lines that appear in one and not in the other. While diff reports list all lines that changed,
314
+ coverage reports usually list code statements. As a result, a change in a multi-line statement may not be analyzed by ``diff-cover``.
315
+
316
+ As a workaround, you can use the argument ``--expand-coverage-report``: lines not appearing in the coverage reports will be added to them with the same number of hits as the previously reported line. ``diff-cover`` will then perform diff coverage analysis on all changed lines.
317
+
318
+ Notes:
319
+ - This argument is only available for XML coverage reports.
320
+ - This workaround is designed under the assumption that the coverage tool reports untested statements with hits set to 0, and it reports statements based on the opening line.
321
+
310
322
  Configuration files
311
323
  -------------------
312
324
  Both tools allow users to specify the options in a configuration file with `--config-file`/`-c`:
@@ -274,6 +274,18 @@ It can be enabled by using the ``-q``/``--quiet`` flag:
274
274
 
275
275
  If enabled, the tool will only print errors and failures but no information or warning messages.
276
276
 
277
+ Compatibility with multi-line statements
278
+ ----------------------------------------
279
+ ``diff-cover`` relies on the comparison of diff reports and coverage reports, and does not report
280
+ lines that appear in one and not in the other. While diff reports list all lines that changed,
281
+ coverage reports usually list code statements. As a result, a change in a multi-line statement may not be analyzed by ``diff-cover``.
282
+
283
+ As a workaround, you can use the argument ``--expand-coverage-report``: lines not appearing in the coverage reports will be added to them with the same number of hits as the previously reported line. ``diff-cover`` will then perform diff coverage analysis on all changed lines.
284
+
285
+ Notes:
286
+ - This argument is only available for XML coverage reports.
287
+ - This workaround is designed under the assumption that the coverage tool reports untested statements with hits set to 0, and it reports statements based on the opening line.
288
+
277
289
  Configuration files
278
290
  -------------------
279
291
  Both tools allow users to specify the options in a configuration file with `--config-file`/`-c`:
@@ -41,6 +41,9 @@ DIFF_RANGE_NOTATION_HELP = (
41
41
  )
42
42
  QUIET_HELP = "Only print errors and failures"
43
43
  SHOW_UNCOVERED = "Show uncovered lines on the console"
44
+ EXPAND_COVERAGE_REPORT = (
45
+ "Append missing lines in coverage reports based on the hits of the previous line."
46
+ )
44
47
  INCLUDE_UNTRACKED_HELP = "Include untracked files"
45
48
  CONFIG_FILE_HELP = "The configuration file to use"
46
49
  DIFF_FILE_HELP = "The diff file to use"
@@ -93,6 +96,13 @@ def parse_coverage_args(argv):
93
96
  "--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED
94
97
  )
95
98
 
99
+ parser.add_argument(
100
+ "--expand-coverage-report",
101
+ action="store_true",
102
+ default=None,
103
+ help=EXPAND_COVERAGE_REPORT,
104
+ )
105
+
96
106
  parser.add_argument(
97
107
  "--external-css-file",
98
108
  metavar="FILENAME",
@@ -183,6 +193,7 @@ def parse_coverage_args(argv):
183
193
  "ignore_whitespace": False,
184
194
  "diff_range_notation": "...",
185
195
  "quiet": False,
196
+ "expand_coverage_report": False,
186
197
  }
187
198
 
188
199
  return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
@@ -204,6 +215,7 @@ def generate_coverage_report(
204
215
  src_roots=None,
205
216
  quiet=False,
206
217
  show_uncovered=False,
218
+ expand_coverage_report=False,
207
219
  ):
208
220
  """
209
221
  Generate the diff coverage report, using kwargs from `parse_args()`.
@@ -231,7 +243,7 @@ def generate_coverage_report(
231
243
  if len(xml_roots) > 0 and len(lcov_roots) > 0:
232
244
  raise ValueError(f"Mixing LCov and XML reports is not supported yet")
233
245
  elif len(xml_roots) > 0:
234
- coverage = XmlCoverageReporter(xml_roots, src_roots)
246
+ coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report)
235
247
  else:
236
248
  coverage = LcovCoverageReporter(lcov_roots, src_roots)
237
249
 
@@ -308,6 +320,7 @@ def main(argv=None, directory=None):
308
320
  src_roots=arg_dict["src_roots"],
309
321
  quiet=quiet,
310
322
  show_uncovered=arg_dict["show_uncovered"],
323
+ expand_coverage_report=arg_dict["expand_coverage_report"],
311
324
  )
312
325
 
313
326
  if percent_covered >= fail_under:
@@ -165,18 +165,31 @@ class GitDiffReporter(BaseDiffReporter):
165
165
 
166
166
  # Get the diff dictionary
167
167
  diff_dict = self._git_diff()
168
-
169
168
  # include untracked files
170
169
  if self._include_untracked:
171
170
  for path in self._git_diff_tool.untracked():
172
- with open(path) as file_handle:
173
- num_lines = len(file_handle.readlines())
171
+ if not self._validate_path_to_diff(path):
172
+ continue
173
+
174
+ num_lines = self._get_file_lines(path)
174
175
  diff_dict[path] = list(range(1, num_lines + 1))
175
176
 
176
177
  # Return the changed file paths (dict keys)
177
178
  # in alphabetical order
178
179
  return sorted(diff_dict.keys(), key=lambda x: x.lower())
179
180
 
181
+ @staticmethod
182
+ def _get_file_lines(path):
183
+ """
184
+ Return the number of lines in a file.
185
+ """
186
+
187
+ try:
188
+ with open(path, encoding="utf-8") as file_handle:
189
+ return len(file_handle.readlines())
190
+ except UnicodeDecodeError:
191
+ return 0
192
+
180
193
  def lines_changed(self, src_path):
181
194
  """
182
195
  See base class docstring.
@@ -224,23 +237,16 @@ class GitDiffReporter(BaseDiffReporter):
224
237
  diff_dict = self._parse_diff_str(diff_str)
225
238
 
226
239
  for src_path, (added_lines, deleted_lines) in diff_dict.items():
227
- if self._is_path_excluded(src_path):
240
+ if not self._validate_path_to_diff(src_path):
228
241
  continue
229
- # If no _supported_extensions provided, or extension present: process
230
- _, extension = os.path.splitext(src_path)
231
- extension = extension[1:].lower()
232
- # 'not self._supported_extensions' tests for both None and empty list []
233
- if (
234
- not self._supported_extensions
235
- or extension in self._supported_extensions
236
- ):
237
- # Remove any lines from the dict that have been deleted
238
- # Include any lines that have been added
239
- result_dict[src_path] = [
240
- line
241
- for line in result_dict.get(src_path, [])
242
- if line not in deleted_lines
243
- ] + added_lines
242
+
243
+ # Remove any lines from the dict that have been deleted
244
+ # Include any lines that have been added
245
+ result_dict[src_path] = [
246
+ line
247
+ for line in result_dict.get(src_path, [])
248
+ if line not in deleted_lines
249
+ ] + added_lines
244
250
 
245
251
  # Eliminate repeats and order line numbers
246
252
  for src_path, lines in result_dict.items():
@@ -252,6 +258,29 @@ class GitDiffReporter(BaseDiffReporter):
252
258
  # Return the diff cache
253
259
  return self._diff_dict
254
260
 
261
+ def _validate_path_to_diff(self, src_path: str) -> bool:
262
+ """
263
+ Validate if a path should be included in the diff.
264
+
265
+ Returns True if the path should be included, otherwise False.
266
+
267
+ A path should be excluded if:
268
+ - If the path is excluded
269
+ - If the path has an extension that is not supported
270
+ """
271
+
272
+ if self._is_path_excluded(src_path):
273
+ return False
274
+
275
+ # If no _supported_extensions provided, or extension present: process
276
+ _, extension = os.path.splitext(src_path)
277
+ extension = extension[1:].lower()
278
+
279
+ if self._supported_extensions and extension not in self._supported_extensions:
280
+ return False
281
+
282
+ return True
283
+
255
284
  # Regular expressions used to parse the diff output
256
285
  SRC_FILE_RE = re.compile(r'^diff --git "?a/.*"? "?b/([^\n"]*)"?')
257
286
  MERGE_CONFLICT_RE = re.compile(r"^diff --cc ([^\n]*)")
@@ -24,7 +24,7 @@ class XmlCoverageReporter(BaseViolationReporter):
24
24
  Query information from a Cobertura|Clover|JaCoCo XML coverage report.
25
25
  """
26
26
 
27
- def __init__(self, xml_roots, src_roots=None):
27
+ def __init__(self, xml_roots, src_roots=None, expand_coverage_report=False):
28
28
  """
29
29
  Load the XML coverage report represented
30
30
  by the cElementTree with root element `xml_root`.
@@ -41,6 +41,7 @@ class XmlCoverageReporter(BaseViolationReporter):
41
41
  self._xml_cache = [{} for i in range(len(xml_roots))]
42
42
 
43
43
  self._src_roots = src_roots or [""]
44
+ self._expand_coverage_report = expand_coverage_report
44
45
 
45
46
  def _get_xml_classes(self, xml_document):
46
47
  """
@@ -216,6 +217,28 @@ class XmlCoverageReporter(BaseViolationReporter):
216
217
  if line_nodes is None:
217
218
  continue
218
219
 
220
+ # Expand coverage report with not reported lines
221
+ if self._expand_coverage_report:
222
+ reported_line_hits = {}
223
+ for line in line_nodes:
224
+ reported_line_hits[int(line.get(_number))] = int(
225
+ line.get(_hits, 0)
226
+ )
227
+ if reported_line_hits:
228
+ last_hit_number = 0
229
+ for line_number in range(
230
+ min(reported_line_hits.keys()),
231
+ max(reported_line_hits.keys()),
232
+ ):
233
+ if line_number in reported_line_hits:
234
+ last_hit_number = reported_line_hits[line_number]
235
+ else:
236
+ # This is an unreported line.
237
+ # We add it with the previous line hit score
238
+ line_nodes.append(
239
+ {_hits: last_hit_number, _number: line_number}
240
+ )
241
+
219
242
  # First case, need to define violations initially
220
243
  if violations is None:
221
244
  violations = {
@@ -264,7 +287,7 @@ class XmlCoverageReporter(BaseViolationReporter):
264
287
 
265
288
  class LcovCoverageReporter(BaseViolationReporter):
266
289
  """
267
- Query information from a Cobertura|Clover|JaCoCo XML coverage report.
290
+ Query information from a LCov coverage report.
268
291
  """
269
292
 
270
293
  def __init__(self, lcov_roots, src_roots=None):
@@ -324,6 +347,7 @@ class LcovCoverageReporter(BaseViolationReporter):
324
347
  "BRF",
325
348
  "BRH",
326
349
  "BRDA",
350
+ "VER",
327
351
  ]:
328
352
  # these are valid lines, but not we don't need them
329
353
  continue
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "diff_cover"
3
- version = "9.1.0"
3
+ version = "9.2.0"
4
4
  description = "Run coverage and linting reports on diffs"
5
5
  authors = ["See Contributors"]
6
6
  homepage = "https://github.com/Bachmann1234/diff-cover"
@@ -55,10 +55,10 @@ pytest-mock = "^3.14.0"
55
55
  pycodestyle = ">=2.9.1"
56
56
  flake8 = "^5.0.4"
57
57
  pyflakes = "^2.5.0"
58
- pylint = "^3.2.2"
58
+ pylint = "^3.2.7"
59
59
  pylint-pytest = "^1.1.8"
60
60
  pydocstyle = "^6.1.1"
61
- black = "^24.4.2"
61
+ black = "^24.8.0"
62
62
  isort = "^5.13.2"
63
63
  doc8 = "1.1.1"
64
64
 
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" ?>
2
+ <!DOCTYPE coverage
3
+ SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
4
+ <coverage branch-rate="0" line-rate="0.8792" timestamp="1371471706873" version="3.6">
5
+ <packages>
6
+ <package branch-rate="0" complexity="0" line-rate="0.9915" name="">
7
+ <classes>
8
+ <class branch-rate="0" complexity="0" filename="test_src.txt" line-rate="0.9643" name="test_src.txt">
9
+ <methods/>
10
+ <lines>
11
+ <line hits="1" number="1"/>
12
+ <line hits="0" number="2"/>
13
+ <line hits="0" number="4"/>
14
+ <line hits="1" number="5"/>
15
+ <line hits="1" number="7"/>
16
+ <line hits="0" number="8"/>
17
+ <line hits="1" number="9"/>
18
+ <line hits="0" number="10"/>
19
+ </lines>
20
+ </class>
21
+ </classes>
22
+ </package>
23
+ </packages>
24
+ </coverage>
@@ -0,0 +1,10 @@
1
+ -------------
2
+ Diff Coverage
3
+ Diff: origin/main...HEAD, staged and unstaged changes
4
+ -------------
5
+ test_src.txt (50.0%): Missing lines 2-4,8,10
6
+ -------------
7
+ Total: 10 lines
8
+ Missing: 5 lines
9
+ Coverage: 50%
10
+ -------------
@@ -618,16 +618,63 @@ def test_include_untracked(mocker, git_diff):
618
618
  {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}
619
619
  )
620
620
  _set_git_diff_output(
621
- reporter, git_diff, staged_diff=diff_output, untracked=["u1.py", " u2.py"]
621
+ reporter,
622
+ git_diff,
623
+ staged_diff=diff_output,
624
+ untracked=["u1.py", " u2.py", "binary1.bin"],
622
625
  )
623
626
 
624
- open_mock = mocker.mock_open(read_data="1\n2\n3\n")
625
- mocker.patch("diff_cover.diff_reporter.open", open_mock)
627
+ base_open_mock = mocker.mock_open(read_data="1\n2\n3\n")
628
+ raise_count = 0
629
+
630
+ def open_side_effect(*args, **kwargs):
631
+ if args[0] == "binary1.bin":
632
+ nonlocal raise_count
633
+ raise_count += 1
634
+
635
+ raise UnicodeDecodeError("utf-8", b"", 0, 1, "invalid start byte")
636
+ return base_open_mock(*args, **kwargs)
637
+
638
+ mocker.patch("diff_cover.diff_reporter.open", open_side_effect)
626
639
  changed = reporter.src_paths_changed()
627
640
 
628
- assert sorted(changed) == [" u2.py", "subdir/file1.py", "u1.py"]
641
+ assert sorted(changed) == [" u2.py", "binary1.bin", "subdir/file1.py", "u1.py"]
629
642
  assert reporter.lines_changed("u1.py") == [1, 2, 3]
630
643
  assert reporter.lines_changed(" u2.py") == [1, 2, 3]
644
+ assert reporter.lines_changed("binary1.bin") == []
645
+
646
+ assert raise_count == 1
647
+
648
+
649
+ @pytest.mark.parametrize(
650
+ "excluded, supported_extensions, path",
651
+ [
652
+ (["file.bin"], ["py"], "file.bin"),
653
+ ([], ["py"], "file.bin"),
654
+ ],
655
+ )
656
+ def test_include_untracked__not_valid_path__not_include_it(
657
+ git_diff, excluded, supported_extensions, path
658
+ ):
659
+ reporter = GitDiffReporter(
660
+ git_diff=git_diff,
661
+ include_untracked=True,
662
+ supported_extensions=supported_extensions,
663
+ exclude=excluded,
664
+ )
665
+ diff_output = git_diff_output(
666
+ {"subdir/file1.py": line_numbers(3, 10) + line_numbers(34, 47)}
667
+ )
668
+ _set_git_diff_output(
669
+ reporter,
670
+ git_diff,
671
+ staged_diff=diff_output,
672
+ untracked=[path],
673
+ )
674
+
675
+ changed = reporter.src_paths_changed()
676
+
677
+ assert sorted(changed) == ["subdir/file1.py"]
631
678
 
632
679
 
633
680
  def _set_git_diff_output(
@@ -412,6 +412,20 @@ class TestDiffCoverIntegration(ToolsIntegrationBase):
412
412
  ["diff-cover", "--show-uncovered", "coverage.xml"],
413
413
  )
414
414
 
415
+ def test_expand_coverage_report_complete_report(self):
416
+ self._check_console_report(
417
+ "git_diff_add.txt",
418
+ "add_console_report.txt",
419
+ ["diff-cover", "coverage.xml", "--expand-coverage-report"],
420
+ )
421
+
422
+ def test_expand_coverage_report_uncomplete_report(self):
423
+ self._check_console_report(
424
+ "git_diff_add.txt",
425
+ "expand_console_report.txt",
426
+ ["diff-cover", "coverage_missing_lines.xml", "--expand-coverage-report"],
427
+ )
428
+
415
429
 
416
430
  class TestDiffQualityIntegration(ToolsIntegrationBase):
417
431
  """
@@ -66,6 +66,21 @@ class TestXmlCoverageReporterTest:
66
66
  ONE_VIOLATION = {Violation(11, None)}
67
67
  VERY_MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27}
68
68
 
69
+ MANY_VIOLATIONS_EXPANDED_MANY_MEASURED = {
70
+ Violation(3, None),
71
+ Violation(4, None),
72
+ Violation(7, None),
73
+ Violation(8, None),
74
+ Violation(9, None),
75
+ Violation(10, None),
76
+ Violation(11, None),
77
+ Violation(12, None),
78
+ Violation(13, None),
79
+ Violation(14, None),
80
+ Violation(15, None),
81
+ Violation(16, None),
82
+ }
83
+
69
84
  @pytest.fixture(autouse=True)
70
85
  def patch_git_patch(self, mocker):
71
86
  # Paths generated by git_path are always the given argument
@@ -266,6 +281,52 @@ class TestXmlCoverageReporterTest:
266
281
  result = coverage.violations("file.py")
267
282
  assert result == set()
268
283
 
284
+ def test_expand_unreported_lines_when_configured(self):
285
+ # Construct the XML report
286
+ file_paths = ["file1.java"]
287
+ # fixture
288
+ violations = self.MANY_VIOLATIONS
289
+ measured = self.MANY_MEASURED
290
+ xml = self._coverage_xml(file_paths, violations, measured)
291
+
292
+ # Parse the reports
293
+ coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
294
+
295
+ # Expect that the name is set
296
+ assert coverage.name() == "XML"
297
+
298
+ # By construction, each file has the same set
299
+ # of covered/uncovered lines
300
+ assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations(
301
+ "file1.java"
302
+ )
303
+
304
+ def test_expand_unreported_lines_without_violations(self):
305
+ # Construct the XML report
306
+ file_paths = ["file1.java"]
307
+ # fixture
308
+ violations = {}
309
+ measured = self.MANY_MEASURED
310
+ xml = self._coverage_xml(file_paths, violations, measured)
311
+
312
+ # Parse the reports
313
+ coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
314
+
315
+ assert set() == coverage.violations("file1.java")
316
+
317
+ def test_expand_unreported_lines_without_measured(self):
318
+ # Construct the XML report
319
+ file_paths = ["file1.java"]
320
+ # fixture
321
+ violations = {}
322
+ measured = {}
323
+ xml = self._coverage_xml(file_paths, violations, measured)
324
+
325
+ # Parse the reports
326
+ coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
327
+
328
+ assert set() == coverage.violations("file1.java")
329
+
269
330
  def _coverage_xml(self, file_paths, violations, measured, source_paths=None):
270
331
  """
271
332
  Build an XML tree with source files specified by `file_paths`.
File without changes
File without changes
File without changes