mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
274 lines
7.5 KiB
PHP
274 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Pest\Logging;
|
|
|
|
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
|
use Pest\Exceptions\ShouldNotHappen;
|
|
use Pest\Support\StateGenerator;
|
|
use Pest\Support\Str;
|
|
use PHPUnit\Event\Code\Test;
|
|
use PHPUnit\Event\Code\TestMethod;
|
|
use PHPUnit\Event\Code\Throwable;
|
|
use PHPUnit\Event\Test\BeforeFirstTestMethodErrored;
|
|
use PHPUnit\Event\Test\ConsideredRisky;
|
|
use PHPUnit\Event\Test\Errored;
|
|
use PHPUnit\Event\Test\Failed;
|
|
use PHPUnit\Event\Test\MarkedIncomplete;
|
|
use PHPUnit\Event\Test\Skipped;
|
|
use PHPUnit\Event\TestSuite\TestSuite;
|
|
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
|
|
use PHPUnit\Framework\Exception as FrameworkException;
|
|
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
final readonly class Converter
|
|
{
|
|
/**
|
|
* The prefix for the test suite name.
|
|
*/
|
|
private const PREFIX = 'P\\';
|
|
|
|
/**
|
|
* The state generator.
|
|
*/
|
|
private StateGenerator $stateGenerator;
|
|
|
|
/**
|
|
* Creates a new instance of the Converter.
|
|
*/
|
|
public function __construct(
|
|
private string $rootPath,
|
|
) {
|
|
$this->stateGenerator = new StateGenerator;
|
|
}
|
|
|
|
/**
|
|
* Gets the test case method name.
|
|
*/
|
|
public function getTestCaseMethodName(Test $test): string
|
|
{
|
|
if (! $test instanceof TestMethod) {
|
|
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
|
|
}
|
|
|
|
return $test->testDox()->prettifiedMethodName();
|
|
}
|
|
|
|
/**
|
|
* Gets the test case location.
|
|
*/
|
|
public function getTestCaseLocation(Test $test): string
|
|
{
|
|
if (! $test instanceof TestMethod) {
|
|
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
|
|
}
|
|
|
|
$path = $test->testDox()->prettifiedClassName();
|
|
$relativePath = $this->toRelativePath($path);
|
|
|
|
// TODO: Get the description without the dataset.
|
|
$description = $test->testDox()->prettifiedMethodName();
|
|
|
|
return "$relativePath::$description";
|
|
}
|
|
|
|
/**
|
|
* Gets the exception message.
|
|
*/
|
|
public function getExceptionMessage(Throwable $throwable): string
|
|
{
|
|
if (is_a($throwable->className(), FrameworkException::class, true)) {
|
|
return $throwable->message();
|
|
}
|
|
|
|
$buffer = $throwable->className();
|
|
$throwableMessage = $throwable->message();
|
|
|
|
if ($throwableMessage !== '') {
|
|
$buffer .= ": $throwableMessage";
|
|
}
|
|
|
|
return $buffer;
|
|
}
|
|
|
|
/**
|
|
* Gets the exception details.
|
|
*/
|
|
public function getExceptionDetails(Throwable $throwable): string
|
|
{
|
|
$buffer = $this->getStackTrace($throwable);
|
|
|
|
while ($throwable->hasPrevious()) {
|
|
$throwable = $throwable->previous();
|
|
|
|
$buffer .= sprintf(
|
|
"\nCaused by\n%s\n%s",
|
|
$throwable->description(),
|
|
$this->getStackTrace($throwable)
|
|
);
|
|
}
|
|
|
|
return $buffer;
|
|
}
|
|
|
|
/**
|
|
* Gets the stack trace.
|
|
*/
|
|
public function getStackTrace(Throwable $throwable): string
|
|
{
|
|
$stackTrace = $throwable->stackTrace();
|
|
|
|
// Split stacktrace per frame.
|
|
$frames = explode("\n", $stackTrace);
|
|
|
|
// Remove empty lines
|
|
$frames = array_filter($frames);
|
|
|
|
// clean the paths of each frame.
|
|
$frames = array_map(
|
|
fn (string $frame): string => $this->toRelativePath($frame),
|
|
$frames
|
|
);
|
|
|
|
// Format stacktrace as `at <path>`
|
|
$frames = array_map(
|
|
fn (string $frame): string => "at $frame",
|
|
$frames
|
|
);
|
|
|
|
return implode("\n", $frames);
|
|
}
|
|
|
|
/**
|
|
* Gets the test suite name.
|
|
*/
|
|
public function getTestSuiteName(TestSuite $testSuite): string
|
|
{
|
|
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
|
$firstTest = $this->getFirstTest($testSuite);
|
|
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
|
|
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
|
}
|
|
}
|
|
|
|
$name = $testSuite->name();
|
|
|
|
if (! str_starts_with($name, self::PREFIX)) {
|
|
return $name;
|
|
}
|
|
|
|
return Str::after($name, self::PREFIX);
|
|
}
|
|
|
|
/**
|
|
* Gets the trimmed test class name.
|
|
*/
|
|
public function getTrimmedTestClassName(TestMethod $test): string
|
|
{
|
|
return Str::after($test->className(), self::PREFIX);
|
|
}
|
|
|
|
/**
|
|
* Gets the test suite location.
|
|
*/
|
|
public function getTestSuiteLocation(TestSuite $testSuite): ?string
|
|
{
|
|
$firstTest = $this->getFirstTest($testSuite);
|
|
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
|
|
return null;
|
|
}
|
|
$path = $firstTest->testDox()->prettifiedClassName();
|
|
$classRelativePath = $this->toRelativePath($path);
|
|
|
|
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
|
|
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
|
|
|
|
return "$classRelativePath::$methodName";
|
|
}
|
|
|
|
return $classRelativePath;
|
|
}
|
|
|
|
/**
|
|
* Gets the prettified test method name without dataset-related suffix.
|
|
*/
|
|
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
|
|
{
|
|
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
|
|
}
|
|
|
|
/**
|
|
* Gets the first test from the test suite.
|
|
*/
|
|
private function getFirstTest(TestSuite $testSuite): ?TestMethod
|
|
{
|
|
$tests = $testSuite->tests()->asArray();
|
|
|
|
// TODO: figure out how to get the file path without a test being there.
|
|
if ($tests === []) {
|
|
return null;
|
|
}
|
|
|
|
$firstTest = $tests[0];
|
|
if (! $firstTest instanceof TestMethod) {
|
|
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
|
|
}
|
|
|
|
return $firstTest;
|
|
}
|
|
|
|
/**
|
|
* Gets the test suite size.
|
|
*/
|
|
public function getTestSuiteSize(TestSuite $testSuite): int
|
|
{
|
|
return $testSuite->count();
|
|
}
|
|
|
|
/**
|
|
* Transforms the given path in relative path.
|
|
*/
|
|
private function toRelativePath(string $path): string
|
|
{
|
|
// Remove cwd from the path.
|
|
return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path);
|
|
}
|
|
|
|
/**
|
|
* Get the test result.
|
|
*/
|
|
public function getStateFromResult(PhpUnitTestResult $result): State
|
|
{
|
|
$events = [
|
|
...$result->testErroredEvents(),
|
|
...$result->testFailedEvents(),
|
|
...$result->testSkippedEvents(),
|
|
...array_merge(...array_values($result->testConsideredRiskyEvents())),
|
|
...$result->testMarkedIncompleteEvents(),
|
|
];
|
|
|
|
$numberOfNotPassedTests = count(
|
|
array_unique(
|
|
array_map(
|
|
function (BeforeFirstTestMethodErrored|Errored|Failed|Skipped|ConsideredRisky|MarkedIncomplete $event): string {
|
|
if ($event instanceof BeforeFirstTestMethodErrored) {
|
|
return $event->testClassName();
|
|
}
|
|
|
|
return $this->getTestCaseLocation($event->test());
|
|
},
|
|
$events
|
|
)
|
|
)
|
|
);
|
|
|
|
$numberOfPassedTests = $result->numberOfTestsRun() - $numberOfNotPassedTests;
|
|
|
|
return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result);
|
|
}
|
|
}
|