1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
17:
18: App::uses('CakeBaseReporter', 'TestSuite/Reporter');
19:
20: 21: 22: 23: 24: 25:
26: class CakeHtmlReporter extends CakeBaseReporter {
27:
28: 29: 30: 31: 32: 33:
34: public function paintHeader() {
35: $this->_headerSent = true;
36: $this->sendContentType();
37: $this->sendNoCacheHeaders();
38: $this->paintDocumentStart();
39: $this->paintTestMenu();
40: echo "<ul class='tests'>\n";
41: }
42:
43: 44: 45: 46: 47:
48: public function sendContentType() {
49: if (!headers_sent()) {
50: header('Content-Type: text/html; charset=' . Configure::read('App.encoding'));
51: }
52: }
53:
54: 55: 56: 57: 58:
59: public function paintDocumentStart() {
60: ob_start();
61: $baseDir = $this->params['baseDir'];
62: include CAKE . 'TestSuite' . DS . 'templates' . DS . 'header.php';
63: }
64:
65: 66: 67: 68: 69: 70:
71: public function paintTestMenu() {
72: $cases = $this->baseUrl() . '?show=cases';
73: $plugins = App::objects('plugin', null, false);
74: sort($plugins);
75: include CAKE . 'TestSuite' . DS . 'templates' . DS . 'menu.php';
76: }
77:
78: 79: 80: 81: 82:
83: public function testCaseList() {
84: $testCases = parent::testCaseList();
85: $core = $this->params['core'];
86: $plugin = $this->params['plugin'];
87:
88: $buffer = "<h3>App Test Cases:</h3>\n<ul>";
89: $urlExtra = null;
90: if ($core) {
91: $buffer = "<h3>Core Test Cases:</h3>\n<ul>";
92: $urlExtra = '&core=true';
93: } elseif ($plugin) {
94: $buffer = "<h3>" . Inflector::humanize($plugin) . " Test Cases:</h3>\n<ul>";
95: $urlExtra = '&plugin=' . $plugin;
96: }
97:
98: if (count($testCases) < 1) {
99: $buffer .= "<strong>EMPTY</strong>";
100: }
101:
102: foreach ($testCases as $testCase) {
103: $title = explode(DS, str_replace('.test.php', '', $testCase));
104: $title[count($title) - 1] = Inflector::camelize($title[count($title) - 1]);
105: $title = implode(' / ', $title);
106: $buffer .= "<li><a href='" . $this->baseUrl() . "?case=" . urlencode($testCase) . $urlExtra . "'>" . $title . "</a></li>\n";
107: }
108: $buffer .= "</ul>\n";
109: echo $buffer;
110: }
111:
112: 113: 114: 115: 116: 117: 118:
119: public function sendNoCacheHeaders() {
120: if (!headers_sent()) {
121: header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
122: header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
123: header("Cache-Control: no-store, no-cache, must-revalidate");
124: header("Cache-Control: post-check=0, pre-check=0", false);
125: header("Pragma: no-cache");
126: }
127: }
128:
129: 130: 131: 132: 133: 134: 135:
136: public function paintFooter($result) {
137: ob_end_flush();
138: $colour = ($result->failureCount() + $result->errorCount() > 0 ? "red" : "green");
139: echo "</ul>\n";
140: echo "<div style=\"";
141: echo "padding: 8px; margin: 1em 0; background-color: $colour; color: white;";
142: echo "\">";
143: echo ($result->count() - $result->skippedCount()) . "/" . $result->count();
144: echo " test methods complete:\n";
145: echo "<strong>" . count($result->passed()) . "</strong> passes, ";
146: echo "<strong>" . $result->failureCount() . "</strong> fails, ";
147: echo "<strong>" . $this->numAssertions . "</strong> assertions and ";
148: echo "<strong>" . $result->errorCount() . "</strong> exceptions.";
149: echo "</div>\n";
150: echo '<div style="padding:0 0 5px;">';
151: echo '<p><strong>Time:</strong> ' . $result->time() . ' seconds</p>';
152: echo '<p><strong>Peak memory:</strong> ' . number_format(memory_get_peak_usage()) . ' bytes</p>';
153: echo $this->_paintLinks();
154: echo '</div>';
155: if (isset($this->params['codeCoverage']) && $this->params['codeCoverage']) {
156: $coverage = $result->getCodeCoverage();
157: if (method_exists($coverage, 'getSummary')) {
158: $report = $coverage->getSummary();
159: echo $this->paintCoverage($report);
160: }
161: if (method_exists($coverage, 'getData')) {
162: $report = $coverage->getData();
163: echo $this->paintCoverage($report);
164: }
165: }
166: $this->paintDocumentEnd();
167: }
168:
169: 170: 171: 172: 173: 174:
175: public function paintCoverage(array $coverage) {
176: App::uses('HtmlCoverageReport', 'TestSuite/Coverage');
177:
178: $reporter = new HtmlCoverageReport($coverage, $this);
179: echo $reporter->report();
180: }
181:
182: 183: 184: 185: 186:
187: protected function _paintLinks() {
188: $show = $query = array();
189: if (!empty($this->params['case'])) {
190: $show['show'] = 'cases';
191: }
192:
193: if (!empty($this->params['core'])) {
194: $show['core'] = $query['core'] = 'true';
195: }
196: if (!empty($this->params['plugin'])) {
197: $show['plugin'] = $query['plugin'] = $this->params['plugin'];
198: }
199: if (!empty($this->params['case'])) {
200: $query['case'] = $this->params['case'];
201: }
202: list($show, $query) = $this->_getQueryLink();
203:
204: echo "<p><a href='" . $this->baseUrl() . $show . "'>Run more tests</a> | <a href='" . $this->baseUrl() . $query . "&show_passes=1'>Show Passes</a> | \n";
205: echo "<a href='" . $this->baseUrl() . $query . "&debug=1'>Enable Debug Output</a> | \n";
206: echo "<a href='" . $this->baseUrl() . $query . "&code_coverage=true'>Analyze Code Coverage</a> | \n";
207: echo "<a href='" . $this->baseUrl() . $query . "&code_coverage=true&show_passes=1&debug=1'>All options enabled</a></p>\n";
208: }
209:
210: 211: 212: 213: 214: 215:
216: protected function _queryString($url) {
217: $out = '?';
218: $params = array();
219: foreach ($url as $key => $value) {
220: $params[] = "$key=$value";
221: }
222: $out .= implode('&', $params);
223: return $out;
224: }
225:
226: 227: 228: 229: 230:
231: public function paintDocumentEnd() {
232: $baseDir = $this->params['baseDir'];
233: include CAKE . 'TestSuite' . DS . 'templates' . DS . 'footer.php';
234: if (ob_get_length()) {
235: ob_end_flush();
236: }
237: }
238:
239: 240: 241: 242: 243: 244: 245: 246: 247: 248:
249: public function paintFail($message, $test) {
250: $trace = $this->_getStackTrace($message);
251: $className = get_class($test);
252: $testName = $className . '::' . $test->getName() . '()';
253:
254: $actualMsg = $expectedMsg = null;
255: if (method_exists($message, 'getComparisonFailure')) {
256: $failure = $message->getComparisonFailure();
257: if (is_object($failure)) {
258: $actualMsg = $failure->getActualAsString();
259: $expectedMsg = $failure->getExpectedAsString();
260: }
261: }
262:
263: echo "<li class='fail'>\n";
264: echo "<span>Failed</span>";
265: echo "<div class='msg'><pre>" . $this->_htmlEntities($message->toString());
266:
267: if ((is_string($actualMsg) && is_string($expectedMsg)) || (is_array($actualMsg) && is_array($expectedMsg))) {
268: echo "<br />" . $this->_htmlEntities(PHPUnit_Util_Diff::diff($expectedMsg, $actualMsg));
269: }
270:
271: echo "</pre></div>\n";
272: echo "<div class='msg'>" . __d('cake_dev', 'Test case: %s', $testName) . "</div>\n";
273: if (strpos($className, "PHPUnit_") === false) {
274: list($show, $query) = $this->_getQueryLink();
275: echo "<div class='msg'><a href='" . $this->baseUrl() . $query . "&filter=" . $test->getName() . "'>" . __d('cake_dev', 'Rerun only this test: %s', $testName) . "</a></div>\n";
276: }
277: echo "<div class='msg'>" . __d('cake_dev', 'Stack trace:') . '<br />' . $trace . "</div>\n";
278: echo "</li>\n";
279: }
280:
281: 282: 283: 284: 285: 286: 287: 288: 289:
290: public function paintPass(PHPUnit_Framework_Test $test, $time = null) {
291: if (isset($this->params['showPasses']) && $this->params['showPasses']) {
292: echo "<li class='pass'>\n";
293: echo "<span>Passed</span> ";
294:
295: echo "<br />" . $this->_htmlEntities($test->getName()) . " ($time seconds)\n";
296: echo "</li>\n";
297: }
298: }
299:
300: 301: 302: 303: 304: 305: 306:
307: public function paintException($message, $test) {
308: $trace = $this->_getStackTrace($message);
309: $testName = get_class($test) . '(' . $test->getName() . ')';
310:
311: echo "<li class='fail'>\n";
312: echo "<span>" . get_class($message) . "</span>";
313:
314: echo "<div class='msg'>" . $this->_htmlEntities($message->getMessage()) . "</div>\n";
315: echo "<div class='msg'>" . __d('cake_dev', 'Test case: %s', $testName) . "</div>\n";
316: echo "<div class='msg'>" . __d('cake_dev', 'Stack trace:') . '<br />' . $trace . "</div>\n";
317: echo "</li>\n";
318: }
319:
320: 321: 322: 323: 324: 325: 326:
327: public function paintSkip($message, $test) {
328: echo "<li class='skipped'>\n";
329: echo "<span>Skipped</span> ";
330: echo $test->getName() . ': ' . $this->_htmlEntities($message->getMessage());
331: echo "</li>\n";
332: }
333:
334: 335: 336: 337: 338: 339:
340: public function paintFormattedMessage($message) {
341: echo '<pre>' . $this->_htmlEntities($message) . '</pre>';
342: }
343:
344: 345: 346: 347: 348: 349:
350: protected function _htmlEntities($message) {
351: return htmlentities($message, ENT_COMPAT, $this->_characterSet);
352: }
353:
354: 355: 356: 357: 358: 359:
360: protected function _getStackTrace(Exception $e) {
361: $trace = $e->getTrace();
362: $out = array();
363: foreach ($trace as $frame) {
364: if (isset($frame['file']) && isset($frame['line'])) {
365: $out[] = $frame['file'] . ' : ' . $frame['line'];
366: } elseif (isset($frame['class']) && isset($frame['function'])) {
367: $out[] = $frame['class'] . '::' . $frame['function'];
368: } else {
369: $out[] = '[internal]';
370: }
371: }
372: return implode('<br />', $out);
373: }
374:
375: 376: 377: 378: 379: 380:
381: public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {
382: if (!$this->_headerSent) {
383: echo $this->paintHeader();
384: }
385: echo '<h2>' . __d('cake_dev', 'Running %s', $suite->getName()) . '</h2>';
386: }
387:
388: 389: 390: 391: 392:
393: protected function _getQueryLink() {
394: $show = $query = array();
395: if (!empty($this->params['case'])) {
396: $show['show'] = 'cases';
397: }
398:
399: if (!empty($this->params['core'])) {
400: $show['core'] = $query['core'] = 'true';
401: }
402: if (!empty($this->params['plugin'])) {
403: $show['plugin'] = $query['plugin'] = $this->params['plugin'];
404: }
405: if (!empty($this->params['case'])) {
406: $query['case'] = $this->params['case'];
407: }
408: if (!empty($this->params['filter'])) {
409: $query['filter'] = $this->params['filter'];
410: }
411: $show = $this->_queryString($show);
412: $query = $this->_queryString($query);
413: return array($show, $query);
414: }
415:
416: }
417: