diff-cover 9.3.2__tar.gz → 9.4.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 (157) hide show
  1. {diff_cover-9.3.2 → diff_cover-9.4.0}/PKG-INFO +1 -1
  2. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/diff_cover_tool.py +8 -7
  3. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/diff_quality_tool.py +4 -3
  4. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/diff_reporter.py +14 -16
  5. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/git_diff.py +1 -3
  6. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/util.py +30 -0
  7. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/violationsreporters/base.py +18 -18
  8. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/violationsreporters/violations_reporter.py +116 -43
  9. {diff_cover-9.3.2 → diff_cover-9.4.0}/pyproject.toml +5 -4
  10. diff_cover-9.4.0/tests/fixtures/cpp_functions_console_report.txt +10 -0
  11. diff_cover-9.4.0/tests/fixtures/cpp_functions_coverage.lcov +30 -0
  12. diff_cover-9.4.0/tests/fixtures/git_diff_cpp.txt +50 -0
  13. diff_cover-9.4.0/tests/fixtures/git_diff_cpp_functions.txt +28 -0
  14. diff_cover-9.4.0/tests/fixtures/git_diff_python.txt +38 -0
  15. diff_cover-9.4.0/tests/fixtures/git_diff_typescript.txt +45 -0
  16. diff_cover-9.4.0/tests/fixtures/git_diff_typescript_branches.txt +29 -0
  17. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/lcov.info +1 -1
  18. diff_cover-9.4.0/tests/fixtures/real_cpp_console_report.txt +10 -0
  19. diff_cover-9.4.0/tests/fixtures/real_cpp_coverage.lcov +45 -0
  20. diff_cover-9.4.0/tests/fixtures/real_python_console_report.txt +10 -0
  21. diff_cover-9.4.0/tests/fixtures/real_python_coverage.lcov +55 -0
  22. diff_cover-9.4.0/tests/fixtures/real_typescript_console_report.txt +10 -0
  23. diff_cover-9.4.0/tests/fixtures/real_typescript_coverage.lcov +46 -0
  24. diff_cover-9.4.0/tests/fixtures/typescript_branches_console_report.txt +10 -0
  25. diff_cover-9.4.0/tests/fixtures/typescript_branches_coverage.lcov +35 -0
  26. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/helpers.py +1 -1
  27. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_diff_cover_main.py +2 -2
  28. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_diff_cover_tool.py +9 -9
  29. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_diff_quality_main.py +0 -1
  30. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_diff_reporter.py +0 -2
  31. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_integration.py +73 -1
  32. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_report_generator.py +122 -123
  33. diff_cover-9.4.0/tests/test_util.py +76 -0
  34. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_violations_reporter.py +94 -15
  35. diff_cover-9.3.2/tests/test_util.py +0 -23
  36. {diff_cover-9.3.2 → diff_cover-9.4.0}/LICENSE +0 -0
  37. {diff_cover-9.3.2 → diff_cover-9.4.0}/NOTICE +0 -0
  38. {diff_cover-9.3.2 → diff_cover-9.4.0}/README.rst +0 -0
  39. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/__init__.py +0 -0
  40. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/command_runner.py +0 -0
  41. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/config_parser.py +0 -0
  42. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/git_path.py +0 -0
  43. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/hook.py +0 -0
  44. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/hookspecs.py +0 -0
  45. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/report_generator.py +0 -0
  46. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/snippets.py +0 -0
  47. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/console_coverage_report.txt +0 -0
  48. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/console_quality_report.txt +0 -0
  49. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/external_style.css +0 -0
  50. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/html_coverage_report.html +0 -0
  51. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/html_quality_report.html +0 -0
  52. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/markdown_coverage_report.md +0 -0
  53. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/markdown_quality_report.md +0 -0
  54. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.html +0 -0
  55. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.md +0 -0
  56. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.txt +0 -0
  57. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/templates/snippet_style.html +0 -0
  58. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/violationsreporters/__init__.py +0 -0
  59. {diff_cover-9.3.2 → diff_cover-9.4.0}/diff_cover/violationsreporters/java_violations_reporter.py +0 -0
  60. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/__init__.py +0 -0
  61. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/add_console_report.txt +0 -0
  62. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/add_html_report.html +0 -0
  63. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/add_json_report.json +0 -0
  64. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/add_markdown_report.md +0 -0
  65. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/changed_console_report.txt +0 -0
  66. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/changed_html_report.html +0 -0
  67. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/coverage.xml +0 -0
  68. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/coverage1.xml +0 -0
  69. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/coverage2.xml +0 -0
  70. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/coverage_missing_lines.xml +0 -0
  71. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/delete_console_report.txt +0 -0
  72. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/delete_html_report.html +0 -0
  73. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/dotnet_coverage.xml +0 -0
  74. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/dotnet_coverage_console_report.txt +0 -0
  75. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/empty.txt +0 -0
  76. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/empty_pycodestyle_violations.txt +0 -0
  77. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/expand_console_report.txt +0 -0
  78. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/external_css_html_report.html +0 -0
  79. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/external_style.css +0 -0
  80. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_add.txt +0 -0
  81. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_changed.txt +0 -0
  82. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_code_dupe.txt +0 -0
  83. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_delete.txt +0 -0
  84. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_dotnet.txt +0 -0
  85. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_external_css.txt +0 -0
  86. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_lua.txt +0 -0
  87. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_moved.txt +0 -0
  88. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_mult.txt +0 -0
  89. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_subdir.txt +0 -0
  90. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_unicode.txt +0 -0
  91. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_violations.txt +0 -0
  92. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/git_diff_violations_two_files.txt +0 -0
  93. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/hello.py +0 -0
  94. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/hi.py +0 -0
  95. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/html_report.html +0 -0
  96. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/html_report_empty.html +0 -0
  97. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/html_report_one_snippet.html +0 -0
  98. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/html_report_two_snippets.html +0 -0
  99. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/lua_console_report.txt +0 -0
  100. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/luacoverage.xml +0 -0
  101. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/markdown_report_one_snippet.md +0 -0
  102. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/markdown_report_two_snippets.md +0 -0
  103. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/moved_console_report.txt +0 -0
  104. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/moved_coverage.xml +0 -0
  105. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/moved_html_report.html +0 -0
  106. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/mult_inputs_console_report.txt +0 -0
  107. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/mult_inputs_html_report.html +0 -0
  108. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_report.txt +0 -0
  109. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.html +0 -0
  110. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.json +0 -0
  111. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.md +0 -0
  112. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.txt +0 -0
  113. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report_external_css.html +0 -0
  114. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pyflakes_two_files.txt +0 -0
  115. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pyflakes_violations_report.html +0 -0
  116. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pyflakes_violations_report.txt +0 -0
  117. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_dupe.txt +0 -0
  118. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_dupe_violations_report.txt +0 -0
  119. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_report.txt +0 -0
  120. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_console_report.txt +0 -0
  121. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_report.html +0 -0
  122. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_report.txt +0 -0
  123. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/pylintrc +0 -0
  124. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/show_uncovered_lines_console.txt +0 -0
  125. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet.css +0 -0
  126. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_8859.py +0 -0
  127. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_arabic_output.html +0 -0
  128. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_default.html +0 -0
  129. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_invalid_violations.html +0 -0
  130. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_list.html +0 -0
  131. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_list.md +0 -0
  132. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_list2.md +0 -0
  133. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_list3.md +0 -0
  134. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_no_filename_ext.html +0 -0
  135. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_src.py +0 -0
  136. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_src2.cpp +0 -0
  137. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_src3.cpp +0 -0
  138. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode.html +0 -0
  139. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode.py +0 -0
  140. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode_html_output.html +0 -0
  141. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/subdir_coverage_console_report.txt +0 -0
  142. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/subdir_coverage_html_report.html +0 -0
  143. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/test_src.txt +0 -0
  144. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/unicode_console_report.txt +0 -0
  145. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/unicode_coverage.xml +0 -0
  146. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/unicode_html_report.html +0 -0
  147. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/unicode_test_src.txt +0 -0
  148. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/fixtures/violations_test_file.py +0 -0
  149. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/snippet_list_unicode.html +0 -0
  150. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_clover_violations_reporter/test.xml +0 -0
  151. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_clover_violations_reporter.py +0 -0
  152. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_config_parser.py +0 -0
  153. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_git_diff.py +0 -0
  154. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_git_diff_file.py +0 -0
  155. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_git_path.py +0 -0
  156. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_java_violations_reporter.py +0 -0
  157. {diff_cover-9.3.2 → diff_cover-9.4.0}/tests/test_snippets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: diff_cover
