<?php
/**
 * PHPUnit
 *
 * Copyright (c) 2002-2008, Sebastian Bergmann <sb@sebastian-bergmann.de>.
 * 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   Testing
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @copyright  2002-2008 Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version    SVN: $Id: File.php 1985 2007-12-26 18:11:55Z sb $
 * @link       http://www.phpunit.de/
 * @since      File available since Release 3.2.0
 */

require_once 'PHPUnit/Util/Filter.php';
require_once 'PHPUnit/Util/Filesystem.php';
require_once 'PHPUnit/Util/Template.php';
require_once 'PHPUnit/Util/Report/Node.php';

PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');

/**
 *
 *
 * @category   Testing
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @copyright  2002-2008 Sebastian Bergmann <sb@sebastian-bergmann.de>
 * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @version    Release: 3.2.9
 * @link       http://www.phpunit.de/
 * @since      Class available since Release 3.2.0
 */
class PHPUnit_Util_Report_Node_File extends PHPUnit_Util_Report_Node {
  /**
   * @var    array
   * @access protected
   */
  protected $codeLines;
  
  /**
   * @var    array
   * @access protected
   */
  protected $codeLinesFillup = array();
  
  /**
   * @var    array
   * @access protected
   */
  protected $executedLines;
  
  /**
   * @var    boolean
   * @access protected
   */
  protected $yui = TRUE;
  
  /**
   * @var    boolean
   * @access protected
   */
  protected $highlight = FALSE;
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numExecutableLines = 0;
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numExecutedLines = 0;
  
  /**
   * @var    array
   * @access protected
   */
  protected $classes = array();
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numClasses = 0;
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numCalledClasses = 0;
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numMethods = 0;
  
  /**
   * @var    integer
   * @access protected
   */
  protected $numCalledMethods = 0;
  
  /**
   * @var    string
   * @access protected
   */
  protected $yuiPanelJS = '';

  /**
   * Constructor.
   *
   * @param  string                   $name
   * @param  PHPUnit_Util_Report_Node $parent
   * @param  array                    $executedLines
   * @param  boolean                  $yui
   * @param  boolean                  $highlight
   * @throws RuntimeException
   * @access public
   */
  public function __construct($name, PHPUnit_Util_Report_Node $parent = NULL, array $executedLines, $yui = TRUE, $highlight = FALSE) {
    parent::__construct($name, $parent);
    
    $path = $this->getPath();
    
    if (! file_exists($path)) {
      throw new RuntimeException();
    }
    
    $this->executedLines = $executedLines;
    $this->highlight = $highlight;
    $this->yui = $yui;
    $this->codeLines = $this->loadFile($path);
    
    $this->calculateStatistics();
  }

  /**
   * Returns the classes of this node.
   *
   * @return array
   * @access public
   */
  public function getClasses() {
    return $this->classes;
  }

  /**
   * Returns the number of executable lines.
   *
   * @return integer
   * @access public
   */
  public function getNumExecutableLines() {
    return $this->numExecutableLines;
  }

  /**
   * Returns the number of executed lines.
   *
   * @return integer
   * @access public
   */
  public function getNumExecutedLines() {
    return $this->numExecutedLines;
  }

  /**
   * Returns the number of classes.
   *
   * @return integer
   * @access public
   */
  public function getNumClasses() {
    return $this->numClasses;
  }

  /**
   * Returns the number of classes of which at least one method
   * has been called at least once.
   *
   * @return integer
   * @access public
   */
  public function getNumCalledClasses() {
    return $this->numCalledClasses;
  }

  /**
   * Returns the number of methods.
   *
   * @return integer
   * @access public
   */
  public function getNumMethods() {
    return $this->numMethods;
  }

  /**
   * Returns the number of methods that has been called at least once.
   *
   * @return integer
   * @access public
   */
  public function getNumCalledMethods() {
    return $this->numCalledMethods;
  }

