quality.py 26.9 KB
Newer Older
1 2 3
"""
Check code quality using pep8, pylint, and diff_quality.
"""
4
from paver.easy import sh, task, cmdopts, needs, BuildFailure
5
import json
6
import os
7 8
import re

9 10
from openedx.core.djangolib.markup import HTML

11
from .utils.envs import Env
12
from .utils.timer import timed
13

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib'


def top_python_dirs(dirname):
    """
    Find the directories to start from in order to find all the Python files in `dirname`.
    """
    top_dirs = []

    dir_init = os.path.join(dirname, "__init__.py")
    if os.path.exists(dir_init):
        top_dirs.append(dirname)

    for directory in ['djangoapps', 'lib']:
        subdir = os.path.join(dirname, directory)
        subdir_init = os.path.join(subdir, "__init__.py")
        if os.path.exists(subdir) and not os.path.exists(subdir_init):
            dirs = os.listdir(subdir)
            top_dirs.extend(d for d in dirs if os.path.isdir(os.path.join(subdir, d)))

    return top_dirs
35

36

37 38 39 40
@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
Christine Lytwynec committed
41
])
42
@timed
Christine Lytwynec committed
43 44 45 46
def find_fixme(options):
    """
    Run pylint on system code, only looking for fixme items.
    """
47 48
    num_fixme = 0
    systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
Christine Lytwynec committed
49

50 51 52
    for system in systems:
        # Directory to put the pylint report in.
        # This makes the folder if it doesn't already exist.
Christine Lytwynec committed
53
        report_dir = (Env.REPORT_DIR / system).makedirs_p()
54

55
        apps_list = ' '.join(top_python_dirs(system))
56 57

        pythonpath_prefix = (
58
            "PYTHONPATH={system}/djangoapps:common/djangoapps:common/lib".format(
59 60 61 62
                system=system
            )
        )

Christine Lytwynec committed
63
        sh(
64 65
            "{pythonpath_prefix} pylint --disable R,C,W,E --enable=fixme "
            "--msg-template={msg_template} {apps} "
David Baumgold committed
66
            "| tee {report_dir}/pylint_fixme.report".format(
67
                pythonpath_prefix=pythonpath_prefix,
David Baumgold committed
68
                msg_template='"{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"',
69
                apps=apps_list,
Christine Lytwynec committed
70 71 72 73
                report_dir=report_dir
            )
        )

74 75 76
        num_fixme += _count_pylint_violations(
            "{report_dir}/pylint_fixme.report".format(report_dir=report_dir))

77
    print "Number of pylint fixmes: " + str(num_fixme)
Christine Lytwynec committed
78 79 80 81 82 83


@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
84
    ("errors", "e", "Check for errors only"),
85
    ("limit=", "l", "limit for number of acceptable violations"),
86
])
87
@timed
88 89
def run_pylint(options):
    """
90 91
    Run pylint on system code. When violations limit is passed in,
    fail the task if too many violations are found.
92
    """
93
    num_violations = 0
94 95 96
    violations_limit = int(getattr(options, 'limit', -1))
    errors = getattr(options, 'errors', False)
    systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
97

98 99
    # Make sure the metrics subdirectory exists
    Env.METRICS_DIR.makedirs_p()
100

101 102 103
    for system in systems:
        # Directory to put the pylint report in.
        # This makes the folder if it doesn't already exist.
104
        report_dir = (Env.REPORT_DIR / system).makedirs_p()
105

106
        flags = []
107
        if errors:
108
            flags.append("--errors-only")
109

110
        apps_list = ' '.join(top_python_dirs(system))
111 112

        pythonpath_prefix = (
113
            "PYTHONPATH={system}/djangoapps:common/djangoapps:common/lib".format(
114 115 116 117
                system=system
            )
        )

118
        sh(
119
            "{pythonpath_prefix} pylint {flags} --msg-template={msg_template} {apps} | "
120
            "tee {report_dir}/pylint.report".format(
121 122
                pythonpath_prefix=pythonpath_prefix,
                flags=" ".join(flags),
David Baumgold committed
123
                msg_template='"{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"',
124
                apps=apps_list,
125 126
                report_dir=report_dir
            )
127
        )
128

129 130 131
        num_violations += _count_pylint_violations(
            "{report_dir}/pylint.report".format(report_dir=report_dir))

132 133
    # Print number of violations to log
    violations_count_str = "Number of pylint violations: " + str(num_violations)
134
    print violations_count_str
