summaryrefslogtreecommitdiff
path: root/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php
diff options
context:
space:
mode:
Diffstat (limited to 'buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php')
-rwxr-xr-xbuildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php564
1 files changed, 564 insertions, 0 deletions
diff --git a/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php b/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php
new file mode 100755
index 00000000..dbfc3093
--- /dev/null
+++ b/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php
@@ -0,0 +1,564 @@
+<?php
+/**
+ * $Id: 564bbde3ec5084ed2db570958548af2b9d1c1127 $
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the LGPL. For more information please see
+ * <http://phing.info>.
+ */
+
+require_once 'phing/Task.php';
+require_once 'phing/system/io/PhingFile.php';
+require_once 'phing/system/io/Writer.php';
+require_once 'phing/system/util/Properties.php';
+require_once 'phing/tasks/ext/phpunit/PHPUnitUtil.php';
+require_once 'phing/tasks/ext/coverage/CoverageReportTransformer.php';
+
+/**
+ * Transforms information in a code coverage database to XML
+ *
+ * @author Michiel Rook <mrook@php.net>
+ * @version $Id: 564bbde3ec5084ed2db570958548af2b9d1c1127 $
+ * @package phing.tasks.ext.coverage
+ * @since 2.1.0
+ */
+class CoverageReportTask extends Task
+{
+ private $outfile = "coverage.xml";
+
+ private $transformers = array();
+
+ /** the classpath to use (optional) */
+ private $classpath = NULL;
+
+ /** the path to the GeSHi library (optional) */
+ private $geshipath = "";
+
+ /** the path to the GeSHi language files (optional) */
+ private $geshilanguagespath = "";
+
+ function setClasspath(Path $classpath)
+ {
+ if ($this->classpath === null)
+ {
+ $this->classpath = $classpath;
+ }
+ else
+ {
+ $this->classpath->append($classpath);
+ }
+ }
+
+ function createClasspath()
+ {
+ $this->classpath = new Path();
+ return $this->classpath;
+ }
+
+ function setGeshiPath($path)
+ {
+ $this->geshipath = $path;
+ }
+
+ function setGeshiLanguagesPath($path)
+ {
+ $this->geshilanguagespath = $path;
+ }
+
+ function __construct()
+ {
+ $this->doc = new DOMDocument();
+ $this->doc->encoding = 'UTF-8';
+ $this->doc->formatOutput = true;
+ $this->doc->appendChild($this->doc->createElement('snapshot'));
+ }
+
+ function setOutfile($outfile)
+ {
+ $this->outfile = $outfile;
+ }
+
+ /**
+ * Generate a report based on the XML created by this task
+ */
+ function createReport()
+ {
+ $transformer = new CoverageReportTransformer($this);
+ $this->transformers[] = $transformer;
+ return $transformer;
+ }
+
+ protected function getPackageElement($packageName)
+ {
+ $packages = $this->doc->documentElement->getElementsByTagName('package');
+
+ foreach ($packages as $package)
+ {
+ if ($package->getAttribute('name') == $packageName)
+ {
+ return $package;
+ }
+ }
+
+ return NULL;
+ }
+
+ protected function addClassToPackage($classname, $element)
+ {
+ $packageName = PHPUnitUtil::getPackageName($classname);
+
+ $package = $this->getPackageElement($packageName);
+
+ if ($package === NULL)
+ {
+ $package = $this->doc->createElement('package');
+ $package->setAttribute('name', $packageName);
+ $this->doc->documentElement->appendChild($package);
+ }
+
+ $package->appendChild($element);
+ }
+
+ /**
+ * Adds a subpackage to their package
+ *
+ * @param string $packageName The name of the package
+ * @param string $subpackageName The name of the subpackage
+ *
+ * @author Benjamin Schultz <bschultz@proqrent.de>
+ * @return void
+ */
+ protected function addSubpackageToPackage($packageName, $subpackageName)
+ {
+ $package = $this->getPackageElement($packageName);
+ $subpackage = $this->getSubpackageElement($subpackageName);
+
+ if ($package === null) {
+ $package = $this->doc->createElement('package');
+ $package->setAttribute('name', $packageName);
+ $this->doc->documentElement->appendChild($package);
+ }
+
+ if ($subpackage === null) {
+ $subpackage = $this->doc->createElement('subpackage');
+ $subpackage->setAttribute('name', $subpackageName);
+ }
+
+ $package->appendChild($subpackage);
+ }
+
+ /**
+ * Returns the subpackage element
+ *
+ * @param string $subpackageName The name of the subpackage
+ *
+ * @author Benjamin Schultz <bschultz@proqrent.de>
+ * @return DOMNode|null null when no DOMNode with the given name exists
+ */
+ protected function getSubpackageElement($subpackageName)
+ {
+ $subpackages = $this->doc->documentElement->getElementsByTagName('subpackage');
+
+ foreach ($subpackages as $subpackage) {
+ if ($subpackage->getAttribute('name') == $subpackageName) {
+ return $subpackage;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a class to their subpackage
+ *
+ * @param string $classname The name of the class
+ * @param DOMNode $element The dom node to append to the subpackage element
+ *
+ * @author Benjamin Schultz <bschultz@proqrent.de>
+ * @return void
+ */
+ protected function addClassToSubpackage($classname, $element)
+ {
+ $subpackageName = PHPUnitUtil::getSubpackageName($classname);
+
+ $subpackage = $this->getSubpackageElement($subpackageName);
+
+ if ($subpackage === null) {
+ $subpackage = $this->doc->createElement('subpackage');
+ $subpackage->setAttribute('name', $subpackageName);
+ $this->doc->documentElement->appendChild($subpackage);
+ }
+
+ $subpackage->appendChild($element);
+ }
+
+ protected function stripDiv($source)
+ {
+ $openpos = strpos($source, "<div");
+ $closepos = strpos($source, ">", $openpos);
+
+ $line = substr($source, $closepos + 1);
+
+ $tagclosepos = strpos($line, "</div>");
+
+ $line = substr($line, 0, $tagclosepos);
+
+ return $line;
+ }
+
+ protected function highlightSourceFile($filename)
+ {
+ if ($this->geshipath)
+ {
+ require_once $this->geshipath . '/geshi.php';
+
+ $source = file_get_contents($filename);
+
+ $geshi = new GeSHi($source, 'php', $this->geshilanguagespath);
+
+ $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
+
+ $geshi->enable_strict_mode(true);
+
+ $geshi->enable_classes(true);
+
+ $geshi->set_url_for_keyword_group(3, '');
+
+ $html = $geshi->parse_code();
+
+ $lines = preg_split("#</?li>#", $html);
+
+ // skip first and last line
+ array_pop($lines);
+ array_shift($lines);
+
+ $lines = array_filter($lines);
+
+ $lines = array_map(array($this, 'stripDiv'), $lines);
+
+ return $lines;
+ }
+ else
+ {
+ $lines = file($filename);
+
+ for ($i = 0; $i < count($lines); $i++)
+ {
+ $line = $lines[$i];
+
+ $line = rtrim($line);
+
+ if (function_exists('mb_check_encoding') && mb_check_encoding($line, 'UTF-8')) {
+ $lines[$i] = $line;
+ }
+ else if (function_exists('mb_convert_encoding'))
+ {
+ $lines[$i] = mb_convert_encoding($line, 'UTF-8');
+ }
+ else
+ {
+ $lines[$i] = utf8_encode($line);
+ }
+ }
+
+ return $lines;
+ }
+ }
+
+ protected function transformSourceFile($filename, $coverageInformation, $classStartLine = 1)
+ {
+ $sourceElement = $this->doc->createElement('sourcefile');
+ $sourceElement->setAttribute('name', basename($filename));
+
+ /**
+ * Add original/full filename to document
+ */
+ $sourceElement->setAttribute('sourcefile', $filename);
+
+ $filelines = $this->highlightSourceFile($filename);
+
+ $linenr = 1;
+
+ foreach ($filelines as $line)
+ {
+ $lineElement = $this->doc->createElement('sourceline');
+ $lineElement->setAttribute('coveredcount', (isset($coverageInformation[$linenr]) ? $coverageInformation[$linenr] : '0'));
+
+ if ($linenr == $classStartLine)
+ {
+ $lineElement->setAttribute('startclass', 1);
+ }
+
+ $textnode = $this->doc->createTextNode($line);
+ $lineElement->appendChild($textnode);
+
+ $sourceElement->appendChild($lineElement);
+
+ $linenr++;
+ }
+
+ return $sourceElement;
+ }
+
+ /**
+ * Transforms the coverage information
+ *
+ * @param string $filename The filename
+ * @param array $coverageInformation Array with covergae information
+ *
+ * @author Michiel Rook <mrook@php.net>
+ * @author Benjamin Schultz <bschultz@proqrent.de>
+ * @return void
+ */
+ protected function transformCoverageInformation($filename, $coverageInformation)
+ {
+ $classes = PHPUnitUtil::getDefinedClasses($filename, $this->classpath);
+
+ if (is_array($classes))
+ {
+ foreach ($classes as $classname)
+ {
+ $reflection = new ReflectionClass($classname);
+
+ $methods = $reflection->getMethods();
+
+ $classElement = $this->doc->createElement('class');
+ $classElement->setAttribute('name', $reflection->getName());
+
+ $packageName = PHPUnitUtil::getPackageName($reflection->getName());
+ $subpackageName = PHPUnitUtil::getSubpackageName($reflection->getName());
+
+ if ($subpackageName !== null) {
+ $this->addSubpackageToPackage($packageName, $subpackageName);
+ $this->addClassToSubpackage($reflection->getName(), $classElement);
+ } else {
+ $this->addClassToPackage($reflection->getName(), $classElement);
+ }
+
+ $classStartLine = $reflection->getStartLine();
+
+ $methodscovered = 0;
+ $methodcount = 0;
+
+ // Strange PHP5 reflection bug, classes without parent class or implemented interfaces seem to start one line off
+ if ($reflection->getParentClass() == NULL && count($reflection->getInterfaces()) == 0)
+ {
+ unset($coverageInformation[$classStartLine + 1]);
+ }
+ else
+ {
+ unset($coverageInformation[$classStartLine]);
+ }
+
+ // Remove out-of-bounds info
+ unset($coverageInformation[0]);
+
+ reset($coverageInformation);
+
+ foreach ($methods as $method)
+ {
+ // PHP5 reflection considers methods of a parent class to be part of a subclass, we don't
+ if ($method->getDeclaringClass()->getName() != $reflection->getName())
+ {
+ continue;
+ }
+
+ // small fix for XDEBUG_CC_UNUSED
+ if (isset($coverageInformation[$method->getStartLine()]))
+ {
+ unset($coverageInformation[$method->getStartLine()]);
+ }
+
+ if (isset($coverageInformation[$method->getEndLine()]))
+ {
+ unset($coverageInformation[$method->getEndLine()]);
+ }
+
+ if ($method->isAbstract())
+ {
+ continue;
+ }
+
+ $linenr = key($coverageInformation);
+
+ while ($linenr !== null && $linenr < $method->getStartLine())
+ {
+ next($coverageInformation);
+ $linenr = key($coverageInformation);
+ }
+
+ $methodCoveredCount = 0;
+ $methodTotalCount = 0;
+
+ $methodHasCoveredLine = false;
+
+ while ($linenr !== null && $linenr <= $method->getEndLine()) {
+ $methodTotalCount++;
+ $methodHasCoveredLine = true;
+
+ // set covered when CODE is other than -1 (not executed)
+ if ($coverageInformation[$linenr] > 0 || $coverageInformation[$linenr] == -2) {
+ $methodCoveredCount++;
+ }
+
+ next($coverageInformation);
+ $linenr = key($coverageInformation);
+ }
+
+ if (($methodTotalCount == $methodCoveredCount) && $methodHasCoveredLine) {
+ $methodscovered++;
+ }
+
+ $methodcount++;
+ }
+
+ $statementcount = count(array_filter(
+ $coverageInformation,
+ create_function('$var', 'return ($var != -2);')
+ ));
+
+ $statementscovered = count(array_filter(
+ $coverageInformation,
+ create_function('$var', 'return ($var >= 0);')
+ ));
+
+ $classElement->appendChild($this->transformSourceFile($filename, $coverageInformation, $classStartLine));
+
+ $classElement->setAttribute('methodcount', $methodcount);
+ $classElement->setAttribute('methodscovered', $methodscovered);
+ $classElement->setAttribute('statementcount', $statementcount);
+ $classElement->setAttribute('statementscovered', $statementscovered);
+ $classElement->setAttribute('totalcount', $methodcount + $statementcount);
+ $classElement->setAttribute('totalcovered', $methodscovered + $statementscovered);
+ }
+ }
+ }
+
+ protected function calculateStatistics()
+ {
+ $packages = $this->doc->documentElement->getElementsByTagName('package');
+
+ $totalmethodcount = 0;
+ $totalmethodscovered = 0;
+
+ $totalstatementcount = 0;
+ $totalstatementscovered = 0;
+
+ foreach ($packages as $package) {
+ $methodcount = 0;
+ $methodscovered = 0;
+
+ $statementcount = 0;
+ $statementscovered = 0;
+
+ $subpackages = $package->getElementsByTagName('subpackage');
+
+ foreach ($subpackages as $subpackage) {
+ $subpackageMethodCount = 0;
+ $subpackageMethodsCovered = 0;
+
+ $subpackageStatementCount = 0;
+ $subpackageStatementsCovered = 0;
+
+ $subpackageClasses = $subpackage->getElementsByTagName('class');
+
+ foreach ($subpackageClasses as $subpackageClass) {
+ $subpackageMethodCount += $subpackageClass->getAttribute('methodcount');
+ $subpackageMethodsCovered += $subpackageClass->getAttribute('methodscovered');
+
+ $subpackageStatementCount += $subpackageClass->getAttribute('statementcount');
+ $subpackageStatementsCovered += $subpackageClass->getAttribute('statementscovered');
+ }
+
+ $subpackage->setAttribute('methodcount', $subpackageMethodCount);
+ $subpackage->setAttribute('methodscovered', $subpackageMethodsCovered);
+
+ $subpackage->setAttribute('statementcount', $subpackageStatementCount);
+ $subpackage->setAttribute('statementscovered', $subpackageStatementsCovered);
+
+ $subpackage->setAttribute('totalcount', $subpackageMethodCount + $subpackageStatementCount);
+ $subpackage->setAttribute('totalcovered', $subpackageMethodsCovered + $subpackageStatementsCovered);
+ }
+
+ $classes = $package->getElementsByTagName('class');
+
+ foreach ($classes as $class) {
+ $methodcount += $class->getAttribute('methodcount');
+ $methodscovered += $class->getAttribute('methodscovered');
+
+ $statementcount += $class->getAttribute('statementcount');
+ $statementscovered += $class->getAttribute('statementscovered');
+ }
+
+ $package->setAttribute('methodcount', $methodcount);
+ $package->setAttribute('methodscovered', $methodscovered);
+
+ $package->setAttribute('statementcount', $statementcount);
+ $package->setAttribute('statementscovered', $statementscovered);
+
+ $package->setAttribute('totalcount', $methodcount + $statementcount);
+ $package->setAttribute('totalcovered', $methodscovered + $statementscovered);
+
+ $totalmethodcount += $methodcount;
+ $totalmethodscovered += $methodscovered;
+
+ $totalstatementcount += $statementcount;
+ $totalstatementscovered += $statementscovered;
+ }
+
+ $this->doc->documentElement->setAttribute('methodcount', $totalmethodcount);
+ $this->doc->documentElement->setAttribute('methodscovered', $totalmethodscovered);
+
+ $this->doc->documentElement->setAttribute('statementcount', $totalstatementcount);
+ $this->doc->documentElement->setAttribute('statementscovered', $totalstatementscovered);
+
+ $this->doc->documentElement->setAttribute('totalcount', $totalmethodcount + $totalstatementcount);
+ $this->doc->documentElement->setAttribute('totalcovered', $totalmethodscovered + $totalstatementscovered);
+ }
+
+ function main()
+ {
+ $coverageDatabase = $this->project->getProperty('coverage.database');
+
+ if (!$coverageDatabase)
+ {
+ throw new BuildException("Property coverage.database is not set - please include coverage-setup in your build file");
+ }
+
+ $database = new PhingFile($coverageDatabase);
+
+ $this->log("Transforming coverage report");
+
+ $props = new Properties();
+ $props->load($database);
+
+ foreach ($props->keys() as $filename)
+ {
+ $file = unserialize($props->getProperty($filename));
+
+ $this->transformCoverageInformation($file['fullname'], $file['coverage']);
+ }
+
+ $this->calculateStatistics();
+
+ $this->doc->save($this->outfile);
+
+ foreach ($this->transformers as $transformer)
+ {
+ $transformer->setXmlDocument($this->doc);
+ $transformer->transform();
+ }
+ }
+}