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