Merge pull request #18421 from edx/jmbowman/TE-2560

TE-2560 Generate quality results XML files
This commit is contained in:
Jeremy Bowman
2018-06-22 10:18:14 -04:00
committed by GitHub
3 changed files with 83 additions and 20 deletions

View File

@@ -6,6 +6,8 @@ Check code quality using pycodestyle, pylint, and diff_quality.
import json
import os
import re
from datetime import datetime
from xml.sax.saxutils import quoteattr
from paver.easy import BuildFailure, cmdopts, needs, sh, task
@@ -15,6 +17,42 @@ from .utils.envs import Env
from .utils.timer import timed
ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib'
JUNIT_XML_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="{name}" tests="1" errors="0" failures="{failure_count}" skip="0">
<testcase classname="pavelib.quality" name="{name}" time="{seconds}">{failure_element}</testcase>
</testsuite>
"""
JUNIT_XML_FAILURE_TEMPLATE = '<failure message={message}/>'
START_TIME = datetime.utcnow()
def write_junit_xml(name, message=None):
"""
Write a JUnit results XML file describing the outcome of a quality check.
"""
if message:
failure_element = JUNIT_XML_FAILURE_TEMPLATE.format(message=quoteattr(message))
else:
failure_element = ''
data = {
'failure_count': 1 if message else 0,
'failure_element': failure_element,
'name': name,
'seconds': (datetime.utcnow() - START_TIME).total_seconds(),
}
Env.QUALITY_DIR.makedirs_p()
filename = Env.QUALITY_DIR / '{}.xml'.format(name)
with open(filename, 'w') as f:
f.write(JUNIT_XML_TEMPLATE.format(**data))
def fail_quality(name, message):
"""
Fail the specified quality check by generating the JUnit XML results file
and raising a ``BuildFailure``.
"""
write_junit_xml(name, message)
raise BuildFailure(message)
def top_python_dirs(dirname):
@@ -133,6 +171,7 @@ def run_pylint(options):
lower_violations_limit, upper_violations_limit, errors, systems = _parse_pylint_options(options)
errors = getattr(options, 'errors', False)
systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
result_name = 'pylint_{}'.format('_'.join(systems))
num_violations, _ = _get_pylint_violations(systems, errors)
@@ -148,7 +187,8 @@ def run_pylint(options):
# which likely means that pylint did not run successfully.
# If pylint *did* run successfully, then great! Modify the lower limit.
if num_violations < lower_violations_limit > -1:
raise BuildFailure(
fail_quality(
result_name,
"FAILURE: Too few pylint violations. "
"Expected to see at least {lower_limit} pylint violations. "
"Either pylint is not running correctly -or- "
@@ -159,10 +199,13 @@ def run_pylint(options):
# Fail when number of violations is greater than the upper limit.
if num_violations > upper_violations_limit > -1:
raise BuildFailure(
fail_quality(
result_name,
"FAILURE: Too many pylint violations. "
"The limit is {upper_limit}.".format(upper_limit=upper_violations_limit)
)
else:
write_junit_xml(result_name)
def _parse_pylint_options(options):
@@ -263,7 +306,9 @@ def run_pep8(options): # pylint: disable=unused-argument
if count:
failure_string = "FAILURE: Too many PEP 8 violations. " + violations_count_str
failure_string += "\n\nViolations:\n{violations_list}".format(violations_list=violations_list)
raise BuildFailure(failure_string)
fail_quality('pep8', failure_string)
else:
write_junit_xml('pep8')
@task
@@ -332,7 +377,8 @@ def run_eslint(options):
try:
num_violations = int(_get_count_from_last_line(eslint_report, "eslint"))
except TypeError:
raise BuildFailure(
fail_quality(
'eslint',
"FAILURE: Number of eslint violations could not be found in {eslint_report}".format(
eslint_report=eslint_report
)
@@ -343,11 +389,14 @@ def run_eslint(options):
# Fail if number of violations is greater than the limit
if num_violations > violations_limit > -1:
raise BuildFailure(
fail_quality(
'eslint',
"FAILURE: Too many eslint violations ({count}).\nThe limit is {violations_limit}.".format(
count=num_violations, violations_limit=violations_limit
)
)
else:
write_junit_xml('eslint')
def _get_stylelint_violations():
@@ -370,7 +419,8 @@ def _get_stylelint_violations():
try:
return int(_get_count_from_last_line(stylelint_report, "stylelint"))
except TypeError:
raise BuildFailure(
fail_quality(
'stylelint',
"FAILURE: Number of stylelint violations could not be found in {stylelint_report}".format(
stylelint_report=stylelint_report
)
@@ -396,12 +446,15 @@ def run_stylelint(options):
# Fail if number of violations is greater than the limit
if num_violations > violations_limit > -1:
raise BuildFailure(
fail_quality(
'stylelint',
"FAILURE: Stylelint failed with too many violations: ({count}).\nThe limit is {violations_limit}.".format(
count=num_violations,
violations_limit=violations_limit,
)
)
else:
write_junit_xml('stylelint')
@task
@@ -423,7 +476,8 @@ def run_xsslint(options):
if isinstance(violation_thresholds, dict) is False or \
any(key not in ("total", "rules") for key in violation_thresholds.keys()):
raise BuildFailure(
fail_quality(
'xsslint',
"""FAILURE: Thresholds option "{thresholds_option}" was not supplied using proper format.\n"""
"""Here is a properly formatted example, '{{"total":100,"rules":{{"javascript-escape":0}}}}' """
"""with property names in double-quotes.""".format(
@@ -461,7 +515,8 @@ def run_xsslint(options):
count=int(xsslint_counts['rules'][rule])
)
except TypeError:
raise BuildFailure(
fail_quality(
'xsslint',
"FAILURE: Number of {xsslint_script} violations could not be found in {xsslint_report}".format(
xsslint_script=xsslint_script, xsslint_report=xsslint_report
)
@@ -501,13 +556,16 @@ def run_xsslint(options):
)
if error_message:
raise BuildFailure(
fail_quality(
'xsslint',
"FAILURE: XSSLinter Failed.\n{error_message}\n"
"See {xsslint_report} or run the following command to hone in on the problem:\n"
" ./scripts/xss-commit-linter.sh -h".format(
error_message=error_message, xsslint_report=xsslint_report
)
)
else:
write_junit_xml('xsslint')
@task
@@ -536,7 +594,8 @@ def run_xsscommitlint():
try:
num_violations = int(xsscommitlint_count)
except TypeError:
raise BuildFailure(
fail_quality(
'xsscommitlint',
"FAILURE: Number of {xsscommitlint_script} violations could not be found in {xsscommitlint_report}".format(
xsscommitlint_script=xsscommitlint_script, xsscommitlint_report=xsscommitlint_report
)
@@ -552,6 +611,7 @@ def run_xsscommitlint():
_write_metric(violations_count_str, metrics_report)
# Output report to console.
sh("cat {metrics_report}".format(metrics_report=metrics_report), ignore_error=True)
write_junit_xml("xsscommitlint")
def _write_metric(metric, filename):
@@ -574,7 +634,7 @@ def _prepare_report_dir(dir_name):
dir_name.mkdir_p()
def _get_report_contents(filename, last_line_only=False):
def _get_report_contents(filename, report_name, last_line_only=False):
"""
Returns the contents of the given file. Use last_line_only to only return
the last line, which can be used for getting output from quality output
@@ -599,7 +659,7 @@ def _get_report_contents(filename, last_line_only=False):
return report_file.read()
else:
file_not_found_message = "FAILURE: The following log file could not be found: {file}".format(file=filename)
raise BuildFailure(file_not_found_message)
fail_quality(report_name, file_not_found_message)
def _get_count_from_last_line(filename, file_type):
@@ -607,7 +667,7 @@ def _get_count_from_last_line(filename, file_type):
This will return the number in the last line of a file.
It is returning only the value (as a floating number).
"""
last_line = _get_report_contents(filename, last_line_only=True).strip()
last_line = _get_report_contents(filename, file_type, last_line_only=True).strip()
if file_type == "python_complexity":
# Example of the last line of a complexity report: "Average complexity: A (1.93953443446)"
@@ -638,7 +698,7 @@ def _get_xsslint_counts(filename):
total: M, where M is the number of total violations
"""
report_contents = _get_report_contents(filename)
report_contents = _get_report_contents(filename, 'xsslint')
rule_count_regex = re.compile(r"^(?P<rule_id>[a-z-]+):\s+(?P<count>\d+) violations", re.MULTILINE)
total_count_regex = re.compile(r"^(?P<count>\d+) violations total", re.MULTILINE)
violations = {'rules': {}}
@@ -667,7 +727,7 @@ def _get_xsscommitlint_count(filename):
The count of xsscommitlint violations, or None if there is a problem.
"""
report_contents = _get_report_contents(filename)
report_contents = _get_report_contents(filename, 'xsscommitlint')
if 'No files linted' in report_contents:
return 0
@@ -795,7 +855,9 @@ def run_quality(options):
# If one of the quality runs fails, then paver exits with an error when it is finished
if not diff_quality_pass:
msg = "FAILURE: " + " ".join(failure_reasons)
raise BuildFailure(msg)
fail_quality('diff_quality', msg)
else:
write_junit_xml('diff_quality')
def run_diff_quality(
@@ -823,7 +885,7 @@ def run_diff_quality(
if is_percentage_failure(error_message):
return False
else:
raise BuildFailure('FAILURE: {}'.format(error_message))
fail_quality('diff_quality', 'FAILURE: {}'.format(error_message))
def is_percentage_failure(error_message):

View File

@@ -54,6 +54,7 @@ class Env(object):
# Reports Directory
REPORT_DIR = REPO_ROOT / 'reports'
METRICS_DIR = REPORT_DIR / 'metrics'
QUALITY_DIR = REPORT_DIR / 'quality_junitxml'
# Generic log dir
GEN_LOG_DIR = REPO_ROOT / "test_root" / "log"

View File

@@ -63,7 +63,7 @@ function emptyxunit {
cat > reports/$1.xml <<END
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="$1" tests="1" errors="0" failures="0" skip="0">
<testcase classname="$1" name="$1" time="0.604"></testcase>
<testcase classname="pavelib.quality" name="$1" time="0.604"></testcase>
</testsuite>
END
@@ -143,7 +143,7 @@ case "$TEST_SUITE" in
# Need to create an empty test result so the post-build
# action doesn't fail the build.
emptyxunit "quality"
emptyxunit "stub"
exit $EXIT
;;