3
- Version: 9.3.2
3
+ Version: 9.4.0
4
4
  Summary: Run coverage and linting reports on diffs
5
5
  License: Apache-2.0
6
6
  Author: See Contributors
@@ -17,6 +17,7 @@ from diff_cover.report_generator import (
17
17
  MarkdownReportGenerator,
18
18
  StringReportGenerator,
19
19
  )
20
+ from diff_cover.util import open_file
20
21
  from diff_cover.violationsreporters.violations_reporter import (
21
22
  LcovCoverageReporter,
22
23
  XmlCoverageReporter,
@@ -81,7 +82,7 @@ def parse_coverage_args(argv):
81
82
  """
82
83
  parser = argparse.ArgumentParser(description=DESCRIPTION)
83
84
 
84
- parser.add_argument("coverage_file", type=str, help=COVERAGE_FILE_HELP, nargs="+")
85
+ parser.add_argument("coverage_files", type=str, help=COVERAGE_FILE_HELP, nargs="+")
85
86
 
86
87
  parser.add_argument(
87
88
  "--format",
@@ -236,9 +237,9 @@ def generate_coverage_report(
236
237
  for coverage_file in coverage_files
237
238
  if not coverage_file.endswith(".xml")
238
239
  ]
239
- if len(xml_roots) > 0 and len(lcov_roots) > 0:
240
- raise ValueError(f"Mixing LCov and XML reports is not supported yet")
241
- elif len(xml_roots) > 0:
240
+ if xml_roots and lcov_roots:
241
+ raise ValueError("Mixing LCov and XML reports is not supported yet")
242
+ if xml_roots:
242
243
  coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report)
243
244
  else:
244
245
  coverage = LcovCoverageReporter(lcov_roots, src_roots)
@@ -250,7 +251,7 @@ def generate_coverage_report(
250
251
  if css_url is not None:
251
252
  css_url = os.path.relpath(css_file, os.path.dirname(html_report))
252
253
  reporter = HtmlReportGenerator(coverage, diff, css_url=css_url)
253
- with open(html_report, "wb") as output_file:
254
+ with open_file(html_report, "wb") as output_file:
254
255
  reporter.generate_report(output_file)
255
256
  if css_file is not None:
256
257
  with open(css_file, "wb") as output_file:
@@ -259,7 +260,7 @@ def generate_coverage_report(
259
260
  if "json" in report_formats:
260
261
  json_report = report_formats["json"] or JSON_REPORT_DEFAULT_PATH
261
262
  reporter = JsonReportGenerator(coverage, diff)
262
- with open(json_report, "wb") as output_file:
263
+ with open_file(json_report, "wb") as output_file:
263
264
  reporter.generate_report(output_file)
264
265
 
265
266
  if "markdown" in report_formats:
@@ -351,7 +352,7 @@ def main(argv=None, directory=None):
351
352
  diff_tool = GitDiffFileTool(arg_dict["diff_file"])
352
353
 
353
354
  percent_covered = generate_coverage_report(
354
- arg_dict["coverage_file"],
355
+ arg_dict["coverage_files"],
355
356
  arg_dict["compare_branch"],
356
357
  diff_tool,
357
358
  report_formats=arg_dict["format"],
@@ -41,6 +41,7 @@ from diff_cover.report_generator import (
41
41
  MarkdownQualityReportGenerator,
42
42
  StringQualityReportGenerator,
43
43
  )
44
+ from diff_cover.util import open_file
44
45
  from diff_cover.violationsreporters.base import QualityReporter
45
46
  from diff_cover.violationsreporters.java_violations_reporter import (
46
47
  CheckstyleXmlDriver,
@@ -248,7 +249,7 @@ def generate_quality_report(
248
249
  if css_url is not None:
249
250
  css_url = os.path.relpath(css_file, os.path.dirname(html_report))
250
251
  reporter = HtmlQualityReportGenerator(tool, diff, css_url=css_url)
251
- with open(html_report, "wb") as output_file:
252
+ with open_file(html_report, "wb") as output_file:
252
253
  reporter.generate_report(output_file)
253
254
  if css_file is not None:
254
255
  with open(css_file, "wb") as output_file:
@@ -257,13 +258,13 @@ def generate_quality_report(
257
258
  if "json" in report_formats:
258
259
  json_report = report_formats["json"] or JSON_REPORT_DEFAULT_PATH
259
260
  reporter = JsonReportGenerator(tool, diff)
260
- with open(json_report, "wb") as output_file:
261
+ with open_file(json_report, "wb") as output_file:
261
262
  reporter.generate_report(output_file)
262
263
 
263
264
  if "markdown" in report_formats:
264
265
  markdown_report = report_formats["markdown"] or MARKDOWN_REPORT_DEFAULT_PATH
265
266
  reporter = MarkdownQualityReportGenerator(tool, diff)
266
- with open(markdown_report, "wb") as output_file:
267
+ with open_file(markdown_report, "wb") as output_file:
267
268
  reporter.generate_report(output_file)
268
269
 
269
270
  # Generate the report for stdout
@@ -349,22 +349,20 @@ class GitDiffReporter(BaseDiffReporter):
349
349
 
350
350
  # Every other line is stored in the dictionary for this source file
351
351
  # once we find a hunk section
352
- else:
353
- # Only add lines if we're in a hunk section
354
- # (ignore index and files changed lines)
355
- if found_hunk or line.startswith("@@"):
356
- # Remember that we found a hunk
357
- found_hunk = True
358
-
359
- if src_path is not None:
360
- source_dict[src_path].append(line)
361
-
362
- else:
363
- # We tolerate other information before we have
364
- # a source file defined, unless it's a hunk line
365
- if line.startswith("@@"):
366
- msg = f"Hunk has no source file: '{line}'"
367
- raise GitDiffError(msg)
352
+ # Only add lines if we're in a hunk section
353
+ # (ignore index and files changed lines)
354
+ elif found_hunk or line.startswith("@@"):
355
+ # Remember that we found a hunk
356
+ found_hunk = True
357
+
358
+ if src_path is not None:
359
+ source_dict[src_path].append(line)
360
+
361
+ # We tolerate other information before we have
362
+ # a source file defined, unless it's a hunk line
363
+ elif line.startswith("@@"):
364
+ msg = f"Hunk has no source file: '{line}'"
365
+ raise GitDiffError(msg)
368
366
 
369
367
  return source_dict
370
368
 
@@ -65,9 +65,7 @@ class GitDiffTool:
65
65
  Raises a `GitDiffError` if `git diff` outputs anything
66
66
  to stderr.
67
67
  """
68
- diff_range = "{branch}{notation}HEAD".format(
69
- branch=compare_branch, notation=self.range_notation
70
- )
68
+ diff_range = f"{compare_branch}{self.range_notation}HEAD"
71
69
  try:
72
70
  return execute(
73
71
  self._default_git_args + self._default_diff_args + [diff_range]
@@ -1,5 +1,35 @@
1
+ import contextlib
1
2
  import os.path
2
3
  import posixpath
4
+ import sys
5
+
6
+
7
+ @contextlib.contextmanager
8
+ def open_file(path, mode, encoding="utf-8"):
9
+ """
10
+ Behaves like open(), but with some special cases for stdout and stderr.
11
+
12
+ :param path: string of the path to open
13
+ :param mode: string of the mode to open the file in
14
+ :param encoding: encoding to use when opening the file (text mode only)
15
+ :return: a context manager that yields the file object
16
+ """
17
+ output_file = None
18
+ if path in ("/dev/stdout", "-"):
19
+ output_file = sys.stdout
20
+ elif path == "/dev/stderr":
21
+ output_file = sys.stderr
22
+
23
+ if output_file:
24
+ if "b" in mode:
25
+ output_file = output_file.buffer
26
+ yield output_file
27
+ else:
28
+ if "b" in mode:
29
+ encoding = None
30
+
31
+ with open(path, mode, encoding=encoding) as f:
32
+ yield f
3
33
 
4
34
 
5
35
  def to_unix_path(path):
@@ -150,27 +150,27 @@ class QualityReporter(BaseViolationReporter):
150
150
  """
151
151
  if not any(src_path.endswith(ext) for ext in self.driver.supported_extensions):
152
152
  return []
153
+
153
154
  if src_path not in self.violations_dict:
154
155
  if self.reports:
155
156
  self.violations_dict = self.driver.parse_reports(self.reports)
156
- else:
157
- if self.driver_tool_installed is None:
158
- self.driver_tool_installed = self.driver.installed()
159
- if not self.driver_tool_installed:
160
- raise OSError(f"{self.driver.name} is not installed")
161
- command = copy.deepcopy(self.driver.command)
162
- if self.options:
163
- for arg in self.options.split():
164
- command.append(arg)
165
- if os.path.exists(src_path):
166
- command.append(src_path.encode(sys.getfilesystemencoding()))
167
-
168
- output = execute(command, self.driver.exit_codes)
169
- if self.driver.output_stderr:
170
- output = output[1]
171
- else:
172
- output = output[0]
173
- self.violations_dict.update(self.driver.parse_reports([output]))
157
+ return self.violations_dict[src_path]
158
+
159
+ if self.driver_tool_installed is None:
160
+ self.driver_tool_installed = self.driver.installed()
161
+ if not self.driver_tool_installed:
162
+ msg = f"{self.driver.name} is not installed"
163
+ raise OSError(msg)
164
+ command = copy.deepcopy(self.driver.command)
165
+ if self.options:
166
+ for arg in self.options.split():
167
+ command.append(arg)
168
+ if os.path.exists(src_path):
169
+ command.append(src_path.encode(sys.getfilesystemencoding()))
170
+
171
+ stdout, stderr = execute(command, self.driver.exit_codes)
172
+ output = stderr if self.driver.output_stderr else stdout
173
+ self.violations_dict.update(self.driver.parse_reports([output]))
174
174
 
175
175
  return self.violations_dict[src_path]
176
176
 
@@ -308,8 +308,15 @@ class LcovCoverageReporter(BaseViolationReporter):
308
308
  def parse(lcov_file):
309
309
  """
310
310
  Parse a single LCov coverage report
311
- File format: https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
311
+ File format: https://linux.die.net/man/1/geninfo
312
+ More info: https://github.com/linux-test-project/lcov/issues/113#issuecomment-762335134
312
313
  """
314
+ branch_coverage = defaultdict(
315
+ lambda: defaultdict(lambda: {"total": 0, "hit": 0, "executions": 0})
316
+ )
317
+ function_lines = defaultdict(
318
+ dict
319
+ ) # { source_file: { func_name: (line_no, hit_count) } }
313
320
  lcov_report = defaultdict(dict)
314
321
  lcov = open(lcov_file)
315
322
  while True:
@@ -336,22 +343,92 @@ class LcovCoverageReporter(BaseViolationReporter):
336
343
  if line_no not in lcov_report[source_file]:
337
344
  lcov_report[source_file][line_no] = 0
338
345
  lcov_report[source_file][line_no] += num_executions
346
+ elif directive == "BRDA":
347
+ args = content.split(",")
348
+ if len(args) != 4:
349
+ raise ValueError(f"Unknown syntax in lcov report: {line}")
350
+ if source_file is None:
351
+ raise ValueError(
352
+ f"No source file specified for line coverage: {line}"
353
+ )
354
+ line_no = int(args[0])
355
+ taken = (
356
+ int(args[3]) if args[3] != "-" else 0
357
+ ) # Handle '-' for untaken branches
358
+ branch_coverage[source_file][line_no]["total"] += 1
359
+ branch_coverage[source_file][line_no]["executions"] += taken
360
+ if taken > 0:
361
+ branch_coverage[source_file][line_no]["hit"] += 1
362
+ elif directive == "FN":
363
+ args = content.split(",")
364
+ if len(args) != 2:
365
+ raise ValueError(f"Unknown syntax in lcov report: {line}")
366
+ if source_file is None:
367
+ raise ValueError(
368
+ f"No source file specified for line coverage: {line}"
369
+ )
370
+ line_no = int(args[0])
371
+ func_name = args[1]
372
+ function_lines[source_file][func_name] = (line_no, 0)
373
+ elif directive == "FNDA":
374
+ args = content.split(",")
375
+ if len(args) != 2:
376
+ raise ValueError(f"Unknown syntax in lcov report: {line}")
377
+ if source_file is None:
378
+ raise ValueError(
379
+ f"No source file specified for line coverage: {line}"
380
+ )
381
+ hit_count = int(args[0])
382
+ func_name = args[1]
383
+ if func_name in function_lines[source_file]:
384
+ line_no, _ = function_lines[source_file][func_name]
385
+ function_lines[source_file][func_name] = (line_no, hit_count)
339
386
  elif directive in [
340
- "TN",
341
- "FNF",
342
- "FNH",
343
- "FN",
344
- "FNDA",
345
- "LH",
346
- "LF",
347
- "BRF",
348
- "BRH",
349
- "BRDA",
350
- "VER",
387
+ "TN", # Test name
388
+ "FNF", # Functions found
389
+ "FNH", # Functions hit
390
+ "LH", # Lines hit
391
+ "LF", # Lines found
392
+ "BRF", # Branches found
393
+ "BRH", # Branches hit
394
+ "VER", # Version
395
+ "FNL", # Function line coverage (alternative format)
396
+ "FNA", # Function name (alternative format)
351
397
  ]:
352
- # these are valid lines, but not we don't need them
398
+ # Valid directives that we don't need to process
353
399
  continue
354
400
  elif directive == "end_of_record":
401
+ # Process collected coverage data for current source file
402
+
403
+ # 1. Apply branch coverage logic
404
+ for line_no, info in branch_coverage[source_file].items():
405
+ has_da_directive = line_no in lcov_report[source_file]
406
+
407
+ if not has_da_directive:
408
+ # No line execution data, use branch coverage
409
+ if info["total"] > 0 and info["hit"] < info["total"]:
410
+ lcov_report[source_file][
411
+ line_no
412
+ ] = 0 # Partial branch coverage
413
+ else:
414
+ lcov_report[source_file][line_no] = info["executions"]
415
+ continue
416
+ if not lcov_report[source_file][line_no]:
417
+ # Line shows 0 executions, but check if branches were hit
418
+ if info["executions"] > 0:
419
+ lcov_report[source_file][line_no] = info["executions"]
420
+ # Note: Don't override existing positive execution counts
421
+
422
+ # 2. Apply function coverage logic
423
+ for func_name, (line_no, hit) in function_lines[source_file].items():
424
+ if line_no not in lcov_report[source_file]:
425
+ # No existing line data, use function hit count
426
+ lcov_report[source_file][line_no] = hit
427
+ # Note: Don't override existing line execution data
428
+
429
+ # 3. Clean up temporary data for current file
430
+ branch_coverage[source_file].clear()
431
+ function_lines[source_file].clear()
355
432
  source_file = None
356
433
  else:
357
434
  raise ValueError(f"Unknown syntax in lcov report: {line}")
@@ -639,7 +716,7 @@ class PylintDriver(QualityDriver):
639
716
  current_line += 1
640
717
  match = self.multi_line_violation_regex.match(lines[current_line])
641
718
  src_path, l_number = match.groups()
642
- src_paths.append(("%s.py" % src_path, l_number))
719
+ src_paths.append((f"{src_path}.py", l_number))
643
720
  return src_paths
644
721
 
645
722
  def parse_reports(self, reports):
@@ -659,36 +736,32 @@ class PylintDriver(QualityDriver):
659
736
 
660
737
  # Ignore any line that isn't matched
661
738
  # (for example, snippets from the source code)
662
- if match is not None:
663
- (
664
- pylint_src_path,
665
- line_number,
666
- pylint_code,
667
- function_name,
668
- message,
669
- ) = match.groups()
670
- if pylint_code == self.dupe_code_violation:
671
- files_involved = self._process_dupe_code_violation(
672
- output_lines, output_line_number, message
673
- )
674
- else:
675
- files_involved = [(pylint_src_path, line_number)]
676
-
677
- for violation in files_involved:
678
- pylint_src_path, line_number = violation
679
- # pylint might uses windows paths
680
- pylint_src_path = util.to_unix_path(pylint_src_path)
681
- # If we're looking for a particular source file,
682
- # ignore any other source files.
683
- if function_name:
684
- error_str = "{}: {}: {}".format(
685
- pylint_code, function_name, message
686
- )
687
- else:
688
- error_str = f"{pylint_code}: {message}"
739
+ if match is None:
740
+ continue
741
+
742
+ (
743
+ pylint_src_path,
744
+ line_number,
745
+ pylint_code,
746
+ function_name,
747
+ message,
748
+ ) = match.groups()
749
+ files_involved = [(pylint_src_path, line_number)]
750
+ if pylint_code == self.dupe_code_violation:
751
+ files_involved = self._process_dupe_code_violation(
752
+ output_lines, output_line_number, message
753
+ )
754
+
755
+ for pylint_src_path, line_number in files_involved:
756
+ # If we're looking for a particular source file,
757
+ # ignore any other source files.
758
+ error_str = f"{pylint_code}: {message}"
759
+ if function_name:
760
+ error_str = f"{pylint_code}: {function_name}: {message}"
689
761
 
690
- violation = Violation(int(line_number), error_str)
691
- violations_dict[pylint_src_path].append(violation)
762
+ clean_path = util.to_unix_path(pylint_src_path)
763
+ violation = Violation(int(line_number), error_str)
764
+ violations_dict[clean_path].append(violation)
692
765
 
693
766
  return violations_dict
694
767
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "diff_cover"
3
- version = "9.3.2"
3
+ version = "9.4.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"
@@ -53,8 +53,8 @@ pytest-datadir = "^1.5.0"
53
53
  pytest-mock = "^3.14.0"
54
54
  pytest-xdist = "^3.6.1"
55
55
  pycodestyle = ">=2.9.1"
56
- flake8 = "^5.0.4"
57
- pyflakes = "^2.5.0"
56
+ flake8 = "^7.2.0"
57
+ pyflakes = "^3.3.2 "
58
58
  pylint = "^3.3.4"
59
59
  pylint-pytest = "^1.1.8"
60
60
  pydocstyle = "^6.1.1"
@@ -99,9 +99,10 @@ show_missing = true
99
99
  exclude_also = [
100
100
  "if typing.TYPE_CHECKING:",
101
101
  "if TYPE_CHECKING:",
102
+ "if __name__ == \"__main__\":",
102
103
  "raise NotImplementedError",
103
104
  "raise AssertionError",
104
- "pass",
105
+ "^\\s*pass\\s*$",
105
106
  ]
106
107
 
107
108
  [tool.coverage.html]
@@ -0,0 +1,10 @@
1
+ -------------
2
+ Diff Coverage
3
+ Diff: origin/main...HEAD, staged and unstaged changes
4
+ -------------
5
+ calculator.cpp (92.3%): Missing lines 12
6
+ -------------
7
+ Total: 13 lines
8
+ Missing: 1 line
9
+ Coverage: 92%
10
+ -------------
@@ -0,0 +1,30 @@
1
+ TN:
2
+ SF:calculator.cpp
3
+ FNL:0,3,4
4
+ FNA:0,1,_ZN10Calculator3addEdd
5
+ FNL:1,7,12
6
+ FNA:1,2,_ZN10Calculator6divideEdd
7
+ FNL:2,14,28
8
+ FNA:2,2,_ZN10Calculator14processNumbersE
9
+ FNF:3
10
+ FNH:3
11
+ DA:3,1
12
+ DA:4,1
13
+ DA:7,2
14
+ DA:8,2
15
+ DA:9,1
16
+ DA:11,1
17
+ DA:12,0
18
+ DA:14,2
19
+ DA:16,2
20
+ DA:17,4
21
+ DA:18,1
22
+ DA:20,2
23
+ DA:21,2
24
+ DA:23,8
25
+ DA:24,6
26
+ DA:27,2
27
+ DA:28,2
28
+ LF:17
29
+ LH:16
30
+ end_of_record
@@ -0,0 +1,50 @@
1
+ diff --git a/calculator.cpp b/calculator.cpp
2
+ new file mode 100644
3
+ index 0000000..1234567
4
+ --- /dev/null
5
+ +++ b/calculator.cpp
6
+ @@ -0,0 +1,44 @@
7
+ +#include "calculator.h"
8
+ +
9
+ +double Calculator::add(double a, double b) {
10
+ + return a + b;
11
+ +}
12
+ +
13
+ +double Calculator::divide(double a, double b) {
14
+ + if (b == 0) {
15
+ + throw std::invalid_argument("Division by zero");
16
+ + }
17
+ + return a / b;
18
+ +}
19
+ +
20
+ +std::vector<double> Calculator::processNumbers(const std::vector<double>& numbers,
21
+ + std::function<double(double)> processor) {
22
+ + if (!processor) {
23
+ + processor = [](double x) { return x * 2; };
24
+ + }
25
+ +
26
+ + std::vector<double> result;
27
+ + result.reserve(numbers.size());
28
+ +
29
+ + for (const auto& num : numbers) {
30
+ + result.push_back(processor(num));
31
+ + }
32
+ +
33
+ + return result;
34
+ +}
35
+ +
36
+ +std::string Calculator::getGrade(double score) {
37
+ + if (score < 0 || score > 100) {
38
+ + throw std::invalid_argument("Invalid score");
39
+ + }
40
+ +
41
+ + if (score >= 90) {
42
+ + return "A";
43
+ + } else if (score >= 80) {
44
+ + return "B";
45
+ + } else if (score >= 70) {
46
+ + return "C";
47
+ + } else {
48
+ + return "F";
49
+ + }
50
+ +}
@@ -0,0 +1,28 @@
1
+ diff --git a/calculator.cpp b/calculator.cpp
2
+ new file mode 100644
3
+ index 0000000..abc1234
4
+ --- /dev/null
5
+ +++ b/calculator.cpp
6
+ @@ -0,0 +1,28 @@
7
+ +#include "calculator.h"
8
+ +
9
+ +double Calculator::add(double a, double b) {
10
+ + return a + b;
11
+ +}
12
+ +
13
+ +double Calculator::divide(double a, double b) {
14
+ + if (b == 0) {
15
+ + throw std::invalid_argument("Division by zero");
16
+ + }
17
+ + return a / b;
18
+ +}
19
+ +
20
+ +std::vector<double> Calculator::processNumbers(const std::vector<double>& numbers, std::function<double(double)> processor) {
21
+ + std::vector<double> result;
22
+ +
23
+ + for (const auto& num : numbers) {
24
+ + result.push_back(processor(num));
25
+ + }
26
+ +
27
+ + return result;
28
+ +}
@@ -0,0 +1,38 @@
1
+ diff --git a/calculator.py b/calculator.py
2
+ new file mode 100644
3
+ index 0000000..abcdef1
4
+ --- /dev/null
5
+ +++ b/calculator.py
6
+ @@ -0,0 +1,32 @@
7
+ +from typing import List, Callable
8
+ +
9
+ +class Calculator:
10
+ + def add(self, a: float, b: float) -> float:
11
+ + """Function coverage example"""
12
+ + return a + b
13
+ +
14
+ + def divide(self, a: float, b: float) -> float:
15
+ + """Branch coverage example"""
16
+ + if b == 0:
17
+ + raise ValueError("Division by zero")
18
+ + return a / b
19
+ +
20
+ + def process_numbers(self, numbers: List[float], processor: Callable[[float], float] = None) -> List[float]:
21
+ + """Lambda function coverage example"""
22
+ + if processor is None:
23
+ + processor = lambda x: x * 2
24
+ + return list(map(processor, numbers))
25
+ +
26
+ + def get_grade(self, score: float) -> str:
27
+ + """Line and branch coverage example"""
28
+ + if score < 0 or score > 100:
29
+ + raise ValueError("Invalid score")
30
+ +
31
+ + if score >= 90:
32
+ + return "A"
33
+ + elif score >= 80:
34
+ + return "B"
35
+ + elif score >= 70:
36
+ + return "C"
37
+ + else:
38
+ + return "F"
@@ -0,0 +1,45 @@
1
+ diff --git a/src/calculator.ts b/src/calculator.ts
2
+ new file mode 100644
3
+ index 0000000..fedcba0
4
+ --- /dev/null
5
+ +++ b/src/calculator.ts
6
+ @@ -0,0 +1,40 @@
7
+ +export class Calculator {
8
+ + // Function coverage
9
+ + add(a: number, b: number): number {
10
+ + return a + b;
11
+ + }
12
+ +
13
+ + // Branch coverage
14
+ + divide(a: number, b: number): number {
15
+ + if (b === 0) {
16
+ + throw new Error("Division by zero");
17
+ + }
18
+ + return a / b;
19
+ + }
20
+ +
21
+ + // Anonymous function coverage
22
+ + processNumbers(
23
+ + numbers: number[],
24
+ + processor: (n: number) => number = (n) => n * 2
25
+ + ): number[] {
26
+ + return numbers.map(processor);
27
+ + }
28
+ +
29
+ + // Line and branch coverage
30
+ + getGrade(score: number): string {
31
+ + if (score < 0 || score > 100) {
32
+ + throw new Error("Invalid score");
33
+ + }
34
+ +
35
+ + if (score >= 90) {
36
+ + return "A";
37
+ + } else if (score >= 80) {
38
+ + return "B";
39
+ + } else if (score >= 70) {
40
+ + return "C";
41
+ + } else {
42
+ + return "F";
43
+ + }
44
+ + }
45
+ +}