PageRenderTime 41ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/source/PHP/ChangeCoverage/TextUI/Command.php

http://github.com/manuelpichler/php-change-coverage
PHP | 507 lines | 349 code | 26 blank | 132 comment | 14 complexity | f9d3658f9cac4593605ccdd04c15bbec MD5 | raw file
Possible License(s): LGPL-3.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * This file is part of PHP_ChangeCoverage.
  4. *
  5. * PHP Version 5
  6. *
  7. * Copyright (c) 2010, Manuel Pichler <mapi@pdepend.org>.
  8. * All rights reserved.
  9. *
  10. * Redistribution and use in source and binary forms, with or without
  11. * modification, are permitted provided that the following conditions
  12. * are met:
  13. *
  14. * * Redistributions of source code must retain the above copyright
  15. * notice, this list of conditions and the following disclaimer.
  16. *
  17. * * Redistributions in binary form must reproduce the above copyright
  18. * notice, this list of conditions and the following disclaimer in
  19. * the documentation and/or other materials provided with the
  20. * distribution.
  21. *
  22. * * Neither the name of Manuel Pichler nor the names of his
  23. * contributors may be used to endorse or promote products derived
  24. * from this software without specific prior written permission.
  25. *
  26. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  27. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  28. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  29. * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  30. * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  31. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  32. * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  33. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  34. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  35. * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  36. * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  37. * POSSIBILITY OF SUCH DAMAGE.
  38. *
  39. * @category QualityAssurance
  40. * @package PHP_ChangeCoverage
  41. * @subpackage TextUI
  42. * @author Manuel Pichler <mapi@pdepend.org>
  43. * @copyright 2010 Manuel Pichler. All rights reserved.
  44. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  45. * @version SVN: $Id$
  46. * @link http://pdepend.org/
  47. */
  48. /**
  49. * Command line interface for the php change coverage tool.
  50. *
  51. * @category QualityAssurance
  52. * @package PHP_ChangeCoverage
  53. * @subpackage TextUI
  54. * @author Manuel Pichler <mapi@pdepend.org>
  55. * @copyright 2010 Manuel Pichler. All rights reserved.
  56. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  57. * @version Release: @package_version@
  58. * @link http://pdepend.org/
  59. */
  60. class PHP_ChangeCoverage_TextUI_Command
  61. {
  62. /**
  63. * Path to the temporary clover xml log file.
  64. *
  65. * @var string
  66. */
  67. private $temporaryClover = null;
  68. /**
  69. * Path to the clover xml log file, specified by the user.
  70. *
  71. * @var string
  72. */
  73. private $coverageClover = null;
  74. /**
  75. * Path to the html report directory, specified by the user.
  76. *
  77. * @var string
  78. */
  79. private $coverageHtml = null;
  80. /**
  81. * Path to the temp- and cache-directory, used by php change coverage.
  82. *
  83. * @var string
  84. */
  85. private $tempDirectory = null;
  86. /**
  87. * Timestamp that represents the lower bound of changes.
  88. *
  89. * @var integer
  90. */
  91. private $modifiedSince = 0;
  92. /**
  93. * The PHPUnit cli tool to use.
  94. *
  95. * @var string
  96. */
  97. private $phpunitBinary = 'phpunit';
  98. /**
  99. * Should the coverage report contain unmodified lines as covered?
  100. *
  101. * @var boolean
  102. */
  103. private $unmodifiedAsCovered = false;
  104. /**
  105. * The coverage report factory to use.
  106. *
  107. * @var PHP_ChangeCoverage_Report_Factory
  108. */
  109. private $reportFactory = null;
  110. /**
  111. * Constructs a new command instance and sets some system default values.
  112. */
  113. public function __construct()
  114. {
  115. $this->modifiedSince = time() - ( 60 * 86400 );
  116. $this->tempDirectory = sys_get_temp_dir() . '/php-change-coverage/';
  117. if ( stripos( PHP_OS, 'win' ) === 0 )
  118. {
  119. $this->phpunitBinary .= '.bat';
  120. }
  121. }
  122. /**
  123. * First runs PHPUnit and then post processes the generated coverage data
  124. * to calculate the change coverage.
  125. *
  126. * @param array(string) $argv The raw command line arguments.
  127. *
  128. * @return integer
  129. */
  130. public function run( array $argv )
  131. {
  132. $this->printVersionString();
  133. try
  134. {
  135. $this->handleArguments( $argv );
  136. $arguments = $this->extractPhpunitArguments( $argv );
  137. }
  138. catch ( InvalidArgumentException $e )
  139. {
  140. $exception = $e->getMessage();
  141. $arguments = array( '--help' );
  142. }
  143. $phpunit = new PHP_ChangeCoverage_PHPUnit( $this->phpunitBinary );
  144. $phpunit->run( $arguments );
  145. if ( $phpunit->isHelp() )
  146. {
  147. $this->writeLine();
  148. $this->writeLine();
  149. $this->writeLine( 'Additional options added by PHP_ChangeCoverage' );
  150. $this->writeLine();
  151. $this->writeLine( ' --temp-dir Temporary directory for generated runtime data.' );
  152. $this->writeLine( ' --phpunit-binary Optional path to phpunit\'s binary.' );
  153. $this->writeLine( ' --modified-since Cover only lines that were changed since this date.' );
  154. $this->writeLine( ' This option accepts textual date expressions.' );
  155. $this->writeLine( ' --unmodified-as-covered Mark all unmodified lines as covered.' );
  156. if ( isset( $exception ) )
  157. {
  158. $this->writeLine();
  159. $this->writeLine( $exception );
  160. return 2;
  161. }
  162. }
  163. else if ( file_exists( $this->temporaryClover ) )
  164. {
  165. PHP_Timer::start();
  166. $report = $this->createCoverageReport();
  167. $codeCoverage = $this->rebuildCoverageData( $report );
  168. $this->writeCoverageClover( $codeCoverage );
  169. $this->writeCoverageHtml( $codeCoverage );
  170. PHP_Timer::stop();
  171. $this->writeLine( PHP_Timer::resourceUsage() );
  172. unlink( $this->temporaryClover );
  173. }
  174. return $phpunit->getExitCode();
  175. }
  176. /**
  177. * Outputs the PHP_ChangeCoverage version string.
  178. *
  179. * @return void
  180. */
  181. protected function printVersionString()
  182. {
  183. $this->writeLine( 'PHP_ChangeCoverage @package_version@ by Manuel Pichler' );
  184. $this->write( ' utilizes ' );
  185. }
  186. /**
  187. * This method extracts the command line arguments that belong to the change
  188. * coverage tool.
  189. *
  190. * @param array(string) $argv The raw command line arguments.
  191. *
  192. * @return void
  193. * @throws InvalidArgumentException When one of the passed command line
  194. * values has an unexpected or incorrect format.
  195. */
  196. protected function handleArguments( array $argv )
  197. {
  198. if ( is_int( $i = array_search( '--temp-dir', $argv ) ) )
  199. {
  200. $this->tempDirectory = $this->parseTemporaryDirectory( $argv[$i + 1] );
  201. }
  202. else
  203. {
  204. $this->tempDirectory = $this->parseTemporaryDirectory( $this->tempDirectory );
  205. }
  206. $temporaryClover = $this->tempDirectory . '/' . uniqid( '~ccov-' ) . '.xml';
  207. // Tests
  208. if ( is_int( $i = array_search( '--coverage-clover', $argv ) ) )
  209. {
  210. $this->temporaryClover = $temporaryClover;
  211. $this->coverageClover = $argv[$i + 1];
  212. }
  213. if ( is_int( $i = array_search( '--coverage-html', $argv ) ) )
  214. {
  215. $this->temporaryClover = $temporaryClover;
  216. $this->coverageHtml = $argv[$i + 1];
  217. }
  218. if ( is_int( $i = array_search( '--modified-since', $argv ) ) )
  219. {
  220. $this->modifiedSince = $this->parseModifiedSince( $argv[$i + 1] );
  221. }
  222. if ( is_int( $i = array_search( '--phpunit-binary', $argv ) ) )
  223. {
  224. $this->phpunitBinary = $this->parsePhpunitBinary( $argv[$i + 1] );
  225. }
  226. if ( is_int( $i = array_search( '--unmodified-as-covered', $argv ) ) )
  227. {
  228. $this->unmodifiedAsCovered = true;
  229. }
  230. }
  231. /**
  232. * Parses the temporary directory that was specified by the user.
  233. *
  234. * @param string $directory Temporary directory provided by the user.
  235. *
  236. * @return string
  237. */
  238. protected function parseTemporaryDirectory( $directory )
  239. {
  240. if ( false === file_exists( $directory ) )
  241. {
  242. mkdir( $directory, 0755, true );
  243. }
  244. if ( is_dir( $directory ) )
  245. {
  246. return $directory;
  247. }
  248. throw new InvalidArgumentException( "Cannot find temp directory: '{$directory}." );
  249. }
  250. /**
  251. * This method parses a user specified start date and returns it's unix
  252. * timestamp representation. The current implementation of this method
  253. * only utilizes PHP's native <b>strtotime()</b> function to parse user
  254. * input.
  255. *
  256. * @param string $modified The user specified start date for modifications.
  257. *
  258. * @return integer
  259. * @throws InvalidArgumentException When the given modified expression
  260. * cannot be parsed by strtotime().
  261. * @todo Support other formats like "3m 15d 12h".
  262. */
  263. protected function parseModifiedSince( $modified )
  264. {
  265. if ( is_int( $timestamp = strtotime( $modified ) ) )
  266. {
  267. return $timestamp;
  268. }
  269. throw new InvalidArgumentException( "Cannot parse modified since: '{$modified}'." );
  270. }
  271. /**
  272. * Handles a user specified phpunit cli tool.
  273. *
  274. * @param string $phpunit The user specified PHPUnit cli tool.
  275. *
  276. * @return string
  277. * @throws InvalidArgumentException When the given binary does not exist.
  278. */
  279. protected function parsePhpunitBinary( $phpunit )
  280. {
  281. if ( file_exists( $phpunit ) )
  282. {
  283. return $phpunit;
  284. }
  285. throw new InvalidArgumentException( "Cannot find phpunit binary: '{$phpunit}'." );
  286. }
  287. /**
  288. * This method extracts all those arguments that are relevant for the nested
  289. * phpunit process.
  290. *
  291. * @param array(string) $argv The raw arguments passed to php change coverage.
  292. *
  293. * @return array(string)
  294. * @todo Move this into a separate PHPUnitBinary class.
  295. */
  296. protected function extractPhpunitArguments( array $argv )
  297. {
  298. $remove = array(
  299. '--coverage-clover' => true,
  300. '--coverage-html' => true,
  301. '--temp-dir' => true,
  302. '--modified-since' => true,
  303. '--phpunit-binary' => true,
  304. '--unmodified-as-covered' => false,
  305. );
  306. $arguments = array();
  307. for ( $i = 1; $i < count( $argv ); ++$i )
  308. {
  309. if ( isset( $remove[$argv[$i]] ) )
  310. {
  311. if ( $remove[$argv[$i]] )
  312. {
  313. ++$i;
  314. }
  315. }
  316. else
  317. {
  318. $arguments[$i] = $argv[$i];
  319. }
  320. }
  321. if ( $this->temporaryClover )
  322. {
  323. array_unshift( $arguments, '--coverage-clover', $this->temporaryClover );
  324. }
  325. return $arguments;
  326. }
  327. /**
  328. * Sets a coverage report factory to use.
  329. *
  330. * @param PHP_ChangeCoverage_Report_Factory $factory The coverage report factory.
  331. *
  332. * @return void
  333. */
  334. public function setReportFactory( PHP_ChangeCoverage_Report_Factory $factory )
  335. {
  336. $this->reportFactory = $factory;
  337. }
  338. /**
  339. * Returns the configured coverage report factory. If no factory was
  340. * configured, this method will create an instance of the default factory
  341. * implementation.
  342. *
  343. * @return PHP_ChangeCoverage_Report_Factory
  344. */
  345. protected function getReportFactory()
  346. {
  347. if ( $this->reportFactory === null )
  348. {
  349. $this->reportFactory = new PHP_ChangeCoverage_Report_Factory();
  350. }
  351. return $this->reportFactory;
  352. }
  353. /**
  354. * Creates a coverage report from a previously generated xml log file.
  355. *
  356. * @return PHP_ChangeCoverage_Report
  357. */
  358. protected function createCoverageReport()
  359. {
  360. return $this->getReportFactory()->createReport( $this->temporaryClover );
  361. }
  362. /**
  363. * This method takes a coverage report and then rebuilds the raw coverage
  364. * data based on the report data and the change history of the covered files.
  365. *
  366. * @param PHP_ChangeCoverage_Report $report The coverage report data.
  367. *
  368. * @return PHP_CodeCoverage
  369. */
  370. protected function rebuildCoverageData( PHP_ChangeCoverage_Report $report )
  371. {
  372. $codeCoverage = new PHP_CodeCoverage();
  373. $factory = new PHP_ChangeCoverage_ChangeSet_Factory();
  374. vcsCache::initialize( $this->tempDirectory );
  375. $this->writeLine();
  376. $this->writeLine( 'Collecting commits and meta data, this may take a moment.' );
  377. $this->writeLine();
  378. $xdebug = new PHP_ChangeCoverage_Xdebug();
  379. if ( $this->unmodifiedAsCovered )
  380. {
  381. $xdebug->setUnmodifiedAsCovered();
  382. }
  383. foreach ( $report->getFiles() as $file )
  384. {
  385. $changeSet = $factory->create( $file );
  386. $changeSet->setStartDate( $this->modifiedSince );
  387. foreach ( $xdebug->generateData( $changeSet->calculate() ) as $data )
  388. {
  389. $codeCoverage->append( $data, md5( microtime() ) );
  390. }
  391. }
  392. return $codeCoverage;
  393. }
  394. /**
  395. * This method generates a xml coverage report compatible with reports
  396. * generated by clover.
  397. *
  398. * @param PHP_CodeCoverage $coverage The raw coverage data.
  399. *
  400. * @return void
  401. */
  402. protected function writeCoverageClover( PHP_CodeCoverage $coverage )
  403. {
  404. if ( $this->coverageClover )
  405. {
  406. $this->writeLine( 'Writing change coverage data to XML file, this may take a moment.' );
  407. $this->writeLine();
  408. $clover = new PHP_CodeCoverage_Report_Clover();
  409. $clover->process( $coverage, $this->coverageClover );
  410. }
  411. }
  412. /**
  413. * This method generates a html coverage report.
  414. *
  415. * @param PHP_CodeCoverage $coverage The raw coverage data.
  416. *
  417. * @return void
  418. */
  419. protected function writeCoverageHtml( PHP_CodeCoverage $coverage )
  420. {
  421. if ( $this->coverageHtml )
  422. {
  423. $this->writeLine( 'Writing change coverage report, this may take a moment.' );
  424. $this->writeLine();
  425. $html = new PHP_CodeCoverage_Report_HTML(
  426. 'Coverage Report for files modified since ' . date( 'Y/m/d', $this->modifiedSince ),
  427. 'UTF-8',
  428. false,
  429. false,
  430. 35,
  431. 70,
  432. ' post processed by PHP_ChangeCoverage'
  433. );
  434. $html->process( $coverage, $this->coverageHtml );
  435. }
  436. }
  437. /**
  438. * Writes the given data string to STDOUT and appends a line feed.
  439. *
  440. * @param string $data Any data that should be send to STDOUT.
  441. *
  442. * @return void
  443. */
  444. protected function writeLine( $data = '' )
  445. {
  446. $this->write( $data . PHP_EOL );
  447. }
  448. /**
  449. * Writes the given data string to STDOUT.
  450. *
  451. * @param string $data Any data that should be send to STDOUT.
  452. *
  453. * @return void
  454. */
  455. protected function write( $data )
  456. {
  457. echo $data;
  458. }
  459. }