Skip to content

Commit

Permalink
Initial work on Open Test Reporting (OTR) XML logging
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Jan 28, 2025
1 parent 75d2571 commit ee3b29c
Show file tree
Hide file tree
Showing 30 changed files with 819 additions and 3 deletions.
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<directory suffix=".phpt">tests/end-to-end/generic</directory>
<directory suffix=".phpt">tests/end-to-end/groups-from-configuration</directory>
<directory suffix=".phpt">tests/end-to-end/logging/junit</directory>
<directory suffix=".phpt">tests/end-to-end/logging/open-test-reporting</directory>
<directory suffix=".phpt">tests/end-to-end/logging/teamcity</directory>
<directory suffix=".phpt">tests/end-to-end/logging/testdox</directory>
<directory suffix=".phpt">tests/end-to-end/metadata</directory>
Expand Down
1 change: 1 addition & 0 deletions phpunit.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
<xs:group name="loggingGroup">
<xs:all>
<xs:element name="junit" type="logToFileType" minOccurs="0" />
<xs:element name="otr" type="logToFileType" minOccurs="0" />
<xs:element name="teamcity" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxHtml" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxText" type="logToFileType" minOccurs="0" />
Expand Down
236 changes: 236 additions & 0 deletions src/Logging/OpenTestReporting/OtrXmlLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\OpenTestReporting;

use function array_pop;
use function assert;
use function count;
use function function_exists;
use function get_current_user;
use function gethostname;
use function posix_geteuid;
use function posix_getpwuid;
use function trim;
use DateTimeImmutable;
use DateTimeZone;
use PHPUnit\Event\Application\Started;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\Test\Prepared as TestStarted;
use PHPUnit\Event\TestSuite\Finished as TestSuiteFinished;
use PHPUnit\Event\TestSuite\Started as TestSuiteStarted;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\TextUI\Output\Printer;
use XMLWriter;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class OtrXmlLogger
{
private readonly Printer $printer;
private XMLWriter $writer;

/**
* @var non-negative-int
*/
private int $idSequence = 0;

/**
* @var ?positive-int
*/
private ?int $parentId = null;

/**
* @var list<positive-int>
*/
private array $parentIdStack = [];

/**
* @var ?positive-int
*/
private ?int $testId = null;

/**
* @var 'ABORTED'|'FAILED'|'SKIPPED'|'SUCCESSFUL'
*/
private string $result = 'SUCCESSFUL';

/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
public function __construct(Printer $printer, Facade $facade)
{
$this->printer = $printer;

$this->registerSubscribers($facade);
}

public function flush(): void
{
assert($this->writer instanceof XMLWriter);

$this->writer->endElement();
$this->writer->endDocument();

$this->printer->print($this->writer->outputMemory());
$this->printer->flush();
}

public function testRunnerStarted(Started $event): void
{
$this->writer = new XMLWriter;

$this->writer->openMemory();
$this->writer->setIndent(true);
$this->writer->startDocument();

$this->writer->startElement('e:events');
$this->writer->writeAttribute('xmlns', 'https://schemas.opentest4j.org/reporting/core/0.2.0');
$this->writer->writeAttribute('xmlns:e', 'https://schemas.opentest4j.org/reporting/events/0.2.0');

$this->writer->startElement('infrastructure');
$this->writer->writeElement('hostName', gethostname());
$this->writer->writeElement('userName', $this->userName());
$this->writer->endElement();
}

public function testSuiteStarted(TestSuiteStarted $event): void
{
$id = ++$this->idSequence;
$this->parentId = $id;
$this->parentIdStack[] = $id;

$this->writer->startElement('e:started');
$this->writer->writeAttribute('id', (string) $id);
$this->writer->writeAttribute('name', $event->testSuite()->name());
$this->writer->writeAttribute('time', $this->timestamp());
$this->writer->endElement();
}

public function testStarted(TestStarted $event): void
{
$id = ++$this->idSequence;
$this->testId = $id;

$this->writer->startElement('e:started');
$this->writer->writeAttribute('id', (string) $id);

if ($this->parentId !== null) {
$this->writer->writeAttribute('parent', (string) $this->parentId);
}

$this->writer->writeAttribute('name', $event->test()->id());
$this->writer->writeAttribute('time', $this->timestamp());
$this->writer->endElement();
}

public function testFailed(): void
{
$this->result = 'FAILED';
}

public function testErrored(): void
{
$this->result = 'FAILED';
}

public function testSkipped(): void
{
$this->result = 'SKIPPED';
}

public function testAborted(): void
{
$this->result = 'ABORTED';
}

public function testFinished(): void
{
assert($this->testId !== null);

$this->writer->startElement('e:finished');
$this->writer->writeAttribute('id', (string) $this->testId);
$this->writer->writeAttribute('time', $this->timestamp());
$this->writer->startElement('result');
$this->writer->writeAttribute('status', $this->result);
$this->writer->endElement();
$this->writer->endElement();

$this->testId = null;
$this->result = 'SUCCESSFUL';
}

public function testSuiteFinished(TestSuiteFinished $event): void
{
$this->writer->startElement('e:finished');
$this->writer->writeAttribute('id', (string) $this->parentId);
$this->writer->writeAttribute('time', $this->timestamp());
$this->writer->endElement();

array_pop($this->parentIdStack);

if (!empty($this->parentIdStack)) {
$this->parentId = $this->parentIdStack[count($this->parentIdStack) - 1];

return;
}

$this->parentId = null;
}

/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
private function registerSubscribers(Facade $facade): void
{
$facade->registerSubscribers(
new TestRunnerStartedSubscriber($this),
new TestSuiteStartedSubscriber($this),
new TestStartedSubscriber($this),
new TestAbortedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestSkippedSubscriber($this),
new TestFinishedSubscriber($this),
new TestSuiteFinishedSubscriber($this),
new TestRunnerFinishedSubscriber($this),
);
}

/**
* @return non-empty-string
*/
private function timestamp(): string
{
return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.u\Z');
}

private function userName(): string
{
if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) {
$userName = posix_getpwuid(posix_geteuid())['name'];
} else {
$userName = get_current_user();
}

$userName = trim($userName);

if ($userName === '') {
$userName = 'unknown';
}

return $userName;
}
}
30 changes: 30 additions & 0 deletions src/Logging/OpenTestReporting/Subscriber/Subscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\OpenTestReporting;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
abstract readonly class Subscriber
{
private OtrXmlLogger $logger;

public function __construct(OtrXmlLogger $logger)
{
$this->logger = $logger;
}

protected function logger(): OtrXmlLogger
{
return $this->logger;
}
}
30 changes: 30 additions & 0 deletions src/Logging/OpenTestReporting/Subscriber/TestAbortedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\OpenTestReporting;

use PHPUnit\Event\InvalidArgumentException;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class TestAbortedSubscriber extends Subscriber implements MarkedIncompleteSubscriber
{
/**
* @throws InvalidArgumentException
*/
public function notify(MarkedIncomplete $event): void
{
$this->logger()->testAborted();
}
}
30 changes: 30 additions & 0 deletions src/Logging/OpenTestReporting/Subscriber/TestErroredSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\OpenTestReporting;

use PHPUnit\Event\InvalidArgumentException;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\ErroredSubscriber;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class TestErroredSubscriber extends Subscriber implements ErroredSubscriber
{
/**
* @throws InvalidArgumentException
*/
public function notify(Errored $event): void
{
$this->logger()->testErrored();
}
}
30 changes: 30 additions & 0 deletions src/Logging/OpenTestReporting/Subscriber/TestFailedSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Logging\OpenTestReporting;

use PHPUnit\Event\InvalidArgumentException;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\FailedSubscriber;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final readonly class TestFailedSubscriber extends Subscriber implements FailedSubscriber
{
/**
* @throws InvalidArgumentException
*/
public function notify(Failed $event): void
{
$this->logger()->testFailed();
}
}
Loading

0 comments on commit ee3b29c

Please sign in to comment.