  /**
   * Renders this node.
   *
   * @param string  $target
   * @param string  $title
   * @param string  $charset
   * @param boolean $highlight
   * @param integer $lowUpperBound
   * @param integer $highLowerBound
   * @access public
   */
  public function render($target, $title, $charset = 'ISO-8859-1', $highlight = FALSE, $lowUpperBound = 35, $highLowerBound = 70) {
    if ($this->yui) {
      $template = new PHPUnit_Util_Template(PHPUnit_Util_Report::$templatePath . 'file.html');
      
      $yuiTemplate = new PHPUnit_Util_Template(PHPUnit_Util_Report::$templatePath . 'yui_item.js');
    } else {
      $template = new PHPUnit_Util_Template(PHPUnit_Util_Report::$templatePath . 'file_no_yui.html');
    }
    
    $i = 1;
    $lines = '';
    $ignore = FALSE;
    
    foreach ($this->codeLines as $line) {
      if (strpos($line, '@codeCoverageIgnore') !== FALSE) {
        if (strpos($line, '@codeCoverageIgnoreStart') !== FALSE) {
          $ignore = TRUE;
        } 

        else if (strpos($line, '@codeCoverageIgnoreEnd') !== FALSE) {
          $ignore = FALSE;
        }
      }
      
      $css = '';
      
      if (! $ignore && isset($this->executedLines[$i])) {
        $count = '';
        
        // Array: Line is executable and was executed.
        // count(Array) = Number of tests that hit this line.
        if (is_array($this->executedLines[$i])) {
          $color = 'lineCov';
          $numTests = count($this->executedLines[$i]);
          $count = sprintf('%8d', $numTests);
          
          if ($this->yui) {
            $buffer = '';
            
            foreach ($this->executedLines[$i] as $test) {
              if (! isset($test->__liHtml)) {
                $test->__liHtml = '';
                
                if ($test instanceof PHPUnit_Framework_SelfDescribing) {
                  $testName = $test->toString();
                  
                  if ($test instanceof PHPUnit_Framework_TestCase) {
                    switch ($test->getStatus()) {
                      case PHPUnit_Runner_BaseTestRunner::STATUS_PASSED:
                        {
                          $testCSS = ' class=\"testPassed\"';
                        }
                        break;
                      
                      case PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE:
                        {
                          $testCSS = ' class=\"testFailure\"';
                        }
                        break;
                      
                      case PHPUnit_Runner_BaseTestRunner::STATUS_ERROR:
                        {
                          $testCSS = ' class=\"testError\"';
                        }
                        break;
                      
                      case PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE:
                      case PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED:
                        {
                          $testCSS = ' class=\"testIncomplete\"';
                        }
                        break;
                      
                      default:
                        {
                          $testCSS = '';
                        }
                    }
                  }
                }
                
                $test->__liHtml .= sprintf('<li%s>%s</li>', 

                $testCSS, $testName);
              }
              
              $buffer .= $test->__liHtml;
            }
            
            if ($numTests > 1) {
              $header = $numTests . ' tests cover';
            } else {
              $header = '1 test covers';
            }
            
            $header .= ' line ' . $i;
            
            $yuiTemplate->setVar(array('line' => $i, 'header' => $header, 
                'tests' => $buffer), FALSE);
            
            $this->yuiPanelJS .= $yuiTemplate->render();
          }
        } 

        // -1: Line is executable and was not executed.
        else if ($this->executedLines[$i] == - 1) {
          $color = 'lineNoCov';
          $count = sprintf('%8d', 0);
        } 

        // -2: Line is dead code.
        else {
          $color = 'lineDeadCode';
          $count = '        ';
        }
        
        $css = sprintf('<span class="%s">       %s : ', 

        $color, $count);
      }
      
      $fillup = array_shift($this->codeLinesFillup);
      
      if ($fillup > 0) {
        $line .= str_repeat(' ', $fillup);
      }
      
      $lines .= sprintf('<span class="lineNum" id="container%d"><a name="%d"></a><a href="#%d" id="line%d">%8d</a> </span>%s%s%s' . "\n", 

      $i, $i, $i, $i, $i, ! empty($css) ? $css : '                : ', ! $this->highlight ? htmlspecialchars($line) : $line, ! empty($css) ? '</span>' : '');
      
      $i ++;
    }
    
    $items = '';
    
    foreach ($this->classes as $className => $classData) {
      $numCalledClasses = $classData['executedLines'] > 0 ? 1 : 0;
      $calledClassesPercent = $numCalledClasses == 1 ? 100 : 0;
      
      $numCalledMethods = 0;
      $numMethods = count($classData['methods']);
      
      foreach ($classData['methods'] as $method) {
        if ($method['executedLines'] > 0) {
          $numCalledMethods ++;
        }
      }
      
      $items .= $this->doRenderItem(array(
          'name' => sprintf('<b><a href="#%d">%s</a></b>', 

          $classData['startLine'], $className), 'numClasses' => 1, 
          'numCalledClasses' => $numCalledClasses, 
          'calledClassesPercent' => sprintf('%01.2f', $calledClassesPercent), 
          'numMethods' => $numMethods, 
          'numCalledMethods' => $numCalledMethods, 
          'calledMethodsPercent' => $this->calculatePercent($numCalledMethods, $numMethods), 
          'numExecutableLines' => $classData['executableLines'], 
          'numExecutedLines' => $classData['executedLines'], 
          'executedLinesPercent' => $this->calculatePercent($classData['executedLines'], $classData['executableLines'])), $lowUpperBound, $highLowerBound);
      
      foreach ($classData['methods'] as $methodName => $methodData) {
        $numCalledMethods = $methodData['executedLines'] > 0 ? 1 : 0;
        $calledMethodsPercent = $numCalledMethods == 1 ? 100 : 0;
        
        $items .= $this->doRenderItem(array(
            'name' => sprintf('&nbsp;<a href="#%d">%s</a>', 

            $methodData['startLine'], PHPUnit_Util_Class::getMethodSignature(new ReflectionMethod($className, $methodName))), 
            'numClasses' => '', 'numCalledClasses' => '', 
            'calledClassesPercent' => '', 'numMethods' => 1, 
            'numCalledMethods' => $numCalledMethods, 
            'calledMethodsPercent' => sprintf('%01.2f', $calledMethodsPercent), 
            'numExecutableLines' => $methodData['executableLines'], 
            'numExecutedLines' => $methodData['executedLines'], 
            'executedLinesPercent' => $this->calculatePercent($methodData['executedLines'], $methodData['executableLines'])), $lowUpperBound, $highLowerBound, 'method_item.html');
      }
    }
    
    $this->setTemplateVars($template, $title, $charset);
    
    $template->setVar(array('lines' => $lines, 
        'total_item' => $this->renderTotalItem($lowUpperBound, $highLowerBound, FALSE), 
        'items' => $items, 'yuiPanelJS' => $this->yuiPanelJS));
    
    $cleanId = PHPUnit_Util_Filesystem::getSafeFilename($this->getId());
    $template->renderTo($target . $cleanId . '.html');
  }

