You are here

CodeCoverage.php in Zircon Profile 8.0

Same filename and directory in other branches
  1. 8 vendor/phpunit/php-code-coverage/src/CodeCoverage.php

File

vendor/phpunit/php-code-coverage/src/CodeCoverage.php
View source
<?php

/*
 * This file is part of the PHP_CodeCoverage package.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
use SebastianBergmann\Environment\Runtime;

/**
 * Provides collection functionality for PHP code coverage information.
 *
 * @since Class available since Release 1.0.0
 */
class PHP_CodeCoverage {

  /**
   * @var PHP_CodeCoverage_Driver
   */
  private $driver;

  /**
   * @var PHP_CodeCoverage_Filter
   */
  private $filter;

  /**
   * @var bool
   */
  private $cacheTokens = false;

  /**
   * @var bool
   */
  private $checkForUnintentionallyCoveredCode = false;

  /**
   * @var bool
   */
  private $forceCoversAnnotation = false;

  /**
   * @var bool
   */
  private $mapTestClassNameToCoveredClassName = false;

  /**
   * @var bool
   */
  private $addUncoveredFilesFromWhitelist = true;

  /**
   * @var bool
   */
  private $processUncoveredFilesFromWhitelist = false;

  /**
   * @var mixed
   */
  private $currentId;

  /**
   * Code coverage data.
   *
   * @var array
   */
  private $data = array();

  /**
   * @var array
   */
  private $ignoredLines = array();

  /**
   * @var bool
   */
  private $disableIgnoredLines = false;

  /**
   * Test data.
   *
   * @var array
   */
  private $tests = array();

