From cfd546c6b3240bd9d1ef34a9f6201b360fac3261 Mon Sep 17 00:00:00 2001 From: ctrlaltca <> Date: Sun, 18 Nov 2012 17:18:54 +0000 Subject: update ping. Part 2: added ping 2.4.12, fixed caller script --- .../tasks/ext/coverage/CoverageThresholdTask.php | 458 +++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageThresholdTask.php (limited to 'buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageThresholdTask.php') diff --git a/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageThresholdTask.php b/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageThresholdTask.php new file mode 100644 index 00000000..d9afbb00 --- /dev/null +++ b/buildscripts/phing/classes/phing/tasks/ext/coverage/CoverageThresholdTask.php @@ -0,0 +1,458 @@ +. + */ + +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 + * @version $Id: ed00d6f1d05bb5dc7c9967c9ec67fa6f958682ec $ + * @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' + ); + } +} \ No newline at end of file -- cgit v1.2.3