changeset 416:ff35be7d2a5e

Add PHP recipe commands contributed by Wei Zhuo.
author cmlenz
date Wed, 08 Aug 2007 12:43:16 +0000
parents b4ec24092b54
children ab2557ff0d97
files bitten/build/phptools.py bitten/build/tests/__init__.py bitten/build/tests/phptools.py doc/commands.txt setup.py
diffstat 5 files changed, 351 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bitten/build/phptools.py
@@ -0,0 +1,113 @@
+# -*- coding: UTF-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2007 Wei Zhuo <weizhuo@gmail.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+
+import logging
+import os
+import shlex
+
+from bitten.util import xmlio
+from bitten.build import shtools
+
+log = logging.getLogger('bitten.build.phptools')
+
+def phing(ctxt, file_=None, target_=None, executable_=None, args=None):
+    """Run a phing build"""
+    if args:
+        args = shlex.split(args)
+    else:
+        args = []
+    args += ['-logger', 'phing.listener.DefaultLogger',
+             '-buildfile', ctxt.resolve(file_ or 'build.xml')]
+    if target_:
+        args.append(target_)
+
+    returncode = shtools.execute(ctxt, file_= executable_ or 'phing', args=args)
+    if returncode != 0:
+        ctxt.error('Phing failed (%s)' % returncode)
+
+def phpunit(ctxt, file_=None):
+    """Extract test results from a PHPUnit XML report."""
+    assert file_, 'Missing required attribute "file"'
+    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 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)
+        finally:
+            fileobj.close()
+        if failed:
+            ctxt.error('%d of %d test%s failed' % (failed, total,
+                        total != 1 and 's' or ''))
+        ctxt.report('test', results)
+    except IOError, e:
+        ctxt.log('Error opening PHPUnit results file (%s)' % e)
+    except xmlio.ParseError, e:
+        ctxt.log('Error parsing PHPUnit results file (%s)' % e)
+
+def coverage(ctxt, file_=None):
+    """Extract data from a Phing code coverage report."""
+    assert file_, 'Missing required attribute "file"'
+    try:
+        summary_file = file(ctxt.resolve(file_), 'r')
+        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)
+        finally:
+            summary_file.close()
+        ctxt.report('coverage', coverage)
+    except IOError, e:
+        ctxt.log('Error opening coverage summary file (%s)' % e)
+    except xmlio.ParseError, e:
+        ctxt.log('Error parsing coverage summary file (%s)' % e)
--- a/bitten/build/tests/__init__.py
+++ b/bitten/build/tests/__init__.py
@@ -10,13 +10,15 @@
 
 import unittest
 
-from bitten.build.tests import api, config, ctools, pythontools, xmltools
+from bitten.build.tests import api, config, ctools, phptools, pythontools, \
+                               xmltools
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(api.suite())
     suite.addTest(config.suite())
     suite.addTest(ctools.suite())