135 136 137 138 139 140

    # Also write the number of violations to a file
    with open(Env.METRICS_DIR / "pylint", "w") as f:
        f.write(violations_count_str)

    # Fail number of violations is greater than the limit
141
    if num_violations > violations_limit > -1:
142 143
        raise BuildFailure("Failed. Too many pylint violations. "
                           "The limit is {violations_limit}.".format(violations_limit=violations_limit))
144

145

146 147 148 149 150 151 152 153
def _count_pylint_violations(report_file):
    """
    Parses a pylint report line-by-line and determines the number of violations reported
    """
    num_violations_report = 0
    # An example string:
    # common/lib/xmodule/xmodule/tests/test_conditional.py:21: [C0111(missing-docstring), DummySystem] Missing docstring
    # More examples can be found in the unit tests for this method
154
    pylint_pattern = re.compile(r".(\d+):\ \[(\D\d+.+\]).")
155 156 157 158 159 160 161 162

    for line in open(report_file):
        violation_list_for_line = pylint_pattern.split(line)
        # If the string is parsed into four parts, then we've found a violation. Example of split parts:
        # test file, line number, violation name, violation details
        if len(violation_list_for_line) == 4:
            num_violations_report += 1
    return num_violations_report
163

164

165
def _get_pep8_violations():
166
    """
167 168 169
    Runs pep8. Returns a tuple of (number_of_violations, violations_string)
    where violations_string is a string of all pep8 violations found, separated
    by new lines.
170
    """
171 172 173
    report_dir = (Env.REPORT_DIR / 'pep8')
    report_dir.rmtree(ignore_errors=True)
    report_dir.makedirs_p()
174

175 176
    # Make sure the metrics subdirectory exists
    Env.METRICS_DIR.makedirs_p()
177

178
    sh('pep8 . | tee {report_dir}/pep8.report -a'.format(report_dir=report_dir))
179

180
    count, violations_list = _pep8_violations(
181 182
        "{report_dir}/pep8.report".format(report_dir=report_dir)
    )
183

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    return (count, violations_list)


def _pep8_violations(report_file):
    """
    Returns a tuple of (num_violations, violations_list) for all
    pep8 violations in the given report_file.
    """
    with open(report_file) as f:
        violations_list = f.readlines()
    num_lines = len(violations_list)
    return num_lines, violations_list


@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
])
203
@timed
204
def run_pep8(options):  # pylint: disable=unused-argument
205 206 207 208 209
    """
    Run pep8 on system code.
    Fail the task if any violations are found.
    """
    (count, violations_list) = _get_pep8_violations()
210
    violations_list = ''.join(violations_list)
211

212 213
    # Print number of violations to log
    violations_count_str = "Number of pep8 violations: {count}".format(count=count)
214 215
    print violations_count_str
    print violations_list
216 217 218

    # Also write the number of violations to a file
    with open(Env.METRICS_DIR / "pep8", "w") as f:
219
        f.write(violations_count_str + '\n\n')
220
        f.write(violations_list)
221 222

    # Fail if any violations are found
223
    if count:
224 225
        failure_string = "Too many pep8 violations. " + violations_count_str
        failure_string += "\n\nViolations:\n{violations_list}".format(violations_list=violations_list)
226
        raise BuildFailure(failure_string)
227

228 229 230

@task
@needs('pavelib.prereqs.install_python_prereqs')
231
@timed
232 233 234 235 236 237
def run_complexity():
    """
    Uses radon to examine cyclomatic complexity.
    For additional details on radon, see http://radon.readthedocs.org/
    """
    system_string = 'cms/ lms/ common/ openedx/'
238 239 240 241 242 243 244 245
    complexity_report_dir = (Env.REPORT_DIR / "complexity")
    complexity_report = complexity_report_dir / "python_complexity.log"

    # Ensure directory structure is in place: metrics dir, and an empty complexity report dir.
    Env.METRICS_DIR.makedirs_p()
    _prepare_report_dir(complexity_report_dir)

    print "--> Calculating cyclomatic complexity of python files..."
246 247
    try:
        sh(
248 249 250
            "radon cc {system_string} --total-average > {complexity_report}".format(
                system_string=system_string,
                complexity_report=complexity_report
251 252
            )
        )
253 254 255 256 257 258 259 260
        complexity_metric = _get_count_from_last_line(complexity_report, "python_complexity")
        _write_metric(
            complexity_metric,
            (Env.METRICS_DIR / "python_complexity")
        )
        print "--> Python cyclomatic complexity report complete."
        print "radon cyclomatic complexity score: {metric}".format(metric=str(complexity_metric))

