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 11use SebastianBergmann\Comparator\ComparisonFailure; 12 13/** 14 * A TestListener that generates a logfile of the test execution using the 15 * TeamCity format (for use with PhpStorm, for instance). 16 */ 17class PHPUnit_Util_Log_TeamCity extends PHPUnit_TextUI_ResultPrinter 18{ 19 /** 20 * @var bool 21 */ 22 private $isSummaryTestCountPrinted = false; 23 24 /** 25 * @var string 26 */ 27 private $startedTestName; 28 29 /** 30 * @var string 31 */ 32 private $flowId; 33 34 /** 35 * @param string $progress 36 */ 37 protected function writeProgress($progress) 38 { 39 } 40 41 /** 42 * @param PHPUnit_Framework_TestResult $result 43 */ 44 public function printResult(PHPUnit_Framework_TestResult $result) 45 { 46 $this->printHeader(); 47 $this->printFooter($result); 48 } 49 50 /** 51 * An error occurred. 52 * 53 * @param PHPUnit_Framework_Test $test 54 * @param Exception $e 55 * @param float $time 56 */ 57 public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) 58 { 59 $this->printEvent( 60 'testFailed', 61 [ 62 'name' => $test->getName(), 63 'message' => self::getMessage($e), 64 'details' => self::getDetails($e), 65 ] 66 ); 67 } 68 69 /** 70 * A warning occurred. 71 * 72 * @param PHPUnit_Framework_Test $test 73 * @param PHPUnit_Framework_Warning $e 74 * @param float $time 75 */ 76 public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time) 77 { 78 $this->printEvent( 79 'testFailed', 80 [ 81 'name' => $test->getName(), 82 'message' => self::getMessage($e), 83 'details' => self::getDetails($e) 84 ] 85 ); 86 } 87 88 /** 89 * A failure occurred. 90 * 91 * @param PHPUnit_Framework_Test $test 92 * @param PHPUnit_Framework_AssertionFailedError $e 93 * @param float $time 94 */ 95 public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) 96 { 97 $parameters = [ 98 'name' => $test->getName(), 99 'message' => self::getMessage($e), 100 'details' => self::getDetails($e), 101 ]; 102 103 if ($e instanceof PHPUnit_Framework_ExpectationFailedException) { 104 $comparisonFailure = $e->getComparisonFailure(); 105 106 if ($comparisonFailure instanceof ComparisonFailure) { 107 $expectedString = $comparisonFailure->getExpectedAsString(); 108 109 if (is_null($expectedString) || empty($expectedString)) { 110 $expectedString = self::getPrimitiveValueAsString($comparisonFailure->getExpected()); 111 } 112 113 $actualString = $comparisonFailure->getActualAsString(); 114 115 if (is_null($actualString) || empty($actualString)) { 116 $actualString = self::getPrimitiveValueAsString($comparisonFailure->getActual()); 117 } 118 119 if (!is_null($actualString) && !is_null($expectedString)) { 120 $parameters['type'] = 'comparisonFailure'; 121 $parameters['actual'] = $actualString; 122 $parameters['expected'] = $expectedString; 123 } 124 } 125 } 126 127 $this->printEvent('testFailed', $parameters); 128 } 129 130 /** 131 * Incomplete test. 132 * 133 * @param PHPUnit_Framework_Test $test 134 * @param Exception $e 135 * @param float $time 136 */ 137 public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) 138 { 139 $this->printIgnoredTest($test->getName(), $e); 140 } 141 142 /** 143 * Risky test. 144 * 145 * @param PHPUnit_Framework_Test $test 146 * @param Exception $e 147 * @param float $time 148 */ 149 public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) 150 { 151 $this->addError($test, $e, $time); 152 } 153 154 /** 155 * Skipped test. 156 * 157 * @param PHPUnit_Framework_Test $test 158 * @param Exception $e 159 * @param float $time 160 */ 161 public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) 162 { 163 $testName = $test->getName(); 164 if ($this->startedTestName != $testName) { 165 $this->startTest($test); 166 $this->printIgnoredTest($testName, $e); 167 $this->endTest($test, $time); 168 } else { 169 $this->printIgnoredTest($testName, $e); 170 } 171 } 172 173 public function printIgnoredTest($testName, Exception $e) 174 { 175 $this->printEvent( 176 'testIgnored', 177 [ 178 'name' => $testName, 179 'message' => self::getMessage($e), 180 'details' => self::getDetails($e), 181 ] 182 ); 183 } 184 185 /** 186 * A testsuite started. 187 * 188 * @param PHPUnit_Framework_TestSuite $suite 189 */ 190 public function startTestSuite(PHPUnit_Framework_TestSuite $suite) 191 { 192 if (stripos(ini_get('disable_functions'), 'getmypid') === false) { 193 $this->flowId = getmypid(); 194 } else { 195 $this->flowId = false; 196 } 197 198 if (!$this->isSummaryTestCountPrinted) { 199 $this->isSummaryTestCountPrinted = true; 200 201 $this->printEvent( 202 'testCount', 203 ['count' => count($suite)] 204 ); 205 } 206 207 $suiteName = $suite->getName(); 208 209 if (empty($suiteName)) { 210 return; 211 } 212 213 $parameters = ['name' => $suiteName]; 214 215 if (class_exists($suiteName, false)) { 216 $fileName = self::getFileName($suiteName); 217 $parameters['locationHint'] = "php_qn://$fileName::\\$suiteName"; 218 } else { 219 $split = preg_split('/::/', $suiteName); 220 221 if (count($split) == 2 && method_exists($split[0], $split[1])) { 222 $fileName = self::getFileName($split[0]); 223 $parameters['locationHint'] = "php_qn://$fileName::\\$suiteName"; 224 $parameters['name'] = $split[1]; 225 } 226 } 227 228 $this->printEvent('testSuiteStarted', $parameters); 229 } 230 231 /** 232 * A testsuite ended. 233 * 234 * @param PHPUnit_Framework_TestSuite $suite 235 */ 236 public function endTestSuite(PHPUnit_Framework_TestSuite $suite) 237 { 238 $suiteName = $suite->getName(); 239 240 if (empty($suiteName)) { 241 return; 242 } 243 244 $parameters = ['name' => $suiteName]; 245 246 if (!class_exists($suiteName, false)) { 247 $split = preg_split('/::/', $suiteName); 248 249 if (count($split) == 2 && method_exists($split[0], $split[1])) { 250 $parameters['name'] = $split[1]; 251 } 252 } 253 254 $this->printEvent('testSuiteFinished', $parameters); 255 } 256 257 /** 258 * A test started. 259 * 260 * @param PHPUnit_Framework_Test $test 261 */ 262 public function startTest(PHPUnit_Framework_Test $test) 263 { 264 $testName = $test->getName(); 265 $this->startedTestName = $testName; 266 $params = ['name' => $testName]; 267 268 if ($test instanceof PHPUnit_Framework_TestCase) { 269 $className = get_class($test); 270 $fileName = self::getFileName($className); 271 $params['locationHint'] = "php_qn://$fileName::\\$className::$testName"; 272 } 273 274 $this->printEvent('testStarted', $params); 275 } 276 277 /** 278 * A test ended. 279 * 280 * @param PHPUnit_Framework_Test $test 281 * @param float $time 282 */ 283 public function endTest(PHPUnit_Framework_Test $test, $time) 284 { 285 parent::endTest($test, $time); 286 287 $this->printEvent( 288 'testFinished', 289 [ 290 'name' => $test->getName(), 291 'duration' => (int) (round($time, 2) * 1000) 292 ] 293 ); 294 } 295 296 /** 297 * @param string $eventName 298 * @param array $params 299 */ 300 private function printEvent($eventName, $params = []) 301 { 302 $this->write("\n##teamcity[$eventName"); 303 304 if ($this->flowId) { 305 $params['flowId'] = $this->flowId; 306 } 307 308 foreach ($params as $key => $value) { 309 $escapedValue = self::escapeValue($value); 310 $this->write(" $key='$escapedValue'"); 311 } 312 313 $this->write("]\n"); 314 } 315 316 /** 317 * @param Exception $e 318 * 319 * @return string 320 */ 321 private static function getMessage(Exception $e) 322 { 323 $message = ''; 324 325 if ($e instanceof PHPUnit_Framework_ExceptionWrapper) { 326 if (strlen($e->getClassName()) != 0) { 327 $message = $message . $e->getClassName(); 328 } 329 330 if (strlen($message) != 0 && strlen($e->getMessage()) != 0) { 331 $message = $message . ' : '; 332 } 333 } 334 335 return $message . $e->getMessage(); 336 } 337 338 /** 339 * @param Exception $e 340 * 341 * @return string 342 */ 343 private static function getDetails(Exception $e) 344 { 345 $stackTrace = PHPUnit_Util_Filter::getFilteredStacktrace($e); 346 $previous = $e->getPrevious(); 347 348 while ($previous) { 349 $stackTrace .= "\nCaused by\n" . 350 PHPUnit_Framework_TestFailure::exceptionToString($previous) . "\n" . 351 PHPUnit_Util_Filter::getFilteredStacktrace($previous); 352 353 $previous = $previous->getPrevious(); 354 } 355 356 return ' ' . str_replace("\n", "\n ", $stackTrace); 357 } 358 359 /** 360 * @param mixed $value 361 * 362 * @return string 363 */ 364 private static function getPrimitiveValueAsString($value) 365 { 366 if (is_null($value)) { 367 return 'null'; 368 } elseif (is_bool($value)) { 369 return $value == true ? 'true' : 'false'; 370 } elseif (is_scalar($value)) { 371 return print_r($value, true); 372 } 373 374 return; 375 } 376 377 /** 378 * @param $text 379 * 380 * @return string 381 */ 382 private static function escapeValue($text) 383 { 384 $text = str_replace('|', '||', $text); 385 $text = str_replace("'", "|'", $text); 386 $text = str_replace("\n", '|n', $text); 387 $text = str_replace("\r", '|r', $text); 388 $text = str_replace(']', '|]', $text); 389 $text = str_replace('[', '|[', $text); 390 391 return $text; 392 } 393 394 /** 395 * @param string $className 396 * 397 * @return string 398 */ 399 private static function getFileName($className) 400 { 401 $reflectionClass = new ReflectionClass($className); 402 $fileName = $reflectionClass->getFileName(); 403 404 return $fileName; 405 } 406} 407