+    suite.addTest(phptools.suite())
     suite.addTest(pythontools.suite())
     suite.addTest(xmltools.suite())
     return suite
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/phptools.py
@@ -0,0 +1,131 @@
+# -*- coding: UTF-8 -*-
+#
+# Copyright (C) 2007 Edgewall Software
+# Copyright (C) 2007 Wei Zhuo <weizhuo@gmail.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from bitten.build import phptools
+from bitten.recipe import Context, Recipe
+
+class PhpUnitTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.basedir = os.path.realpath(tempfile.mkdtemp())
+        self.ctxt = Context(self.basedir)
+
+    def tearDown(self):
+        shutil.rmtree(self.basedir)
+
+    def test_missing_param_file(self):
+        self.assertRaises(AssertionError, phptools.phpunit, self.ctxt)
+
+    def test_sample_unit_test_result(self):
+        phpunit_xml = file(self.ctxt.resolve('phpunit.xml'), 'w')
+        phpunit_xml.write("""<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+  <testsuite name="FooTest" file="FooTest.php" tests="2" failures="1" errors="0" time="0.147397">
+    <testcase name="testBar" class="FooTest" time="0.122265">
+      <failure message="expected same: &lt;1&gt; was not: &lt;2&gt;" type="PHPUnit2_Framework_AssertionFailedError">
+      ...
+</failure>
+    </testcase>
+    <testcase name="testBar2" class="FooTest" time="0.025132"/>
+  </testsuite>
+  <testsuite name="BarTest" file="BarTest.php" tests="1" failures="0" errors="0" time="0.050713">
+    <testcase name="testFoo" class="BarTest" time="0.026046"/>
+  </testsuite>
+</testsuites>""")
+        phpunit_xml.close()
+        phptools.phpunit(self.ctxt, file_='phpunit.xml')
+        type, category, generator, xml = self.ctxt.output.pop()
+        self.assertEqual(Recipe.REPORT, type)
+        self.assertEqual('test', category)
+
+        tests = list(xml.children)
+        self.assertEqual(3, len(tests))
+        self.assertEqual('FooTest', tests[0].attr['fixture'])
+        self.assertEqual('testBar', tests[0].attr['name'])
+        self.assertEqual('failure', tests[0].attr['status'])
+        self.assert_('FooTest.php' in tests[0].attr['file'])
+
+        self.assertEqual('FooTest', tests[1].attr['fixture'])
+        self.assertEqual('testBar2', tests[1].attr['name'])
+        self.assertEqual('success', tests[1].attr['status'])
+
+        self.assertEqual('BarTest', tests[2].attr['fixture'])
+        self.assertEqual('testFoo', tests[2].attr['name'])
+        self.assertEqual('success', tests[2].attr['status'])
+        
+class PhpCodeCoverageTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.basedir = os.path.realpath(tempfile.mkdtemp())
+        self.ctxt = Context(self.basedir)
+
+    def tearDown(self):
+        shutil.rmtree(self.basedir)
+
+    def test_missing_param_file(self):
+        self.assertRaises(AssertionError, phptools.coverage, self.ctxt)
+
+    def test_sample_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">
+      <sourcefile name="Foo.php" sourcefile="xxxx/Foo.php">
+        ...
+      </sourcefile>
+    </class>
+    <class name="Bar" methodcount="1" methodscovered="0" statementcount="0" statementscovered="0" totalcount="1" totalcovered="0">
+      <sourcefile name="Bar.php" sourcefile="xxxx/Bar.php">
+        ...
+      </sourcefile>
+    </class>
+  </package>
+</snapshot>""")
+        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(3, len(coverage))
+        self.assertEqual(7, coverage[0].attr['lines'])
+        self.assertEqual('Foo', coverage[0].attr['name'])
+        self.assert_('xxxx/Foo.php' in coverage[0].attr['file'])
+
+        self.assertEqual(4, coverage[1].attr['lines'])
+        self.assertEqual(50.0, coverage[1].attr['percentage'])
+        self.assertEqual('Foo2', coverage[1].attr['name'])
+        self.assert_('xxxx/Foo.php' in coverage[1].attr['file'])
+        
+        self.assertEqual(0, coverage[2].attr['lines'])
+        self.assertEqual(100.0, coverage[2].attr['percentage'])
+        self.assertEqual('Bar', coverage[2].attr['name'])
+        self.assert_('xxxx/Bar.php' in coverage[2].attr['file'])
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(PhpUnitTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(PhpCodeCoverageTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/doc/commands.txt
+++ b/doc/commands.txt
@@ -366,6 +366,107 @@
 files to Java source files in the directory `src/tests`.
 
 
+PHP Tools
+=========
+
+A bundle of recipe commands for PHP_ projects.
+
+:Namespace: ``http://bitten.cmlenz.net/tools/php``
+:Common prefix: ``php``
+
+.. _php: http://php.net/
+
+------------------
+``<python:phing>``
+------------------
+
+Runs a Phing_ build.
+
+.. _phing: http://phing.info/
+
+Parameters
+----------
+
++-------------------+-------------------------------------------------------+
+| Name              | Description                                           |
++===================+=======================================================+
+| ``file``          | Path of the build file, relative to the project       |
+|                   | source directory (default is ``build.xml``).          |
++-------------------+-------------------------------------------------------+
+| ``target``        | Name of the build target(s) to execute.               |
++-------------------+-------------------------------------------------------+
+| ``args``          | Additional arguments to pass to Phing, separated by   |
+|                   | whitespace.                                           |
++-------------------+-------------------------------------------------------+
+| ``executable``    | Phing executable program (default is ``phing``).      |
++-------------------+-------------------------------------------------------+
+
+
+Examples
+--------
+
+.. code-block:: xml
+
+  <php:phing target="compile" />
+
+Executes the target ``compile`` of the ``build.xml`` buildfile at the top of the
+project source directory.
+
+
+-----------------
+``<php:phpunit>``
+-----------------
+
+Extracts information from PHPUnit_ test results recorded in an XML file.
+
+.. _phpunit: http://www.phpunit.de/
+
+Parameters
+----------
+
++----------------+-----------------------------------------------------------+
+| Name           | Description                                               |
++================+===========================================================+
+| ``file``       | Path to the XML results file, relative to the project     |
+|                | source directory.                                         |
++----------------+-----------------------------------------------------------+
+
+Examples
+--------
+
+.. code-block:: xml
+
+  <php:phpunit file="build/test-results.xml"/>
+
+Extracts the test results from the XML file located at
+``build/test-results.xml``.
+
+
+------------------
+``<php:coverage>``
+------------------
+
+Extracts coverage information Phing_'s code coverage task recorded in an XML
+file.
+
+Parameters
+----------
+
++---------------+-----------------------------------------------------------+
+| Name          | Description                                               |
++===============+===========================================================+
+| ``file``      | Path to the XML coverage file, relative to the project    |
+|               | source directory.                                         |
++---------------+-----------------------------------------------------------+
+
+Examples
+--------
+
+.. code-block:: xml
+
+  <php:coverage file="build/coverage.xml" />
+
+
 Python Tools
 ============
 
--- a/setup.py
+++ b/setup.py
@@ -67,6 +67,9 @@
             NS + 'java#ant = bitten.build.javatools:ant',
             NS + 'java#junit = bitten.build.javatools:junit',
             NS + 'java#cobertura = bitten.build.javatools:cobertura',
+            NS + 'php#phing = bitten.build.phptools:phing',
+            NS + 'php#phpunit = bitten.build.phptools:phpunit',
+            NS + 'php#coverage = bitten.build.phptools:coverage',
             NS + 'python#distutils = bitten.build.pythontools:distutils',
             NS + 'python#exec = bitten.build.pythontools:exec_',
             NS + 'python#pylint = bitten.build.pythontools:pylint',
Copyright (C) 2012-2017 Edgewall Software