414 lines
13 KiB
PHP
414 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* $Id: CoverageThresholdTask.php 905 2010-10-05 16:28:03Z mrook $
|
|
*
|
|
* 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';
|
|
|
|
/**
|
|
* Stops the build if any of the specified coverage threshold was not reached
|
|
*
|
|
* @author Benjamin Schultz <bschultz@proqrent.de>
|
|
* @version $Id: CoverageThresholdTask.php 905 2010-10-05 16:28:03Z mrook $
|
|
* @package phing.tasks.ext.coverage
|
|
* @since 2.4.1
|
|
*/
|
|
class CoverageThresholdTask extends Task
|
|
{
|
|
/**
|
|
* Holds an optional classpath
|
|
*
|
|
* @var Path
|
|
*/
|
|
private $_classpath = 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);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
$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;
|
|
}
|
|
|
|
$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 (' . $methodCoverage . '%) '
|
|
. 'for method "' . $method->getName() . '" is lower'
|
|
. ' than the specified threshold ('
|
|
. $this->_perMethod . '%), see file: "'
|
|
. $filename . '"'
|
|
);
|
|
} elseif ($methodCoverage < $this->_perMethod
|
|
&& $method->isAbstract()
|
|
) {
|
|
if ($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 (' . $classCoverage . '%) for class "'
|
|
. $reflection->getName() . '" is lower than the '
|
|
. 'specified threshold (' . $this->_perClass . '%), '
|
|
. 'see file: "' . $filename . '"'
|
|
);
|
|
} elseif ($classCoverage < $this->_perClass
|
|
&& $reflection->isAbstract()
|
|
) {
|
|
if ($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));
|
|
|
|
$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 (' . $coverage . '%) 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'
|
|
);
|
|
}
|
|
} |