5 * Copyright (c) 2009-2011, Sebastian Bergmann <sb@sebastian-bergmann.de>.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
12 * * Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * * Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
20 * * Neither the name of Sebastian Bergmann nor the names of his
21 * contributors may be used to endorse or promote products derived
22 * from this software without specific prior written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35 * POSSIBILITY OF SUCH DAMAGE.
38 * @package CodeCoverage
39 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
40 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
41 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
42 * @link http://github.com/sebastianbergmann/php-code-coverage
43 * @since File available since Release 1.0.0
46 if (!defined('T_NAMESPACE')) {
47 define('T_NAMESPACE', 377);
50 require_once 'PHP/Token/Stream/CachingFactory.php';
53 * Represents a file in the code coverage information tree.
56 * @package CodeCoverage
57 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
58 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
59 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
60 * @version Release: 1.0.4
61 * @link http://github.com/sebastianbergmann/php-code-coverage
62 * @since Class available since Release 1.0.0
64 class PHP_CodeCoverage_Report_HTML_Node_File extends PHP_CodeCoverage_Report_HTML_Node
74 protected $codeLinesFillup = array();
79 protected $executedLines;
84 protected $yui = TRUE;
89 protected $highlight = FALSE;
94 protected $numExecutableLines = 0;
99 protected $numExecutedLines = 0;
104 protected $classes = array();
109 protected $numTestedClasses = 0;
114 protected $numClasses = NULL;
119 protected $numMethods = NULL;
124 protected $numTestedMethods = NULL;
129 protected $yuiPanelJS = '';
134 protected $startLines = array();
139 protected $endLines = array();
144 * @param string $name
145 * @param PHP_CodeCoverage_Report_HTML_Node $parent
146 * @param array $executedLines
147 * @param boolean $yui
148 * @param boolean $highlight
149 * @throws RuntimeException
151 public function __construct($name, PHP_CodeCoverage_Report_HTML_Node $parent, array $executedLines, $yui = TRUE, $highlight = FALSE)
153 parent::__construct($name, $parent);
155 $path = $this->getPath();
157 if (!file_exists($path)) {
158 throw new RuntimeException(
159 sprintf('Path "%s" does not exist.', $path)
163 $this->executedLines = $executedLines;
164 $this->highlight = $highlight;
166 $this->codeLines = $this->loadFile($path);
167 $this->ignoredLines = PHP_CodeCoverage_Util::getLinesToBeIgnored(
171 $this->calculateStatistics();
175 * Returns the classes of this node.
179 public function getClasses()
181 return $this->classes;
185 * Returns the number of executable lines.
189 public function getNumExecutableLines()
191 return $this->numExecutableLines;
195 * Returns the number of executed lines.
199 public function getNumExecutedLines()
201 return $this->numExecutedLines;
205 * Returns the number of classes.
209 public function getNumClasses()
211 if ($this->numClasses === NULL) {
212 $this->numClasses = count($this->classes);
214 if (isset($this->classes['*'])) {
219 return $this->numClasses;
223 * Returns the number of tested classes.
227 public function getNumTestedClasses()
229 return $this->numTestedClasses;
233 * Returns the number of methods.
237 public function getNumMethods()
239 if ($this->numMethods === NULL) {
240 $this->numMethods = 0;
242 foreach ($this->classes as $class) {
243 foreach ($class['methods'] as $method) {
244 if ($method['executableLines'] > 0) {
251 return $this->numMethods;
255 * Returns the number of tested methods.
259 public function getNumTestedMethods()
261 if ($this->numTestedMethods === NULL) {
262 $this->numTestedMethods = 0;
264 foreach ($this->classes as $class) {
265 foreach ($class['methods'] as $method) {
266 if ($method['executableLines'] > 0 &&
267 $method['coverage'] == 100) {
268 $this->numTestedMethods++;
274 return $this->numTestedMethods;
280 * @param string $target
281 * @param string $title
282 * @param string $charset
283 * @param integer $lowUpperBound
284 * @param integer $highLowerBound
285 * @param string $generator
287 public function render($target, $title, $charset = 'UTF-8', $lowUpperBound = 35, $highLowerBound = 70, $generator = '')
290 $template = new Text_Template(
291 PHP_CodeCoverage_Report_HTML::$templatePath . 'file.html'
294 $yuiTemplate = new Text_Template(
295 PHP_CodeCoverage_Report_HTML::$templatePath . 'yui_item.js'
298 $template = new Text_Template(
299 PHP_CodeCoverage_Report_HTML::$templatePath . 'file_no_yui.html'
306 foreach ($this->codeLines as $line) {
309 if (!isset($this->ignoredLines[$i]) &&
310 isset($this->executedLines[$i])) {
313 // Array: Line is executable and was executed.
314 // count(Array) = Number of tests that hit this line.
315 if (is_array($this->executedLines[$i])) {
317 $numTests = count($this->executedLines[$i]);
318 $count = sprintf('%8d', $numTests);
324 foreach ($this->executedLines[$i] as $test) {
325 switch ($test['status']) {
327 $testCSS = ' class=\"testPassed\"';
333 $testCSS = ' class=\"testIncomplete\"';
338 $testCSS = ' class=\"testFailure\"';
343 $testCSS = ' class=\"testError\"';
356 addslashes(htmlspecialchars($test['id']))
361 $header = $numTests . ' tests cover';
363 $header = '1 test covers';
366 $header .= ' line ' . $i;
368 $yuiTemplate->setVar(
377 $this->yuiPanelJS .= $yuiTemplate->render();
381 // -1: Line is executable and was not executed.
382 else if ($this->executedLines[$i] == -1) {
383 $color = 'lineNoCov';
384 $count = sprintf('%8d', 0);
387 // -2: Line is dead code.
389 $color = 'lineDeadCode';
394 '<span class="%s"> %s : ',
401 $fillup = array_shift($this->codeLinesFillup);
404 $line .= str_repeat(' ', $fillup);
408 '<span class="lineNum" id="container%d"><a name="%d"></a>'.
409 '<a href="#%d" id="line%d">%8d</a> </span>%s%s%s' . "\n",
416 !empty($css) ? $css : ' : ',
417 !$this->highlight ? htmlspecialchars($line) : $line,
418 !empty($css) ? '</span>' : ''
426 foreach ($this->classes as $className => $classData) {
427 if ($classData['executedLines'] == $classData['executableLines']) {
428 $numTestedClasses = 1;
429 $testedClassesPercent = 100;
431 $numTestedClasses = 0;
432 $testedClassesPercent = 0;
436 $numTestedMethods = 0;
438 foreach ($classData['methods'] as $method) {
439 if ($method['executableLines'] > 0) {
442 if ($method['executedLines'] == $method['executableLines']) {
448 $items .= $this->doRenderItem(
451 '<b><a href="#%d">%s</a></b>',
453 $classData['startLine'],
457 'numTestedClasses' => $numTestedClasses,
458 'testedClassesPercent' => sprintf(
459 '%01.2f', $testedClassesPercent
461 'numMethods' => $numMethods,
462 'numTestedMethods' => $numTestedMethods,
463 'testedMethodsPercent' => PHP_CodeCoverage_Util::percent(
464 $numTestedMethods, $numMethods, TRUE
466 'numExecutableLines' => $classData['executableLines'],
467 'numExecutedLines' => $classData['executedLines'],
468 'executedLinesPercent' => PHP_CodeCoverage_Util::percent(
469 $classData['executedLines'],
470 $classData['executableLines'],
478 foreach ($classData['methods'] as $methodData) {
479 if ($methodData['executableLines'] > 0) {
480 if ($methodData['executedLines'] == $methodData['executableLines']) {
481 $numTestedMethods = 1;
482 $testedMethodsPercent = 100;
484 $numTestedMethods = 0;
485 $testedMethodsPercent = 0;
488 $items .= $this->doRenderItem(
491 ' <a href="#%d">%s</a>',
493 $methodData['startLine'],
494 htmlspecialchars($methodData['signature'])
497 'numTestedClasses' => '',
498 'testedClassesPercent' => '',
500 'numTestedMethods' => $numTestedMethods,
501 'testedMethodsPercent' => sprintf(
502 '%01.2f', $testedMethodsPercent
504 'numExecutableLines' => $methodData['executableLines'],
505 'numExecutedLines' => $methodData['executedLines'],
506 'executedLinesPercent' => PHP_CodeCoverage_Util::percent(
507 $methodData['executedLines'],
508 $methodData['executableLines'],
511 'crap' => PHP_CodeCoverage_Util::crap(
513 PHP_CodeCoverage_Util::percent(
514 $methodData['executedLines'],
515 $methodData['executableLines']
527 $this->setTemplateVars($template, $title, $charset, $generator);
532 'total_item' => $this->renderTotalItem(
533 $lowUpperBound, $highLowerBound, FALSE
536 'yuiPanelJS' => $this->yuiPanelJS
540 $cleanId = PHP_CodeCoverage_Util::getSafeFilename($this->getId());
541 $template->renderTo($target . $cleanId . '.html');
543 $this->yuiPanelJS = '';
544 $this->executedLines = array();
548 * Calculates coverage statistics for the file.
551 protected function calculateStatistics()
553 $this->processClasses();
554 $this->processFunctions();
556 $max = count($this->codeLines);
558 for ($lineNumber = 1; $lineNumber <= $max; $lineNumber++) {
559 if (isset($this->startLines[$lineNumber])) {
560 // Start line of a class.
561 if (isset($this->startLines[$lineNumber]['methods'])) {
562 $currentClass = &$this->startLines[$lineNumber];
565 // Start line of a method.
567 $currentMethod = &$this->startLines[$lineNumber];
571 if (isset($this->executedLines[$lineNumber])) {
572 // Array: Line is executable and was executed.
573 if (is_array($this->executedLines[$lineNumber])) {
574 if (isset($currentClass)) {
575 $currentClass['executableLines']++;
576 $currentClass['executedLines']++;
579 if (isset($currentMethod)) {
580 $currentMethod['executableLines']++;
581 $currentMethod['executedLines']++;
584 $this->numExecutableLines++;
585 $this->numExecutedLines++;
588 // -1: Line is executable and was not executed.
589 else if ($this->executedLines[$lineNumber] == -1) {
590 if (isset($currentClass)) {
591 $currentClass['executableLines']++;
594 if (isset($currentMethod)) {
595 $currentMethod['executableLines']++;
598 $this->numExecutableLines++;
600 if (isset($this->ignoredLines[$lineNumber])) {
601 if (isset($currentClass)) {
602 $currentClass['executedLines']++;
605 if (isset($currentMethod)) {
606 $currentMethod['executedLines']++;
609 $this->numExecutedLines++;
614 if (isset($this->endLines[$lineNumber])) {
615 // End line of a class.
616 if (isset($this->endLines[$lineNumber]['methods'])) {
617 unset($currentClass);
620 // End line of a method.
622 unset($currentMethod);
627 foreach ($this->classes as $className => &$class) {
628 foreach ($class['methods'] as &$method) {
629 if ($method['executableLines'] > 0) {
630 $method['coverage'] = ($method['executedLines'] /
631 $method['executableLines']) * 100;
633 $method['coverage'] = 100;
636 $method['crap'] = PHP_CodeCoverage_Util::crap(
637 $method['ccn'], $method['coverage']
640 $class['ccn'] += $method['ccn'];
643 if ($className != '*') {
644 if ($class['executableLines'] > 0) {
645 $class['coverage'] = ($class['executedLines'] /
646 $class['executableLines']) * 100;
648 $class['coverage'] = 100;
651 if ($class['coverage'] == 100) {
652 $this->numTestedClasses++;
655 $class['crap'] = PHP_CodeCoverage_Util::crap(
656 $class['ccn'], $class['coverage']
663 * @param string $file
665 * @author Aidan Lister <aidan@php.net>
667 protected function loadFile($file)
669 $buffer = file_get_contents($file);
670 $lines = explode("\n", str_replace("\t", ' ', $buffer));
673 if (count($lines) == 0) {
677 $lines = array_map('rtrim', $lines);
678 $linesLength = array_map('strlen', $lines);
679 $width = max($linesLength);
681 foreach ($linesLength as $line => $length) {
682 $this->codeLinesFillup[$line] = $width - $length;
685 if (!$this->highlight) {
686 unset($lines[count($lines)-1]);
690 $tokens = token_get_all($buffer);
695 foreach ($tokens as $j => $token) {
696 if (is_string($token)) {
697 if ($token === '"' && $tokens[$j - 1] !== '\\') {
698 $result[$i] .= sprintf(
699 '<span class="string">%s</span>',
701 htmlspecialchars($token)
704 $stringFlag = !$stringFlag;
706 $result[$i] .= sprintf(
707 '<span class="keyword">%s</span>',
709 htmlspecialchars($token)
716 list ($token, $value) = $token;
718 $value = str_replace(
720 array(' ', ' '),
721 htmlspecialchars($value)
724 if ($value === "\n") {
727 $lines = explode("\n", $value);
729 foreach ($lines as $jj => $line) {
737 case T_INLINE_HTML: {
743 case T_DOC_COMMENT: {
791 case T_IS_NOT_IDENTICAL:
792 case T_IS_SMALLER_OR_EQUAL:
796 case T_OBJECT_OPERATOR:
797 case T_PAAMAYIM_NEKUDOTAYIM:
808 case T_START_HEREDOC:
827 $result[$i] .= sprintf(
828 '<span class="%s">%s</span>',
835 if (isset($lines[$jj + 1])) {
842 unset($result[count($result)-1]);
847 protected function processClasses()
849 $file = $this->getId() . '.html#';
850 $tokens = PHP_Token_Stream_CachingFactory::get($this->getPath());
851 $classes = $tokens->getClasses();
854 foreach ($classes as $className => $class) {
855 $this->classes[$className] = array(
856 'methods' => array(),
857 'startLine' => $class['startLine'],
858 'executableLines' => 0,
859 'executedLines' => 0,
863 'file' => $file . $class['startLine']
866 $this->startLines[$class['startLine']] = &$this->classes[$className];
867 $this->endLines[$class['endLine']] = &$this->classes[$className];
869 foreach ($class['methods'] as $methodName => $method) {
870 $this->classes[$className]['methods'][$methodName] = array(
871 'signature' => $method['signature'],
872 'startLine' => $method['startLine'],
873 'executableLines' => 0,
874 'executedLines' => 0,
875 'ccn' => $method['ccn'],
878 'file' => $file . $method['startLine']
881 $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName];
882 $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName];
887 protected function processFunctions()
889 $tokens = PHP_Token_Stream_CachingFactory::get($this->getPath());
890 $functions = $tokens->getFunctions();
893 if (count($functions) > 0 && !isset($this->classes['*'])) {
894 $this->classes['*'] = array(
895 'methods' => array(),
897 'executableLines' => 0,
898 'executedLines' => 0,
903 foreach ($functions as $functionName => $function) {
904 $this->classes['*']['methods'][$functionName] = array(
905 'signature' => $function['signature'],
906 'startLine' => $function['startLine'],
907 'executableLines' => 0,
908 'executedLines' => 0,
909 'ccn' => $function['ccn']
912 $this->startLines[$function['startLine']] = &$this->classes['*']['methods'][$functionName];
913 $this->endLines[$function['endLine']] = &$this->classes['*']['methods'][$functionName];