261 262 263 264
    except BuildFailure:
        print "ERROR: Unable to calculate python-only code-complexity."


265
@task
266 267 268 269
@needs('pavelib.prereqs.install_node_prereqs')
@cmdopts([
    ("limit=", "l", "limit for number of acceptable violations"),
])
270
@timed
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
def run_eslint(options):
    """
    Runs eslint on static asset directories.
    If limit option is passed, fails build if more violations than the limit are found.
    """

    eslint_report_dir = (Env.REPORT_DIR / "eslint")
    eslint_report = eslint_report_dir / "eslint.report"
    _prepare_report_dir(eslint_report_dir)
    violations_limit = int(getattr(options, 'limit', -1))

    sh(
        "eslint --format=compact . | tee {eslint_report}".format(
            eslint_report=eslint_report
        ),
        ignore_error=True
    )

    try:
        num_violations = int(_get_count_from_last_line(eslint_report, "eslint"))
    except TypeError:
        raise BuildFailure(
            "Error. Number of eslint violations could not be found in {eslint_report}".format(
                eslint_report=eslint_report
            )
        )

    # Record the metric
    _write_metric(num_violations, (Env.METRICS_DIR / "eslint"))

    # Fail if number of violations is greater than the limit
    if num_violations > violations_limit > -1:
        raise BuildFailure(
            "ESLint Failed. Too many violations ({count}).\nThe limit is {violations_limit}.".format(
                count=num_violations, violations_limit=violations_limit
            )
        )


@task
311 312
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
313
    ("thresholds=", "t", "json containing limit for number of acceptable violations per rule"),
314
])
315
@timed
316 317 318 319 320
def run_safelint(options):
    """
    Runs safe_template_linter.py on the codebase
    """

321 322 323 324 325 326 327
    thresholds_option = getattr(options, 'thresholds', '{}')
    try:
        violation_thresholds = json.loads(thresholds_option)
    except ValueError:
        violation_thresholds = None
    if isinstance(violation_thresholds, dict) is False or \
            any(key not in ("total", "rules") for key in violation_thresholds.keys()):
328

