1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19

20
use PHPUnit\Framework\AssertionFailedError;
21
use PHPUnit\Framework\ExceptionWrapper;
22
use PHPUnit\Framework\SelfDescribing;
23
use PHPUnit\Framework\Test;
24
use PHPUnit\Framework\TestFailure;
25
use PHPUnit\Framework\TestListener;
26
use PHPUnit\Framework\TestSuite;
27
use PHPUnit\Framework\Warning;
28
use PHPUnit\Util\Filter;
29
use PHPUnit\Util\Printer;
30
use PHPUnit\Util\Xml;
31

32
/**
33
 * A TestListener that generates a logfile of the test execution in XML markup.
34
 *
35
 * The XML markup used is the same as the one that is used by the JUnit Ant task.
36
 */
37
class JUnit extends Printer implements TestListener
38
{
39
    /**
40
     * @var DOMDocument
41
     */
42
    protected $document;
43

44
    /**
45
     * @var DOMElement
46
     */
47
    protected $root;
48

49
    /**
50
     * @var bool
51
     */
52
    protected $reportUselessTests = false;
53

54
    /**
55
     * @var bool
56
     */
57
    protected $writeDocument = true;
58

59
    /**
60
     * @var DOMElement[]
61
     */
62
    protected $testSuites = [];
63

64
    /**
65
     * @var int[]
66
     */
67
    protected $testSuiteTests = [0];
68

69
    /**
70
     * @var int[]
71
     */
72
    protected $testSuiteAssertions = [0];
73

74
    /**
75
     * @var int[]
76
     */
77
    protected $testSuiteErrors = [0];
78

79
    /**
80
     * @var int[]
81
     */
82
    protected $testSuiteFailures = [0];
83

84
    /**
85
     * @var int[]
86
     */
87
    protected $testSuiteSkipped = [0];
88

89
    /**
90
     * @var int[]
91
     */
92
    protected $testSuiteTimes = [0];
93

94
    /**
95
     * @var int
96
     */
97
    protected $testSuiteLevel = 0;
98

99
    /**
100
     * @var DOMElement
101
     */
102
    protected $currentTestCase;
103

104
    /**
105
     * Constructor.
106
     *
107
     * @param null|mixed $out
108
     *
109
     * @throws \PHPUnit\Framework\Exception
110
     */
111 1
    public function __construct($out = null, bool $reportUselessTests = false)
112
    {
113 1
        $this->document               = new DOMDocument('1.0', 'UTF-8');
114 1
        $this->document->formatOutput = true;
115

116 1
        $this->root = $this->document->createElement('testsuites');
117 1
        $this->document->appendChild($this->root);
118

119 1
        parent::__construct($out);
120

121 1
        $this->reportUselessTests = $reportUselessTests;
122
    }
123

124
    /**
125
     * Flush buffer and close output.
126
     */
127 0
    public function flush(): void
128
    {
129 0
        if ($this->writeDocument === true) {
130 0
            $this->write($this->getXML());
131
        }
132

133 0
        parent::flush();
134
    }
135

136
    /**
137
     * An error occurred.
138
     *
139
     * @throws \InvalidArgumentException
140
     */
141 0
    public function addError(Test $test, \Throwable $t, float $time): void
142
    {
143 0
        $this->doAddFault($test, $t, $time, 'error');
144 0
        $this->testSuiteErrors[$this->testSuiteLevel]++;
145
    }
146

147
    /**
148
     * A warning occurred.
149
     *
150
     * @throws \InvalidArgumentException
151
     */
152 0
    public function addWarning(Test $test, Warning $e, float $time): void
153
    {
154 0
        $this->doAddFault($test, $e, $time, 'warning');
155 0
        $this->testSuiteFailures[$this->testSuiteLevel]++;
156
    }
157

158
    /**
159
     * A failure occurred.
160
     *
161
     * @throws \InvalidArgumentException
162
     */
163 0
    public function addFailure(Test $test, AssertionFailedError $e, float $time): void
164
    {
165 0
        $this->doAddFault($test, $e, $time, 'failure');
166 0
        $this->testSuiteFailures[$this->testSuiteLevel]++;
167
    }
168

169
    /**
170
     * Incomplete test.
171
     */
172 0
    public function addIncompleteTest(Test $test, \Throwable $t, float $time): void
173
    {
174 0
        $this->doAddSkipped($test);
175
    }
176

177
    /**
178
     * Risky test.
179
     */
180 0
    public function addRiskyTest(Test $test, \Throwable $t, float $time): void
181
    {
182 0
        if (!$this->reportUselessTests || $this->currentTestCase === null) {
183 0
            return;
184
        }
185

186 0
        $error = $this->document->createElement(
187 0
            'error',
188 0
            Xml::prepareString(
189
                "Risky Test\n" .
190 0
                Filter::getFilteredStacktrace($t)
191
            )
192
        );
193

194 0
        $error->setAttribute('type', \get_class($t));
195

196 0
        $this->currentTestCase->appendChild($error);
197

198 0
        $this->testSuiteErrors[$this->testSuiteLevel]++;
199
    }
200

201
    /**
202
     * Skipped test.
203
     */
204 0
    public function addSkippedTest(Test $test, \Throwable $t, float $time): void
205
    {
206 0
        $this->doAddSkipped($test);
207
    }
208

209
    /**
210
     * A testsuite started.
211
     */
212 1
    public function startTestSuite(TestSuite $suite): void
213
    {
214 1
        $testSuite = $this->document->createElement('testsuite');
215 1
        $testSuite->setAttribute('name', $suite->getName());
216

217 1
        if (\class_exists($suite->getName(), false)) {
218
            try {
219 1
                $class = new ReflectionClass($suite->getName());
220

221 1
                $testSuite->setAttribute('file', $class->getFileName());
222 0
            } catch (ReflectionException $e) {
223
            }
224
        }
225

226 1
        if ($this->testSuiteLevel > 0) {
227 1
            $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
228
        } else {
229 1
            $this->root->appendChild($testSuite);
230
        }
231

232 1
        $this->testSuiteLevel++;
233 1
        $this->testSuites[$this->testSuiteLevel]          = $testSuite;
234 1
        $this->testSuiteTests[$this->testSuiteLevel]      = 0;
235 1
        $this->testSuiteAssertions[$this->testSuiteLevel] = 0;
236 1
        $this->testSuiteErrors[$this->testSuiteLevel]     = 0;
237 1
        $this->testSuiteFailures[$this->testSuiteLevel]   = 0;
238 1
        $this->testSuiteSkipped[$this->testSuiteLevel]    = 0;
239 1
        $this->testSuiteTimes[$this->testSuiteLevel]      = 0;
240
    }
241

242
    /**
243
     * A testsuite ended.
244
     */
245 1
    public function endTestSuite(TestSuite $suite): void
246
    {
247 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
248 1
            'tests',
249 1
            $this->testSuiteTests[$this->testSuiteLevel]
250
        );
251

252 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
253 1
            'assertions',
254 1
            $this->testSuiteAssertions[$this->testSuiteLevel]
255
        );