  /**
   * Calculates coverage statistics for the file.
   *
   * @access protected
   */
  protected function calculateStatistics() {
    $classes = PHPUnit_Util_Class::getClassesInFile($this->getPath());
    
    $startLines = array();
    $endLines = array();
    
    foreach ($classes as $class) {
      if (! $class->isInterface()) {
        $className = $class->getName();
        $classStartLine = $class->getStartLine();
        $classEndLine = $class->getEndLine();
        
        $this->classes[$className] = array('methods' => array(), 
            'startLine' => $classStartLine, 'executableLines' => 0, 
            'executedLines' => 0);
        
        $startLines[$classStartLine] = &$this->classes[$className];
        $endLines[$classEndLine] = &$this->classes[$className];
        
        foreach ($class->getMethods() as $method) {
          if (! $method->isAbstract() && $method->getDeclaringClass()->getName() == $className) {
            $methodName = $method->getName();
            $methodStartLine = $method->getStartLine();
            $methodEndLine = $method->getEndLine();
            
            $this->classes[$className]['methods'][$methodName] = array(
                'startLine' => $methodStartLine, 
                'executableLines' => 0, 
                'executedLines' => 0);
            
            $startLines[$methodStartLine] = &$this->classes[$className]['methods'][$methodName];
            $endLines[$methodEndLine] = &$this->classes[$className]['methods'][$methodName];
            
            $this->numMethods ++;
          }
        }
        
        $this->numClasses ++;
      }
    }
    
    $ignoreStart = - 1;
    $lineNumber = 1;
    
    foreach ($this->codeLines as $line) {
      if (isset($startLines[$lineNumber])) {
        // Start line of a class.
        if (isset($startLines[$lineNumber]['methods'])) {
          $currentClass = &$startLines[$lineNumber];
        } 

        // Start line of a method.
        else {
          $currentMethod = &$startLines[$lineNumber];
        }
      }
      
      if (strpos($line, '@codeCoverageIgnore') !== FALSE) {
        if (strpos($line, '@codeCoverageIgnoreStart') !== FALSE) {
          $ignoreStart = $line;
        } 

        else if (strpos($line, '@codeCoverageIgnoreEnd') !== FALSE) {
          $ignoreStart = - 1;
        }
      }
      
      if (isset($this->executedLines[$lineNumber])) {
        // Array: Line is executable and was executed.
        if (is_array($this->executedLines[$lineNumber])) {
          if (isset($currentClass)) {
            $currentClass['executableLines'] ++;
            $currentClass['executedLines'] ++;
          }
          
          if (isset($currentMethod)) {
            $currentMethod['executableLines'] ++;
            $currentMethod['executedLines'] ++;
          }
          
          $this->numExecutableLines ++;
          $this->numExecutedLines ++;
        } 

        // -1: Line is executable and was not executed.
        else if ($this->executedLines[$lineNumber] == - 1) {
          if (isset($currentClass)) {
            $currentClass['executableLines'] ++;
          }
          
          if (isset($currentMethod)) {
            $currentMethod['executableLines'] ++;
          }
          
          $this->numExecutableLines ++;
          
          if ($ignoreStart != - 1 && $line > $ignoreStart) {
            if (isset($currentClass)) {
              $currentClass['executedLines'] ++;
            }
            
            if (isset($currentMethod)) {
              $currentMethod['executedLines'] ++;
            }
            
            $this->numExecutedLines ++;
          }
        }
      }
      
      if (isset($endLines[$lineNumber])) {
        // End line of a class.
        if (isset($endLines[$lineNumber]['methods'])) {
          unset($currentClass);
        } 

        // End line of a method.
        else {
          unset($currentMethod);
        }
      }
      
      $lineNumber ++;
    }
    
    foreach ($this->classes as $class) {
      foreach ($class['methods'] as $method) {
        if ($method['executedLines'] > 0) {
          $this->numCalledMethods ++;
        }
      }
      
      if ($class['executedLines'] > 0) {
        $this->numCalledClasses ++;
      }
    }
  }

