diff-cover 10.4.2__tar.gz → 10.5.1__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 (161) hide show
  1. {diff_cover-10.4.2 → diff_cover-10.5.1}/PKG-INFO +1 -1
  2. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/command_runner.py +32 -1
  3. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/diff_cover_tool.py +6 -1
  4. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/diff_quality_tool.py +6 -1
  5. diff_cover-10.5.1/diff_cover/violationsreporters/clover.py +84 -0
  6. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/violationsreporters/violations_reporter.py +36 -20
  7. {diff_cover-10.4.2 → diff_cover-10.5.1}/pyproject.toml +1 -1
  8. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_clover_violations_reporter.py +2 -3
  9. diff_cover-10.5.1/tests/test_command_runner_missing_executable.py +92 -0
  10. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_violations_reporter.py +122 -7
  11. {diff_cover-10.4.2 → diff_cover-10.5.1}/LICENSE +0 -0
  12. {diff_cover-10.4.2 → diff_cover-10.5.1}/NOTICE +0 -0
  13. {diff_cover-10.4.2 → diff_cover-10.5.1}/README.rst +0 -0
  14. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/__init__.py +0 -0
  15. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/config_parser.py +0 -0
  16. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/diff_reporter.py +0 -0
  17. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/git_diff.py +0 -0
  18. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/git_path.py +0 -0
  19. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/hook.py +0 -0
  20. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/hookspecs.py +0 -0
  21. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/report_generator.py +0 -0
  22. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/snippets.py +0 -0
  23. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/console_coverage_report.txt +0 -0
  24. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/console_quality_report.txt +0 -0
  25. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/external_style.css +0 -0
  26. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/github_coverage_annotations.txt +0 -0
  27. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/html_coverage_report.html +0 -0
  28. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/html_quality_report.html +0 -0
  29. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/markdown_coverage_report.md +0 -0
  30. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/markdown_quality_report.md +0 -0
  31. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/snippet_content.html +0 -0
  32. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/snippet_content.md +0 -0
  33. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/snippet_content.txt +0 -0
  34. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/templates/snippet_style.html +0 -0
  35. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/util.py +0 -0
  36. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/violationsreporters/__init__.py +0 -0
  37. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/violationsreporters/base.py +0 -0
  38. {diff_cover-10.4.2 → diff_cover-10.5.1}/diff_cover/violationsreporters/java_violations_reporter.py +0 -0
  39. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/__init__.py +0 -0
  40. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/conftest.py +0 -0
  41. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/add_console_report.txt +0 -0
  42. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/add_html_report.html +0 -0
  43. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/add_json_report.json +0 -0
  44. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/add_json_report_float.json +0 -0
  45. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/add_markdown_report.md +0 -0
  46. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/changed_console_report.txt +0 -0
  47. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/changed_html_report.html +0 -0
  48. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/coverage.xml +0 -0
  49. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/coverage1.xml +0 -0
  50. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/coverage2.xml +0 -0
  51. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/coverage_missing_lines.xml +0 -0
  52. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/cpp_functions_console_report.txt +0 -0
  53. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/cpp_functions_coverage.lcov +0 -0
  54. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/delete_console_report.txt +0 -0
  55. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/delete_html_report.html +0 -0
  56. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/dotnet_coverage.xml +0 -0
  57. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/empty.txt +0 -0
  58. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/empty_pycodestyle_violations.txt +0 -0
  59. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/expand_console_report.txt +0 -0
  60. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/external_css_html_report.html +0 -0
  61. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/external_style.css +0 -0
  62. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_add.txt +0 -0
  63. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_changed.txt +0 -0
  64. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_code_dupe.txt +0 -0
  65. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_cpp.txt +0 -0
  66. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_cpp_functions.txt +0 -0
  67. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_delete.txt +0 -0
  68. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_dotnet.txt +0 -0
  69. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_external_css.txt +0 -0
  70. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_lua.txt +0 -0
  71. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_moved.txt +0 -0
  72. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_mult.txt +0 -0
  73. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_python.txt +0 -0
  74. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_subdir.txt +0 -0
  75. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_typescript.txt +0 -0
  76. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_typescript_branches.txt +0 -0
  77. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_unicode.txt +0 -0
  78. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_violations.txt +0 -0
  79. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/git_diff_violations_two_files.txt +0 -0
  80. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/hello.py +0 -0
  81. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/hi.py +0 -0
  82. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/html_report.html +0 -0
  83. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/html_report_empty.html +0 -0
  84. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/html_report_one_snippet.html +0 -0
  85. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/html_report_two_snippets.html +0 -0
  86. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/lcov.info +0 -0
  87. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/lua_console_report.txt +0 -0
  88. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/luacoverage.xml +0 -0
  89. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/markdown_report_one_snippet.md +0 -0
  90. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/markdown_report_two_snippets.md +0 -0
  91. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/moved_console_report.txt +0 -0
  92. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/moved_coverage.xml +0 -0
  93. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/moved_html_report.html +0 -0
  94. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/mult_inputs_console_report.txt +0 -0
  95. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/mult_inputs_html_report.html +0 -0
  96. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_report.txt +0 -0
  97. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report.html +0 -0
  98. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report.json +0 -0
  99. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report.md +0 -0
  100. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report.txt +0 -0
  101. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report_external_css.html +0 -0
  102. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pycodestyle_violations_report_float.json +0 -0
  103. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pyflakes_two_files.txt +0 -0
  104. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pyflakes_violations_report.html +0 -0
  105. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pyflakes_violations_report.txt +0 -0
  106. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_dupe.txt +0 -0
  107. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_dupe_violations_report.txt +0 -0
  108. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_report.txt +0 -0
  109. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_violations_console_report.txt +0 -0
  110. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_violations_report.html +0 -0
  111. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylint_violations_report.txt +0 -0
  112. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/pylintrc +0 -0
  113. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_cpp_console_report.txt +0 -0
  114. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_cpp_coverage.lcov +0 -0
  115. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_python_console_report.txt +0 -0
  116. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_python_coverage.lcov +0 -0
  117. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_typescript_console_report.txt +0 -0
  118. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/real_typescript_coverage.lcov +0 -0
  119. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/show_uncovered_lines_console.txt +0 -0
  120. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet.css +0 -0
  121. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_8859.py +0 -0
  122. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_arabic_output.html +0 -0
  123. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_default.html +0 -0
  124. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_invalid_violations.html +0 -0
  125. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_list.html +0 -0
  126. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_list.md +0 -0
  127. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_list2.md +0 -0
  128. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_list3.md +0 -0
  129. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_no_filename_ext.html +0 -0
  130. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_src.py +0 -0
  131. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_src2.cpp +0 -0
  132. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_src3.cpp +0 -0
  133. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_unicode.html +0 -0
  134. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_unicode.py +0 -0
  135. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/snippet_unicode_html_output.html +0 -0
  136. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/subdir_coverage_console_report.txt +0 -0
  137. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/subdir_coverage_html_report.html +0 -0
  138. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/test_src.txt +0 -0
  139. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/typescript_branches_console_report.txt +0 -0
  140. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/typescript_branches_coverage.lcov +0 -0
  141. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/unicode_console_report.txt +0 -0
  142. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/unicode_coverage.xml +0 -0
  143. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/unicode_html_report.html +0 -0
  144. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/unicode_test_src.txt +0 -0
  145. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/fixtures/violations_test_file.py +0 -0
  146. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/helpers.py +0 -0
  147. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/snippet_list_unicode.html +0 -0
  148. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_clover_violations_reporter/test.xml +0 -0
  149. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_config_parser.py +0 -0
  150. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_diff_cover_main.py +0 -0
  151. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_diff_cover_tool.py +0 -0
  152. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_diff_quality_main.py +0 -0
  153. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_diff_reporter.py +0 -0
  154. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_git_diff.py +0 -0
  155. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_git_diff_file.py +0 -0
  156. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_git_path.py +0 -0
  157. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_integration.py +0 -0
  158. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_java_violations_reporter.py +0 -0
  159. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_report_generator.py +0 -0
  160. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_snippets.py +0 -0
  161. {diff_cover-10.4.2 → diff_cover-10.5.1}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: diff_cover
