Skip to content

Commit e62e484

Browse files
Closes #6501
1 parent 6b0c3a2 commit e62e484

File tree

5 files changed

+207
-33
lines changed

5 files changed

+207
-33
lines changed

ChangeLog-13.1.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ All notable changes of the PHPUnit 13.1 release series are documented in this fi
44

55
## [13.1.0] - 2026-04-03
66

7+
### Added
8+
9+
* [#6501](https://github.com/sebastianbergmann/phpunit/issues/6501): Include unexpected test output in Open Test Reporting (OTR) XML logfile
10+
711
[13.1.0]: https://github.com/sebastianbergmann/phpunit/compare/13.0...main

src/Logging/OpenTestReporting/OtrXmlLogger.php

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use PHPUnit\Event\Test\PreparationErrored;
3333
use PHPUnit\Event\Test\PreparationFailed;
3434
use PHPUnit\Event\Test\Prepared as TestStarted;
35+
use PHPUnit\Event\Test\PrintedUnexpectedOutput;
3536
use PHPUnit\Event\Test\Skipped;
3637
use PHPUnit\Event\TestSuite\Skipped as TestSuiteSkipped;
3738
use PHPUnit\Event\TestSuite\Started as TestSuiteStarted;
@@ -65,10 +66,16 @@ final class OtrXmlLogger
6566
/**
6667
* @var ?positive-int
6768
*/
68-
private ?int $testId = null;
69-
private ?Throwable $parentErrored = null;
70-
private ?Throwable $parentFailed = null;
71-
private bool $alreadyFinished = false;
69+
private ?int $testId = null;
70+
private ?Throwable $parentErrored = null;
71+
private ?Throwable $parentFailed = null;
72+
private bool $testWasPrepared = false;
73+
private bool $alreadyFinished = false;
74+
private ?string $unexpectedOutput = null;
75+
private ?Status $pendingStatus = null;
76+
private ?string $pendingReason = null;
77+
private ?Throwable $pendingThrowable = null;
78+
private ?bool $pendingAssertionError = null;
7279
private bool $includeGitInformation;
7380

7481
/**
@@ -249,6 +256,15 @@ public function testPrepared(PreparationErrored|PreparationFailed|TestStarted $e
249256
$this->testId,
250257
$this->parentId,
251258
);
259+
260+
if ($event instanceof TestStarted) {
261+
$this->testWasPrepared = true;
262+
}
263+
}
264+
265+
public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void
266+
{
267+
$this->unexpectedOutput = $event->output();
252268
}
253269

254270
public function testFinished(): void
@@ -257,18 +273,63 @@ public function testFinished(): void
257273
$this->writer->startElement('e:finished');
258274
$this->writer->writeAttribute('id', (string) $this->testId);
259275
$this->writer->writeAttribute('time', $this->timestamp());
276+
277+
if ($this->unexpectedOutput !== null) {
278+
$this->writer->startElement('attachments');
279+
$this->writer->startElement('output');
280+
$this->writer->writeAttribute('source', 'stdout');
281+
$this->writer->writeAttribute('time', $this->timestamp());
282+
$this->writer->writeCdata($this->unexpectedOutput);
283+
$this->writer->endElement();
284+
$this->writer->endElement();
285+
}
286+
287+
$status = Status::Successful;
288+
289+
if ($this->pendingStatus !== null) {
290+
$status = $this->pendingStatus;
291+
}
292+
260293
$this->writer->startElement('result');
261-
$this->writer->writeAttribute('status', Status::Successful->value);
294+
$this->writer->writeAttribute('status', $status->value);
295+
296+
if ($this->pendingReason !== null) {
297+
$this->writer->writeElement('reason', $this->pendingReason);
298+
}
299+
300+
if ($this->pendingThrowable !== null) {
301+
assert($this->pendingAssertionError !== null);
302+
303+
$this->writeThrowable($this->pendingThrowable, $this->pendingAssertionError);
304+
}
305+
262306
$this->writer->endElement();
263307
$this->writer->endElement();
308+
309+
$this->writer->flush();
264310
}
265311

266-
$this->alreadyFinished = false;
267-
$this->testId = null;
312+
$this->alreadyFinished = false;
313+
$this->testWasPrepared = false;
314+
$this->testId = null;
315+
$this->unexpectedOutput = null;
316+
$this->pendingStatus = null;
317+
$this->pendingReason = null;
318+
$this->pendingThrowable = null;
319+
$this->pendingAssertionError = null;
268320
}
269321

270322
public function testFailed(Failed $event): void
271323
{
324+
if ($this->testWasPrepared) {
325+
$this->pendingStatus = Status::Failed;
326+
$this->pendingReason = $event->throwable()->message();
327+
$this->pendingThrowable = $event->throwable();
328+
$this->pendingAssertionError = true;
329+
330+
return;
331+
}
332+
272333
$this->writer->startElement('e:finished');
273334
$this->writer->writeAttribute('id', (string) $this->testId);
274335
$this->writer->writeAttribute('time', $this->timestamp());
@@ -288,6 +349,15 @@ public function testFailed(Failed $event): void
288349

289350
public function testErrored(Errored $event): void
290351
{
352+
if ($this->testWasPrepared) {
353+
$this->pendingStatus = Status::Errored;
354+
$this->pendingReason = $event->throwable()->message();
355+
$this->pendingThrowable = $event->throwable();
356+
$this->pendingAssertionError = false;
357+
358+
return;
359+
}
360+
291361
$this->writer->startElement('e:finished');
292362
$this->writer->writeAttribute('id', (string) $this->testId);
293363
$this->writer->writeAttribute('time', $this->timestamp());
@@ -315,41 +385,35 @@ public function testSkipped(Skipped $event): void
315385
$this->testId,
316386
$this->parentId,
317387
);
318-
}
319388

320-
$this->writer->startElement('e:finished');
321-
$this->writer->writeAttribute('id', (string) $this->testId);
322-
$this->writer->writeAttribute('time', $this->timestamp());
323-
$this->writer->startElement('result');
324-
$this->writer->writeAttribute('status', Status::Skipped->value);
389+
$this->writer->startElement('e:finished');
390+
$this->writer->writeAttribute('id', (string) $this->testId);
391+
$this->writer->writeAttribute('time', $this->timestamp());
392+
$this->writer->startElement('result');
393+
$this->writer->writeAttribute('status', Status::Skipped->value);
325394

326-
$this->writer->writeElement('reason', $event->message());
395+
$this->writer->writeElement('reason', $event->message());
327396

328-
$this->writer->endElement();
329-
$this->writer->endElement();
397+
$this->writer->endElement();
398+
$this->writer->endElement();
330399

331-
$this->writer->flush();
400+
$this->writer->flush();
332401

333-
$this->alreadyFinished = true;
402+
$this->testId = null;
403+
404+
return;
405+
}
406+
407+
$this->pendingStatus = Status::Skipped;
408+
$this->pendingReason = $event->message();
334409
}
335410

336411
public function markTestIncomplete(MarkedIncomplete $event): void
337412
{
338-
$this->writer->startElement('e:finished');
339-
$this->writer->writeAttribute('id', (string) $this->testId);
340-
$this->writer->writeAttribute('time', $this->timestamp());
341-
$this->writer->startElement('result');
342-
$this->writer->writeAttribute('status', Status::Aborted->value);
343-
344-
$this->writer->writeElement('reason', $event->throwable()->message());
345-
$this->writeThrowable($event->throwable(), false);
346-
347-
$this->writer->endElement();
348-
$this->writer->endElement();
349-
350-
$this->writer->flush();
351-
352-
$this->alreadyFinished = true;
413+
$this->pendingStatus = Status::Aborted;
414+
$this->pendingReason = $event->throwable()->message();
415+
$this->pendingThrowable = $event->throwable();
416+
$this->pendingAssertionError = false;
353417
}
354418

355419
public function parentErrored(AfterLastTestMethodErrored|BeforeFirstTestMethodErrored $event): void
@@ -375,6 +439,7 @@ private function registerSubscribers(Facade $facade): void
375439
new TestPreparationErroredSubscriber($this),
376440
new TestPreparationFailedSubscriber($this),
377441
new TestPreparedSubscriber($this),
442+
new TestPrintedUnexpectedOutputSubscriber($this),
378443
new TestAbortedSubscriber($this),
379444
new TestErroredSubscriber($this),
380445
new TestFailedSubscriber($this),
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types=1);
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+
namespace PHPUnit\Logging\OpenTestReporting;
11+
12+
use PHPUnit\Event\Test\PrintedUnexpectedOutput;
13+
use PHPUnit\Event\Test\PrintedUnexpectedOutputSubscriber;
14+
15+
/**
16+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
17+
*
18+
* @internal This class is not covered by the backward compatibility promise for PHPUnit
19+
*/
20+
final readonly class TestPrintedUnexpectedOutputSubscriber extends Subscriber implements PrintedUnexpectedOutputSubscriber
21+
{
22+
public function notify(PrintedUnexpectedOutput $event): void
23+
{
24+
$this->logger()->testPrintedUnexpectedOutput($event);
25+
}
26+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
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+
namespace PHPUnit\TestFixture\Basic;
11+
12+
use PHPUnit\Framework\TestCase;
13+
14+
class UnexpectedOutputTest extends TestCase
15+
{
16+
public function testWithOutput(): void
17+
{
18+
print 'unexpected output';
19+
20+
$this->assertTrue(true);
21+
}
22+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
phpunit --log-otr /path/to/logfile ../_files/UnexpectedOutputTest.php
3+
--FILE--
4+
<?php declare(strict_types=1);
5+
use function PHPUnit\TestFixture\validate_and_print;
6+
7+
$logfile = tempnam(sys_get_temp_dir(), __FILE__);
8+
9+
$_SERVER['argv'][] = '--do-not-cache-result';
10+
$_SERVER['argv'][] = '--no-configuration';
11+
$_SERVER['argv'][] = '--no-output';
12+
$_SERVER['argv'][] = '--log-otr';
13+
$_SERVER['argv'][] = $logfile;
14+
$_SERVER['argv'][] = __DIR__ . '/_files/UnexpectedOutputTest.php';
15+
16+
require __DIR__ . '/../../../bootstrap.php';
17+
require __DIR__ . '/validate_and_print.php';
18+
19+
(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);
20+
21+
validate_and_print($logfile);
22+
23+
unlink($logfile);
24+
--EXPECTF--
25+
<?xml version="1.0"?>
26+
<e:events xmlns="https://schemas.opentest4j.org/reporting/core/0.2.0" xmlns:e="https://schemas.opentest4j.org/reporting/events/0.2.0" xmlns:php="https://schema.phpunit.de/otr/php/0.0.1" xmlns:phpunit="https://schema.phpunit.de/otr/phpunit/0.0.1">
27+
<infrastructure>
28+
<hostName>%s</hostName>
29+
<userName>%s</userName>
30+
<operatingSystem>%s</operatingSystem>
31+
<php:phpVersion>%s</php:phpVersion>
32+
<php:threadModel>%s</php:threadModel>
33+
</infrastructure>
34+
<e:started id="1" name="PHPUnit\TestFixture\Basic\UnexpectedOutputTest" time="%s">
35+
<sources>
36+
<fileSource path="%sUnexpectedOutputTest.php">
37+
<filePosition line="%d"/>
38+
</fileSource>
39+
<phpunit:classSource className="PHPUnit\TestFixture\Basic\UnexpectedOutputTest"/>
40+
</sources>
41+
</e:started>
42+
<e:started id="2" parentId="1" name="testWithOutput" time="%s">
43+
<sources>
44+
<fileSource path="%sUnexpectedOutputTest.php">
45+
<filePosition line="%d"/>
46+
</fileSource>
47+
<phpunit:methodSource className="PHPUnit\TestFixture\Basic\UnexpectedOutputTest" methodName="testWithOutput"/>
48+
</sources>
49+
</e:started>
50+
<e:finished id="2" time="%s">
51+
<attachments>
52+
<output source="stdout" time="%s"><![CDATA[unexpected output]]></output>
53+
</attachments>
54+
<result status="SUCCESSFUL"/>
55+
</e:finished>
56+
<e:finished id="1" time="%s"/>
57+
</e:events>

0 commit comments

Comments
 (0)