HtmlCoverageReport.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. /**
  3. * Generates code coverage reports in HTML from data obtained from PHPUnit
  4. *
  5. * PHP5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.TestSuite.Coverage
  16. * @since CakePHP(tm) v 2.0
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. PHP_CodeCoverage_Filter::getInstance()->addFileToBlacklist(__FILE__, 'DEFAULT');
  20. App::uses('BaseCoverageReport', 'TestSuite/Coverage');
  21. class HtmlCoverageReport extends BaseCoverageReport {
  22. /**
  23. * Generates report html to display.
  24. *
  25. * @return string compiled html report.
  26. */
  27. public function report() {
  28. $pathFilter = $this->getPathFilter();
  29. $coverageData = $this->filterCoverageDataByPath($pathFilter);
  30. if (empty($coverageData)) {
  31. return '<h3>No files to generate coverage for</h3>';
  32. }
  33. $output = $this->coverageScript();
  34. $output .= <<<HTML
  35. <h3>Code coverage results
  36. <a href="#" onclick="coverage_toggle_all()" class="coverage-toggle">Toggle all files</a>
  37. </h3>
  38. HTML;
  39. foreach ($coverageData as $file => $coverageData) {
  40. $fileData = file($file);
  41. $output .= $this->generateDiff($file, $fileData, $coverageData);
  42. }
  43. return $output;
  44. }
  45. /**
  46. * Generates an HTML diff for $file based on $coverageData.
  47. *
  48. * @param string $filename Name of the file having coverage generated
  49. * @param array $fileLines File data as an array. See file() for how to get one of these.
  50. * @param array $coverageData Array of coverage data to use to generate HTML diffs with
  51. * @return string HTML diff.
  52. */
  53. public function generateDiff($filename, $fileLines, $coverageData) {
  54. $output = '';
  55. $diff = array();
  56. list($covered, $total) = $this->_calculateCoveredLines($fileLines, $coverageData);
  57. //shift line numbers forward one;
  58. array_unshift($fileLines, ' ');
  59. unset($fileLines[0]);
  60. foreach ($fileLines as $lineno => $line) {
  61. $class = 'ignored';
  62. $coveringTests = array();
  63. if (isset($coverageData[$lineno]) && is_array($coverageData[$lineno])) {
  64. $coveringTests = array();
  65. foreach ($coverageData[$lineno] as $test) {
  66. $testReflection = new ReflectionClass(current(explode('::', $test['id'])));
  67. $this->_testNames[] = $this->_guessSubjectName($testReflection);
  68. $coveringTests[] = $test['id'];
  69. }
  70. $class = 'covered';
  71. } elseif (isset($coverageData[$lineno]) && $coverageData[$lineno] === -1) {
  72. $class = 'uncovered';
  73. } elseif (isset($coverageData[$lineno]) && $coverageData[$lineno] === -2) {
  74. $class .= ' dead';
  75. }
  76. $diff[] = $this->_paintLine($line, $lineno, $class, $coveringTests);
  77. }
  78. $percentCovered = 100;
  79. if ($total > 0) {
  80. $percentCovered = round(100 * $covered / $total, 2);
  81. }
  82. $output .= $this->coverageHeader($filename, $percentCovered);
  83. $output .= implode("", $diff);
  84. $output .= $this->coverageFooter();
  85. return $output;
  86. }
  87. /**
  88. * Guess the classname the test was for based on the test case filename.
  89. *
  90. * @param ReflectionClass $testReflection.
  91. * @return string Possible test subject name.
  92. */
  93. protected function _guessSubjectName($testReflection) {
  94. $basename = basename($testReflection->getFilename());
  95. if (strpos($basename, '.test') !== false) {
  96. list($subject, ) = explode('.', $basename, 2);
  97. return $subject;
  98. }
  99. $subject = str_replace('Test.php', '', $basename);
  100. return $subject;
  101. }
  102. /**
  103. * Renders the html for a single line in the html diff.
  104. *
  105. * @return void
  106. */
  107. protected function _paintLine($line, $linenumber, $class, $coveringTests) {
  108. $coveredBy = '';
  109. if (!empty($coveringTests)) {
  110. $coveredBy = "Covered by:\n";
  111. foreach ($coveringTests as $test) {
  112. $coveredBy .= $test . "\n";
  113. }
  114. }
  115. return sprintf(
  116. '<div class="code-line %s" title="%s"><span class="line-num">%s</span><span class="content">%s</span></div>',
  117. $class,
  118. $coveredBy,
  119. $linenumber,
  120. htmlspecialchars($line)
  121. );
  122. }
  123. /**
  124. * generate some javascript for the coverage report.
  125. *
  126. * @return void
  127. */
  128. public function coverageScript() {
  129. return <<<HTML
  130. <script type="text/javascript">
  131. function coverage_show_hide(selector) {
  132. var element = document.getElementById(selector);
  133. element.style.display = (element.style.display == 'none') ? '' : 'none';
  134. }
  135. function coverage_toggle_all () {
  136. var divs = document.querySelectorAll('div.coverage-container');
  137. var i = divs.length;
  138. while (i--) {
  139. if (divs[i] && divs[i].className.indexOf('primary') == -1) {
  140. divs[i].style.display = (divs[i].style.display == 'none') ? '' : 'none';
  141. }
  142. }
  143. }
  144. </script>
  145. HTML;
  146. }
  147. /**
  148. * Generate an HTML snippet for coverage headers
  149. *
  150. * @return void
  151. */
  152. public function coverageHeader($filename, $percent) {
  153. $filename = basename($filename);
  154. list($file, $ext) = explode('.', $filename);
  155. $display = in_array($file, $this->_testNames) ? 'block' : 'none';
  156. $primary = $display == 'block' ? 'primary' : '';
  157. return <<<HTML
  158. <div class="coverage-container $primary" style="display:$display;">
  159. <h4>
  160. <a href="#coverage-$filename" onclick="coverage_show_hide('coverage-$filename');">
  161. $filename Code coverage: $percent%
  162. </a>
  163. </h4>
  164. <div class="code-coverage-results" id="coverage-$filename" style="display:none;">
  165. <pre>
  166. HTML;
  167. }
  168. /**
  169. * Generate an HTML snippet for coverage footers
  170. *
  171. * @return void
  172. */
  173. public function coverageFooter() {
  174. return "</pre></div></div>";
  175. }
  176. }