5 * Copyright (c) 2002-2009, 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.
39 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
40 * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
41 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
43 * @link http://www.phpunit.de/
44 * @since File available since Release 3.2.0
47 require_once 'PHPUnit/Util/Filter.php';
48 require_once 'PHPUnit/Util/Filesystem.php';
49 require_once 'PHPUnit/Util/Template.php';
50 require_once 'PHPUnit/Util/Report/Node.php';
52 PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
59 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
60 * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
61 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
62 * @version Release: 3.3.17
63 * @link http://www.phpunit.de/
64 * @since Class available since Release 3.2.0
66 class PHPUnit_Util_Report_Node_File extends PHPUnit_Util_Report_Node
76 protected $codeLinesFillup = array();
81 protected $executedLines;
86 protected $yui = TRUE;
91 protected $highlight = FALSE;
96 protected $numExecutableLines = 0;
101 protected $numExecutedLines = 0;
106 protected $classes = array();
111 protected $numClasses = 0;
116 protected $numCalledClasses = 0;
121 protected $numMethods = 0;
126 protected $numCalledMethods = 0;
131 protected $yuiPanelJS = '';
136 protected $startLines = array();
141 protected $endLines = array();
146 * @param string $name
147 * @param PHPUnit_Util_Report_Node $parent
148 * @param array $executedLines
149 * @param boolean $yui
150 * @param boolean $highlight
151 * @throws RuntimeException
153 public function __construct($name, PHPUnit_Util_Report_Node $parent = NULL, array $executedLines, $yui = TRUE, $highlight = FALSE)
155 parent::__construct($name, $parent);
157 $path = $this->getPath();
159 if (!file_exists($path)) {
160 throw new RuntimeException;
163 $this->executedLines = $executedLines;
164 $this->highlight = $highlight;
166 $this->codeLines = $this->loadFile($path);
168 $this->calculateStatistics();
172 * Returns the classes of this node.
176 public function getClasses()
178 return $this->classes;
182 * Returns the number of executable lines.
186 public function getNumExecutableLines()
188 return $this->numExecutableLines;
192 * Returns the number of executed lines.
196 public function getNumExecutedLines()
198 return $this->numExecutedLines;
202 * Returns the number of classes.
206 public function getNumClasses()
208 return $this->numClasses;
212 * Returns the number of classes of which at least one method
213 * has been called at least once.
217 public function getNumCalledClasses()
219 return $this->numCalledClasses;
223 * Returns the number of methods.
227 public function getNumMethods()
229 return $this->numMethods;
233 * Returns the number of methods that has been called at least once.
237 public function getNumCalledMethods()
239 return $this->numCalledMethods;
245 * @param string $target
246 * @param string $title
247 * @param string $charset
248 * @param integer $lowUpperBound
249 * @param integer $highLowerBound
251 public function render($target, $title, $charset = 'ISO-8859-1', $lowUpperBound = 35, $highLowerBound = 70)
254 $template = new PHPUnit_Util_Template(
255 PHPUnit_Util_Report::$templatePath . 'file.html'
258 $yuiTemplate = new PHPUnit_Util_Template(
259 PHPUnit_Util_Report::$templatePath . 'yui_item.js'
262 $template = new PHPUnit_Util_Template(
263 PHPUnit_Util_Report::$templatePath . 'file_no_yui.html'
271 foreach ($this->codeLines as $line) {
272 if (strpos($line, '@codeCoverageIgnore') !== FALSE) {
273 if (strpos($line, '@codeCoverageIgnoreStart') !== FALSE) {
277 else if (strpos($line, '@codeCoverageIgnoreEnd') !== FALSE) {
284 if (!$ignore && isset($this->executedLines[$i])) {
287 // Array: Line is executable and was executed.
288 // count(Array) = Number of tests that hit this line.
289 if (is_array($this->executedLines[$i])) {
291 $numTests = count($this->executedLines[$i]);
292 $count = sprintf('%8d', $numTests);
298 foreach ($this->executedLines[$i] as $test) {
299 if (!isset($test->__liHtml)) {
300 $test->__liHtml = '';
302 if ($test instanceof PHPUnit_Framework_SelfDescribing) {
303 $testName = $test->toString();
305 if ($test instanceof PHPUnit_Framework_TestCase) {
306 switch ($test->getStatus()) {
307 case PHPUnit_Runner_BaseTestRunner::STATUS_PASSED: {
308 $testCSS = ' class=\"testPassed\"';
312 case PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE: {
313 $testCSS = ' class=\"testFailure\"';
317 case PHPUnit_Runner_BaseTestRunner::STATUS_ERROR: {
318 $testCSS = ' class=\"testError\"';
322 case PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE:
323 case PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED: {
324 $testCSS = ' class=\"testIncomplete\"';
335 $test->__liHtml .= sprintf(
339 addslashes(htmlspecialchars($testName))
343 $buffer .= $test->__liHtml;
347 $header = $numTests . ' tests cover';
349 $header = '1 test covers';
352 $header .= ' line ' . $i;
354 $yuiTemplate->setVar(
363 $this->yuiPanelJS .= $yuiTemplate->render();
367 // -1: Line is executable and was not executed.
368 else if ($this->executedLines[$i] == -1) {
369 $color = 'lineNoCov';
370 $count = sprintf('%8d', 0);
373 // -2: Line is dead code.
375 $color = 'lineDeadCode';
380 '<span class="%s"> %s : ',
387 $fillup = array_shift($this->codeLinesFillup);
390 $line .= str_repeat(' ', $fillup);
394 '<span class="lineNum" id="container%d"><a name="%d"></a><a href="#%d" id="line%d">%8d</a> </span>%s%s%s' . "\n",
401 !empty($css) ? $css : ' : ',
402 !$this->highlight ? htmlspecialchars($line) : $line,
403 !empty($css) ? '</span>' : ''
411 foreach ($this->classes as $className => $classData) {
412 $numCalledClasses = $classData['executedLines'] > 0 ? 1 : 0;
413 $calledClassesPercent = $numCalledClasses == 1 ? 100 : 0;
415 $numCalledMethods = 0;
416 $numMethods = count($classData['methods']);
418 foreach ($classData['methods'] as $method) {
419 if ($method['executedLines'] > 0) {
424 $items .= $this->doRenderItem(
427 '<b><a href="#%d">%s</a></b>',
429 $classData['startLine'],
433 'numCalledClasses' => $numCalledClasses,
434 'calledClassesPercent' => sprintf('%01.2f', $calledClassesPercent),
435 'numMethods' => $numMethods,
436 'numCalledMethods' => $numCalledMethods,
437 'calledMethodsPercent' => $this->calculatePercent(
438 $numCalledMethods, $numMethods
440 'numExecutableLines' => $classData['executableLines'],
441 'numExecutedLines' => $classData['executedLines'],
442 'executedLinesPercent' => $this->calculatePercent(
443 $classData['executedLines'], $classData['executableLines']
450 foreach ($classData['methods'] as $methodName => $methodData) {
451 $numCalledMethods = $methodData['executedLines'] > 0 ? 1 : 0;
452 $calledMethodsPercent = $numCalledMethods == 1 ? 100 : 0;
454 if ($className == '*') {
455 $signature = PHPUnit_Util_Class::getFunctionSignature(
456 new ReflectionFunction($methodName)
459 $signature = PHPUnit_Util_Class::getMethodSignature(
460 new ReflectionMethod($className, $methodName)
464 $items .= $this->doRenderItem(
467 ' <a href="#%d">%s</a>',
469 $methodData['startLine'],
473 'numCalledClasses' => '',
474 'calledClassesPercent' => '',
476 'numCalledMethods' => $numCalledMethods,
477 'calledMethodsPercent' => sprintf('%01.2f', $calledMethodsPercent),
478 'numExecutableLines' => $methodData['executableLines'],
479 'numExecutedLines' => $methodData['executedLines'],
480 'executedLinesPercent' => $this->calculatePercent(
481 $methodData['executedLines'], $methodData['executableLines']
491 $this->setTemplateVars($template, $title, $charset);
496 'total_item' => $this->renderTotalItem($lowUpperBound, $highLowerBound, FALSE),
498 'yuiPanelJS' => $this->yuiPanelJS
502 $cleanId = PHPUnit_Util_Filesystem::getSafeFilename($this->getId());
503 $template->renderTo($target . $cleanId . '.html');
505 $this->yuiPanelJS = '';
506 $this->executedLines = array();
510 * Calculates coverage statistics for the file.
513 protected function calculateStatistics()
515 $this->processClasses();
516 $this->processFunctions();
521 foreach ($this->codeLines as $line) {
522 if (isset($this->startLines[$lineNumber])) {
523 // Start line of a class.
524 if (isset($this->startLines[$lineNumber]['methods'])) {
525 $currentClass = &$this->startLines[$lineNumber];
528 // Start line of a method.
530 $currentMethod = &$this->startLines[$lineNumber];
534 if (strpos($line, '@codeCoverageIgnore') !== FALSE) {
535 if (strpos($line, '@codeCoverageIgnoreStart') !== FALSE) {
536 $ignoreStart = $lineNumber;
539 else if (strpos($line, '@codeCoverageIgnoreEnd') !== FALSE) {
544 if (isset($this->executedLines[$lineNumber])) {
545 // Array: Line is executable and was executed.
546 if (is_array($this->executedLines[$lineNumber])) {
547 if (isset($currentClass)) {
548 $currentClass['executableLines']++;
549 $currentClass['executedLines']++;
552 if (isset($currentMethod)) {
553 $currentMethod['executableLines']++;
554 $currentMethod['executedLines']++;
557 $this->numExecutableLines++;
558 $this->numExecutedLines++;
561 // -1: Line is executable and was not executed.
562 else if ($this->executedLines[$lineNumber] == -1) {
563 if (isset($currentClass)) {
564 $currentClass['executableLines']++;
567 if (isset($currentMethod)) {
568 $currentMethod['executableLines']++;
571 $this->numExecutableLines++;
573 if ($ignoreStart != -1 && $lineNumber > $ignoreStart) {
574 if (isset($currentClass)) {
575 $currentClass['executedLines']++;
578 if (isset($currentMethod)) {
579 $currentMethod['executedLines']++;
582 $this->numExecutedLines++;
587 if (isset($this->endLines[$lineNumber])) {
588 // End line of a class.
589 if (isset($this->endLines[$lineNumber]['methods'])) {
590 unset($currentClass);
593 // End line of a method.
595 unset($currentMethod);
602 foreach ($this->classes as $class) {
603 foreach ($class['methods'] as $method) {
604 if ($method['executedLines'] > 0) {
605 $this->numCalledMethods++;
609 if ($class['executedLines'] > 0) {
610 $this->numCalledClasses++;
616 * @author Aidan Lister <aidan@php.net>
617 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
618 * @param string $file
621 protected function loadFile($file)
623 $lines = explode("\n", str_replace("\t", ' ', file_get_contents($file)));
626 if (count($lines) == 0) {
630 $lines = array_map('rtrim', $lines);
631 $linesLength = array_map('strlen', $lines);
632 $width = max($linesLength);
634 foreach ($linesLength as $line => $length) {
635 $this->codeLinesFillup[$line] = $width - $length;
638 if (!$this->highlight) {
639 unset($lines[count($lines)-1]);
643 $tokens = token_get_all(file_get_contents($file));
648 foreach ($tokens as $j => $token) {
649 if (is_string($token)) {
650 if ($token === '"' && $tokens[$j - 1] !== '\\') {
651 $result[$i] .= sprintf(
652 '<span class="string">%s</span>',
654 htmlspecialchars($token)
657 $stringFlag = !$stringFlag;
659 $result[$i] .= sprintf(
660 '<span class="keyword">%s</span>',
662 htmlspecialchars($token)
669 list ($token, $value) = $token;
671 $value = str_replace(
673 array(' ', ' '),
674 htmlspecialchars($value)
677 if ($value === "\n") {
680 $lines = explode("\n", $value);
682 foreach ($lines as $jj => $line) {
690 case T_INLINE_HTML: {
696 case T_DOC_COMMENT: {
744 case T_IS_NOT_IDENTICAL:
745 case T_IS_SMALLER_OR_EQUAL:
748 case T_OBJECT_OPERATOR:
749 case T_PAAMAYIM_NEKUDOTAYIM:
760 case T_START_HEREDOC:
778 $result[$i] .= sprintf(
779 '<span class="%s">%s</span>',
786 if (isset($lines[$jj + 1])) {
793 unset($result[count($result)-1]);
798 protected function processClasses()
800 $classes = PHPUnit_Util_Class::getClassesInFile($this->getPath());
802 foreach ($classes as $class) {
803 if (!$class->isInterface()) {
804 $className = $class->getName();
805 $classStartLine = $class->getStartLine();
806 $classEndLine = $class->getEndLine();
808 $this->classes[$className] = array(
809 'methods' => array(),
810 'startLine' => $classStartLine,
811 'executableLines' => 0,
815 $this->startLines[$classStartLine] = &$this->classes[$className];
816 $this->endLines[$classEndLine] = &$this->classes[$className];
818 foreach ($class->getMethods() as $method) {
819 if (!$method->isAbstract() &&
820 $method->getDeclaringClass()->getName() == $className) {
821 $methodName = $method->getName();
822 $methodStartLine = $method->getStartLine();
823 $methodEndLine = $method->getEndLine();
825 $this->classes[$className]['methods'][$methodName] = array(
826 'startLine' => $methodStartLine,
827 'executableLines' => 0,
831 $this->startLines[$methodStartLine] = &$this->classes[$className]['methods'][$methodName];
832 $this->endLines[$methodEndLine] = &$this->classes[$className]['methods'][$methodName];
843 protected function processFunctions()
845 $functions = PHPUnit_Util_Class::getFunctionsInFile($this->getPath());
847 if (count($functions) > 0 && !isset($this->classes['*'])) {
848 $this->classes['*'] = array(
849 'methods' => array(),
851 'executableLines' => 0,
856 foreach ($functions as $function) {
857 $functionName = $function->getName();
858 $functionStartLine = $function->getStartLine();
859 $functionEndLine = $function->getEndLine();
861 $this->classes['*']['methods'][$functionName] = array(
862 'startLine' => $functionStartLine,
863 'executableLines' => 0,
867 $this->startLines[$functionStartLine] = &$this->classes['*']['methods'][$functionName];
868 $this->endLines[$functionEndLine] = &$this->classes['*']['methods'][$functionName];