# HG changeset patch # User osimons # Date 1248864228 0 # Node ID 4c3d43adaa483077a60d2a6ec2a5e35c373ed713 # Parent b1c90136f84a63e4e789fb69a56551021a32cafe 0.6dev: Fixed `php:phpunit` parsing including support for nested tests. Extended `php:coverage` to also parse PHPUnit coverage-clover format. Closes #199 and #316. Thanks to all those that have helped develop the patch, and special thanks to Roland Wilczek for contributing, testing and helping me get working php builds for development. diff --git a/bitten/build/phptools.py b/bitten/build/phptools.py --- a/bitten/build/phptools.py +++ b/bitten/build/phptools.py @@ -35,36 +35,45 @@ def phpunit(ctxt, file_=None): """Extract test results from a PHPUnit XML report.""" assert file_, 'Missing required attribute "file"' + + def _process_testsuite(testsuite, results, parent_file=''): + for testcase in testsuite.children(): + if testcase.name == 'testsuite': + _process_testsuite(testcase, results, + parent_file=testcase.attr.get('file', parent_file)) + continue + test = xmlio.Element('test') + test.attr['fixture'] = testsuite.attr['name'] + test.attr['name'] = testcase.attr['name'] + test.attr['duration'] = testcase.attr['time'] + result = list(testcase.children()) + if result: + test.append(xmlio.Element('traceback')[ + result[0].gettext() + ]) + test.attr['status'] = result[0].name + else: + test.attr['status'] = 'success' + if 'file' in testsuite.attr or parent_file: + testfile = os.path.realpath( + testsuite.attr.get('file', parent_file)) + if testfile.startswith(ctxt.basedir): + testfile = testfile[len(ctxt.basedir) + 1:] + testfile = testfile.replace(os.sep, '/') + test.attr['file'] = testfile + results.append(test) + try: total, failed = 0, 0 results = xmlio.Fragment() fileobj = file(ctxt.resolve(file_), 'r') try: - for testsuit in xmlio.parse(fileobj).children('testsuite'): - total += int(testsuit.attr['tests']) - failed += int(testsuit.attr['failures']) + \ - int(testsuit.attr['errors']) + for testsuite in xmlio.parse(fileobj).children('testsuite'): + total += int(testsuite.attr['tests']) + failed += int(testsuite.attr['failures']) + \ + int(testsuite.attr['errors']) - for testcase in testsuit.children(): - test = xmlio.Element('test') - test.attr['fixture'] = testcase.attr['class'] - test.attr['name'] = testcase.attr['name'] - test.attr['duration'] = testcase.attr['time'] - result = list(testcase.children()) - if result: - test.append(xmlio.Element('traceback')[ - result[0].gettext() - ]) - test.attr['status'] = result[0].name - else: - test.attr['status'] = 'success' - if 'file' in testsuit.attr: - testfile = os.path.realpath(testsuit.attr['file']) - if testfile.startswith(ctxt.basedir): - testfile = testfile[len(ctxt.basedir) + 1:] - testfile = testfile.replace(os.sep, '/') - test.attr['file'] = testfile - results.append(test) + _process_testsuite(testsuite, results) finally: fileobj.close() if failed: @@ -77,33 +86,67 @@ ctxt.log('Error parsing PHPUnit results file (%s)' % e) def coverage(ctxt, file_=None): - """Extract data from a Phing code coverage report.""" + """Extract data from Phing or PHPUnit code coverage report.""" assert file_, 'Missing required attribute "file"' + + def _process_phing_coverage(ctxt, element, coverage): + for cls in element.children('class'): + statements = float(cls.attr['statementcount']) + covered = float(cls.attr['statementscovered']) + if statements: + percentage = covered / statements * 100 + else: + percentage = 100 + class_coverage = xmlio.Element('coverage', + name=cls.attr['name'], + lines=int(statements), + percentage=percentage + ) + source = list(cls.children())[0] + if 'sourcefile' in source.attr: + sourcefile = os.path.realpath(source.attr['sourcefile']) + if sourcefile.startswith(ctxt.basedir): + sourcefile = sourcefile[len(ctxt.basedir) + 1:] + sourcefile = sourcefile.replace(os.sep, '/') + class_coverage.attr['file'] = sourcefile + coverage.append(class_coverage) + + def _process_phpunit_coverage(ctxt, element, coverage): + for cls in element._node.getElementsByTagName('class'): + sourcefile = cls.parentNode.getAttribute('name') + if not os.path.isabs(sourcefile): + sourcefile = os.path.join(ctxt.basedir, sourcefile) + if sourcefile.startswith(ctxt.basedir): + loc, ncloc = 0, 0.0 + for line in cls.parentNode.getElementsByTagName('line'): + if str(line.getAttribute('type')) == 'stmt': + loc += 1 + if int(line.getAttribute('count')) == 0: + ncloc += 1 + if loc > 0: + percentage = 100 - (ncloc / loc * 100) + else: + percentage = 100 + + if sourcefile.startswith(ctxt.basedir): + sourcefile = sourcefile[len(ctxt.basedir) + 1:] + class_coverage = xmlio.Element('coverage', + name=cls.getAttribute('name'), + lines=int(loc), + percentage=int(percentage), + file=sourcefile.replace(os.sep, '/')) + coverage.append(class_coverage) + try: summary_file = file(ctxt.resolve(file_), 'r') + summary = xmlio.parse(summary_file) + coverage = xmlio.Fragment() try: - coverage = xmlio.Fragment() - for package in xmlio.parse(summary_file).children('package'): - for cls in package.children('class'): - statements = float(cls.attr['statementcount']) - covered = float(cls.attr['statementscovered']) - if statements: - percentage = covered / statements * 100 - else: - percentage = 100 - class_coverage = xmlio.Element('coverage', - name=cls.attr['name'], - lines=int(statements), - percentage=percentage - ) - source = list(cls.children())[0] - if 'sourcefile' in source.attr: - sourcefile = os.path.realpath(source.attr['sourcefile']) - if sourcefile.startswith(ctxt.basedir): - sourcefile = sourcefile[len(ctxt.basedir) + 1:] - sourcefile = sourcefile.replace(os.sep, '/') - class_coverage.attr['file'] = sourcefile - coverage.append(class_coverage) + for element in summary.children(): + if element.name == 'package': + _process_phing_coverage(ctxt, element, coverage) + elif element.name == 'project': + _process_phpunit_coverage(ctxt, element, coverage) finally: summary_file.close() ctxt.report('coverage', coverage) diff --git a/bitten/build/tests/phptools.py b/bitten/build/tests/phptools.py --- a/bitten/build/tests/phptools.py +++ b/bitten/build/tests/phptools.py @@ -77,14 +77,14 @@ def test_missing_param_file(self): self.assertRaises(AssertionError, phptools.coverage, self.ctxt) - def test_sample_code_coverage(self): + def test_sample_phing_code_coverage(self): coverage_xml = file(self.ctxt.resolve('phpcoverage.xml'), 'w') coverage_xml.write(""" - ... + ... @@ -121,6 +121,128 @@ self.assertEqual('Bar', coverage[2].attr['name']) self.assert_('xxxx/Bar.php' in coverage[2].attr['file']) + def test_sample_phpunit_code_coverage(self): + coverage_xml = file(self.ctxt.resolve('phpcoverage.xml'), 'w') + coverage_xml.write(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" % ((self.basedir,)*6)) # One relative path, remaining is absolute + coverage_xml.close() + phptools.coverage(self.ctxt, file_='phpcoverage.xml') + type, category, generator, xml = self.ctxt.output.pop() + self.assertEqual(Recipe.REPORT, type) + self.assertEqual('coverage', category) + + coverage = list(xml.children) + self.assertEqual(6, len(coverage)) + + self.assertEqual(27, sum([int(c.attr['lines']) for c in coverage])) + self.assertEqual(['Foo', 'All_Foo_Tests', 'AllTests', 'All_Bar_Tests', + 'All_Bar_Nested_Tests', 'Bar'], + [c.attr['name'] for c in coverage]) + self.assertEqual(['Foo/classes/Foo.php', + 'Foo/tests/Foo/AllTests.php', + 'Foo/tests/AllTests.php', + 'Foo/tests/Bar/AllTests.php', + 'Foo/tests/Bar/Nested/AllTests.php', + 'Foo/classes/Bar.php'], + [c.attr['file'] for c in coverage]) + self.assertEqual([100, 0, 0, 0, 0, 100], + [c.attr['percentage'] for c in coverage]) + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(PhpUnitTestCase, 'test')) diff --git a/doc/commands.txt b/doc/commands.txt --- a/doc/commands.txt +++ b/doc/commands.txt @@ -545,8 +545,8 @@ ```` ------------------ -Extracts coverage information Phing_'s code coverage task recorded in an XML -file. +Extracts coverage information from Phing_'s code coverage task XML file or +from PHPUnit_ coverage-clover XML file. Parameters ----------