256

257 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
258 1
            'errors',
259 1
            $this->testSuiteErrors[$this->testSuiteLevel]
260
        );
261

262 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
263 1
            'failures',
264 1
            $this->testSuiteFailures[$this->testSuiteLevel]
265
        );
266

267 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
268 1
            'skipped',
269 1
            $this->testSuiteSkipped[$this->testSuiteLevel]
270
        );
271

272 1
        $this->testSuites[$this->testSuiteLevel]->setAttribute(
273 1
            'time',
274 1
            \sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel])
275
        );
276

277 1
        if ($this->testSuiteLevel > 1) {
278 1
            $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
279 1
            $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
280 1
            $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
281 1
            $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
282 1
            $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
283 1
            $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
284
        }
285

286 1
        $this->testSuiteLevel--;
287
    }
288

289
    /**
290
     * A test started.
291
     *
292
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
293
     * @throws ReflectionException
294
     */
295 1
    public function startTest(Test $test): void
296
    {
297 1
        $usesDataprovider = false;
298

299 1
        if (\method_exists($test, 'usesDataProvider')) {
300 1
            $usesDataprovider = $test->usesDataProvider();
301
        }
302

303 1
        $testCase = $this->document->createElement('testcase');
304 1
        $testCase->setAttribute('name', $test->getName());
305

306 1
        $class      = new ReflectionClass($test);
307 1
        $methodName = $test->getName(!$usesDataprovider);
308

309 1
        if ($class->hasMethod($methodName)) {
310 1
            $method = $class->getMethod($methodName);
311

312 1
            $testCase->setAttribute('class', $class->getName());
313 1
            $testCase->setAttribute('classname', \str_replace('\\', '.', $class->getName()));
314 1
            $testCase->setAttribute('file', $class->getFileName());
315 1
            $testCase->setAttribute('line', $method->getStartLine());
316
        }
317

318 1
        $this->currentTestCase = $testCase;
319
    }
