Skip to content

Commit

Permalink
Initial work on Open Test Reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Feb 26, 2025
1 parent efca62b commit 9bcb2d3
Show file tree
Hide file tree
Showing 43 changed files with 1,319 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 @@ -293,6 +293,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
270 changes: 270 additions & 0 deletions src/Logging/OpenTestReporting/OtrXmlLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<?php declare(strict_types=1);
/*
* 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\Logging\OpenTestReporting;

use const PHP_OS_FAMILY;
use function array_pop;
use function assert;
use function count;
use function function_exists;
use function gethostname;
use function posix_geteuid;
use function posix_getpwuid;
use function trim;
use DateTimeImmutable;
use DateTimeZone;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\Test\Prepared as TestStarted;
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'|'ERRORED'|'FAILED'|'SKIPPED'|'SUCCESSFUL'
*/
private string $result = 'SUCCESSFUL';
private bool $parentErrored = false;

/**
* @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(): 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', $this->hostName());
$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 parentErrored(): void
{
$this->parentErrored = true;
}

public function testPrepared(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 = 'ERRORED';
}

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(): void
{
$this->writer->startElement('e:finished');
$this->writer->writeAttribute('id', (string) $this->parentId);
$this->writer->writeAttribute('time', $this->timestamp());

if ($this->parentErrored) {
$this->writer->startElement('result');
$this->writer->writeAttribute('status', 'ERRORED');
$this->writer->endElement();
}

$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 BeforeFirstTestMethodErroredSubscriber($this),
new AfterLastTestMethodErroredSubscriber($this),
new TestPreparedSubscriber($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');
}

/**
* @return non-empty-string
*/
private function hostName(): string
{
$candidate = gethostname();

if ($candidate === false) {
return 'unknown';
}

$candidate = trim($candidate);

if ($candidate === '') {
return 'unknown';
}

return $candidate;
}

/**
* @return non-empty-string
*/
private function userName(): string
{
if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) {
$candidate = trim(posix_getpwuid(posix_geteuid())['name']);
} elseif (PHP_OS_FAMILY === 'Windows' && isset($_SERVER['USERNAME'])) {
$candidate = trim($_SERVER['USERNAME']);
}

if (!isset($candidate) || $candidate === '') {
return 'unknown';
}

return $candidate;
}
}
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 <sebastian@phpunit.de>
*
* 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\AfterLastTestMethodErrored;
use PHPUnit\Event\Test\AfterLastTestMethodErroredSubscriber as AfterLastTestMethodErroredSubscriberInterface;

/**
* @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 AfterLastTestMethodErroredSubscriber extends Subscriber implements AfterLastTestMethodErroredSubscriberInterface
{
/**
* @throws InvalidArgumentException
*/
public function notify(AfterLastTestMethodErrored $event): void
{
$this->logger()->parentErrored();
}
}
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 <sebastian@phpunit.de>
*
* 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\BeforeFirstTestMethodErrored;
use PHPUnit\Event\Test\BeforeFirstTestMethodErroredSubscriber as BeforeFirstTestMethodErroredSubscriberInterface;

/**
* @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 BeforeFirstTestMethodErroredSubscriber extends Subscriber implements BeforeFirstTestMethodErroredSubscriberInterface
{
/**
* @throws InvalidArgumentException
*/
public function notify(BeforeFirstTestMethodErrored $event): void
{
$this->logger()->parentErrored();
}
}
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 <sebastian@phpunit.de>
*
* 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;
}
}
Loading

0 comments on commit 9bcb2d3

Please sign in to comment.