  /**
   * @author Aidan Lister <aidan@php.net>
   * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
   * @param  string  $file
   * @return array
   * @access protected
   */
  protected function loadFile($file) {
    $lines = explode("\n", str_replace("\t", '    ', file_get_contents($file)));
    $result = array();
    
    if (count($lines) == 0) {
      return $result;
    }
    
    $lines = array_map('rtrim', $lines);
    $linesLength = array_map('strlen', $lines);
    $width = max($linesLength);
    
    foreach ($linesLength as $line => $length) {
      $this->codeLinesFillup[$line] = $width - $length;
    }
    
    if (! $this->highlight) {
      return $lines;
    }
    
    $tokens = token_get_all(file_get_contents($file));
    $stringFlag = FALSE;
    $i = 0;
    $result[$i] = '';
    
    foreach ($tokens as $j => $token) {
      if (is_string($token)) {
        if ($token === '"' && $tokens[$j - 1] !== '\\') {
          $result[$i] .= sprintf('<span class="string">%s</span>', 

          htmlspecialchars($token));
          
          $stringFlag = ! $stringFlag;
        } else {
          $result[$i] .= sprintf('<span class="keyword">%s</span>', 

          htmlspecialchars($token));
        }
        
        continue;
      }
      
      list($token, $value) = $token;
      
      $value = str_replace(array("\t", ' '), array('&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'), htmlspecialchars($value));
      
      if ($value === "\n") {
        $result[++ $i] = '';
      } else {
        $lines = explode("\n", $value);
        
        foreach ($lines as $jj => $line) {
          $line = trim($line);
          
          if ($line !== '') {
            if ($stringFlag) {
              $colour = 'string';
            } else {
              switch ($token) {
                case T_INLINE_HTML:
                  {
                    $colour = 'html';
                  }
                  break;
                
                case T_COMMENT:
                case T_DOC_COMMENT:
                  {
                    $colour = 'comment';
                  }
                  break;
                
                case T_ABSTRACT:
                case T_ARRAY:
                case T_ARRAY_CAST:
                case T_AS:
                case T_BOOLEAN_AND:
                case T_BOOLEAN_OR:
                case T_BOOL_CAST:
                case T_BREAK:
                case T_CASE:
                case T_CATCH:
                case T_CLASS:
                case T_CLONE:
                case T_CONCAT_EQUAL:
                case T_CONTINUE:
                case T_DEFAULT:
                case T_DOUBLE_ARROW:
                case T_DOUBLE_CAST:
                case T_ECHO:
                case T_ELSE:
                case T_ELSEIF:
                case T_EMPTY:
                case T_ENDDECLARE:
                case T_ENDFOR:
                case T_ENDFOREACH:
                case T_ENDIF:
                case T_ENDSWITCH:
                case T_ENDWHILE:
                case T_END_HEREDOC:
                case T_EXIT:
                case T_EXTENDS:
                case T_FINAL:
                case T_FOREACH:
                case T_FUNCTION:
                case T_GLOBAL:
                case T_IF:
                case T_INC:
                case T_INCLUDE:
                case T_INCLUDE_ONCE:
                case T_INSTANCEOF:
                case T_INT_CAST:
                case T_ISSET:
                case T_IS_EQUAL:
                case T_IS_IDENTICAL:
                case T_IS_NOT_IDENTICAL:
                case T_IS_SMALLER_OR_EQUAL:
                case T_NEW:
                case T_OBJECT_CAST:
                case T_OBJECT_OPERATOR:
                case T_PAAMAYIM_NEKUDOTAYIM:
                case T_PRIVATE:
                case T_PROTECTED:
                case T_PUBLIC:
                case T_REQUIRE:
                case T_REQUIRE_ONCE:
                case T_RETURN:
                case T_SL:
                case T_SL_EQUAL:
                case T_SR:
                case T_SR_EQUAL:
                case T_START_HEREDOC:
                case T_STATIC:
                case T_STRING_CAST:
                case T_THROW:
                case T_TRY:
                case T_UNSET_CAST:
                case T_VAR:
                case T_WHILE:
                  {
                    $colour = 'keyword';
                  }
                  break;
                
                default:
                  {
                    $colour = 'default';
                  }
              }
            }
            
            $result[$i] .= sprintf('<span class="%s">%s</span>', 

            $colour, $line);
          }
          
          if (isset($lines[$jj + 1])) {
            $result[++ $i] = '';
          }
        }
      }
    }
    
    unset($result[count($result) - 1]);
    
    return $result;
  }
}
?>
