diff --git a/diff_cover/diff_quality_tool.py b/diff_cover/diff_quality_tool.py index 1262ad95..1c27b053 100644 --- a/diff_cover/diff_quality_tool.py +++ b/diff_cover/diff_quality_tool.py @@ -25,7 +25,7 @@ jshint_driver, eslint_driver, pydocstyle_driver, pycodestyle_driver) from diff_cover.violationsreporters.java_violations_reporter import ( - CheckstyleXmlDriver, checkstyle_driver, FindbugsXmlDriver) + CheckstyleXmlDriver, checkstyle_driver, FindbugsXmlDriver, PmdXmlDriver) QUALITY_DRIVERS = { 'pycodestyle': pycodestyle_driver, @@ -37,7 +37,8 @@ 'pydocstyle': pydocstyle_driver, 'checkstyle': checkstyle_driver, 'checkstylexml': CheckstyleXmlDriver(), - 'findbugs': FindbugsXmlDriver() + 'findbugs': FindbugsXmlDriver(), + 'pmd': PmdXmlDriver() } VIOLATION_CMD_HELP = "Which code quality tool to use (%s)" % "/".join(sorted(QUALITY_DRIVERS)) diff --git a/diff_cover/tests/test_java_violations_reporter.py b/diff_cover/tests/test_java_violations_reporter.py index a0b4e46b..bc139bb5 100644 --- a/diff_cover/tests/test_java_violations_reporter.py +++ b/diff_cover/tests/test_java_violations_reporter.py @@ -13,7 +13,7 @@ from diff_cover.violationsreporters.base import QualityReporter from diff_cover.violationsreporters.java_violations_reporter import ( Violation, checkstyle_driver, - CheckstyleXmlDriver, FindbugsXmlDriver) + CheckstyleXmlDriver, FindbugsXmlDriver, PmdXmlDriver) def _patch_so_all_files_exist(): @@ -354,3 +354,75 @@ def test_quality_pregenerated_report(self): self.assertEqual(len(actual_violations), len(expected_violations)) for expected in expected_violations: self.assertIn(expected, actual_violations) + + +class PmdXmlQualityReporterTest(unittest.TestCase): + + def setUp(self): + _patch_so_all_files_exist() + # Paths generated by git_path are always the given argument + _git_path_mock = patch('diff_cover.violationsreporters.java_violations_reporter.GitPathTool').start() + _git_path_mock.relative_path = lambda path: path + _git_path_mock.absolute_path = lambda path: path + + def tearDown(self): + """ + Undo all patches. + """ + patch.stopall() + + def test_no_such_file(self): + quality = QualityReporter(PmdXmlDriver()) + + # Expect that we get no results + result = quality.violations('') + self.assertEqual(result, []) + + def test_no_java_file(self): + quality = QualityReporter(PmdXmlDriver()) + file_paths = ['file1.coffee', 'subdir/file2.js'] + # Expect that we get no results because no Java files + for path in file_paths: + result = quality.violations(path) + self.assertEqual(result, []) + + def test_quality_pregenerated_report(self): + # When the user provides us with a pre-generated findbugs report + # then use that instead of calling findbugs directly. + pmd_reports = [ + BytesIO(dedent(""" + + + + + must have @author comment + + + + + interface method must include javadoc comment + + + + """).strip().encode('utf-8')) + ] + + pmd_xml_driver = PmdXmlDriver() + # Generate the violation report + quality = QualityReporter(pmd_xml_driver, reports=pmd_reports) + + # Expect that pmd is not installed + self.assertEqual(pmd_xml_driver.installed(), False) + + # Expect that we get the right violations + expected_violations = [ + Violation(21, 'ClassMustHaveAuthorRule: must have @author comment'), + Violation(10, 'AbstractMethodOrInterfaceMethodMustUseJavadocRule: interface method must include javadoc comment') + ] + + # We're not guaranteed that the violations are returned + # in any particular order. + actual_violations = quality.violations('path/to/file.java') + self.assertEqual(len(actual_violations), len(expected_violations)) + for expected in expected_violations: + self.assertIn(expected, actual_violations) diff --git a/diff_cover/violationsreporters/java_violations_reporter.py b/diff_cover/violationsreporters/java_violations_reporter.py index c2010086..072a42d1 100644 --- a/diff_cover/violationsreporters/java_violations_reporter.py +++ b/diff_cover/violationsreporters/java_violations_reporter.py @@ -3,6 +3,7 @@ """ from __future__ import unicode_literals +import os from collections import defaultdict from xml.etree import cElementTree @@ -115,3 +116,46 @@ def installed(self): Returns: boolean False: As findbugs analyses bytecode, it would be hard to run it from outside the build framework. """ return False + + +class PmdXmlDriver(QualityDriver): + def __init__(self): + """ + See super for args + """ + super(PmdXmlDriver, self).__init__( + 'pmd', + ['java'], + [] + ) + + def parse_reports(self, reports): + """ + Args: + reports: list[str] - output from the report + Return: + A dict[Str:Violation] + Violation is a simple named tuple Defined above + """ + violations_dict = defaultdict(list) + for report in reports: + xml_document = cElementTree.fromstring("".join(report)) + node_files = xml_document.findall(".//file") + for node_file in node_files: + for error in node_file.findall('violation'): + line_number = error.get('beginline') + error_str = "{}: {}".format(error.get('rule'), + error.text.strip()) + violation = Violation(int(line_number), error_str) + filename = GitPathTool.relative_path(node_file.get('name')) + filename = filename.replace(os.sep, "/") + violations_dict[filename].append(violation) + + return violations_dict + + def installed(self): + """ + Method checks if the provided tool is installed. + Returns: boolean False: As findbugs analyses bytecode, it would be hard to run it from outside the build framework. + """ + return False