<?php
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\Log;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExceptionWrapper;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\ResultPrinter;
use PHPUnit\Util\Filter;
use ReflectionClass;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* A TestListener that generates a logfile of the test execution using the
* TeamCity format (for use with PhpStorm, for instance).
*/
class TeamCity extends ResultPrinter
{
/**
* @var bool
*/
private $isSummaryTestCountPrinted = false;
/**
* @var string
*/
private $startedTestName;
/**
* @var int|false
*/
private $flowId;
/**
* @param string $progress
*/
protected function writeProgress($progress)
{
}
/**
* @param TestResult $result
*/
public function printResult(TestResult $result)
{
$this->printHeader();
$this->printFooter($result);
}
/**
* An error occurred.
*
* @param Test $test
* @param \Exception $e
* @param float $time
*/
public function addError(Test $test, \Exception $e, $time)
{
$this->printEvent(
'testFailed',
[
'name' => $test->getName(),
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A warning occurred.
*
* @param Test $test
* @param Warning $e
* @param float $time
*/
public function addWarning(Test $test, Warning $e, $time)
{
$this->printEvent(
'testFailed',
[
'name' => $test->getName(),
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A failure occurred.
*
* @param Test $test
* @param AssertionFailedError $e
* @param float $time
*/
public function addFailure(Test $test, AssertionFailedError $e, $time)
{
$parameters = [
'name' => $test->getName(),
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
];
if ($e instanceof ExpectationFailedException) {
$comparisonFailure = $e->getComparisonFailure();
if ($comparisonFailure instanceof ComparisonFailure) {
$expectedString = $comparisonFailure->getExpectedAsString();
if (null === $expectedString || empty($expectedString)) {
$expectedString = self::getPrimitiveValueAsString($comparisonFailure->getExpected());
}
$actualString = $comparisonFailure->getActualAsString();
if (null === $actualString || empty($actualString)) {
$actualString = self::getPrimitiveValueAsString($comparisonFailure->getActual());
}
if (null !== $actualString && null !== $expectedString) {
$parameters['type'] = 'comparisonFailure';
$parameters['actual'] = $actualString;
$parameters['expected'] = $expectedString;
}
}
}
$this->printEvent('testFailed', $parameters);
}
/**
* Incomplete test.
*
* @param Test $test
* @param \Exception $e
* @param float $time
*/
public function addIncompleteTest(Test $test, \Exception $e, $time)
{
$this->printIgnoredTest($test->getName(), $e, $time);
}
/**
* Risky test.
*
* @param Test $test
* @param \Exception $e
* @param float $time
*/
public function addRiskyTest(Test $test, \Exception $e, $time)
{
$this->addError($test, $e, $time);
}
/**
* Skipped test.
*
* @param Test $test
* @param \Exception $e
* @param float $time
*/
public function addSkippedTest(Test $test, \Exception $e, $time)
{
$testName = $test->getName();
if ($this->startedTestName != $testName) {
$this->startTest($test);
$this->printIgnoredTest($testName, $e, $time);
$this->endTest($test, $time);
} else {
$this->printIgnoredTest($testName, $e, $time);
}
}
public function printIgnoredTest($testName, \Exception $e, $time)
{
$this->printEvent(
'testIgnored',
[
'name' => $testName,
'message' => self::getMessage($e),
'details' => self::getDetails($e),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* A testsuite started.
*
* @param TestSuite $suite
*/
public function startTestSuite(TestSuite $suite)
{
if (\stripos(\ini_get('disable_functions'), 'getmypid') === false) {
$this->flowId = \getmypid();
} else {
$this->flowId = false;
}
if (!$this->isSummaryTestCountPrinted) {
$this->isSummaryTestCountPrinted = true;
$this->printEvent(
'testCount',
['count' => \count($suite)]
);
}
$suiteName = $suite->getName();
if (empty($suiteName)) {
return;
}
$parameters = ['name' => $suiteName];
if (\class_exists($suiteName, false)) {
$fileName = self::getFileName($suiteName);
$parameters['locationHint'] = "php_qn://$fileName::\\$suiteName";
} else {
$split = \preg_split('/::/', $suiteName);
if (\count($split) == 2 && \method_exists($split[0], $split[1])) {
$fileName = self::getFileName($split[0]);
$parameters['locationHint'] = "php_qn://$fileName::\\$suiteName";
$parameters['name'] = $split[1];
}
}
$this->printEvent('testSuiteStarted', $parameters);
}
/**
* A testsuite ended.
*
* @param TestSuite $suite
*/
public function endTestSuite(TestSuite $suite)
{
$suiteName = $suite->getName();
if (empty($suiteName)) {
return;
}
$parameters = ['name' => $suiteName];
if (!\class_exists($suiteName, false)) {
$split = \preg_split('/::/', $suiteName);
if (\count($split) == 2 && \method_exists($split[0], $split[1])) {
$parameters['name'] = $split[1];
}
}
$this->printEvent('testSuiteFinished', $parameters);
}
/**
* A test started.
*
* @param Test $test
*/
public function startTest(Test $test)
{
$testName = $test->getName();
$this->startedTestName = $testName;
$params = ['name' => $testName];
if ($test instanceof TestCase) {
$className = \get_class($test);
$fileName = self::getFileName($className);
$params['locationHint'] = "php_qn://$fileName::\\$className::$testName";
}
$this->printEvent('testStarted', $params);
}
/**
* A test ended.
*
* @param Test $test
* @param float $time
*/
public function endTest(Test $test, $time)
{
parent::endTest($test, $time);
$this->printEvent(
'testFinished',
[
'name' => $test->getName(),
'duration' => self::toMilliseconds($time),
]
);
}
/**
* @param string $eventName
* @param array $params
*/
private function printEvent($eventName, $params = [])
{
$this->write("\n##teamcity[$eventName");
if ($this->flowId) {
$params['flowId'] = $this->flowId;
}
foreach ($params as $key => $value) {
$escapedValue = self::escapeValue($value);
$this->write(" $key='$escapedValue'");
}
$this->write("]\n");
}
/**
* @param \Exception $e
*
* @return string
*/
private static function getMessage(\Exception $e)
{
$message = '';
if ($e instanceof ExceptionWrapper) {
if (\strlen($e->getClassName()) != 0) {
$message .= $e->getClassName();
}
if (\strlen($message) != 0 && \strlen($e->getMessage()) != 0) {
$message .= ' : ';
}
}
return $message . $e->getMessage();
}
/**
* @param \Exception $e
*
* @return string
*/
private static function getDetails(\Exception $e)
{
$stackTrace = Filter::getFilteredStacktrace($e);
$previous = $e instanceof ExceptionWrapper ?
$e->getPreviousWrapped() : $e->getPrevious();
while ($previous) {
$stackTrace .= "\nCaused by\n" .
TestFailure::exceptionToString($previous) . "\n" .
Filter::getFilteredStacktrace($previous);
$previous = $previous instanceof ExceptionWrapper ?
$previous->getPreviousWrapped() : $previous->getPrevious();
}
return ' ' . \str_replace("\n", "\n ", $stackTrace);
}
/**
* @param mixed $value
*
* @return string
*/
private static function getPrimitiveValueAsString($value)
{
if (null === $value) {
return 'null';
}
if (\is_bool($value)) {
return $value == true ? 'true' : 'false';
}
if (\is_scalar($value)) {
return \print_r($value, true);
}
}
/**
* @param $text
*
* @return string
*/
private static function escapeValue($text)
{
$text = \str_replace('|', '||', $text);
$text = \str_replace("'", "|'", $text);
$text = \str_replace("\n", '|n', $text);
$text = \str_replace("\r", '|r', $text);
$text = \str_replace(']', '|]', $text);
$text = \str_replace('[', '|[', $text);
return $text;
}
/**
* @param string $className
*
* @return string
*/
private static function getFileName($className)
{
$reflectionClass = new ReflectionClass($className);
return $reflectionClass->getFileName();
}
/**
* @param float $time microseconds
*
* @return int
*/
private static function toMilliseconds($time)
{
return \round($time * 1000);
}
}