vendor and env first commit
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SplFileInfo;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function ksort;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final class LogMerger
|
||||
{
|
||||
/** @param list<SplFileInfo> $junitFiles */
|
||||
public function merge(array $junitFiles): TestSuite
|
||||
{
|
||||
$mainSuite = null;
|
||||
foreach ($junitFiles as $junitFile) {
|
||||
if (! $junitFile->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$otherSuite = TestSuite::fromFile($junitFile);
|
||||
if ($mainSuite === null) {
|
||||
$mainSuite = $otherSuite;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($mainSuite->name !== $otherSuite->name) {
|
||||
if ($mainSuite->name !== '') {
|
||||
$mainSuite = new TestSuite(
|
||||
'',
|
||||
$mainSuite->tests,
|
||||
$mainSuite->assertions,
|
||||
$mainSuite->failures,
|
||||
$mainSuite->errors,
|
||||
$mainSuite->skipped,
|
||||
$mainSuite->time,
|
||||
'',
|
||||
[$mainSuite->name => $mainSuite],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
if ($otherSuite->name !== '') {
|
||||
$otherSuite = new TestSuite(
|
||||
'',
|
||||
$otherSuite->tests,
|
||||
$otherSuite->assertions,
|
||||
$otherSuite->failures,
|
||||
$otherSuite->errors,
|
||||
$otherSuite->skipped,
|
||||
$otherSuite->time,
|
||||
'',
|
||||
[$otherSuite->name => $otherSuite],
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$mainSuite = $this->mergeSuites($mainSuite, $otherSuite);
|
||||
}
|
||||
|
||||
assert($mainSuite !== null);
|
||||
|
||||
return $mainSuite;
|
||||
}
|
||||
|
||||
private function mergeSuites(TestSuite $suite1, TestSuite $suite2): TestSuite
|
||||
{
|
||||
assert($suite1->name === $suite2->name);
|
||||
|
||||
$suites = $suite1->suites;
|
||||
foreach ($suite2->suites as $suite2suiteName => $suite2suite) {
|
||||
if (! isset($suites[$suite2suiteName])) {
|
||||
$suites[$suite2suiteName] = $suite2suite;
|
||||
continue;
|
||||
}
|
||||
|
||||
$suites[$suite2suiteName] = $this->mergeSuites(
|
||||
$suites[$suite2suiteName],
|
||||
$suite2suite,
|
||||
);
|
||||
}
|
||||
|
||||
ksort($suites);
|
||||
|
||||
return new TestSuite(
|
||||
$suite1->name,
|
||||
$suite1->tests + $suite2->tests,
|
||||
$suite1->assertions + $suite2->assertions,
|
||||
$suite1->failures + $suite2->failures,
|
||||
$suite1->errors + $suite2->errors,
|
||||
$suite1->skipped + $suite2->skipped,
|
||||
$suite1->time + $suite2->time,
|
||||
$suite1->file,
|
||||
$suites,
|
||||
array_merge($suite1->cases, $suite2->cases),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
/** @internal */
|
||||
enum MessageType
|
||||
{
|
||||
case error;
|
||||
case failure;
|
||||
case skipped;
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::error => 'error',
|
||||
self::failure => 'failure',
|
||||
self::skipped => 'skipped',
|
||||
};
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function current;
|
||||
use function iterator_to_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class TestCase
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $class,
|
||||
public readonly string $file,
|
||||
public readonly int $line,
|
||||
public readonly int $assertions,
|
||||
public readonly float $time
|
||||
) {
|
||||
}
|
||||
|
||||
final public static function caseFromNode(SimpleXMLElement $node): self
|
||||
{
|
||||
$getFirstNode = static function (array $nodes): SimpleXMLElement {
|
||||
assert(count($nodes) === 1);
|
||||
$node = current($nodes);
|
||||
assert($node instanceof SimpleXMLElement);
|
||||
|
||||
return $node;
|
||||
};
|
||||
$getType = static function (SimpleXMLElement $node): string {
|
||||
$element = $node->attributes();
|
||||
assert($element !== null);
|
||||
$attributes = iterator_to_array($element);
|
||||
assert($attributes !== []);
|
||||
|
||||
return (string) $attributes['type'];
|
||||
};
|
||||
|
||||
if (($errors = $node->xpath('error')) !== []) {
|
||||
$error = $getFirstNode($errors);
|
||||
$type = $getType($error);
|
||||
$text = (string) $error;
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
$type,
|
||||
$text,
|
||||
MessageType::error,
|
||||
);
|
||||
}
|
||||
|
||||
if (($failures = $node->xpath('failure')) !== []) {
|
||||
$failure = $getFirstNode($failures);
|
||||
$type = $getType($failure);
|
||||
$text = (string) $failure;
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
$type,
|
||||
$text,
|
||||
MessageType::failure,
|
||||
);
|
||||
}
|
||||
|
||||
if ($node->xpath('skipped') !== []) {
|
||||
$text = (string) $node['name'];
|
||||
if ((string) $node['class'] !== '') {
|
||||
$text = sprintf(
|
||||
"%s::%s\n\n%s:%s",
|
||||
$node['class'],
|
||||
$node['name'],
|
||||
$node['file'],
|
||||
$node['line'],
|
||||
);
|
||||
}
|
||||
|
||||
return new TestCaseWithMessage(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
null,
|
||||
$text,
|
||||
MessageType::skipped,
|
||||
);
|
||||
}
|
||||
|
||||
return new self(
|
||||
(string) $node['name'],
|
||||
(string) $node['class'],
|
||||
(string) $node['file'],
|
||||
(int) $node['line'],
|
||||
(int) $node['assertions'],
|
||||
(float) $node['time'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final class TestCaseWithMessage extends TestCase
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $class,
|
||||
string $file,
|
||||
int $line,
|
||||
int $assertions,
|
||||
float $time,
|
||||
public readonly ?string $type,
|
||||
public readonly string $text,
|
||||
public readonly MessageType $xmlTagName
|
||||
) {
|
||||
parent::__construct($name, $class, $file, $line, $assertions, $time);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
use SplFileInfo;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function file_get_contents;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
final class TestSuite
|
||||
{
|
||||
/**
|
||||
* @param array<string, TestSuite> $suites
|
||||
* @param list<TestCase> $cases
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly int $tests,
|
||||
public readonly int $assertions,
|
||||
public readonly int $failures,
|
||||
public readonly int $errors,
|
||||
public readonly int $skipped,
|
||||
public readonly float $time,
|
||||
public readonly string $file,
|
||||
public readonly array $suites,
|
||||
public readonly array $cases
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromFile(SplFileInfo $logFile): self
|
||||
{
|
||||
assert($logFile->isFile() && 0 < (int) $logFile->getSize());
|
||||
|
||||
$logFileContents = file_get_contents($logFile->getPathname());
|
||||
assert($logFileContents !== false);
|
||||
|
||||
return self::parseTestSuite(
|
||||
new SimpleXMLElement($logFileContents),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private static function parseTestSuite(SimpleXMLElement $node, bool $isRootSuite): self
|
||||
{
|
||||
if ($isRootSuite) {
|
||||
$tests = 0;
|
||||
$assertions = 0;
|
||||
$failures = 0;
|
||||
$errors = 0;
|
||||
$skipped = 0;
|
||||
$time = 0;
|
||||
} else {
|
||||
$tests = (int) $node['tests'];
|
||||
$assertions = (int) $node['assertions'];
|
||||
$failures = (int) $node['failures'];
|
||||
$errors = (int) $node['errors'];
|
||||
$skipped = (int) $node['skipped'];
|
||||
$time = (float) $node['time'];
|
||||
}
|
||||
|
||||
$count = count($node->testsuite);
|
||||
$suites = [];
|
||||
foreach ($node->testsuite as $singleTestSuiteXml) {
|
||||
$testSuite = self::parseTestSuite($singleTestSuiteXml, false);
|
||||
if ($isRootSuite && $count === 1) {
|
||||
return $testSuite;
|
||||
}
|
||||
|
||||
$suites[$testSuite->name] = $testSuite;
|
||||
|
||||
if (! $isRootSuite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tests += $testSuite->tests;
|
||||
$assertions += $testSuite->assertions;
|
||||
$failures += $testSuite->failures;
|
||||
$errors += $testSuite->errors;
|
||||
$skipped += $testSuite->skipped;
|
||||
$time += $testSuite->time;
|
||||
}
|
||||
|
||||
$cases = [];
|
||||
foreach ($node->testcase as $singleTestCase) {
|
||||
$cases[] = TestCase::caseFromNode($singleTestCase);
|
||||
}
|
||||
|
||||
return new self(
|
||||
(string) $node['name'],
|
||||
$tests,
|
||||
$assertions,
|
||||
$failures,
|
||||
$errors,
|
||||
$skipped,
|
||||
$time,
|
||||
(string) $node['file'],
|
||||
$suites,
|
||||
$cases,
|
||||
);
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ParaTest\JUnit;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
use function assert;
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function htmlspecialchars;
|
||||
use function is_dir;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
use function mkdir;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
|
||||
use const ENT_XML1;
|
||||
|
||||
/** @internal */
|
||||
final class Writer
|
||||
{
|
||||
private readonly DOMDocument $document;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||
$this->document->formatOutput = true;
|
||||
}
|
||||
|
||||
public function write(TestSuite $testSuite, string $path): void
|
||||
{
|
||||
$dir = dirname($path);
|
||||
if (! is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$result = file_put_contents($path, $this->getXml($testSuite));
|
||||
assert(is_int($result) && 0 < $result);
|
||||
}
|
||||
|
||||
/** @return non-empty-string */
|
||||
private function getXml(TestSuite $testSuite): string
|
||||
{
|
||||
$xmlTestsuites = $this->document->createElement('testsuites');
|
||||
$xmlTestsuites->appendChild($this->createSuiteNode($testSuite));
|
||||
$this->document->appendChild($xmlTestsuites);
|
||||
|
||||
$xml = $this->document->saveXML();
|
||||
assert(is_string($xml) && $xml !== '');
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function createSuiteNode(TestSuite $parentSuite): DOMElement
|
||||
{
|
||||
$suiteNode = $this->document->createElement('testsuite');
|
||||
$suiteNode->setAttribute('name', $parentSuite->name);
|
||||
if ($parentSuite->file !== '') {
|
||||
$suiteNode->setAttribute('file', $parentSuite->file);
|
||||
}
|
||||
|
||||
$suiteNode->setAttribute('tests', (string) $parentSuite->tests);
|
||||
$suiteNode->setAttribute('assertions', (string) $parentSuite->assertions);
|
||||
$suiteNode->setAttribute('errors', (string) $parentSuite->errors);
|
||||
$suiteNode->setAttribute('failures', (string) $parentSuite->failures);
|
||||
$suiteNode->setAttribute('skipped', (string) $parentSuite->skipped);
|
||||
$suiteNode->setAttribute('time', (string) $parentSuite->time);
|
||||
|
||||
foreach ($parentSuite->suites as $suite) {
|
||||
$suiteNode->appendChild($this->createSuiteNode($suite));
|
||||
}
|
||||
|
||||
foreach ($parentSuite->cases as $case) {
|
||||
$suiteNode->appendChild($this->createCaseNode($case));
|
||||
}
|
||||
|
||||
return $suiteNode;
|
||||
}
|
||||
|
||||
private function createCaseNode(TestCase $case): DOMElement
|
||||
{
|
||||
$caseNode = $this->document->createElement('testcase');
|
||||
|
||||
$caseNode->setAttribute('name', $case->name);
|
||||
$caseNode->setAttribute('class', $case->class);
|
||||
$caseNode->setAttribute('classname', str_replace('\\', '.', $case->class));
|
||||
$caseNode->setAttribute('file', $case->file);
|
||||
$caseNode->setAttribute('line', (string) $case->line);
|
||||
$caseNode->setAttribute('assertions', (string) $case->assertions);
|
||||
$caseNode->setAttribute('time', sprintf('%F', $case->time));
|
||||
|
||||
if ($case instanceof TestCaseWithMessage) {
|
||||
if ($case->xmlTagName === MessageType::skipped) {
|
||||
$defectNode = $this->document->createElement($case->xmlTagName->toString());
|
||||
} else {
|
||||
$defectNode = $this->document->createElement($case->xmlTagName->toString(), htmlspecialchars($case->text, ENT_XML1));
|
||||
$type = $case->type;
|
||||
if ($type !== null) {
|
||||
$defectNode->setAttribute('type', $type);
|
||||
}
|
||||
}
|
||||
|
||||
$caseNode->appendChild($defectNode);
|
||||
}
|
||||
|
||||
return $caseNode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user