123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- <?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'
- );
- }
- }
|