1<?php 2/* 3 * This file is part of PHPUnit. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11/** 12 * A TestListener that generates a logfile of the test execution in XML markup. 13 * 14 * The XML markup used is the same as the one that is used by the JUnit Ant task. 15 */ 16class PHPUnit_Util_Log_JUnit extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener 17{ 18 /** 19 * @var DOMDocument 20 */ 21 protected $document; 22 23 /** 24 * @var DOMElement 25 */ 26 protected $root; 27 28 /** 29 * @var bool 30 */ 31 protected $logIncompleteSkipped = false; 32 33 /** 34 * @var bool 35 */ 36 protected $writeDocument = true; 37 38 /** 39 * @var DOMElement[] 40 */ 41 protected $testSuites = []; 42 43 /** 44 * @var int[] 45 */ 46 protected $testSuiteTests = [0]; 47 48 /** 49 * @var int[] 50 */ 51 protected $testSuiteAssertions = [0]; 52 53 /** 54 * @var int[] 55 */ 56 protected $testSuiteErrors = [0]; 57 58 /** 59 * @var int[] 60 */ 61 protected $testSuiteFailures = [0]; 62 63 /** 64 * @var int[] 65 */ 66 protected $testSuiteTimes = [0]; 67 68 /** 69 * @var int 70 */ 71 protected $testSuiteLevel = 0; 72 73 /** 74 * @var DOMElement 75 */ 76 protected $currentTestCase = null; 77 78 /** 79 * @var bool 80 */ 81 protected $attachCurrentTestCase = true; 82 83 /** 84 * Constructor. 85 * 86 * @param mixed $out 87 * @param bool $logIncompleteSkipped 88 */ 89 public function __construct($out = null, $logIncompleteSkipped = false) 90 { 91 $this->document = new DOMDocument('1.0', 'UTF-8'); 92 $this->document->formatOutput = true; 93 94 $this->root = $this->document->createElement('testsuites'); 95 $this->document->appendChild($this->root); 96 97 parent::__construct($out); 98 99 $this->logIncompleteSkipped = $logIncompleteSkipped; 100 } 101 102 /** 103 * Flush buffer and close output. 104 */ 105 public function flush() 106 { 107 if ($this->writeDocument === true) { 108 $this->write($this->getXML()); 109 } 110 111 parent::flush(); 112 } 113 114 /** 115 * An error occurred. 116 * 117 * @param PHPUnit_Framework_Test $test 118 * @param Exception $e 119 * @param float $time 120 */ 121 public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) 122 { 123 $this->doAddFault($test, $e, $time, 'error'); 124 $this->testSuiteErrors[$this->testSuiteLevel]++; 125 } 126 127 /** 128 * A warning occurred. 129 * 130 * @param PHPUnit_Framework_Test $test 131 * @param PHPUnit_Framework_Warning $e 132 * @param float $time 133 */ 134 public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time) 135 { 136 if (!$this->logIncompleteSkipped) { 137 return; 138 } 139 140 $this->doAddFault($test, $e, $time, 'warning'); 141 $this->testSuiteFailures[$this->testSuiteLevel]++; 142 } 143 144 /** 145 * A failure occurred. 146 * 147 * @param PHPUnit_Framework_Test $test 148 * @param PHPUnit_Framework_AssertionFailedError $e 149 * @param float $time 150 */ 151 public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) 152 { 153 $this->doAddFault($test, $e, $time, 'failure'); 154 $this->testSuiteFailures[$this->testSuiteLevel]++; 155 } 156 157 /** 158 * Incomplete test. 159 * 160 * @param PHPUnit_Framework_Test $test 161 * @param Exception $e 162 * @param float $time 163 */ 164 public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) 165 { 166 if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { 167 $error = $this->document->createElement( 168 'error', 169 PHPUnit_Util_XML::prepareString( 170 "Incomplete Test\n" . 171 PHPUnit_Util_Filter::getFilteredStacktrace($e) 172 ) 173 ); 174 175 $error->setAttribute('type', get_class($e)); 176 177 $this->currentTestCase->appendChild($error); 178 179 $this->testSuiteErrors[$this->testSuiteLevel]++; 180 } else { 181 $this->attachCurrentTestCase = false; 182 } 183 } 184 185 /** 186 * Risky test. 187 * 188 * @param PHPUnit_Framework_Test $test 189 * @param Exception $e 190 * @param float $time 191 */ 192 public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) 193 { 194 if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { 195 $error = $this->document->createElement( 196 'error', 197 PHPUnit_Util_XML::prepareString( 198 "Risky Test\n" . 199 PHPUnit_Util_Filter::getFilteredStacktrace($e) 200 ) 201 ); 202 203 $error->setAttribute('type', get_class($e)); 204 205 $this->currentTestCase->appendChild($error); 206 207 $this->testSuiteErrors[$this->testSuiteLevel]++; 208 } else { 209 $this->attachCurrentTestCase = false; 210 } 211 } 212 213 /** 214 * Skipped test. 215 * 216 * @param PHPUnit_Framework_Test $test 217 * @param Exception $e 218 * @param float $time 219 */ 220 public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) 221 { 222 if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { 223 $error = $this->document->createElement( 224 'error', 225 PHPUnit_Util_XML::prepareString( 226 "Skipped Test\n" . 227 PHPUnit_Util_Filter::getFilteredStacktrace($e) 228 ) 229 ); 230 231 $error->setAttribute('type', get_class($e)); 232 233 $this->currentTestCase->appendChild($error); 234 235 $this->testSuiteErrors[$this->testSuiteLevel]++; 236 } else { 237 $this->attachCurrentTestCase = false; 238 } 239 } 240 241 /** 242 * A testsuite started. 243 * 244 * @param PHPUnit_Framework_TestSuite $suite 245 */ 246 public function startTestSuite(PHPUnit_Framework_TestSuite $suite) 247 { 248 $testSuite = $this->document->createElement('testsuite'); 249 $testSuite->setAttribute('name', $suite->getName()); 250 251 if (class_exists($suite->getName(), false)) { 252 try { 253 $class = new ReflectionClass($suite->getName()); 254 255 $testSuite->setAttribute('file', $class->getFileName()); 256 } catch (ReflectionException $e) { 257 } 258 } 259 260 if ($this->testSuiteLevel > 0) { 261 $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); 262 } else { 263 $this->root->appendChild($testSuite); 264 } 265 266 $this->testSuiteLevel++; 267 $this->testSuites[$this->testSuiteLevel] = $testSuite; 268 $this->testSuiteTests[$this->testSuiteLevel] = 0; 269 $this->testSuiteAssertions[$this->testSuiteLevel] = 0; 270 $this->testSuiteErrors[$this->testSuiteLevel] = 0; 271 $this->testSuiteFailures[$this->testSuiteLevel] = 0; 272 $this->testSuiteTimes[$this->testSuiteLevel] = 0; 273 } 274 275 /** 276 * A testsuite ended. 277 * 278 * @param PHPUnit_Framework_TestSuite $suite 279 */ 280 public function endTestSuite(PHPUnit_Framework_TestSuite $suite) 281 { 282 $this->testSuites[$this->testSuiteLevel]->setAttribute( 283 'tests', 284 $this->testSuiteTests[$this->testSuiteLevel] 285 ); 286 287 $this->testSuites[$this->testSuiteLevel]->setAttribute( 288 'assertions', 289 $this->testSuiteAssertions[$this->testSuiteLevel] 290 ); 291 292 $this->testSuites[$this->testSuiteLevel]->setAttribute( 293 'failures', 294 $this->testSuiteFailures[$this->testSuiteLevel] 295 ); 296 297 $this->testSuites[$this->testSuiteLevel]->setAttribute( 298 'errors', 299 $this->testSuiteErrors[$this->testSuiteLevel] 300 ); 301 302 $this->testSuites[$this->testSuiteLevel]->setAttribute( 303 'time', 304 sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]) 305 ); 306 307 if ($this->testSuiteLevel > 1) { 308 $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; 309 $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; 310 $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; 311 $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; 312 $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; 313 } 314 315 $this->testSuiteLevel--; 316 } 317 318 /** 319 * A test started. 320 * 321 * @param PHPUnit_Framework_Test $test 322 */ 323 public function startTest(PHPUnit_Framework_Test $test) 324 { 325 $testCase = $this->document->createElement('testcase'); 326 $testCase->setAttribute('name', $test->getName()); 327 328 if ($test instanceof PHPUnit_Framework_TestCase) { 329 $class = new ReflectionClass($test); 330 $methodName = $test->getName(); 331 332 if ($class->hasMethod($methodName)) { 333 $method = $class->getMethod($test->getName()); 334 335 $testCase->setAttribute('class', $class->getName()); 336 $testCase->setAttribute('file', $class->getFileName()); 337 $testCase->setAttribute('line', $method->getStartLine()); 338 } 339 } 340 341 $this->currentTestCase = $testCase; 342 } 343 344 /** 345 * A test ended. 346 * 347 * @param PHPUnit_Framework_Test $test 348 * @param float $time 349 */ 350 public function endTest(PHPUnit_Framework_Test $test, $time) 351 { 352 if ($this->attachCurrentTestCase) { 353 if ($test instanceof PHPUnit_Framework_TestCase) { 354 $numAssertions = $test->getNumAssertions(); 355 $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; 356 357 $this->currentTestCase->setAttribute( 358 'assertions', 359 $numAssertions 360 ); 361 } 362 363 $this->currentTestCase->setAttribute( 364 'time', 365 sprintf('%F', $time) 366 ); 367 368 $this->testSuites[$this->testSuiteLevel]->appendChild( 369 $this->currentTestCase 370 ); 371 372 $this->testSuiteTests[$this->testSuiteLevel]++; 373 $this->testSuiteTimes[$this->testSuiteLevel] += $time; 374 375 if (method_exists($test, 'hasOutput') && $test->hasOutput()) { 376 $systemOut = $this->document->createElement('system-out'); 377 $systemOut->appendChild( 378 $this->document->createTextNode($test->getActualOutput()) 379 ); 380 $this->currentTestCase->appendChild($systemOut); 381 } 382 } 383 384 $this->attachCurrentTestCase = true; 385 $this->currentTestCase = null; 386 } 387 388 /** 389 * Returns the XML as a string. 390 * 391 * @return string 392 */ 393 public function getXML() 394 { 395 return $this->document->saveXML(); 396 } 397 398 /** 399 * Enables or disables the writing of the document 400 * in flush(). 401 * 402 * This is a "hack" needed for the integration of 403 * PHPUnit with Phing. 404 * 405 * @return string 406 */ 407 public function setWriteDocument($flag) 408 { 409 if (is_bool($flag)) { 410 $this->writeDocument = $flag; 411 } 412 } 413 414 /** 415 * Method which generalizes addError() and addFailure() 416 * 417 * @param PHPUnit_Framework_Test $test 418 * @param Exception $e 419 * @param float $time 420 * @param string $type 421 */ 422 private function doAddFault(PHPUnit_Framework_Test $test, Exception $e, $time, $type) 423 { 424 if ($this->currentTestCase === null) { 425 return; 426 } 427 428 if ($test instanceof PHPUnit_Framework_SelfDescribing) { 429 $buffer = $test->toString() . PHP_EOL; 430 } else { 431 $buffer = ''; 432 } 433 434 $buffer .= PHPUnit_Framework_TestFailure::exceptionToString($e) . PHP_EOL . 435 PHPUnit_Util_Filter::getFilteredStacktrace($e); 436 437 $fault = $this->document->createElement( 438 $type, 439 PHPUnit_Util_XML::prepareString($buffer) 440 ); 441 442 if ($e instanceof PHPUnit_Framework_ExceptionWrapper) { 443 $fault->setAttribute('type', $e->getClassName()); 444 } else { 445 $fault->setAttribute('type', get_class($e)); 446 } 447 448 $this->currentTestCase->appendChild($fault); 449 } 450} 451