3
- Version: 10.4.2
3
+ Version: 10.5.1
4
4
  Summary: Run coverage and linting reports on diffs
5
5
  License: Apache-2.0
6
6
  Author: See Contributors
@@ -1,6 +1,8 @@
1
1
  import subprocess
2
2
  import sys
3
3
 
4
+ GIT_INSTALL_URL = "https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
5
+
4
6
 
5
7
  class CommandError(Exception):
6
8
  """
@@ -8,6 +10,16 @@ class CommandError(Exception):
8
10
  """
9
11
 
10
12
 
13
+ class ExecutableNotFoundError(CommandError):
14
+ """
15
+ Error raised when the executable of a command cannot be found at all.
16
+
17
+ This is a `CommandError` so that existing handling keeps working, but a
18
+ distinct type so the command line tools can tell "your tooling is not
19
+ installed" apart from "the command ran and failed".
20
+ """
21
+
22
+
11
23
  def execute(command, exit_codes=None):
12
24
  """Execute provided command returning the stdout
13
25
  Args:
@@ -24,7 +36,12 @@ def execute(command, exit_codes=None):
24
36
  exit_codes = [0]
25
37
 
26
38
  stdout_pipe = subprocess.PIPE
27
- with subprocess.Popen(command, stdout=stdout_pipe, stderr=stdout_pipe) as process:
39
+ try:
40
+ popen = subprocess.Popen(command, stdout=stdout_pipe, stderr=stdout_pipe)
41
+ except FileNotFoundError as exc:
42
+ raise ExecutableNotFoundError(_executable_not_found_message(command)) from exc
43
+
44
+ with popen as process:
28
45
  try:
29
46
  stdout, stderr = process.communicate()
30
47
  except OSError:
@@ -52,6 +69,20 @@ def run_command_for_code(command):
52
69
  return process.returncode
53
70
 
54
71
 
72
+ def _executable_not_found_message(command):
73
+ """
74
+ Explain that the executable of `command` is not installed or not on PATH.
75
+ """
76
+ executable = _ensure_unicode(command[0]) if command else ""
77
+ message = (
78
+ f"'{executable}' was not found. diff-cover needs '{executable}' to be "
79
+ "installed and on your PATH in order to run."
80
+ )
81
+ if executable == "git":
82
+ message += f" See {GIT_INSTALL_URL} for installation instructions."
83
+ return message
84
+
85
+
55
86
  def _ensure_unicode(text):
56
87
  """
57
88
  Ensures the text passed in becomes unicode
@@ -7,6 +7,7 @@ import warnings
7
7
  import xml.etree.ElementTree as etree
8
8
 
9
9
  from diff_cover import DESCRIPTION, VERSION
10
+ from diff_cover.command_runner import ExecutableNotFoundError
10
11
  from diff_cover.config_parser import Tool, get_config
11
12
  from diff_cover.diff_reporter import GitDiffReporter
12
13
  from diff_cover.git_diff import GitDiffFileTool, GitDiffTool
@@ -418,7 +419,11 @@ def main(argv=None, directory=None):
418
419
  level = logging.ERROR if quiet else logging.WARNING
419
420
  logging.basicConfig(format="%(message)s", level=level)
420
421
 
421
- GitPathTool.set_cwd(directory)
422
+ try:
423
+ GitPathTool.set_cwd(directory)
424
+ except ExecutableNotFoundError as exc:
425
+ LOGGER.error("%s", exc)
426
+ return 1
422
427
  fail_under = arg_dict.get("fail_under")
423
428
  diff_tool = None
424
429
 
@@ -14,6 +14,7 @@ import pluggy
14
14
 
15
15
  import diff_cover
16
16
  from diff_cover import hookspecs
17
+ from diff_cover.command_runner import ExecutableNotFoundError
17
18
  from diff_cover.config_parser import Tool, get_config
18
19
  from diff_cover.diff_cover_tool import (
19
20
  COMPARE_BRANCH_HELP,
@@ -339,7 +340,11 @@ def main(argv=None, directory=None):
339
340
  level = logging.ERROR if quiet else logging.WARNING
340
341
  logging.basicConfig(format="%(message)s", level=level)
341
342
 
342
- GitPathTool.set_cwd(directory)
343
+ try:
344
+ GitPathTool.set_cwd(directory)
345
+ except ExecutableNotFoundError as exc:
346
+ LOGGER.error("%s", exc)
347
+ return 1
343
348
  fail_under = arg_dict.get("fail_under")
344
349
  tool = arg_dict["violations"]
345
350
  user_options = arg_dict.get("options")
@@ -0,0 +1,84 @@
1
+ """
2
+ Reading the ``<file>`` elements of a Clover XML coverage report.
3
+ """
4
+
5
+ import itertools
6
+ from collections import defaultdict
7
+
8
+ from diff_cover import util
9
+ from diff_cover.git_path import GitPathTool
10
+
11
+
12
+ class CloverFileIndex:
13
+ """
14
+ The ``<file>`` elements of one Clover report, indexed for lookup by path.
15
+
16
+ Building the index costs one walk of the document. Looking a source path up
17
+ by walking the document instead costs that walk once per source path, which
18
+ is quadratic in a report with many files.
19
+ """
20
+
21
+ def __init__(self, xml_document):
22
+ """
23
+ Index every ``<file>`` element of `xml_document`.
24
+
25
+ A file is matched either by its path relative to the repository root
26
+ or, when the report carries absolute paths, by a suffix test. The
27
+ suffix can only match when the last path segment does, so candidates
28
+ for it are grouped by that segment and the test is applied to the few
29
+ that share it.
30
+
31
+ Both tables store the document position of each element so that
32
+ lookups can return matches in document order, as a direct walk would.
33
+ """
34
+ self._by_relative_path = defaultdict(list)
35
+ self._by_last_segment = defaultdict(list)
36
+ for position, file_tree in enumerate(xml_document.findall(".//file")):
37
+ file_path = file_tree.get("path") or file_tree.get("name")
38
+ if not file_path:
39
+ continue
40
+
41
+ normalized_file_path = util.to_unix_path(file_path)
42
+ relative_file_path = util.to_unix_path(GitPathTool.relative_path(file_path))
43
+ self._by_relative_path[relative_file_path].append((position, file_tree))
44
+ self._by_last_segment[normalized_file_path.rsplit("/", 1)[-1]].append(
45
+ (position, normalized_file_path, file_tree)
46
+ )
47
+
48
+ def _files(self, src_path):
49
+ """
50
+ Return the ``<file>`` elements for `src_path`, in document order.
51
+ """
52
+ normalized_src_path = util.to_unix_path(src_path)
53
+
54
+ matches = {}
55
+ for position, file_tree in self._by_relative_path.get(normalized_src_path, ()):
56
+ matches[position] = file_tree
57
+ suffix = f"/{normalized_src_path}"
58
+ for position, normalized_file_path, file_tree in self._by_last_segment.get(
59
+ normalized_src_path.rsplit("/", 1)[-1], ()
60
+ ):
61
+ if normalized_file_path.endswith(suffix):
62
+ matches[position] = file_tree
63
+
64
+ return [matches[position] for position in sorted(matches)]
65
+
66
+ def line_nodes(self, src_path):
67
+ """
68
+ Return a list of nodes containing line information for `src_path`.
69
+
70
+ If the file is not present in the report, return None
71
+ """
72
+ files = self._files(src_path)
73
+ if not files:
74
+ return None
75
+ lines = []
76
+ for file_tree in files:
77
+ # Clover marks an executable line as one of these three types. PHPUnit's
78
+ # writer emits `method` for the declaration line of every function it
79
+ # measured; leaving it out reported those lines as unmeasured.
80
+ # https://github.com/sebastianbergmann/php-code-coverage/blob/main/src/Report/Clover.php
81
+ lines.append(file_tree.findall('./line[@type="method"]'))
82
+ lines.append(file_tree.findall('./line[@type="stmt"]'))
83
+ lines.append(file_tree.findall('./line[@type="cond"]'))
84
+ return list(itertools.chain(*lines))
@@ -17,6 +17,7 @@ from diff_cover.violationsreporters.base import (
17
17
  RegexBasedDriver,
18
18
  Violation,
19
19
  )
20
+ from diff_cover.violationsreporters.clover import CloverFileIndex
20
21
 
21
22
 
22
23
  class XmlCoverageReporter(BaseViolationReporter):
@@ -46,6 +47,14 @@ class XmlCoverageReporter(BaseViolationReporter):
46
47
  # Values are output of `self._get_xml_classes()`
47
48
  self._xml_cache = [{} for i in range(len(xml_roots))]
48
49
 
50
+ # Which format each report is in. Classifying costs a search through the
51
+ # document, and the answer cannot change, so do it once here rather than
52
+ # once per source file in `_cache_file`.
53
+ self._report_formats = [self._detect_report_format(root) for root in xml_roots]
54
+
55
+ # A `CloverFileIndex` for each Clover report, built on first use.
56
+ self._clover_file_index = [None] * len(xml_roots)
57
+
49
58
  self._src_roots = src_roots or [""]
50
59
  self._expand_coverage_report = expand_coverage_report
51
60
  self._branch_coverage = branch_coverage
@@ -120,27 +129,36 @@ class XmlCoverageReporter(BaseViolationReporter):
120
129
  lines = [clazz.findall("./lines/line") for clazz in classes]
121
130
  return list(itertools.chain(*lines))
122
131
 
123
- @staticmethod
124
- def get_src_path_line_nodes_clover(xml_document, src_path):
132
+ def get_src_path_line_nodes_clover(self, index, xml_document, src_path):
125
133
  """
126
134
  Return a list of nodes containing line information for `src_path`
127
135
  in `xml_document`.
128
136
 
129
137
  If file is not present in `xml_document`, return None
130
138
  """
139
+ if self._clover_file_index[index] is None:
140
+ self._clover_file_index[index] = CloverFileIndex(xml_document)
141
+ return self._clover_file_index[index].line_nodes(src_path)
131
142
 
132
- files = [
133
- file_tree
134
- for file_tree in xml_document.findall(".//file")
135
- if GitPathTool.relative_path(file_tree.get("path")) == src_path
136
- ]
137
- if not files:
138
- return None
139
- lines = []
140
- for file_tree in files:
141
- lines.append(file_tree.findall('./line[@type="stmt"]'))
142
- lines.append(file_tree.findall('./line[@type="cond"]'))
143
- return list(itertools.chain(*lines))
143
+ @staticmethod
144
+ def _detect_report_format(xml_document):
145
+ """
146
+ Return which of the supported formats `xml_document` is in.
147
+
148
+ Clover writes a `clover` attribute on the root, but PHPUnit's Clover
149
+ writer does not, so fall back to the shape of its line elements.
150
+ """
151
+ if (
152
+ xml_document.findall(".[@clover]")
153
+ or xml_document.find(".//file/line[@num][@count]") is not None
154
+ ):
155
+ # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src
156
+ return "clover"
157
+ if xml_document.findall(".[@name]"):
158
+ # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
159
+ return "jacoco"
160
+ # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd
161
+ return "cobertura"
144
162
 
145
163
  def _measured_source_path_matches(self, package_name, file_name, src_path):
146
164
  # find src_path in any of the source roots
@@ -204,22 +222,20 @@ class XmlCoverageReporter(BaseViolationReporter):
204
222
 
205
223
  # Loop through the files that contain the xml roots
206
224
  for i, xml_document in enumerate(self._xml_roots):
207
- if xml_document.findall(".[@clover]"):
208
- # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src
225
+ report_format = self._report_formats[i]
226
+ if report_format == "clover":
209
227
  line_nodes = self.get_src_path_line_nodes_clover(
210
- xml_document, src_path
228
+ i, xml_document, src_path
211
229
  )
212
230
  _number = "num"
213
231
  _hits = "count"
214
- elif xml_document.findall(".[@name]"):
215
- # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
232
+ elif report_format == "jacoco":
216
233
  line_nodes = self.get_src_path_line_nodes_jacoco(
217
234
  xml_document, src_path
218
235
  )
219
236
  _number = "nr"
220
237
  _hits = "ci"
221
238
  else:
222
- # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd
223
239
  line_nodes = self.get_src_path_line_nodes_cobertura(
224
240
  i, xml_document, src_path
225
241
  )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "diff_cover"
3
- version = "10.4.2"
3
+ version = "10.5.1"
4
4
  description = "Run coverage and linting reports on diffs"
5
5
  authors = ["See Contributors"]
6
6
  homepage = "https://github.com/Bachmann1234/diff-cover"
@@ -13,7 +13,6 @@ def test_get_src_path_clover(datadir):
13
13
  GitPathTool._cwd = "/"
14
14
  GitPathTool._root = "/"
15
15
  clover_report = etree.parse(str(datadir / "test.xml"))
16
- result = XmlCoverageReporter.get_src_path_line_nodes_clover(
17
- clover_report, "isLucky.js"
18
- )
16
+ reporter = XmlCoverageReporter([clover_report])
17
+ result = reporter.get_src_path_line_nodes_clover(0, clover_report, "isLucky.js")
19
18
  assert sorted([int(line.attrib["num"]) for line in result]) == [2, 3, 5, 6, 8, 12]
@@ -0,0 +1,92 @@
1
+ # pylint: disable=missing-function-docstring
2
+
3
+ """Tests for the message shown when a required executable is not on PATH.
4
+
5
+ See https://github.com/Bachmann1234/diff_cover/issues/303
6
+ """
7
+
8
+ import pytest
9
+
10
+ from diff_cover.command_runner import (
11
+ GIT_INSTALL_URL,
12
+ CommandError,
13
+ ExecutableNotFoundError,
14
+ execute,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def missing_git(mocker):
20
+ """Make every subprocess launch fail the way a missing `git` does."""
21
+ return mocker.patch(
22
+ "diff_cover.command_runner.subprocess.Popen",
23
+ side_effect=FileNotFoundError(2, "No such file or directory", "git"),
24
+ )
25
+
26
+
27
+ @pytest.fixture
28
+ def failing_git(mocker):
29
+ """Make `git` run and fail, which is a different thing entirely."""
30
+ process = mocker.Mock()
31
+ process.returncode = 1
32
+ process.communicate.return_value = (b"", b"fatal: not a git repository")
33
+ popen = mocker.patch("diff_cover.command_runner.subprocess.Popen")
34
+ popen.return_value.__enter__.return_value = process
35
+ popen.return_value.__exit__.return_value = None
36
+ return popen
37
+
38
+
39
+ def test_execute_turns_a_missing_executable_into_a_helpful_error(missing_git):
40
+ with pytest.raises(ExecutableNotFoundError) as exc_info:
41
+ execute(["git", "rev-parse", "--show-toplevel"])
42
+
43
+ message = str(exc_info.value)
44
+ assert "git" in message
45
+ assert "PATH" in message
46
+ assert message.endswith(f"See {GIT_INSTALL_URL} for installation instructions.")
47
+
48
+
49
+ def test_the_new_error_stays_a_command_error(missing_git):
50
+ """Callers that already catch CommandError must keep working."""
51
+ assert issubclass(ExecutableNotFoundError, CommandError)
52
+ with pytest.raises(CommandError):
53
+ execute(["git", "rev-parse", "--show-toplevel"])
54
+
55
+
56
+ def test_execute_does_not_link_git_docs_for_other_executables(mocker):
57
+ mocker.patch(
58
+ "diff_cover.command_runner.subprocess.Popen",
59
+ side_effect=FileNotFoundError(2, "No such file or directory", "pycodestyle"),
60
+ )
61
+
62
+ with pytest.raises(ExecutableNotFoundError) as exc_info:
63
+ execute(["pycodestyle", "--version"])
64
+
65
+ message = str(exc_info.value)
66
+ assert "pycodestyle" in message
67
+ assert GIT_INSTALL_URL not in message
68
+
69
+
70
+ def test_diff_cover_main_reports_missing_git_without_a_traceback(missing_git, caplog):
71
+ from diff_cover.diff_cover_tool import main
72
+
73
+ assert main(["diff-cover", "coverage.xml"]) == 1
74
+ assert "PATH" in caplog.text
75
+
76
+
77
+ def test_diff_quality_main_reports_missing_git_without_a_traceback(missing_git, caplog):
78
+ from diff_cover.diff_quality_tool import main
79
+
80
+ assert main(["diff-quality", "--violations", "pycodestyle"]) == 1
81
+ assert "PATH" in caplog.text
82
+
83
+
84
+ def test_a_command_that_runs_and_fails_is_left_alone(failing_git):
85
+ """Control. Green before and after: only a *missing* binary is reworded."""
86
+ with pytest.raises(CommandError) as exc_info:
87
+ execute(["git", "status"])
88
+
89
+ assert not isinstance(exc_info.value, ExecutableNotFoundError)
90
+ message = str(exc_info.value)
91
+ assert "fatal: not a git repository" in message
92
+ assert "PATH" not in message
@@ -464,12 +464,15 @@ class TestCloverXmlCoverageReporterTest:
464
464
 
465
465
  @pytest.fixture(autouse=True)
466
466
  def patch_git_patch(self, mocker):
467
- # Paths generated by git_path are always the given argument
468
- _git_path_mock = mocker.patch(
469
- "diff_cover.violationsreporters.violations_reporter.GitPathTool"
470
- )
471
- _git_path_mock.relative_path = lambda path: path
472
- _git_path_mock.absolute_path = lambda path: path
467
+ # Paths generated by git_path are always the given argument. The Clover
468
+ # lookup resolves them in its own module, so patch both.
469
+ for module in (
470
+ "diff_cover.violationsreporters.violations_reporter",
471
+ "diff_cover.violationsreporters.clover",
472
+ ):
473
+ _git_path_mock = mocker.patch(f"{module}.GitPathTool")
474
+ _git_path_mock.relative_path = lambda path: path
475
+ _git_path_mock.absolute_path = lambda path: path
473
476
 
474
477
  def test_violations(self):
475
478
  # Construct the XML report
@@ -497,6 +500,109 @@ class TestCloverXmlCoverageReporterTest:
497
500
  result = coverage.violations("file1.java")
498
501
  assert result == violations
499
502
 
503
+ def test_phpunit_clover_without_clover_attribute(self):
504
+ xml = self._coverage_xml(
505
+ ["/workspace/project/subdir/file.java"],
506
+ self.FEW_VIOLATIONS,
507
+ self.FEW_MEASURED,
508
+ )
509
+ del xml.attrib["clover"]
510
+ file_node = xml.find(".//file")
511
+ file_node.set("name", file_node.attrib.pop("path"))
512
+
513
+ coverage = XmlCoverageReporter([xml])
514
+
515
+ assert coverage.violations("subdir/file.java") == self.FEW_VIOLATIONS
516
+ assert coverage.measured_lines("subdir/file.java") == self.FEW_MEASURED
517
+
518
+ def test_method_lines_are_measured(self):
519
+ # PHPUnit's Clover writer emits type="method" for the declaration line of
520
+ # every measured function. Those lines carry a count like any other and
521
+ # must not be reported as unmeasured.
522
+ xml = self._coverage_xml(
523
+ ["file1.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED, method_lines={42}
524
+ )
525
+
526
+ coverage = XmlCoverageReporter([xml])
527
+
528
+ assert 42 in coverage.measured_lines("file1.java")
529
+ assert coverage.violations("file1.java") == self.FEW_VIOLATIONS
530
+
531
+ def test_method_line_can_be_a_violation(self):
532
+ xml = self._coverage_xml(
533
+ ["file1.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED, method_lines={42}
534
+ )
535
+ method_line = xml.find('.//line[@type="method"]')
536
+ method_line.set("count", "0")
537
+
538
+ coverage = XmlCoverageReporter([xml])
539
+
540
+ assert 42 in coverage.measured_lines("file1.java")
541
+ assert Violation(42, None) in coverage.violations("file1.java")
542
+
543
+ def test_report_format_is_detected_once(self):
544
+ # Classifying the report is a search through the whole document; doing it
545
+ # per source file was the review comment on #617.
546
+ xml = self._coverage_xml(
547
+ ["file1.java", "file2.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED
548
+ )
549
+
550
+ coverage = XmlCoverageReporter([xml])
551
+
552
+ assert coverage._report_formats == ["clover"]
553
+ coverage.violations("file1.java")
554
+ coverage.violations("file2.java")
555
+ assert coverage._report_formats == ["clover"]
556
+
557
+ def test_file_lookup_is_built_once(self, mocker):
558
+ # Locating a source file used to walk every `.//file` again, so a report
559
+ # with N files cost N walks for N source paths. Each `<file>` is now
560
+ # classified once, which shows up as one `relative_path` call per file
561
+ # however many source paths are looked up.
562
+ file_paths = ["file1.java", "subdir/file2.java", "subdir/file3.java"]
563
+ xml = self._coverage_xml(file_paths, self.FEW_VIOLATIONS, self.FEW_MEASURED)
564
+
565
+ relative_path = mocker.patch(
566
+ "diff_cover.violationsreporters.clover.GitPathTool.relative_path",
567
+ side_effect=lambda path: path,
568
+ )
569
+ coverage = XmlCoverageReporter([xml])
570
+ for src_path in file_paths:
571
+ assert coverage.violations(src_path) == self.FEW_VIOLATIONS
572
+
573
+ assert relative_path.call_count == len(file_paths), (
574
+ "expected each <file> to be classified once, got "
575
+ f"{relative_path.call_count} calls for {len(file_paths)} files"
576
+ )
577
+
578
+ def test_absolute_paths_still_match_by_suffix(self):
579
+ # Reports that carry absolute paths are matched by suffix, not equality.
580
+ # The lookup groups those candidates by their last path segment, so a
581
+ # file whose segment collides with another must still resolve correctly.
582
+ xml = self._coverage_xml(
583
+ ["/build/one/shared.java", "/build/two/shared.java"],
584
+ self.FEW_VIOLATIONS,
585
+ self.FEW_MEASURED,
586
+ )
587
+
588
+ coverage = XmlCoverageReporter([xml])
589
+
590
+ assert coverage.violations("two/shared.java") == self.FEW_VIOLATIONS
591
+ assert coverage.measured_lines("two/shared.java") == self.FEW_MEASURED
592
+ assert coverage.violations("nowhere/shared.java") == set()
593
+
594
+ def test_file_without_a_path_is_skipped(self):
595
+ # A `<file>` carries its path in `path`, or in `name` from PHPUnit's
596
+ # writer. One with neither cannot be matched against a source path, and
597
+ # it must not keep the rest of the report from being indexed.
598
+ xml = self._coverage_xml(["file1.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED)
599
+ xml.find(".//package").insert(0, etree.Element("file"))
600
+
601
+ coverage = XmlCoverageReporter([xml])
602
+
603
+ assert coverage.violations("file1.java") == self.FEW_VIOLATIONS
604
+ assert coverage.measured_lines("file1.java") == self.FEW_MEASURED
605
+
500
606
  def test_two_inputs_first_violate(self):
501
607
  # Construct the XML report
502
608
  file_paths = ["file1.java"]
@@ -620,7 +726,7 @@ class TestCloverXmlCoverageReporterTest:
620
726
  result = coverage.violations("file.java")
621
727
  assert result == set()
622
728
 
623
- def _coverage_xml(self, file_paths, violations, measured):
729
+ def _coverage_xml(self, file_paths, violations, measured, method_lines=None):
624
730
  """
625
731
  Build an XML tree with source files specified by `file_paths`.
626
732
  Each source fill will have the same set of covered and
@@ -630,6 +736,9 @@ class TestCloverXmlCoverageReporterTest:
630
736
  `line_dict` is a dictionary with keys that are line numbers
631
737
  and values that are True/False indicating whether the line
632
738
  is covered
739
+ `method_lines` is an optional set of line numbers to emit as
740
+ `type="method"` instead of `type="stmt"`, the way PHPUnit's Clover
741
+ writer marks the declaration line of a measured function
633
742
 
634
743
  This leaves out some attributes of the Cobertura format,
635
744
  but includes all the elements.
@@ -654,6 +763,12 @@ class TestCloverXmlCoverageReporterTest:
654
763
  line.set("count", str(hits))
655
764
  line.set("num", str(line_num))
656
765
  line.set("type", "stmt")
766
+
767
+ for line_num in method_lines or ():
768
+ line = etree.SubElement(src_node, "line")
769
+ line.set("count", "1")
770
+ line.set("num", str(line_num))
771
+ line.set("type", "method")
657
772
  return root
658
773
 
659
774
 
File without changes
File without changes
File without changes