diff-cover 9.3.1__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.
- {diff_cover-9.3.1 → diff_cover-9.4.0}/PKG-INFO +1 -1
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/diff_cover_tool.py +10 -12
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/diff_quality_tool.py +4 -3
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/diff_reporter.py +14 -16
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/git_diff.py +1 -3
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/util.py +30 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/violationsreporters/base.py +18 -18
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/violationsreporters/violations_reporter.py +116 -43
- {diff_cover-9.3.1 → diff_cover-9.4.0}/pyproject.toml +5 -4
- diff_cover-9.4.0/tests/fixtures/cpp_functions_console_report.txt +10 -0
- diff_cover-9.4.0/tests/fixtures/cpp_functions_coverage.lcov +30 -0
- diff_cover-9.4.0/tests/fixtures/git_diff_cpp.txt +50 -0
- diff_cover-9.4.0/tests/fixtures/git_diff_cpp_functions.txt +28 -0
- diff_cover-9.4.0/tests/fixtures/git_diff_python.txt +38 -0
- diff_cover-9.4.0/tests/fixtures/git_diff_typescript.txt +45 -0
- diff_cover-9.4.0/tests/fixtures/git_diff_typescript_branches.txt +29 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/lcov.info +1 -1
- diff_cover-9.4.0/tests/fixtures/real_cpp_console_report.txt +10 -0
- diff_cover-9.4.0/tests/fixtures/real_cpp_coverage.lcov +45 -0
- diff_cover-9.4.0/tests/fixtures/real_python_console_report.txt +10 -0
- diff_cover-9.4.0/tests/fixtures/real_python_coverage.lcov +55 -0
- diff_cover-9.4.0/tests/fixtures/real_typescript_console_report.txt +10 -0
- diff_cover-9.4.0/tests/fixtures/real_typescript_coverage.lcov +46 -0
- diff_cover-9.4.0/tests/fixtures/typescript_branches_console_report.txt +10 -0
- diff_cover-9.4.0/tests/fixtures/typescript_branches_coverage.lcov +35 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/helpers.py +1 -1
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_diff_cover_main.py +2 -2
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_diff_cover_tool.py +21 -9
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_diff_quality_main.py +0 -1
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_diff_reporter.py +0 -2
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_integration.py +73 -1
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_report_generator.py +122 -123
- diff_cover-9.4.0/tests/test_util.py +76 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_violations_reporter.py +94 -15
- diff_cover-9.3.1/tests/test_util.py +0 -23
- {diff_cover-9.3.1 → diff_cover-9.4.0}/LICENSE +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/NOTICE +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/README.rst +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/__init__.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/command_runner.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/config_parser.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/git_path.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/hook.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/hookspecs.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/report_generator.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/snippets.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/console_coverage_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/console_quality_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/external_style.css +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/html_coverage_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/html_quality_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/markdown_coverage_report.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/markdown_quality_report.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/snippet_content.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/templates/snippet_style.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/violationsreporters/__init__.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/diff_cover/violationsreporters/java_violations_reporter.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/__init__.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/add_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/add_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/add_json_report.json +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/add_markdown_report.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/changed_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/changed_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/coverage.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/coverage1.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/coverage2.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/coverage_missing_lines.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/delete_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/delete_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/dotnet_coverage.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/dotnet_coverage_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/empty.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/empty_pycodestyle_violations.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/expand_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/external_css_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/external_style.css +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_add.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_changed.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_code_dupe.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_delete.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_dotnet.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_external_css.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_lua.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_moved.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_mult.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_subdir.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_unicode.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_violations.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/git_diff_violations_two_files.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/hello.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/hi.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/html_report_empty.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/html_report_one_snippet.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/html_report_two_snippets.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/lua_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/luacoverage.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/markdown_report_one_snippet.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/markdown_report_two_snippets.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/moved_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/moved_coverage.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/moved_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/mult_inputs_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/mult_inputs_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.json +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pycodestyle_violations_report_external_css.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pyflakes_two_files.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pyflakes_violations_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pyflakes_violations_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_dupe.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_dupe_violations_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylint_violations_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/pylintrc +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/show_uncovered_lines_console.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet.css +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_8859.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_arabic_output.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_default.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_invalid_violations.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_list.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_list.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_list2.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_list3.md +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_no_filename_ext.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_src.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_src2.cpp +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_src3.cpp +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/snippet_unicode_html_output.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/subdir_coverage_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/subdir_coverage_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/test_src.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/unicode_console_report.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/unicode_coverage.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/unicode_html_report.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/unicode_test_src.txt +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/fixtures/violations_test_file.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/snippet_list_unicode.html +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_clover_violations_reporter/test.xml +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_clover_violations_reporter.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_config_parser.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_git_diff.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_git_diff_file.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_git_path.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_java_violations_reporter.py +0 -0
- {diff_cover-9.3.1 → diff_cover-9.4.0}/tests/test_snippets.py +0 -0
|
@@ -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,
|
|
@@ -56,14 +57,11 @@ LOGGER = logging.getLogger(__name__)
|
|
|
56
57
|
def format_type(value):
|
|
57
58
|
"""
|
|
58
59
|
Accepts:
|
|
59
|
-
--format html
|
|
60
|
-
--format json
|
|
61
|
-
--format json,html
|
|
62
|
-
--format html,json:path/to/file.json
|
|
60
|
+
--format html:path/to/file.html,json:path/to/file.json
|
|
63
61
|
|
|
64
62
|
return: dict of strings to paths
|
|
65
63
|
"""
|
|
66
|
-
return dict((item.split(":") for item in value.split(",")) if value else {})
|
|
64
|
+
return dict((item.split(":", 1) for item in value.split(",")) if value else {})
|
|
67
65
|
|
|
68
66
|
|
|
69
67
|
def parse_coverage_args(argv):
|
|
@@ -84,7 +82,7 @@ def parse_coverage_args(argv):
|
|
|
84
82
|
"""
|
|
85
83
|
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
86
84
|
|
|
87
|
-
parser.add_argument("
|
|
85
|
+
parser.add_argument("coverage_files", type=str, help=COVERAGE_FILE_HELP, nargs="+")
|
|
88
86
|
|
|
89
87
|
parser.add_argument(
|
|
90
88
|
"--format",
|
|
@@ -239,9 +237,9 @@ def generate_coverage_report(
|
|
|
239
237
|
for coverage_file in coverage_files
|
|
240
238
|
if not coverage_file.endswith(".xml")
|
|
241
239
|
]
|
|
242
|
-
if
|
|
243
|
-
raise ValueError(
|
|
244
|
-
|
|
240
|
+
if xml_roots and lcov_roots:
|
|
241
|
+
raise ValueError("Mixing LCov and XML reports is not supported yet")
|
|
242
|
+
if xml_roots:
|
|
245
243
|
coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report)
|
|
246
244
|
else:
|
|
247
245
|
coverage = LcovCoverageReporter(lcov_roots, src_roots)
|
|
@@ -253,7 +251,7 @@ def generate_coverage_report(
|
|
|
253
251
|
if css_url is not None:
|
|
254
252
|
css_url = os.path.relpath(css_file, os.path.dirname(html_report))
|
|
255
253
|
reporter = HtmlReportGenerator(coverage, diff, css_url=css_url)
|
|
256
|
-
with
|
|
254
|
+
with open_file(html_report, "wb") as output_file:
|
|
257
255
|
reporter.generate_report(output_file)
|
|
258
256
|
if css_file is not None:
|
|
259
257
|
with open(css_file, "wb") as output_file:
|
|
@@ -262,7 +260,7 @@ def generate_coverage_report(
|
|
|
262
260
|
if "json" in report_formats:
|
|
263
261
|
json_report = report_formats["json"] or JSON_REPORT_DEFAULT_PATH
|
|
264
262
|
reporter = JsonReportGenerator(coverage, diff)
|
|
265
|
-
with
|
|
263
|
+
with open_file(json_report, "wb") as output_file:
|
|
266
264
|
reporter.generate_report(output_file)
|
|
267
265
|
|
|
268
266
|
if "markdown" in report_formats:
|
|
@@ -354,7 +352,7 @@ def main(argv=None, directory=None):
|
|
|
354
352
|
diff_tool = GitDiffFileTool(arg_dict["diff_file"])
|
|
355
353
|
|
|
356
354
|
percent_covered = generate_coverage_report(
|
|
357
|
-
arg_dict["
|
|
355
|
+
arg_dict["coverage_files"],
|
|
358
356
|
arg_dict["compare_branch"],
|
|
359
357
|
diff_tool,
|
|
360
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
|
|
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
|
|
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
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 = "{
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
command.append(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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://
|
|
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
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"
|
|
349
|
-
"
|
|
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
|
-
#
|
|
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(("
|
|
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
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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
|
+
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 = "^
|
|
57
|
-
pyflakes = "^
|
|
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,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"
|