Viewing file: CoverageThresholdTask.php (14.74 KB) -rw-rw-rw- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php /** * $Id: CoverageThresholdTask.php 1206 2011-07-03 22:46:44Z bschultz $ * * 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/util/Properties.php'; require_once 'phing/types/Excludes.php';
/** * Stops the build if any of the specified coverage threshold was not reached * * @author Benjamin Schultz <bschultz@proqrent.de> * @version $Id: CoverageThresholdTask.php 1206 2011-07-03 22:46:44Z bschultz $ * @package phing.tasks.ext.coverage * @since 2.4.1 */ class CoverageThresholdTask extends Task { /** * Holds an optional classpath * * @var Path */ private $_classpath = null;
/** * Holds the exclusions * * @var Excludes */ private $_excludes = null;
/** * Holds an optional database file * * @var PhingFile */ private $_database = null;
/** * Holds the coverage threshold for the entire project * * @var integer */ private $_perProject = 25;
/** * Holds the coverage threshold for any class * * @var integer */ private $_perClass = 25;
/** * Holds the coverage threshold for any method * * @var integer */ private $_perMethod = 25;
/** * Holds the minimum found coverage value for a class * * @var integer */ private $_minClassCoverageFound = null;
/** * Holds the minimum found coverage value for a method * * @var integer */ private $_minMethodCoverageFound = null;
/** * Number of statements in the entire project * * @var integer */ private $_projectStatementCount = 0;
/** * Number of covered statements in the entire project * * @var integer */ private $_projectStatementsCovered = 0;
/** * Whether to enable detailed logging * * @var boolean */ private $_verbose = false;
/** * Sets an optional classpath * * @param Path $classpath The classpath */ public function setClasspath(Path $classpath) { if ($this->_classpath === null) { $this->_classpath = $classpath; } else { $this->_classpath->append($classpath); } }
/** * Sets the optional coverage database to use * * @param PhingFile The database file */ public function setDatabase(PhingFile $database) { $this->_database = $database; }
/** * Create classpath object * * @return Path */ public function createClasspath() { $this->classpath = new Path(); return $this->classpath; }
/** * Sets the coverage threshold for entire project * * @param integer $threshold Coverage threshold for entire project */ public function setPerProject($threshold) { $this->_perProject = $threshold; }
/** * Sets the coverage threshold for any class * * @param integer $threshold Coverage threshold for any class */ public function setPerClass($threshold) { $this->_perClass = $threshold; }
/** * Sets the coverage threshold for any method * * @param integer $threshold Coverage threshold for any method */ public function setPerMethod($threshold) { $this->_perMethod = $threshold; }
/** * Sets whether to enable detailed logging or not * * @param boolean $verbose */ public function setVerbose($verbose) { $this->_verbose = StringHelper::booleanValue($verbose); }
/** * Filter covered statements * * @param integer $var Coverage CODE/count * @return boolean */ protected function filterCovered($var) { return ($var >= 0 || $var === -2); }
/** * Create excludes object * * @return Excludes */ public function createExcludes() { $this->_excludes = new Excludes($this->project); return $this->_excludes; }
/** * Calculates the coverage threshold * * @param string $filename The filename to analyse * @param array $coverageInformation Array with coverage information */ protected function calculateCoverageThreshold($filename, $coverageInformation) { $classes = PHPUnitUtil::getDefinedClasses($filename, $this->_classpath);
if (is_array($classes)) { foreach ($classes as $className) { // Skip class if excluded from coverage threshold validation if ($this->_excludes !== null) { if (in_array($className, $this->_excludes->getExcludedClasses())) { continue; } }
$reflection = new ReflectionClass($className); $classStartLine = $reflection->getStartLine();
// 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]); }
reset($coverageInformation);
$methods = $reflection->getMethods();
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; }
// Skip method if excluded from coverage threshold validation if ($this->_excludes !== null) { $excludedMethods = $this->_excludes->getExcludedMethods();
if (isset($excludedMethods[$className])) { if (in_array($method->getName(), $excludedMethods[$className]) || in_array($method->getName() . '()', $excludedMethods[$className]) ) { continue; } } }
$methodStartLine = $method->getStartLine(); $methodEndLine = $method->getEndLine();
// small fix for XDEBUG_CC_UNUSED if (isset($coverageInformation[$methodStartLine])) { unset($coverageInformation[$methodStartLine]); }
if (isset($coverageInformation[$methodEndLine])) { unset($coverageInformation[$methodEndLine]); }
if ($method->isAbstract()) { continue; }
$lineNr = key($coverageInformation);
while ($lineNr !== null && $lineNr < $methodStartLine) { next($coverageInformation); $lineNr = key($coverageInformation); }
$methodStatementsCovered = 0; $methodStatementCount = 0;
while ($lineNr !== null && $lineNr <= $methodEndLine) { $methodStatementCount++;
$lineCoverageInfo = $coverageInformation[$lineNr]; // set covered when CODE is other than -1 (not executed) if ($lineCoverageInfo > 0 || $lineCoverageInfo === -2) { $methodStatementsCovered++; }
next($coverageInformation); $lineNr = key($coverageInformation); }
if ($methodStatementCount > 0) { $methodCoverage = ( $methodStatementsCovered / $methodStatementCount) * 100; } else { $methodCoverage = 0; }
if ($methodCoverage < $this->_perMethod && !$method->isAbstract() ) { throw new BuildException( 'The coverage (' . round($methodCoverage, 2) . '%) ' . 'for method "' . $method->getName() . '" is lower' . ' than the specified threshold (' . $this->_perMethod . '%), see file: "' . $filename . '"' ); } elseif ($methodCoverage < $this->_perMethod && $method->isAbstract() && $this->_verbose === true ) { $this->log( 'Skipped coverage threshold for abstract method "' . $method->getName() . '"' ); }
// store the minimum coverage value for logging (see #466) if ($this->_minMethodCoverageFound !== null) { if ($this->_minMethodCoverageFound > $methodCoverage) { $this->_minMethodCoverageFound = $methodCoverage; } } else { $this->_minMethodCoverageFound = $methodCoverage; } }
$classStatementCount = count($coverageInformation); $classStatementsCovered = count( array_filter( $coverageInformation, array($this, 'filterCovered') ) );
if ($classStatementCount > 0) { $classCoverage = ( $classStatementsCovered / $classStatementCount) * 100; } else { $classCoverage = 0; }
if ($classCoverage < $this->_perClass && !$reflection->isAbstract() ) { throw new BuildException( 'The coverage (' . round($classCoverage, 2) . '%) for class "' . $reflection->getName() . '" is lower than the ' . 'specified threshold (' . $this->_perClass . '%), ' . 'see file: "' . $filename . '"' ); } elseif ($classCoverage < $this->_perClass && $reflection->isAbstract() && $this->_verbose === true ) { $this->log( 'Skipped coverage threshold for abstract class "' . $reflection->getName() . '"' ); }
// store the minimum coverage value for logging (see #466) if ($this->_minClassCoverageFound !== null) { if ($this->_minClassCoverageFound > $classCoverage) { $this->_minClassCoverageFound = $classCoverage; } } else { $this->_minClassCoverageFound = $classCoverage; }
$this->_projectStatementCount += $classStatementCount; $this->_projectStatementsCovered += $classStatementsCovered; } } }
public function main() { if ($this->_database === null) { $coverageDatabase = $this->project ->getProperty('coverage.database');
if (! $coverageDatabase) { throw new BuildException( 'Either include coverage-setup in your build file or set ' . 'the "database" attribute' ); }
$database = new PhingFile($coverageDatabase); } else { $database = $this->_database; }
$this->log( 'Calculating coverage threshold: min. ' . $this->_perProject . '% per project, ' . $this->_perClass . '% per class and ' . $this->_perMethod . '% per method is required' );
$props = new Properties(); $props->load($database);
foreach ($props->keys() as $filename) { $file = unserialize($props->getProperty($filename));
// Skip file if excluded from coverage threshold validation if ($this->_excludes !== null) { if (in_array($file['fullname'], $this->_excludes->getExcludedFiles())) { continue; } }
$this->calculateCoverageThreshold( $file['fullname'], $file['coverage'] ); }
if ($this->_projectStatementCount > 0) { $coverage = ( $this->_projectStatementsCovered / $this->_projectStatementCount) * 100; } else { $coverage = 0; }
if ($coverage < $this->_perProject) { throw new BuildException( 'The coverage (' . round($coverage, 2) . '%) for the entire project ' . 'is lower than the specified threshold (' . $this->_perProject . '%)' ); }
$this->log( 'Passed coverage threshold. Minimum found coverage values are: ' . round($coverage, 2) . '% per project, ' . round($this->_minClassCoverageFound, 2) . '% per class and ' . round($this->_minMethodCoverageFound, 2) . '% per method' ); } }
|