  /**
   * Constructor.
   *
   * @param  PHP_CodeCoverage_Driver    $driver
   * @param  PHP_CodeCoverage_Filter    $filter
   * @throws PHP_CodeCoverage_Exception
   */
  public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null) {
    if ($driver === null) {
      $driver = $this
        ->selectDriver();
    }
    if ($filter === null) {
      $filter = new PHP_CodeCoverage_Filter();
    }
    $this->driver = $driver;
    $this->filter = $filter;
  }

  /**
   * Returns the PHP_CodeCoverage_Report_Node_* object graph
   * for this PHP_CodeCoverage object.
   *
   * @return PHP_CodeCoverage_Report_Node_Directory
   * @since  Method available since Release 1.1.0
   */
  public function getReport() {
    $factory = new PHP_CodeCoverage_Report_Factory();
    return $factory
      ->create($this);
  }

  /**
   * Clears collected code coverage data.
   */
  public function clear() {
    $this->currentId = null;
    $this->data = array();
    $this->tests = array();
  }

  /**
   * Returns the PHP_CodeCoverage_Filter used.
   *
   * @return PHP_CodeCoverage_Filter
   */
  public function filter() {
    return $this->filter;
  }

  /**
   * Returns the collected code coverage data.
   * Set $raw = true to bypass all filters.
   *
   * @param  bool  $raw
   * @return array
   * @since  Method available since Release 1.1.0
   */
  public function getData($raw = false) {
    if (!$raw && $this->addUncoveredFilesFromWhitelist) {
      $this
        ->addUncoveredFilesFromWhitelist();
    }

    // We need to apply the blacklist filter a second time
    // when no whitelist is used.
    if (!$raw && !$this->filter
      ->hasWhitelist()) {
      $this
        ->applyListsFilter($this->data);
    }
    return $this->data;
  }

  /**
   * Sets the coverage data.
   *
   * @param array $data
   * @since Method available since Release 2.0.0
   */
  public function setData(array $data) {
    $this->data = $data;
  }

  /**
   * Returns the test data.
   *
   * @return array
   * @since  Method available since Release 1.1.0
   */
  public function getTests() {
    return $this->tests;
  }

  /**
   * Sets the test data.
   *
   * @param array $tests
   * @since Method available since Release 2.0.0
   */
  public function setTests(array $tests) {
    $this->tests = $tests;
  }

  /**
   * Start collection of code coverage information.
   *
   * @param  mixed                      $id
   * @param  bool                       $clear
   * @throws PHP_CodeCoverage_Exception
   */
  public function start($id, $clear = false) {
    if (!is_bool($clear)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    if ($clear) {
      $this
        ->clear();
    }
    $this->currentId = $id;
    $this->driver
      ->start();
  }

  /**
   * Stop collection of code coverage information.
   *
   * @param  bool                       $append
   * @param  mixed                      $linesToBeCovered
   * @param  array                      $linesToBeUsed
   * @return array
   * @throws PHP_CodeCoverage_Exception
   */
  public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) {
    if (!is_bool($append)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(2, 'array or false');
    }
    $data = $this->driver
      ->stop();
    $this
      ->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
    $this->currentId = null;
    return $data;
  }

  /**
   * Appends code coverage data.
   *
   * @param  array                      $data
   * @param  mixed                      $id
   * @param  bool                       $append
   * @param  mixed                      $linesToBeCovered
   * @param  array                      $linesToBeUsed
   * @throws PHP_CodeCoverage_Exception
   */
  public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) {
    if ($id === null) {
      $id = $this->currentId;
    }
    if ($id === null) {
      throw new PHP_CodeCoverage_Exception();
    }
    $this
      ->applyListsFilter($data);
    $this
      ->applyIgnoredLinesFilter($data);
    $this
      ->initializeFilesThatAreSeenTheFirstTime($data);
    if (!$append) {
      return;
    }
    if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
      $this
        ->applyCoversAnnotationFilter($data, $linesToBeCovered, $linesToBeUsed);
    }
    if (empty($data)) {
      return;
    }
    $size = 'unknown';
    $status = null;
    if ($id instanceof PHPUnit_Framework_TestCase) {
      $_size = $id
        ->getSize();
      if ($_size == PHPUnit_Util_Test::SMALL) {
        $size = 'small';
      }
      elseif ($_size == PHPUnit_Util_Test::MEDIUM) {
        $size = 'medium';
      }
      elseif ($_size == PHPUnit_Util_Test::LARGE) {
        $size = 'large';
      }
      $status = $id
        ->getStatus();
      $id = get_class($id) . '::' . $id
        ->getName();
    }
    elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) {
      $size = 'large';
      $id = $id
        ->getName();
    }
    $this->tests[$id] = array(
      'size' => $size,
      'status' => $status,
    );
    foreach ($data as $file => $lines) {
      if (!$this->filter
        ->isFile($file)) {
        continue;
      }
      foreach ($lines as $k => $v) {
        if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
          if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
            $this->data[$file][$k][] = $id;
          }
        }
      }
    }
  }

  /**
   * Merges the data from another instance of PHP_CodeCoverage.
   *
   * @param PHP_CodeCoverage $that
   */
  public function merge(PHP_CodeCoverage $that) {
    $this->filter
      ->setBlacklistedFiles(array_merge($this->filter
      ->getBlacklistedFiles(), $that
      ->filter()
      ->getBlacklistedFiles()));
    $this->filter
      ->setWhitelistedFiles(array_merge($this->filter
      ->getWhitelistedFiles(), $that
      ->filter()
      ->getWhitelistedFiles()));
    foreach ($that->data as $file => $lines) {
      if (!isset($this->data[$file])) {
        if (!$this->filter
          ->isFiltered($file)) {
          $this->data[$file] = $lines;
        }
        continue;
      }
      foreach ($lines as $line => $data) {
        if ($data !== null) {
          if (!isset($this->data[$file][$line])) {
            $this->data[$file][$line] = $data;
          }
          else {
            $this->data[$file][$line] = array_unique(array_merge($this->data[$file][$line], $data));
          }
        }
      }
    }
    $this->tests = array_merge($this->tests, $that
      ->getTests());
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   * @since  Method available since Release 1.1.0
   */
  public function setCacheTokens($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->cacheTokens = $flag;
  }

  /**
   * @since Method available since Release 1.1.0
   */
  public function getCacheTokens() {
    return $this->cacheTokens;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   * @since  Method available since Release 2.0.0
   */
  public function setCheckForUnintentionallyCoveredCode($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->checkForUnintentionallyCoveredCode = $flag;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   */
  public function setForceCoversAnnotation($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->forceCoversAnnotation = $flag;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   */
  public function setMapTestClassNameToCoveredClassName($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->mapTestClassNameToCoveredClassName = $flag;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   */
  public function setAddUncoveredFilesFromWhitelist($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->addUncoveredFilesFromWhitelist = $flag;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   */
  public function setProcessUncoveredFilesFromWhitelist($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->processUncoveredFilesFromWhitelist = $flag;
  }

  /**
   * @param  bool                       $flag
   * @throws PHP_CodeCoverage_Exception
   */
  public function setDisableIgnoredLines($flag) {
    if (!is_bool($flag)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
    }
    $this->disableIgnoredLines = $flag;
  }

  /**
   * Applies the @covers annotation filtering.
   *
   * @param  array                                                 $data
   * @param  mixed                                                 $linesToBeCovered
   * @param  array                                                 $linesToBeUsed
   * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
   */
  private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) {
    if ($linesToBeCovered === false || $this->forceCoversAnnotation && empty($linesToBeCovered)) {
      $data = array();
      return;
    }
    if (empty($linesToBeCovered)) {
      return;
    }
    if ($this->checkForUnintentionallyCoveredCode) {
      $this
        ->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
    }
    $data = array_intersect_key($data, $linesToBeCovered);
    foreach (array_keys($data) as $filename) {
      $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
      $data[$filename] = array_intersect_key($data[$filename], $_linesToBeCovered);
    }
  }

  /**
   * Applies the blacklist/whitelist filtering.
   *
   * @param array $data
   */
  private function applyListsFilter(array &$data) {
    foreach (array_keys($data) as $filename) {
      if ($this->filter
        ->isFiltered($filename)) {
        unset($data[$filename]);
      }
    }
  }

  /**
   * Applies the "ignored lines" filtering.
   *
   * @param array $data
   */
  private function applyIgnoredLinesFilter(array &$data) {
    foreach (array_keys($data) as $filename) {
      if (!$this->filter
        ->isFile($filename)) {
        continue;
      }
      foreach ($this
        ->getLinesToBeIgnored($filename) as $line) {
        unset($data[$filename][$line]);
      }
    }
  }

  /**
   * @param array $data
   * @since Method available since Release 1.1.0
   */
  private function initializeFilesThatAreSeenTheFirstTime(array $data) {
    foreach ($data as $file => $lines) {
      if ($this->filter
        ->isFile($file) && !isset($this->data[$file])) {
        $this->data[$file] = array();
        foreach ($lines as $k => $v) {
          $this->data[$file][$k] = $v == -2 ? null : array();
        }
      }
    }
  }

  /**
   * Processes whitelisted files that are not covered.
   */
  private function addUncoveredFilesFromWhitelist() {
    $data = array();
    $uncoveredFiles = array_diff($this->filter
      ->getWhitelist(), array_keys($this->data));
    foreach ($uncoveredFiles as $uncoveredFile) {
      if (!file_exists($uncoveredFile)) {
        continue;
      }
      if ($this->processUncoveredFilesFromWhitelist) {
        $this
          ->processUncoveredFileFromWhitelist($uncoveredFile, $data, $uncoveredFiles);
      }
      else {
        $data[$uncoveredFile] = array();
        $lines = count(file($uncoveredFile));
        for ($i = 1; $i <= $lines; $i++) {
          $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
        }
      }
    }
    $this
      ->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  }

  /**
   * @param string $uncoveredFile
   * @param array  $data
   * @param array  $uncoveredFiles
   */
  private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) {
    $this->driver
      ->start();
    include_once $uncoveredFile;
    $coverage = $this->driver
      ->stop();
    foreach ($coverage as $file => $fileCoverage) {
      if (!isset($data[$file]) && in_array($file, $uncoveredFiles)) {
        foreach (array_keys($fileCoverage) as $key) {
          if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
            $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
          }
        }
        $data[$file] = $fileCoverage;
      }
    }
  }

  /**
   * Returns the lines of a source file that should be ignored.
   *
   * @param  string                     $filename
   * @return array
   * @throws PHP_CodeCoverage_Exception
   * @since  Method available since Release 2.0.0
   */
  private function getLinesToBeIgnored($filename) {
    if (!is_string($filename)) {
      throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'string');
    }
    if (!isset($this->ignoredLines[$filename])) {
      $this->ignoredLines[$filename] = array();
      if ($this->disableIgnoredLines) {
        return $this->ignoredLines[$filename];
      }
      $ignore = false;
      $stop = false;
      $lines = file($filename);
      $numLines = count($lines);
      foreach ($lines as $index => $line) {
        if (!trim($line)) {
          $this->ignoredLines[$filename][] = $index + 1;
        }
      }
      if ($this->cacheTokens) {
        $tokens = PHP_Token_Stream_CachingFactory::get($filename);
      }
      else {
        $tokens = new PHP_Token_Stream($filename);
      }
      $classes = array_merge($tokens
        ->getClasses(), $tokens
        ->getTraits());
      $tokens = $tokens
        ->tokens();
      foreach ($tokens as $token) {
        switch (get_class($token)) {
          case 'PHP_Token_COMMENT':
          case 'PHP_Token_DOC_COMMENT':
            $_token = trim($token);
            $_line = trim($lines[$token
              ->getLine() - 1]);
            if ($_token == '// @codeCoverageIgnore' || $_token == '//@codeCoverageIgnore') {
              $ignore = true;
              $stop = true;
            }
            elseif ($_token == '// @codeCoverageIgnoreStart' || $_token == '//@codeCoverageIgnoreStart') {
              $ignore = true;
            }
            elseif ($_token == '// @codeCoverageIgnoreEnd' || $_token == '//@codeCoverageIgnoreEnd') {
              $stop = true;
            }
            if (!$ignore) {
              $start = $token
                ->getLine();
              $end = $start + substr_count($token, "\n");

              // Do not ignore the first line when there is a token
              // before the comment
              if (0 !== strpos($_token, $_line)) {
                $start++;
              }
              for ($i = $start; $i < $end; $i++) {
                $this->ignoredLines[$filename][] = $i;
              }

              // A DOC_COMMENT token or a COMMENT token starting with "/*"
              // does not contain the final \n character in its text
              if (isset($lines[$i - 1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i - 1]), -2)) {
                $this->ignoredLines[$filename][] = $i;
              }
            }
            break;
          case 'PHP_Token_INTERFACE':
          case 'PHP_Token_TRAIT':
          case 'PHP_Token_CLASS':
          case 'PHP_Token_FUNCTION':
            $docblock = $token
              ->getDocblock();
            $this->ignoredLines[$filename][] = $token
              ->getLine();
            if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) {
              $endLine = $token
                ->getEndLine();
              for ($i = $token
                ->getLine(); $i <= $endLine; $i++) {
                $this->ignoredLines[$filename][] = $i;
              }
            }
            elseif ($token instanceof PHP_Token_INTERFACE || $token instanceof PHP_Token_TRAIT || $token instanceof PHP_Token_CLASS) {
              if (empty($classes[$token
                ->getName()]['methods'])) {
                for ($i = $token
                  ->getLine(); $i <= $token
                  ->getEndLine(); $i++) {
                  $this->ignoredLines[$filename][] = $i;
                }
              }
              else {
                $firstMethod = array_shift($classes[$token
                  ->getName()]['methods']);
                do {
                  $lastMethod = array_pop($classes[$token
                    ->getName()]['methods']);
                } while ($lastMethod !== null && substr($lastMethod['signature'], 0, 18) == 'anonymous function');
                if ($lastMethod === null) {
                  $lastMethod = $firstMethod;
                }
                for ($i = $token
                  ->getLine(); $i < $firstMethod['startLine']; $i++) {
                  $this->ignoredLines[$filename][] = $i;
                }
                for ($i = $token
                  ->getEndLine(); $i > $lastMethod['endLine']; $i--) {
                  $this->ignoredLines[$filename][] = $i;
                }
              }
            }
            break;
          case 'PHP_Token_NAMESPACE':
            $this->ignoredLines[$filename][] = $token
              ->getEndLine();

          // Intentional fallthrough
          case 'PHP_Token_OPEN_TAG':
          case 'PHP_Token_CLOSE_TAG':
          case 'PHP_Token_USE':
            $this->ignoredLines[$filename][] = $token
              ->getLine();
            break;
        }
        if ($ignore) {
          $this->ignoredLines[$filename][] = $token
            ->getLine();
          if ($stop) {
            $ignore = false;
            $stop = false;
          }
        }
      }
      $this->ignoredLines[$filename][] = $numLines + 1;
      $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
      sort($this->ignoredLines[$filename]);
    }
    return $this->ignoredLines[$filename];
  }

  /**
   * @param  array                                                 $data
   * @param  array                                                 $linesToBeCovered
   * @param  array                                                 $linesToBeUsed
   * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
   * @since Method available since Release 2.0.0
   */
  private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) {
    $allowedLines = $this
      ->getAllowedLines($linesToBeCovered, $linesToBeUsed);
    $message = '';
    foreach ($data as $file => $_data) {
      foreach ($_data as $line => $flag) {
        if ($flag == 1 && (!isset($allowedLines[$file]) || !isset($allowedLines[$file][$line]))) {
          $message .= sprintf('- %s:%d' . PHP_EOL, $file, $line);
        }
      }
    }
    if (!empty($message)) {
      throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode($message);
    }
  }

  /**
   * @param  array $linesToBeCovered
   * @param  array $linesToBeUsed
   * @return array
   * @since Method available since Release 2.0.0
   */
  private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) {
    $allowedLines = array();
    foreach (array_keys($linesToBeCovered) as $file) {
      if (!isset($allowedLines[$file])) {
        $allowedLines[$file] = array();
      }
      $allowedLines[$file] = array_merge($allowedLines[$file], $linesToBeCovered[$file]);
    }
    foreach (array_keys($linesToBeUsed) as $file) {
      if (!isset($allowedLines[$file])) {
        $allowedLines[$file] = array();
      }
      $allowedLines[$file] = array_merge($allowedLines[$file], $linesToBeUsed[$file]);
    }
    foreach (array_keys($allowedLines) as $file) {
      $allowedLines[$file] = array_flip(array_unique($allowedLines[$file]));
    }
    return $allowedLines;
  }

  /**
   * @return PHP_CodeCoverage_Driver
   * @throws PHP_CodeCoverage_Exception
   */
  private function selectDriver() {
    $runtime = new Runtime();
    if (!$runtime
      ->canCollectCodeCoverage()) {
      throw new PHP_CodeCoverage_Exception('No code coverage driver available');
    }
    if ($runtime
      ->isHHVM()) {
      return new PHP_CodeCoverage_Driver_HHVM();
    }
    elseif ($runtime
      ->isPHPDBG()) {
      return new PHP_CodeCoverage_Driver_PHPDBG();
    }
    else {
      return new PHP_CodeCoverage_Driver_Xdebug();
    }
  }

}

Classes

Namesort descending Description
PHP_CodeCoverage Provides collection functionality for PHP code coverage information.