PageRenderTime 26ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/PHP/CodeCoverage.php

http://github.com/sebastianbergmann/php-code-coverage
PHP | 539 lines | 275 code | 77 blank | 187 comment | 48 complexity | 3e860e66b32ef8594c98052f1c96c298 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * PHP_CodeCoverage
  4. *
  5. * Copyright (c) 2009-2012, Sebastian Bergmann <sb@sebastian-bergmann.de>.
  6. * All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions
  10. * are met:
  11. *
  12. * * Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. *
  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
  18. * distribution.
  19. *
  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.
  23. *
  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.
  36. *
  37. * @category PHP
  38. * @package CodeCoverage
  39. * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
  40. * @copyright 2009-2012 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
  44. */
  45. /**
  46. * Provides collection functionality for PHP code coverage information.
  47. *
  48. * @category PHP
  49. * @package CodeCoverage
  50. * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
  51. * @copyright 2009-2012 Sebastian Bergmann <sb@sebastian-bergmann.de>
  52. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  53. * @version Release: @package_version@
  54. * @link http://github.com/sebastianbergmann/php-code-coverage
  55. * @since Class available since Release 1.0.0
  56. */
  57. class PHP_CodeCoverage
  58. {
  59. /**
  60. * @var PHP_CodeCoverage_Driver
  61. */
  62. protected $driver;
  63. /**
  64. * @var PHP_CodeCoverage_Filter
  65. */
  66. protected $filter;
  67. /**
  68. * @var boolean
  69. */
  70. protected $cacheTokens = TRUE;
  71. /**
  72. * @var boolean
  73. */
  74. protected $forceCoversAnnotation = FALSE;
  75. /**
  76. * @var boolean
  77. */
  78. protected $mapTestClassNameToCoveredClassName = FALSE;
  79. /**
  80. * @var boolean
  81. */
  82. protected $processUncoveredFilesFromWhitelist = TRUE;
  83. /**
  84. * @var mixed
  85. */
  86. protected $currentId;
  87. /**
  88. * Code coverage data.
  89. *
  90. * @var array
  91. */
  92. protected $data = array();
  93. /**
  94. * Test data.
  95. *
  96. * @var array
  97. */
  98. protected $tests = array();
  99. /**
  100. * Constructor.
  101. *
  102. * @param PHP_CodeCoverage_Driver $driver
  103. * @param PHP_CodeCoverage_Filter $filter
  104. * @throws InvalidArgumentException
  105. */
  106. public function __construct(PHP_CodeCoverage_Driver $driver = NULL, PHP_CodeCoverage_Filter $filter = NULL)
  107. {
  108. if ($driver === NULL) {
  109. $driver = new PHP_CodeCoverage_Driver_Xdebug;
  110. }
  111. if ($filter === NULL) {
  112. $filter = new PHP_CodeCoverage_Filter;
  113. }
  114. $this->driver = $driver;
  115. $this->filter = $filter;
  116. }
  117. /**
  118. * Returns the PHP_CodeCoverage_Report_Node_* object graph
  119. * for this PHP_CodeCoverage object.
  120. *
  121. * @return PHP_CodeCoverage_Report_Node_Directory
  122. * @since Method available since Release 1.1.0
  123. */
  124. public function getReport()
  125. {
  126. $factory = new PHP_CodeCoverage_Report_Factory;
  127. return $factory->create($this);
  128. }
  129. /**
  130. * Clears collected code coverage data.
  131. */
  132. public function clear()
  133. {
  134. $this->currentId = NULL;
  135. $this->data = array();
  136. $this->tests = array();
  137. }
  138. /**
  139. * Returns the PHP_CodeCoverage_Filter used.
  140. *
  141. * @return PHP_CodeCoverage_Filter
  142. */
  143. public function filter()
  144. {
  145. return $this->filter;
  146. }
  147. /**
  148. * Returns the collected code coverage data.
  149. *
  150. * @return array
  151. * @since Method available since Release 1.1.0
  152. */
  153. public function getData()
  154. {
  155. if ($this->processUncoveredFilesFromWhitelist) {
  156. $this->processUncoveredFilesFromWhitelist();
  157. }
  158. // We need to apply the blacklist filter a second time
  159. // when no whitelist is used.
  160. if (!$this->filter->hasWhitelist()) {
  161. $this->applyListsFilter($this->data);
  162. }
  163. return $this->data;
  164. }
  165. /**
  166. * Returns the test data.
  167. *
  168. * @return array
  169. * @since Method available since Release 1.1.0
  170. */
  171. public function getTests()
  172. {
  173. return $this->tests;
  174. }
  175. /**
  176. * Start collection of code coverage information.
  177. *
  178. * @param mixed $id
  179. * @param boolean $clear
  180. * @throws InvalidArgumentException
  181. */
  182. public function start($id, $clear = FALSE)
  183. {
  184. if (!is_bool($clear)) {
  185. throw new InvalidArgumentException;
  186. }
  187. if ($clear) {
  188. $this->clear();
  189. }
  190. $this->currentId = $id;
  191. $this->driver->start();
  192. }
  193. /**
  194. * Stop collection of code coverage information.
  195. *
  196. * @param boolean $append
  197. * @return array
  198. * @throws InvalidArgumentException
  199. */
  200. public function stop($append = TRUE)
  201. {
  202. if (!is_bool($append)) {
  203. throw new InvalidArgumentException;
  204. }
  205. $data = $this->driver->stop();
  206. $this->append($data, NULL, $append);
  207. $this->currentId = NULL;
  208. return $data;
  209. }
  210. /**
  211. * Appends code coverage data.
  212. *
  213. * @param array $data
  214. * @param mixed $id
  215. * @param boolean $append
  216. */
  217. public function append(array $data, $id = NULL, $append = TRUE)
  218. {
  219. if ($id === NULL) {
  220. $id = $this->currentId;
  221. }
  222. if ($id === NULL) {
  223. throw new InvalidArgumentException;
  224. }
  225. $this->applyListsFilter($data);
  226. $this->initializeFilesThatAreSeenTheFirstTime($data);
  227. if (!$append) {
  228. return;
  229. }
  230. if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
  231. $this->applyCoversAnnotationFilter($data, $id);
  232. }
  233. if (empty($data)) {
  234. return;
  235. }
  236. $status = NULL;
  237. if ($id instanceof PHPUnit_Framework_TestCase) {
  238. $status = $id->getStatus();
  239. $id = get_class($id) . '::' . $id->getName();
  240. }
  241. else if ($id instanceof PHPUnit_Extensions_PhptTestCase) {
  242. $id = $id->getName();
  243. }
  244. $this->tests[$id] = $status;
  245. foreach ($data as $file => $lines) {
  246. if (!$this->filter->isFile($file)) {
  247. continue;
  248. }
  249. foreach ($lines as $k => $v) {
  250. if ($v == 1) {
  251. $this->data[$file][$k][] = $id;
  252. }
  253. }
  254. }
  255. }
  256. /**
  257. * Merges the data from another instance of PHP_CodeCoverage.
  258. *
  259. * @param PHP_CodeCoverage $that
  260. */
  261. public function merge(PHP_CodeCoverage $that)
  262. {
  263. foreach ($that->data as $file => $lines) {
  264. if (!isset($this->data[$file])) {
  265. if (!$this->filter->isFiltered($file)) {
  266. $this->data[$file] = $lines;
  267. }
  268. continue;
  269. }
  270. foreach ($lines as $line => $data) {
  271. if ($data !== NULL) {
  272. if (!isset($this->data[$file][$line])) {
  273. $this->data[$file][$line] = $data;
  274. } else {
  275. $this->data[$file][$line] = array_unique(
  276. array_merge($this->data[$file][$line], $data)
  277. );
  278. }
  279. }
  280. }
  281. }
  282. $this->tests = array_merge($this->tests, $that->getTests());
  283. }
  284. /**
  285. * @param boolean $flag
  286. * @throws InvalidArgumentException
  287. * @since Method available since Release 1.1.0
  288. */
  289. public function setCacheTokens($flag)
  290. {
  291. if (!is_bool($flag)) {
  292. throw new InvalidArgumentException;
  293. }
  294. $this->cacheTokens = $flag;
  295. }
  296. /**
  297. * @param boolean $flag
  298. * @throws InvalidArgumentException
  299. * @since Method available since Release 1.1.0
  300. */
  301. public function getCacheTokens()
  302. {
  303. return $this->cacheTokens;
  304. }
  305. /**
  306. * @param boolean $flag
  307. * @throws InvalidArgumentException
  308. */
  309. public function setForceCoversAnnotation($flag)
  310. {
  311. if (!is_bool($flag)) {
  312. throw new InvalidArgumentException;
  313. }
  314. $this->forceCoversAnnotation = $flag;
  315. }
  316. /**
  317. * @param boolean $flag
  318. * @throws InvalidArgumentException
  319. */
  320. public function setMapTestClassNameToCoveredClassName($flag)
  321. {
  322. if (!is_bool($flag)) {
  323. throw new InvalidArgumentException;
  324. }
  325. $this->mapTestClassNameToCoveredClassName = $flag;
  326. }
  327. /**
  328. * @param boolean $flag
  329. * @throws InvalidArgumentException
  330. */
  331. public function setProcessUncoveredFilesFromWhitelist($flag)
  332. {
  333. if (!is_bool($flag)) {
  334. throw new InvalidArgumentException;
  335. }
  336. $this->processUncoveredFilesFromWhitelist = $flag;
  337. }
  338. /**
  339. * Applies the @covers annotation filtering.
  340. *
  341. * @param array $data
  342. * @param mixed $id
  343. */
  344. protected function applyCoversAnnotationFilter(&$data, $id)
  345. {
  346. if ($id instanceof PHPUnit_Framework_TestCase) {
  347. $testClassName = get_class($id);
  348. $linesToBeCovered = PHP_CodeCoverage_Util::getLinesToBeCovered(
  349. $testClassName, $id->getName()
  350. );
  351. if ($this->mapTestClassNameToCoveredClassName &&
  352. empty($linesToBeCovered)) {
  353. $testedClass = substr($testClassName, 0, -4);
  354. if (class_exists($testedClass)) {
  355. $class = new ReflectionClass($testedClass);
  356. $linesToBeCovered = array(
  357. $class->getFileName() => range(
  358. $class->getStartLine(), $class->getEndLine()
  359. )
  360. );
  361. }
  362. }
  363. } else {
  364. $linesToBeCovered = array();
  365. }
  366. if (!empty($linesToBeCovered)) {
  367. $data = array_intersect_key($data, $linesToBeCovered);
  368. foreach (array_keys($data) as $filename) {
  369. $data[$filename] = array_intersect_key(
  370. $data[$filename], array_flip($linesToBeCovered[$filename])
  371. );
  372. }
  373. }
  374. else if ($this->forceCoversAnnotation) {
  375. $data = array();
  376. }
  377. }
  378. /**
  379. * Applies the blacklist/whitelist filtering.
  380. *
  381. * @param array $data
  382. */
  383. protected function applyListsFilter(&$data)
  384. {
  385. foreach (array_keys($data) as $filename) {
  386. if ($this->filter->isFiltered($filename)) {
  387. unset($data[$filename]);
  388. }
  389. }
  390. }
  391. /**
  392. * @since Method available since Release 1.1.0
  393. */
  394. protected function initializeFilesThatAreSeenTheFirstTime($data)
  395. {
  396. foreach ($data as $file => $lines) {
  397. if ($this->filter->isFile($file) && !isset($this->data[$file])) {
  398. $this->data[$file] = array();
  399. foreach ($lines as $k => $v) {
  400. $this->data[$file][$k] = $v == -2 ? NULL : array();
  401. }
  402. }
  403. }
  404. }
  405. /**
  406. * Processes whitelisted files that are not covered.
  407. */
  408. protected function processUncoveredFilesFromWhitelist()
  409. {
  410. $data = array();
  411. $uncoveredFiles = array_diff(
  412. $this->filter->getWhitelist(), array_keys($this->data)
  413. );
  414. foreach ($uncoveredFiles as $uncoveredFile) {
  415. if (!file_exists($uncoveredFile)) {
  416. continue;
  417. }
  418. if ($this->cacheTokens) {
  419. $tokens = PHP_Token_Stream_CachingFactory::get($uncoveredFile);
  420. } else {
  421. $tokens = new PHP_Token_Stream($uncoveredFile);
  422. }
  423. $classes = $tokens->getClasses();
  424. $interfaces = $tokens->getInterfaces();
  425. $functions = $tokens->getFunctions();
  426. unset($tokens);
  427. foreach (array_keys($classes) as $class) {
  428. if (class_exists($class, FALSE)) {
  429. continue 2;
  430. }
  431. }
  432. unset($classes);
  433. foreach (array_keys($interfaces) as $interface) {
  434. if (interface_exists($interface, FALSE)) {
  435. continue 2;
  436. }
  437. }
  438. unset($interfaces);
  439. foreach (array_keys($functions) as $function) {
  440. if (function_exists($function)) {
  441. continue 2;
  442. }
  443. }
  444. unset($functions);
  445. $this->driver->start();
  446. include_once $uncoveredFile;
  447. $coverage = $this->driver->stop();
  448. foreach ($coverage as $file => $fileCoverage) {
  449. if (!isset($data[$file]) &&
  450. in_array($file, $uncoveredFiles)) {
  451. foreach (array_keys($fileCoverage) as $key) {
  452. if ($fileCoverage[$key] == 1) {
  453. $fileCoverage[$key] = -1;
  454. }
  455. }
  456. $data[$file] = $fileCoverage;
  457. }
  458. }
  459. }
  460. $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
  461. }
  462. }