320

321
    /**
322
     * A test ended.
323
     */
324 1
    public function endTest(Test $test, float $time): void
325
    {
326 1
        $numAssertions = 0;
327

328 1
        if (\method_exists($test, 'getNumAssertions')) {
329 1
            $numAssertions = $test->getNumAssertions();
330
        }
331

332 1
        $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
333

334 1
        $this->currentTestCase->setAttribute(
335 1
            'assertions',
336
            $numAssertions
337
        );
338

339 1
        $this->currentTestCase->setAttribute(
340 1
            'time',
341 1
            \sprintf('%F', $time)
342
        );
343

344 1
        $this->testSuites[$this->testSuiteLevel]->appendChild(
345 1
            $this->currentTestCase
346
        );
347

348 1
        $this->testSuiteTests[$this->testSuiteLevel]++;
349 1
        $this->testSuiteTimes[$this->testSuiteLevel] += $time;
350

351 1
        $testOutput = '';
352

353 1
        if (\method_exists($test, 'hasOutput') && \method_exists($test, 'getActualOutput')) {
354 1
            $testOutput = $test->hasOutput() ? $test->getActualOutput() : '';
355
        }
356

357 1
        if (!empty($testOutput)) {
358 0
            $systemOut = $this->document->createElement(
359 0
                'system-out',
360 0
                Xml::prepareString($testOutput)
361
            );
362

363 0
            $this->currentTestCase->appendChild($systemOut);
364
        }
365

366 1
        $this->currentTestCase = null;
367
    }
368

369
    /**
370
     * Returns the XML as a string.
371
     */
372 1
    public function getXML(): string
373
    {
374 1
        return $this->document->saveXML();
375
    }
376

377
    /**
378
     * Enables or disables the writing of the document
379
     * in flush().
380
     *
381
     * This is a "hack" needed for the integration of
382
     * PHPUnit with Phing.
383
     */
384 1
    public function setWriteDocument(bool $flag): void
385
    {
386 1
        if (\is_bool($flag)) {
387 1
            $this->writeDocument = $flag;
388
        }
389
    }
390

391
    /**
392
     * Method which generalizes addError() and addFailure()
393
     *
394
     * @throws \InvalidArgumentException
395
     */
396 0
    private function doAddFault(Test $test, \Throwable $t, float $time, $type): void
397
    {
398 0
        if ($this->currentTestCase === null) {
399 0
            return;
400
        }
401

402 0
        if ($test instanceof SelfDescribing) {
403 0
            $buffer = $test->toString() . "\n";
404
        } else {
405 0
            $buffer = '';
406
        }
407

408 0
        $buffer .= TestFailure::exceptionToString($t) . "\n" .
409 0
            Filter::getFilteredStacktrace($t);
410

411 0
        $fault = $this->document->createElement(
412 0
            $type,
413 0
            Xml::prepareString($buffer)
414
        );
415

416 0
        if ($t instanceof ExceptionWrapper) {
417 0
            $fault->setAttribute('type', $t->getClassName());
418
        } else {
419 0
            $fault->setAttribute('type', \get_class($t));
420
        }
421

422 0
        $this->currentTestCase->appendChild($fault);
423
    }
424

425 0
    private function doAddSkipped(Test $test): void
426
    {
427 0
        if ($this->currentTestCase === null) {
428 0
            return;
429
        }
430

431 0
        $skipped = $this->document->createElement('skipped');
432 0
        $this->currentTestCase->appendChild($skipped);
433

434 0
        $this->testSuiteSkipped[$this->testSuiteLevel]++;
435
    }
436
}

Read our documentation on viewing source code .

Loading