. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * * Neither the name of Sebastian Bergmann nor the names of his * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * 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. * * @category PHP * @package CodeCoverage * @author Sebastian Bergmann * @copyright 2009-2011 Sebastian Bergmann * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @link http://github.com/sebastianbergmann/php-code-coverage * @since File available since Release 1.0.0 */ if (!defined('T_NAMESPACE')) { define('T_NAMESPACE', 377); } require_once 'PHP/Token/Stream/CachingFactory.php'; /** * Utility methods. * * @category PHP * @package CodeCoverage * @author Sebastian Bergmann * @copyright 2009-2011 Sebastian Bergmann * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @version Release: 1.0.4 * @link http://github.com/sebastianbergmann/php-code-coverage * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage_Util { /** * @var string */ const REGEX = '(@covers\s+(?P.*?)\s*$)m'; /** * @var array */ protected static $ignoredLines = array(); /** * @var array */ protected static $templateMethods = array( 'setUp', 'assertPreConditions', 'assertPostConditions', 'tearDown' ); /** * Builds an array representation of the directory structure. * * For instance, * * * Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * * * is transformed into * * * Array * ( * [.] => Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * ) * * * @param array $files * @return array */ public static function buildDirectoryStructure($files) { $result = array(); foreach ($files as $path => $file) { $path = explode('/', $path); $pointer = &$result; $max = count($path); for ($i = 0; $i < $max; $i++) { if ($i == ($max - 1)) { $type = '/f'; } else { $type = ''; } $pointer = &$pointer[$path[$i] . $type]; } $pointer = $file; } return $result; } /** * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code * based on its cyclomatic complexity and percentage of code coverage. * * @param integer $ccn * @param float $coverage * @return string */ public static function crap($ccn, $coverage) { if ($coverage == 0) { return (string)pow($ccn, 2) + $ccn; } if ($coverage >= 95) { return (string)$ccn; } return sprintf( '%01.2F', pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn ); } /** * @param string $directory * @return string * @throws RuntimeException */ public static function getDirectory($directory) { if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) { $directory .= DIRECTORY_SEPARATOR; } if (is_dir($directory)) { return $directory; } if (mkdir($directory, 0777, TRUE)) { return $directory; } throw new RuntimeException( sprintf( 'Directory "%s" does not exist.', $directory ) ); } /** * Returns the files and lines a test method wants to cover. * * @param string $className * @param string $methodName * @return array */ public static function getLinesToBeCovered($className, $methodName) { $codeToCoverList = array(); $result = array(); // @codeCoverageIgnoreStart if (($pos = strpos($methodName, ' ')) !== FALSE) { $methodName = substr($methodName, 0, $pos); } // @codeCoverageIgnoreEnd $class = new ReflectionClass($className); $method = new ReflectionMethod($className, $methodName); $docComment = $class->getDocComment() . $method->getDocComment(); foreach (self::$templateMethods as $templateMethod) { if ($class->hasMethod($templateMethod)) { $reflector = $class->getMethod($templateMethod); $docComment .= $reflector->getDocComment(); unset($reflector); } } if (preg_match_all(self::REGEX, $docComment, $matches)) { foreach ($matches['coveredElement'] as $coveredElement) { $codeToCoverList = array_merge( $codeToCoverList, self::resolveCoversToReflectionObjects($coveredElement) ); } foreach ($codeToCoverList as $codeToCover) { $fileName = $codeToCover->getFileName(); if (!isset($result[$fileName])) { $result[$fileName] = array(); } $result[$fileName] = array_unique( array_merge( $result[$fileName], range( $codeToCover->getStartLine(), $codeToCover->getEndLine() ) ) ); } } return $result; } /** * Returns the lines of a source file that should be ignored. * * @param string $filename * @return array */ public static function getLinesToBeIgnored($filename) { if (!isset(self::$ignoredLines[$filename])) { self::$ignoredLines[$filename] = array(); $ignore = FALSE; $stop = FALSE; $tokens = PHP_Token_Stream_CachingFactory::get($filename)->tokens(); foreach ($tokens as $token) { switch (get_class($token)) { case 'PHP_Token_CLASS': case 'PHP_Token_FUNCTION': { $docblock = $token->getDocblock(); $endLine = $token->getEndLine(); if (strpos($docblock, '@codeCoverageIgnore')) { for ($i = $token->getLine(); $i <= $endLine; $i++) { self::$ignoredLines[$filename][$i] = TRUE; } } } break; case 'PHP_Token_COMMENT': { $_token = trim($token); if ($_token == '// @codeCoverageIgnoreStart' || $_token == '//@codeCoverageIgnoreStart') { $ignore = TRUE; } else if ($_token == '// @codeCoverageIgnoreEnd' || $_token == '//@codeCoverageIgnoreEnd') { $stop = TRUE; } } break; } if ($ignore) { self::$ignoredLines[$filename][$token->getLine()] = TRUE; if ($stop) { $ignore = FALSE; $stop = FALSE; } } } } return self::$ignoredLines[$filename]; } /** * Returns the package information of a user-defined class. * * @param string $className * @param string $docComment * @return array */ public static function getPackageInformation($className, $docComment) { $result = array( 'namespace' => '', 'fullPackage' => '', 'category' => '', 'package' => '', 'subpackage' => '' ); if (strpos($className, '\\') !== FALSE) { $result['namespace'] = self::arrayToName( explode('\\', $className) ); } if (preg_match('/@category[\s]+([\.\w]+)/', $docComment, $matches)) { $result['category'] = $matches[1]; } if (preg_match('/@package[\s]+([\.\w]+)/', $docComment, $matches)) { $result['package'] = $matches[1]; $result['fullPackage'] = $matches[1]; } if (preg_match('/@subpackage[\s]+([\.\w]+)/', $docComment, $matches)) { $result['subpackage'] = $matches[1]; $result['fullPackage'] .= '.' . $matches[1]; } if (empty($result['fullPackage'])) { $result['fullPackage'] = self::arrayToName( explode('_', str_replace('\\', '_', $className)), '.' ); } return $result; } /** * Returns a filesystem safe version of the passed filename. * This function does not operate on full paths, just filenames. * * @param string $filename * @return string * @author Michael Lively Jr. */ public static function getSafeFilename($filename) { /* characters allowed: A-Z, a-z, 0-9, _ and . */ return preg_replace('#[^\w.]#', '_', $filename); } /** * @param float $a * @param float $b * @return float ($a / $b) * 100 */ public static function percent($a, $b, $asString = FALSE) { if ($b > 0) { $percent = ($a / $b) * 100; } else { $percent = 100; } if ($asString) { return sprintf('%01.2F', $percent); } else { return $percent; } } /** * Reduces the paths by cutting the longest common start path. * * For instance, * * * Array * ( * [/home/sb/Money/Money.php] => Array * ( * ... * ) * * [/home/sb/Money/MoneyBag.php] => Array * ( * ... * ) * ) * * * is reduced to * * * Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * * * @param array $files * @return string */ public static function reducePaths(&$files) { if (empty($files)) { return '.'; } $commonPath = ''; $paths = array_keys($files); if (count($files) == 1) { $commonPath = dirname($paths[0]) . '/'; $files[basename($paths[0])] = $files[$paths[0]]; unset($files[$paths[0]]); return $commonPath; } $max = count($paths); for ($i = 0; $i < $max; $i++) { $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]); if (empty($paths[$i][0])) { $paths[$i][0] = DIRECTORY_SEPARATOR; } } $done = FALSE; $max = count($paths); while (!$done) { for ($i = 0; $i < $max - 1; $i++) { if (!isset($paths[$i][0]) || !isset($paths[$i+1][0]) || $paths[$i][0] != $paths[$i+1][0]) { $done = TRUE; break; } } if (!$done) { $commonPath .= $paths[0][0]; if ($paths[0][0] != DIRECTORY_SEPARATOR) { $commonPath .= DIRECTORY_SEPARATOR; } for ($i = 0; $i < $max; $i++) { array_shift($paths[$i]); } } } $original = array_keys($files); $max = count($original); for ($i = 0; $i < $max; $i++) { $files[join('/', $paths[$i])] = $files[$original[$i]]; unset($files[$original[$i]]); } ksort($files); return $commonPath; } /** * Returns the package information of a user-defined class. * * @param array $parts * @param string $join * @return string */ protected static function arrayToName(array $parts, $join = '\\') { $result = ''; if (count($parts) > 1) { array_pop($parts); $result = join($join, $parts); } return $result; } /** * @param string $coveredElement * @return array */ protected static function resolveCoversToReflectionObjects($coveredElement) { $codeToCoverList = array(); if (strpos($coveredElement, '::') !== FALSE) { list($className, $methodName) = explode('::', $coveredElement); if ($methodName[0] == '<') { $classes = array($className); foreach ($classes as $className) { if (!class_exists($className) && !interface_exists($className)) { throw new RuntimeException( sprintf( 'Trying to @cover not existing class or ' . 'interface "%s".', $className ) ); } $class = new ReflectionClass($className); $methods = $class->getMethods(); $inverse = isset($methodName[1]) && $methodName[1] == '!'; if (strpos($methodName, 'protected')) { $visibility = 'isProtected'; } else if (strpos($methodName, 'private')) { $visibility = 'isPrivate'; } else if (strpos($methodName, 'public')) { $visibility = 'isPublic'; } foreach ($methods as $method) { if ($inverse && !$method->$visibility()) { $codeToCoverList[] = $method; } else if (!$inverse && $method->$visibility()) { $codeToCoverList[] = $method; } } } } else { $classes = array($className); foreach ($classes as $className) { if ($className == '' && function_exists($methodName)) { $codeToCoverList[] = new ReflectionFunction( $methodName ); } else { if (!((class_exists($className) || interface_exists($className)) && method_exists($className, $methodName))) { throw new RuntimeException( sprintf( 'Trying to @cover not existing method "%s::%s".', $className, $methodName ) ); } $codeToCoverList[] = new ReflectionMethod( $className, $methodName ); } } } } else { $extended = FALSE; if (strpos($coveredElement, '') !== FALSE) { $coveredElement = str_replace( '', '', $coveredElement ); $extended = TRUE; } $classes = array($coveredElement); if ($extended) { $classes = array_merge( $classes, class_implements($coveredElement), class_parents($coveredElement) ); } foreach ($classes as $className) { if (!class_exists($className) && !interface_exists($className)) { throw new RuntimeException( sprintf( 'Trying to @cover not existing class or ' . 'interface "%s".', $className ) ); } $codeToCoverList[] = new ReflectionClass($className); } } return $codeToCoverList; } }