1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18:
19: App::uses('BaseCoverageReport', 'TestSuite/Coverage');
20:
21: 22: 23: 24: 25:
26: class HtmlCoverageReport extends BaseCoverageReport {
27:
28: 29: 30: 31: 32:
33: protected $_total = 0;
34:
35: 36: 37: 38: 39:
40: protected $_covered = 0;
41:
42: 43: 44: 45: 46:
47: public function report() {
48: $pathFilter = $this->getPathFilter();
49: $coverageData = $this->filterCoverageDataByPath($pathFilter);
50: if (empty($coverageData)) {
51: return '<h3>No files to generate coverage for</h3>';
52: }
53: $output = $this->coverageScript();
54: $output .= <<<HTML
55: <h3>Code coverage results
56: <a href="#" onclick="coverage_toggle_all()" class="coverage-toggle">Toggle all files</a>
57: </h3>
58: HTML;
59: foreach ($coverageData as $file => $coverageData) {
60: $fileData = file($file);
61: $output .= $this->generateDiff($file, $fileData, $coverageData);
62: }
63:
64: $percentCovered = 100;
65: if ($this->_total > 0) {
66: $percentCovered = round(100 * $this->_covered / $this->_total, 2);
67: }
68: $output .= '<div class="total">Overall coverage: <span class="coverage">' . $percentCovered . '%</span></div>';
69: return $output;
70: }
71:
72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84:
85: public function generateDiff($filename, $fileLines, $coverageData) {
86: $output = '';
87: $diff = array();
88:
89: list($covered, $total) = $this->_calculateCoveredLines($fileLines, $coverageData);
90: $this->_covered += $covered;
91: $this->_total += $total;
92:
93:
94: array_unshift($fileLines, ' ');
95: unset($fileLines[0]);
96:
97: foreach ($fileLines as $lineno => $line) {
98: $class = 'ignored';
99: $coveringTests = array();
100: if (!empty($coverageData[$lineno]) && is_array($coverageData[$lineno])) {
101: $coveringTests = array();
102: foreach ($coverageData[$lineno] as $test) {
103: $class = (is_array($test) && isset($test['id'])) ? $test['id'] : $test;
104: $testReflection = new ReflectionClass(current(explode('::', $class)));
105: $this->_testNames[] = $this->_guessSubjectName($testReflection);
106: $coveringTests[] = $class;
107: }
108: $class = 'covered';
109: } elseif (isset($coverageData[$lineno]) && ($coverageData[$lineno] === -1 || $coverageData[$lineno] === array())) {
110: $class = 'uncovered';
111: } elseif (array_key_exists($lineno, $coverageData) && ($coverageData[$lineno] === -2 || $coverageData[$lineno] === null)) {
112: $class .= ' dead';
113: }
114: $diff[] = $this->_paintLine($line, $lineno, $class, $coveringTests);
115: }
116:
117: $percentCovered = 100;
118: if ($total > 0) {
119: $percentCovered = round(100 * $covered / $total, 2);
120: }
121: $output .= $this->coverageHeader($filename, $percentCovered);
122: $output .= implode("", $diff);
123: $output .= $this->coverageFooter();
124: return $output;
125: }
126:
127: 128: 129: 130: 131: 132:
133: protected function _guessSubjectName($testReflection) {
134: $basename = basename($testReflection->getFilename());
135: if (strpos($basename, '.test') !== false) {
136: list($subject, ) = explode('.', $basename, 2);
137: return $subject;
138: }
139: $subject = str_replace('Test.php', '', $basename);
140: return $subject;
141: }
142:
143: 144: 145: 146: 147: 148: 149: 150: 151:
152: protected function _paintLine($line, $linenumber, $class, $coveringTests) {
153: $coveredBy = '';
154: if (!empty($coveringTests)) {
155: $coveredBy = "Covered by:\n";
156: foreach ($coveringTests as $test) {
157: $coveredBy .= $test . "\n";
158: }
159: }
160:
161: return sprintf(
162: '<div class="code-line %s" title="%s"><span class="line-num">%s</span><span class="content">%s</span></div>',
163: $class,
164: $coveredBy,
165: $linenumber,
166: htmlspecialchars($line)
167: );
168: }
169:
170: 171: 172: 173: 174:
175: public function coverageScript() {
176: return <<<HTML
177: <script type="text/javascript">
178: function coverage_show_hide(selector) {
179: var element = document.getElementById(selector);
180: element.style.display = (element.style.display === 'none') ? '' : 'none';
181: }
182: function coverage_toggle_all() {
183: var divs = document.querySelectorAll('div.coverage-container');
184: var i = divs.length;
185: while (i--) {
186: if (divs[i] && divs[i].className.indexOf('primary') == -1) {
187: divs[i].style.display = (divs[i].style.display === 'none') ? '' : 'none';
188: }
189: }
190: }
191: </script>
192: HTML;
193: }
194:
195: 196: 197: 198: 199: 200: 201:
202: public function coverageHeader($filename, $percent) {
203: $hash = md5($filename);
204: $filename = basename($filename);
205: list($file) = explode('.', $filename);
206: $display = in_array($file, $this->_testNames) ? 'block' : 'none';
207: $primary = $display === 'block' ? 'primary' : '';
208: return <<<HTML
209: <div class="coverage-container $primary" style="display:$display;">
210: <h4>
211: <a href="#coverage-$filename-$hash" onclick="coverage_show_hide('coverage-$filename-$hash');">
212: $filename Code coverage: $percent%
213: </a>
214: </h4>
215: <div class="code-coverage-results" id="coverage-$filename-$hash" style="display:none;">
216: <pre>
217: HTML;
218: }
219:
220: 221: 222: 223: 224:
225: public function coverageFooter() {
226: return "</pre></div></div>";
227: }
228:
229: }
230: