changeset 597:4c3d43adaa48

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.
author osimons
date Wed, 29 Jul 2009 10:43:48 +0000
parents b1c90136f84a
children 5f3e66e5b451
files bitten/build/phptools.py bitten/build/tests/phptools.py doc/commands.txt
diffstat 3 files changed, 216 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- 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)
--- 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("""<?xml version="1.0" encoding="UTF-8"?>
 <snapshot methodcount="4" methodscovered="2" statementcount="11" statementscovered="5" totalcount="15" totalcovered="7">
   <package name="default" methodcount="4" methodscovered="2" statementcount="11" statementscovered="5" totalcount="15" totalcovered="7">
     <class name="Foo" methodcount="1" methodscovered="1" statementcount="7" statementscovered="3" totalcount="8" totalcovered="4">
       <sourcefile name="Foo.php" sourcefile="xxxx/Foo.php">
-	  ...
+        ...
       </sourcefile>
     </class>
     <class name="Foo2" methodcount="2" methodscovered="1" statementcount="4" statementscovered="2" totalcount="6" totalcovered="3">
@@ -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("""<?xml version="1.0" encoding="UTF-8"?>
+<coverage generated="1248813201" phpunit="3.3.17">
+  <project name="All Tests" timestamp="1248813201">
+    <file name="%s/Foo/classes/Foo.php">
+      <class name="Foo" namespace="global">
+        <metrics methods="0" coveredmethods="0" statements="0"
+          coveredstatements="0" elements="0" coveredelements="0"/>
+      </class>
+      <line num="3" type="stmt" count="1"/>
+      <line num="6" type="stmt" count="1"/>
+      <metrics loc="5" ncloc="3" classes="1" methods="0" coveredmethods="0"
+        statements="2" coveredstatements="2" elements="2" coveredelements="2"/>
+    </file>
+    <file name="%s/Foo/tests/environment.config.php">
+      <line num="0" type="stmt" count="2"/>
+      <line num="4" type="stmt" count="2"/>
+      <line num="5" type="stmt" count="2"/>
+      <metrics loc="6" ncloc="6" classes="0" methods="0" coveredmethods="0"
+        statements="3" coveredstatements="3" elements="3" coveredelements="3"/>
+    </file>
+    <file name="%s/Foo/tests/Foo/AllTests.php">
+      <class name="All_Foo_Tests" namespace="global" fullPackage="All.Foo">
+        <metrics methods="2" coveredmethods="0" statements="4"
+          coveredstatements="0" elements="6" coveredelements="0"/>
+      </class>
+      <line num="7" type="method" count="0"/>
+      <line num="9" type="stmt" count="0"/>
+      <line num="10" type="stmt" count="0"/>
+      <line num="12" type="method" count="0"/>
+      <line num="14" type="stmt" count="0"/>
+      <line num="15" type="stmt" count="0"/>
+      <line num="16" type="stmt" count="0"/>
+      <metrics loc="19" ncloc="19" classes="1" methods="2" coveredmethods="0"
+        statements="5" coveredstatements="0" elements="7" coveredelements="0"/>
+    </file>
+    <file name="%s/Foo/tests/AllTests.php">
+      <class name="AllTests" namespace="global">
+        <metrics methods="2" coveredmethods="0" statements="5"
+          coveredstatements="0" elements="7" coveredelements="0"/>
+      </class>
+      <line num="8" type="method" count="0"/>
+      <line num="10" type="stmt" count="0"/>
+      <line num="11" type="stmt" count="0"/>
+      <line num="13" type="method" count="0"/>
+      <line num="15" type="stmt" count="0"/>
+      <line num="16" type="stmt" count="0"/>
+      <line num="17" type="stmt" count="0"/>
+      <line num="18" type="stmt" count="0"/>
+      <metrics loc="22" ncloc="22" classes="1" methods="2" coveredmethods="0"
+        statements="6" coveredstatements="0" elements="8" coveredelements="0"/>
+    </file>
+    <file name="%s/Foo/tests/Bar/AllTests.php">
+      <class name="All_Bar_Tests" namespace="global" fullPackage="All.Bar">
+        <metrics methods="2" coveredmethods="0" statements="5"
+          coveredstatements="0" elements="7" coveredelements="0"/>
+      </class>
+      <line num="8" type="method" count="0"/>
+      <line num="10" type="stmt" count="0"/>
+      <line num="11" type="stmt" count="0"/>
+      <line num="13" type="method" count="0"/>
+      <line num="15" type="stmt" count="0"/>
+      <line num="16" type="stmt" count="0"/>
+      <line num="17" type="stmt" count="0"/>
+      <line num="18" type="stmt" count="0"/>
+      <metrics loc="20" ncloc="20" classes="1" methods="2" coveredmethods="0"
+        statements="6" coveredstatements="0" elements="8" coveredelements="0"/>
+    </file>
+    <file name="%s/Foo/tests/Bar/Nested/AllTests.php">
+      <class name="All_Bar_Nested_Tests" namespace="global" fullPackage="All.Bar.Nested">
+        <metrics methods="2" coveredmethods="0" statements="5"
+          coveredstatements="0" elements="7" coveredelements="0"/>
+      </class>
+      <line num="8" type="method" count="0"/>
+      <line num="10" type="stmt" count="0"/>
+      <line num="11" type="stmt" count="0"/>
+      <line num="13" type="method" count="0"/>
+      <line num="15" type="stmt" count="0"/>
+      <line num="16" type="stmt" count="0"/>
+      <line num="17" type="stmt" count="0"/>
+      <line num="18" type="stmt" count="0"/>
+      <metrics loc="21" ncloc="21" classes="1" methods="2" coveredmethods="0"
+        statements="6" coveredstatements="0" elements="8" coveredelements="0"/>
+    </file>
+    <file name="Foo/classes/Bar.php">
+      <class name="Bar" namespace="global">
+        <metrics methods="0" coveredmethods="0" statements="0"
+          coveredstatements="0" elements="0" coveredelements="0"/>
+      </class>
+      <line num="3" type="stmt" count="1"/>
+      <line num="6" type="stmt" count="1"/>
+      <metrics loc="5" ncloc="3" classes="1" methods="0" coveredmethods="0"
+        statements="2" coveredstatements="2" elements="2" coveredelements="2"/>
+    </file>
+    <metrics files="7" loc="98" ncloc="94" classes="6" methods="8" coveredmethods="0"
+      statements="30" coveredstatements="7" elements="38" coveredelements="7"/>
+  </project>
+</coverage>""" % ((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'))
--- a/doc/commands.txt
+++ b/doc/commands.txt
@@ -545,8 +545,8 @@
 ``<php:coverage>``
 ------------------
 
-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
 ----------
Copyright (C) 2012-2017 Edgewall Software