329 330 331 332 333 334 335 336 337
        raise BuildFailure(
            """Error. 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(
                thresholds_option=thresholds_option
            )
        )

    safelint_script = "safe_template_linter.py"
338 339 340 341 342
    safelint_report_dir = (Env.REPORT_DIR / "safelint")
    safelint_report = safelint_report_dir / "safelint.report"
    _prepare_report_dir(safelint_report_dir)

    sh(
343
        "{repo_root}/scripts/{safelint_script} --rule-totals >> {safelint_report}".format(
344
            repo_root=Env.REPO_ROOT,
345
            safelint_script=safelint_script,
346 347 348 349 350
            safelint_report=safelint_report,
        ),
        ignore_error=True
    )

351 352
    safelint_counts = _get_safelint_counts(safelint_report)

353
    try:
354 355 356 357 358 359 360 361 362 363 364
        metrics_str = "Number of {safelint_script} violations: {num_violations}\n".format(
            safelint_script=safelint_script, num_violations=int(safelint_counts['total'])
        )
        if 'rules' in safelint_counts and any(safelint_counts['rules']):
            metrics_str += "\n"
            rule_keys = sorted(safelint_counts['rules'].keys())
            for rule in rule_keys:
                metrics_str += "{rule} violations: {count}\n".format(
                    rule=rule,
                    count=int(safelint_counts['rules'][rule])
                )
365 366
    except TypeError:
        raise BuildFailure(
367 368
            "Error. Number of {safelint_script} violations could not be found in {safelint_report}".format(
                safelint_script=safelint_script, safelint_report=safelint_report
369 370 371
            )
        )

372
    metrics_report = (Env.METRICS_DIR / "safelint")
373
    # Record the metric
374 375 376
    _write_metric(metrics_str, metrics_report)
    # Print number of violations to log.
    sh("cat {metrics_report}".format(metrics_report=metrics_report), ignore_error=True)
377

378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
    error_message = ""

    # Test total violations against threshold.
    if 'total' in violation_thresholds.keys():
        if violation_thresholds['total'] < safelint_counts['total']:
            error_message = "Too many violations total ({count}).\nThe limit is {violations_limit}.".format(
                count=safelint_counts['total'], violations_limit=violation_thresholds['total']
            )

    # Test rule violations against thresholds.
    if 'rules' in violation_thresholds:
        threshold_keys = sorted(violation_thresholds['rules'].keys())
        for threshold_key in threshold_keys:
            if threshold_key not in safelint_counts['rules']:
                error_message += (
                    "\nNumber of {safelint_script} violations for {rule} could not be found in "
                    "{safelint_report}."
                ).format(
                    safelint_script=safelint_script, rule=threshold_key, safelint_report=safelint_report
                )
            elif violation_thresholds['rules'][threshold_key] < safelint_counts['rules'][threshold_key]:
                error_message += \
                    "\nToo many {rule} violations ({count}).\nThe {rule} limit is {violations_limit}.".format(
                        rule=threshold_key, count=safelint_counts['rules'][threshold_key],
                        violations_limit=violation_thresholds['rules'][threshold_key],
                    )

    if error_message is not "":
        raise BuildFailure(
            "SafeTemplateLinter Failed.\n{error_message}\n"
            "See {safelint_report} or run the following command to hone in on the problem:\n"
            "  ./scripts/safe-commit-linter.sh -h".format(
                error_message=error_message, safelint_report=safelint_report
411 412 413 414
            )
        )


415 416
@task
@needs('pavelib.prereqs.install_python_prereqs')
417
@timed
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
def run_safecommit_report():
    """
    Runs safe-commit-linter.sh on the current branch.
    """
    safecommit_script = "safe-commit-linter.sh"
    safecommit_report_dir = (Env.REPORT_DIR / "safecommit")
    safecommit_report = safecommit_report_dir / "safecommit.report"
    _prepare_report_dir(safecommit_report_dir)

    sh(
        "{repo_root}/scripts/{safecommit_script} | tee {safecommit_report}".format(
            repo_root=Env.REPO_ROOT,
            safecommit_script=safecommit_script,
            safecommit_report=safecommit_report,
        ),
        ignore_error=True
    )

    safecommit_count = _get_safecommit_count(safecommit_report)

    try:
        num_violations = int(safecommit_count)
    except TypeError:
        raise BuildFailure(
            "Error. Number of {safecommit_script} violations could not be found in {safecommit_report}".format(
                safecommit_script=safecommit_script, safecommit_report=safecommit_report
            )
        )

    # Print number of violations to log.
    violations_count_str = "Number of {safecommit_script} violations: {num_violations}\n".format(
        safecommit_script=safecommit_script, num_violations=num_violations
    )

    # Record the metric
    metrics_report = (Env.METRICS_DIR / "safecommit")
    _write_metric(violations_count_str, metrics_report)
    # Output report to console.
    sh("cat {metrics_report}".format(metrics_report=metrics_report), ignore_error=True)


459 460 461
def _write_metric(metric, filename):
    """
    Write a given metric to a given file
462 463
    Used for things like reports/metrics/eslint, which will simply tell you the number of
    eslint violations found
464
    """
465 466
    Env.METRICS_DIR.makedirs_p()

467
    with open(filename, "w") as metric_file:
468
        metric_file.write(str(metric))
469 470 471 472 473 474 475 476 477 478


def _prepare_report_dir(dir_name):
    """
    Sets a given directory to a created, but empty state
    """
    dir_name.rmtree_p()
    dir_name.mkdir_p()


479
def _get_report_contents(filename, last_line_only=False):
480
    """
481 482 483 484 485 486 487 488 489 490 491
    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
    files.

    Arguments:
        last_line_only: True to return the last line only, False to return a
            string with full contents.

    Returns:
        String containing full contents of the report, or the last line.

492
    """
493 494 495
    file_not_found_message = "The following log file could not be found: {file}".format(file=filename)
    if os.path.isfile(filename):
        with open(filename, 'r') as report_file:
496 497
            if last_line_only:
                lines = report_file.readlines()
498 499 500
                for line in reversed(lines):
                    if line != '\n':
                        return line
501 502
            else:
                return report_file.read()
503 504 505
    else:
        # Raise a build error if the file is not found
        raise BuildFailure(file_not_found_message)
506 507


508
def _get_count_from_last_line(filename, file_type):
509
    """
510 511
    This will return the number in the last line of a file.
    It is returning only the value (as a floating number).
512
    """
513
    last_line = _get_report_contents(filename, last_line_only=True)
514 515 516 517
    if file_type is "python_complexity":
        # Example of the last line of a complexity report: "Average complexity: A (1.93953443446)"
        regex = r'\d+.\d+'
    else:
518
        # Example of the last line of a compact-formatted eslint report (for example): "62829 problems"
519 520
        regex = r'^\d+'

521
    try:
522 523 524 525
        return float(re.search(regex, last_line).group(0))
    # An AttributeError will occur if the regex finds no matches.
    # A ValueError will occur if the returned regex cannot be cast as a float.
    except (AttributeError, ValueError):
526 527 528
        return None


529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
def _get_safelint_counts(filename):
    """
    This returns a dict of violations from the safelint report.

    Arguments:
        filename: The name of the safelint report.

    Returns:
        A dict containing the following:
            rules: A dict containing the count for each rule as follows:
                violation-rule-id: N, where N is the number of violations
            total: M, where M is the number of total violations

    """
    report_contents = _get_report_contents(filename)
    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': {}}
    for violation_match in rule_count_regex.finditer(report_contents):
        try:
            violations['rules'][violation_match.group('rule_id')] = int(violation_match.group('count'))
        except ValueError:
            violations['rules'][violation_match.group('rule_id')] = None
    try:
        violations['total'] = int(total_count_regex.search(report_contents).group('count'))
    # An AttributeError will occur if the regex finds no matches.
    # A ValueError will occur if the returned regex cannot be cast as a float.
    except (AttributeError, ValueError):
        violations['total'] = None
    return violations


def _get_safecommit_count(filename):
    """
    Returns the violation count from the safecommit report.

    Arguments:
        filename: The name of the safecommit report.

    Returns:
        The count of safecommit violations, or None if there is  a problem.

    """
    report_contents = _get_report_contents(filename)

    if 'No files linted' in report_contents:
        return 0

    file_count_regex = re.compile(r"^(?P<count>\d+) violations total", re.MULTILINE)
    try:
        validation_count = None
        for count_match in file_count_regex.finditer(report_contents):
            if validation_count is None:
                validation_count = 0
            validation_count += int(count_match.group('count'))
        return validation_count
    except ValueError:
        return None


589
@task
590
@needs('pavelib.prereqs.install_python_prereqs')
591
@cmdopts([
592
    ("compare-branch=", "b", "Branch to compare against, defaults to origin/master"),
593 594
    ("percentage=", "p", "fail if diff-quality is below this percentage"),
])
595
@timed
596
def run_quality(options):
597 598
    """
    Build the html diff quality reports, and print the reports to the console.
599
    :param: b, the branch to compare against, defaults to origin/master
600 601
    :param: p, diff-quality will fail if the quality percentage calculated is
        below this percentage. For example, if p is set to 80, and diff-quality finds
602
        quality of the branch vs the compare branch is less than 80%, then this task will fail.
603
        This threshold would be applied to both pep8 and pylint.
604 605 606
    """
    # Directory to put the diff reports in.
    # This makes the folder if it doesn't already exist.
607
    dquality_dir = (Env.REPORT_DIR / "diff_quality").makedirs_p()
608 609 610

    # Save the pass variable. It will be set to false later if failures are detected.
    diff_quality_percentage_pass = True
611

612
    def _pep8_output(count, violations_list, is_html=False):
613 614 615 616
        """
        Given a count & list of pep8 violations, pretty-print the pep8 output.
        If `is_html`, will print out with HTML markup.
        """
617 618 619 620 621
        if is_html:
            lines = ['<body>\n']
            sep = '-------------<br/>\n'
            title = "<h1>Quality Report: pep8</h1>\n"
            violations_bullets = ''.join(
622
                [HTML('<li>{violation}</li><br/>\n').format(violation=violation) for violation in violations_list]
623
            )
624
            violations_str = HTML('<ul>\n{bullets}</ul>\n').format(bullets=HTML(violations_bullets))
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
            violations_count_str = "<b>Violations</b>: {count}<br/>\n"
            fail_line = "<b>FAILURE</b>: pep8 count should be 0<br/>\n"
        else:
            lines = []
            sep = '-------------\n'
            title = "Quality Report: pep8\n"
            violations_str = ''.join(violations_list)
            violations_count_str = "Violations: {count}\n"
            fail_line = "FAILURE: pep8 count should be 0\n"

        violations_count_str = violations_count_str.format(count=count)

        lines.extend([sep, title, sep, violations_str, sep, violations_count_str])

        if count > 0:
            lines.append(fail_line)
        lines.append(sep + '\n')
        if is_html:
            lines.append('</body>')

        return ''.join(lines)

647
    # Run pep8 directly since we have 0 violations on master
648 649 650 651 652 653 654 655 656 657
    (count, violations_list) = _get_pep8_violations()

    # Print number of violations to log
    print _pep8_output(count, violations_list)

    # Also write the number of violations to a file
    with open(dquality_dir / "diff_quality_pep8.html", "w") as f:
        f.write(_pep8_output(count, violations_list, is_html=True))

    if count > 0:
658
        diff_quality_percentage_pass = False
659 660

    # ----- Set up for diff-quality pylint call -----
661 662
    # Set the string, if needed, to be used for the diff-quality --compare-branch switch.
    compare_branch = getattr(options, 'compare_branch', None)
663
    compare_branch_string = u''
664
    if compare_branch:
665
        compare_branch_string = u'--compare-branch={0}'.format(compare_branch)
666

667 668
    # Set the string, if needed, to be used for the diff-quality --fail-under switch.
    diff_threshold = int(getattr(options, 'percentage', -1))
669
    percentage_string = u''
670
    if diff_threshold > -1:
671
        percentage_string = u'--fail-under={0}'.format(diff_threshold)
672

673
    # Generate diff-quality html report for pylint, and print to console
674 675 676
    # If pylint reports exist, use those
    # Otherwise, `diff-quality` will call pylint itself

677
    pylint_files = get_violations_reports("pylint")
678
    pylint_reports = u' '.join(pylint_files)
679 680 681 682

    eslint_files = get_violations_reports("eslint")
    eslint_reports = u' '.join(eslint_files)

683
    pythonpath_prefix = (
684
        "PYTHONPATH=$PYTHONPATH:lms:lms/djangoapps:cms:cms/djangoapps:"
685 686 687
        "common:common/djangoapps:common/lib"
    )

688 689 690 691 692 693 694 695 696 697 698
    # run diff-quality for pylint.
    if not run_diff_quality(
            violations_type="pylint",
            prefix=pythonpath_prefix,
            reports=pylint_reports,
            percentage_string=percentage_string,
            branch_string=compare_branch_string,
            dquality_dir=dquality_dir
    ):
        diff_quality_percentage_pass = False

699 700 701 702 703 704 705 706 707 708 709
    # run diff-quality for eslint.
    if not run_diff_quality(
            violations_type="eslint",
            prefix=pythonpath_prefix,
            reports=eslint_reports,
            percentage_string=percentage_string,
            branch_string=compare_branch_string,
            dquality_dir=dquality_dir
    ):
        diff_quality_percentage_pass = False

710 711 712 713 714 715 716 717 718
    # If one of the quality runs fails, then paver exits with an error when it is finished
    if not diff_quality_percentage_pass:
        raise BuildFailure("Diff-quality failure(s).")


def run_diff_quality(
        violations_type=None, prefix=None, reports=None, percentage_string=None, branch_string=None, dquality_dir=None
):
    """
719
    This executes the diff-quality commandline tool for the given violation type (e.g., pylint, eslint).
720 721 722
    If diff-quality fails due to quality issues, this method returns False.

    """
723 724
    try:
        sh(
725 726 727 728 729 730
            "{pythonpath_prefix} diff-quality --violations={type} "
            "{reports} {percentage_string} {compare_branch_string} "
            "--html-report {dquality_dir}/diff_quality_{type}.html ".format(
                type=violations_type,
                pythonpath_prefix=prefix,
                reports=reports,
731
                percentage_string=percentage_string,
732
                compare_branch_string=branch_string,
733
                dquality_dir=dquality_dir,
734
            )
735
        )
736
        return True
737 738
    except BuildFailure, error_message:
        if is_percentage_failure(error_message):
739
            return False
740 741 742 743 744 745 746 747 748 749
        else:
            raise BuildFailure(error_message)


def is_percentage_failure(error_message):
    """
    When diff-quality is run with a threshold percentage, it ends with an exit code of 1. This bubbles up to
    paver with a subprocess return code error. If the subprocess exits with anything other than 1, raise
    a paver exception.
    """
750 751 752 753
    if "Subprocess return code: 1" not in error_message:
        return False
    else:
        return True
754 755 756 757 758 759 760 761 762 763 764 765


def get_violations_reports(violations_type):
    """
    Finds violations reports files by naming convention (e.g., all "pep8.report" files)
    """
    violations_files = []
    for subdir, _dirs, files in os.walk(os.path.join(Env.REPORT_DIR)):
        for f in files:
            if f == "{violations_type}.report".format(violations_type=violations_type):
                violations_files.append(os.path.join(subdir, f))
    return violations_files