diff options
Diffstat (limited to 'buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php')
-rwxr-xr-x | buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageReportTask.php | 564 |
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(); + } + } +} |