vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
+107
View File
@@ -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),
);
}
}
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}