Add Initial teamcity support

This commit is contained in:
Oliver
2023-01-08 11:21:08 +01:00
parent 15931e2418
commit 0839c7e127
36 changed files with 1087 additions and 78 deletions

View File

@ -6,6 +6,7 @@ namespace Pest\Bootstrappers;
use Pest\Contracts\Bootstrapper;
use Pest\Subscribers;
use Pest\Support\Container;
use PHPUnit\Event;
use PHPUnit\Event\Subscriber;
@ -25,16 +26,24 @@ final class BootSubscribers implements Bootstrapper
Subscribers\EnsureRetryRepositoryExists::class,
Subscribers\EnsureErroredTestsAreRetryable::class,
Subscribers\EnsureFailedTestsAreRetryable::class,
Subscribers\EnsureTeamCityEnabled::class,
];
public function __construct(
private readonly Container $container,
) {
}
/**
* Boots the Subscribers.
*/
public function boot(): void
{
foreach (self::SUBSCRIBERS as $subscriber) {
/** @var Subscriber $instance */
$instance = $this->container->get($subscriber);
Event\Facade::registerSubscriber(
new $subscriber()
$instance
);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Factories\TestCaseMethodFactory;
/**
* @interal
*/
interface AddsAnnotation
{
/**
* Adds annotations to method
*
* @param array<int, string> $annotations
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array;
}

View File

@ -10,7 +10,7 @@ use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class CoversNothing
final class CoversNothing implements AddsAnnotation
{
/**
* Adds annotations regarding the "depends" feature.

View File

@ -10,7 +10,7 @@ use Pest\Support\Str;
/**
* @internal
*/
final class Depends
final class Depends implements AddsAnnotation
{
/**
* Adds annotations regarding the "depends" feature.

View File

@ -9,7 +9,7 @@ use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Groups
final class Groups implements AddsAnnotation
{
/**
* Adds annotations regarding the "groups" feature.

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Factories\TestCaseMethodFactory;
final class TestDox implements AddsAnnotation
{
/**
* Add metadata via test dox for TeamCity
*
* @param array<int, string> $annotations
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
// First test dox on class overrides the method name.
$annotations[] = "@testdox $method->description";
return $annotations;
}
}

View File

@ -11,6 +11,7 @@ use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Annotations\AddsAnnotation;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Plugins\Environment;
use Pest\Support\Reflection;
@ -29,12 +30,13 @@ final class TestCaseFactory
/**
* The list of annotations.
*
* @var array<int, class-string>
* @var array<int, class-string<AddsAnnotation>>
*/
private const ANNOTATIONS = [
Annotations\Depends::class,
Annotations\Groups::class,
Annotations\CoversNothing::class,
Annotations\TestDox::class,
];
/**
@ -198,6 +200,10 @@ final class TestCaseFactory
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
/**
* @testdox $filename
*/
$classAttributesCode
#[\AllowDynamicProperties]
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {

View File

@ -6,6 +6,7 @@ namespace Pest\Factories;
use Closure;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Factories\Annotations\AddsAnnotation;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Plugins\Retry;
use Pest\Repositories\DatasetsRepository;
@ -112,7 +113,7 @@ final class TestCaseMethodFactory
/**
* Creates a PHPUnit method as a string ready for evaluation.
*
* @param array<int, class-string> $annotationsToUse
* @param array<int, class-string<AddsAnnotation>> $annotationsToUse
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
*/
public function buildForEvaluation(string $classFQN, array $annotationsToUse, array $attributesToUse): string
@ -134,7 +135,6 @@ final class TestCaseMethodFactory
$attributes = [];
foreach ($annotationsToUse as $annotation) {
/** @phpstan-ignore-next-line */
$annotations = (new $annotation())->__invoke($this, $annotations);
}

View File

@ -73,9 +73,7 @@ final class Kernel
{
$argv = (new Plugins\Actions\CallsHandleArguments())->__invoke($argv);
$this->application->run(
$argv, false,
);
$this->application->run($argv, false);
return (new CallsAddsOutput())->__invoke(
Result::exitCode(),

View File

@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity;
use NunoMaduro\Collision\Adapters\Phpunit\State;
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Support\Str;
use PHPUnit\Event\Code\Test;
use PHPUnit\Event\Code\TestDox;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\TestData\TestDataCollection;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Framework\Exception as FrameworkException;
use PHPUnit\Framework\IncompleteTestError;
use PHPUnit\Framework\SkippedWithMessageException;
use PHPUnit\Metadata\MetadataCollection;
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
/**
* @internal
*/
final class Converter
{
/**
* @var string
*/
private const PREFIX = 'P\\';
public function __construct(
private readonly string $rootPath,
) {
}
public function getTestCaseMethodName(Test $test): string
{
if (! $test instanceof TestMethod) {
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
}
return $test->testDox()->prettifiedMethodName();
}
public function getTestCaseLocation(Test $test): string
{
if (! $test instanceof TestMethod) {
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
}
$fileName = $test->testDox()->prettifiedClassName();
$fileName = $this->cleanPath($fileName);
// TODO: Get the description without the dataset.
$description = $test->testDox()->prettifiedMethodName();
return "$fileName::$description";
}
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;
}
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;
}
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->cleanPath($frame),
$frames
);
// Format stacktrace as `at <path>`
$frames = array_map(
fn (string $frame) => "at $frame",
$frames
);
return implode("\n", $frames);
}
public function getTestSuiteName(TestSuite $testSuite): string
{
$name = $testSuite->name();
if (! str_starts_with($name, self::PREFIX)) {
return $name;
}
return Str::after($name, self::PREFIX);
}
public function getTestSuiteLocation(TestSuite $testSuite): string|null
{
$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');
}
$path = $firstTest->testDox()->prettifiedClassName();
return $this->cleanPath($path);
}
private function cleanPath(string $path): string
{
// Remove cwd from the path.
return str_replace("$this->rootPath/", '', $path);
}
public function getStateFromResult(PhpUnitTestResult $result): State
{
$state = new State();
foreach ($result->testErroredEvents() as $resultEvent) {
assert($resultEvent instanceof Errored);
$state->add(TestResult::fromTestCase(
$resultEvent->test(),
TestResult::FAIL,
$resultEvent->throwable()
));
}
foreach ($result->testFailedEvents() as $resultEvent) {
$state->add(TestResult::fromTestCase(
$resultEvent->test(),
TestResult::FAIL,
$resultEvent->throwable()
));
}
foreach ($result->testMarkedIncompleteEvents() as $resultEvent) {
$state->add(TestResult::fromTestCase(
$resultEvent->test(),
TestResult::INCOMPLETE,
$resultEvent->throwable()
));
}
foreach ($result->testConsideredRiskyEvents() as $riskyEvents) {
foreach ($riskyEvents as $riskyEvent) {
$state->add(TestResult::fromTestCase(
$riskyEvent->test(),
TestResult::RISKY,
Throwable::from(new IncompleteTestError($riskyEvent->message()))
));
}
}
foreach ($result->testSkippedEvents() as $resultEvent) {
if ($resultEvent->message() === '__TODO__') {
$state->add(TestResult::fromTestCase($resultEvent->test(), TestResult::TODO));
continue;
}
$state->add(TestResult::fromTestCase(
$resultEvent->test(),
TestResult::SKIPPED,
Throwable::from(new SkippedWithMessageException($resultEvent->message()))
));
}
$numberOfPassedTests = $result->numberOfTests()
- $result->numberOfTestErroredEvents()
- $result->numberOfTestFailedEvents()
- $result->numberOfTestSkippedEvents()
- $result->numberOfTestsWithTestConsideredRiskyEvents()
- $result->numberOfTestMarkedIncompleteEvents();
for ($i = 0; $i < $numberOfPassedTests; $i++) {
$state->add(TestResult::fromTestCase(
new TestMethod(
/** @phpstan-ignore-next-line */
"$i",
/** @phpstan-ignore-next-line */
'',
'',
1,
/** @phpstan-ignore-next-line */
TestDox::fromClassNameAndMethodName('', ''),
MetadataCollection::fromArray([]),
TestDataCollection::fromArray([])
),
TestResult::PASS
));
}
return $state;
}
}

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity;
final class ServiceMessage
{
private static int|null $flowId = null;
/**
* @param array<string, string|int|null> $parameters
*/
public function __construct(
private readonly string $type,
private readonly array $parameters,
) {
}
public function toString(): string
{
$paramsToString = '';
foreach ([...$this->parameters, 'flowId' => self::$flowId] as $key => $value) {
$value = self::escapeServiceMessage((string) $value);
$paramsToString .= " $key='$value'";
}
return "##teamcity[$this->type$paramsToString]";
}
public static function testSuiteStarted(string $name, string|null $location): self
{
return new self('testSuiteStarted', [
'name' => $name,
'locationHint' => $location === null ? null : "file://$location",
]);
}
public static function testSuiteFinished(string $name): self
{
return new self('testSuiteFinished', [
'name' => $name,
]);
}
public static function testStarted(string $name, string $location): self
{
return new self('testStarted', [
'name' => $name,
'locationHint' => "pest_qn://$location",
]);
}
/**
* @param int $duration in milliseconds
*/
public static function testFinished(string $name, int $duration): self
{
return new self('testFinished', [
'name' => $name,
'duration' => $duration,
]);
}
public static function testStdOut(string $name, string $data): self
{
if (! str_ends_with($data, "\n")) {
$data .= "\n";
}
return new self('testStdOut', [
'name' => $name,
'out' => $data,
]);
}
public static function testFailed(string $name, string $message, string $details): self
{
return new self('testFailed', [
'name' => $name,
'message' => $message,
'details' => $details,
]);
}
public static function testStdErr(string $name, string $data): self
{
if (! str_ends_with($data, "\n")) {
$data .= "\n";
}
return new self('testStdErr', [
'name' => $name,
'out' => $data,
]);
}
public static function testIgnored(string $name, string $message, string $details = null): self
{
return new self('testIgnored', [
'name' => $name,
'message' => $message,
'details' => $details,
]);
}
public static function comparisonFailure(string $name, string $message, string $details, string $actual, string $expected): self
{
return new self('testFailed', [
'name' => $name,
'message' => $message,
'details' => $details,
'type' => 'comparisonFailure',
'actual' => $actual,
'expected' => $expected,
]);
}
private static function escapeServiceMessage(string $text): string
{
return str_replace(
['|', "'", "\n", "\r", ']', '['],
['||', "|'", '|n', '|r', '|]', '|['],
$text
);
}
public static function setFlowId(int $flowId): void
{
self::$flowId = $flowId;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use Pest\Logging\TeamCity\TeamCityLogger;
/**
* @internal
*/
abstract class Subscriber
{
public function __construct(private readonly TeamCityLogger $logger)
{
}
final protected function logger(): TeamCityLogger
{
return $this->logger;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\ConsideredRiskySubscriber;
/**
* @internal
*/
final class TestConsideredRiskySubscriber extends Subscriber implements ConsideredRiskySubscriber
{
public function notify(ConsideredRisky $event): void
{
$this->logger()->testConsideredRisky($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\ErroredSubscriber;
/**
* @internal
*/
final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber
{
public function notify(Errored $event): void
{
$this->logger()->testErrored($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;
/**
* @internal
*/
final class TestExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber
{
public function notify(ExecutionFinished $event): void
{
$this->logger()->testExecutionFinished($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\FailedSubscriber;
/**
* @internal
*/
final class TestFailedSubscriber extends Subscriber implements FailedSubscriber
{
public function notify(Failed $event): void
{
$this->logger()->testFailed($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
/**
* @internal
*/
final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber
{
public function notify(Finished $event): void
{
$this->logger()->testFinished($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
/**
* @internal
*/
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
{
public function notify(MarkedIncomplete $event): void
{
$this->logger()->testMarkedIncomplete($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
/**
* @internal
*/
final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber
{
public function notify(Prepared $event): void
{
$this->logger()->testPrepared($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber;
/**
* @internal
*/
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{
public function notify(Skipped $event): void
{
$this->logger()->testSkipped($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\TestSuite\Finished;
use PHPUnit\Event\TestSuite\FinishedSubscriber;
/**
* @internal
*/
final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber
{
public function notify(Finished $event): void
{
$this->logger()->testSuiteFinished($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity\Subscriber;
use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\TestSuite\StartedSubscriber;
/**
* @internal
*/
final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber
{
public function notify(Started $event): void
{
$this->logger()->testSuiteStarted($event);
}
}

View File

@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\TeamCity;
use NunoMaduro\Collision\Adapters\Phpunit\Style;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFailedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestMarkedIncompleteSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestPreparedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSkippedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSuiteFinishedSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestSuiteStartedSubscriber;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\Telemetry\Duration;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Telemetry\Info;
use PHPUnit\Event\Telemetry\Snapshot;
use PHPUnit\Event\Test\ConsideredRisky;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestSuite\Finished as TestSuiteFinished;
use PHPUnit\Event\TestSuite\Started as TestSuiteStarted;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class TeamCityLogger
{
private ?HRTime $time = null;
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
public function __construct(
private readonly OutputInterface $output,
private readonly Converter $converter,
private readonly int|null $flowId,
private readonly bool $withoutDuration,
) {
$this->registerSubscribers();
$this->setFlowId();
}
public function testSuiteStarted(TestSuiteStarted $event): void
{
$message = ServiceMessage::testSuiteStarted(
$this->converter->getTestSuiteName($event->testSuite()),
$this->converter->getTestSuiteLocation($event->testSuite())
);
$this->output($message);
}
public function testSuiteFinished(TestSuiteFinished $event): void
{
$message = ServiceMessage::testSuiteFinished(
$this->converter->getTestSuiteName($event->testSuite()),
);
$this->output($message);
}
public function testPrepared(Prepared $event): void
{
$message = ServiceMessage::testStarted(
$this->converter->getTestCaseMethodName($event->test()),
$this->converter->getTestCaseLocation($event->test()),
);
$this->output($message);
$this->time = $event->telemetryInfo()->time();
}
public function testMarkedIncomplete(MarkedIncomplete $event): never
{
// TODO: when does this trigger?
throw ShouldNotHappen::fromMessage('testMarkedIncomplete not implemented.');
}
public function testSkipped(Skipped $event): void
{
$message = ServiceMessage::testIgnored(
$this->converter->getTestCaseMethodName($event->test()),
'This test was ignored.'
);
$this->output($message);
}
/**
* This will trigger in the following scenarios
* - When an exception is thrown
*/
public function testErrored(Errored $event): void
{
$testName = $this->converter->getTestCaseMethodName($event->test());
$message = $this->converter->getExceptionMessage($event->throwable());
$details = $this->converter->getExceptionDetails($event->throwable());
$message = ServiceMessage::testFailed(
$testName,
$message,
$details,
);
$this->output($message);
}
/**
* This will trigger in the following scenarios
* - When an assertion fails
*/
public function testFailed(Failed $event): void
{
$testName = $this->converter->getTestCaseMethodName($event->test());
$message = $this->converter->getExceptionMessage($event->throwable());
$details = $this->converter->getExceptionDetails($event->throwable());
if ($event->hasComparisonFailure()) {
$comparison = $event->comparisonFailure();
$message = ServiceMessage::comparisonFailure(
$testName,
$message,
$details,
$comparison->actual(),
$comparison->expected()
);
} else {
$message = ServiceMessage::testFailed(
$testName,
$message,
$details,
);
}
$this->output($message);
}
/**
* This will trigger in the following scenarios
* - When no assertions in a test
*/
public function testConsideredRisky(ConsideredRisky $event): void
{
$message = ServiceMessage::testIgnored(
$this->converter->getTestCaseMethodName($event->test()),
$event->message()
);
$this->output($message);
}
public function testFinished(Finished $event): void
{
if ($this->time === null) {
throw ShouldNotHappen::fromMessage('Start time has not been set.');
}
$testName = $this->converter->getTestCaseMethodName($event->test());
$duration = $event->telemetryInfo()->time()->duration($this->time)->asFloat();
if ($this->withoutDuration) {
$duration = 100;
}
$message = ServiceMessage::testFinished(
$testName,
(int) ($duration * 1000)
);
$this->output($message);
}
public function testExecutionFinished(ExecutionFinished $event): void
{
$result = TestResultFacade::result();
$state = $this->converter->getStateFromResult($result);
assert($this->output instanceof ConsoleOutput);
$style = new Style($this->output);
$telemetry = $event->telemetryInfo();
if ($this->withoutDuration) {
$telemetry = new Info(
new Snapshot(
$telemetry->time(),
$telemetry->memoryUsage(),
$telemetry->peakMemoryUsage(),
),
Duration::fromSecondsAndNanoseconds(1, 0),
$telemetry->memoryUsageSinceStart(),
$telemetry->durationSincePrevious(),
$telemetry->memoryUsageSincePrevious(),
$telemetry->emitter(),
);
}
$style->writeRecap($state, $telemetry);
}
public function output(ServiceMessage $message): void
{
$this->output->writeln("{$message->toString()}");
}
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
private function registerSubscribers(): void
{
Facade::registerSubscribers(
new TestSuiteStartedSubscriber($this),
new TestSuiteFinishedSubscriber($this),
new TestPreparedSubscriber($this),
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestConsideredRiskySubscriber($this),
new TestExecutionFinishedSubscriber($this),
);
}
private function setFlowId(): void
{
if ($this->flowId === null) {
return;
}
ServiceMessage::setFlowId($this->flowId);
}
}

View File

@ -29,7 +29,7 @@ final class TestRepository
private array $uses = [];
/**
* @var array<int, TestCaseFilter>
* @var array<int, TestCaseFilter>
*/
private array $filters = [];

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Pest\Subscribers;
use Pest\Logging\TeamCity\Converter;
use Pest\Logging\TeamCity\TeamCityLogger;
use Pest\TestSuite;
use PHPUnit\Event\TestRunner\Configured;
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class EnsureTeamCityEnabled implements ConfiguredSubscriber
{
public function __construct(
private readonly OutputInterface $output,
private readonly InputInterface $input,
private readonly TestSuite $testSuite,
) {
}
/**
* Runs the subscriber.
*/
public function notify(Configured $event): void
{
if (! $this->input->hasParameterOption('--teamcity')) {
return;
}
$flowId = getenv('FLOW_ID');
$flowId = is_string($flowId) ? (int) $flowId : getmypid();
new TeamCityLogger(
$this->output,
new Converter($this->testSuite->rootPath),
$flowId === false ? null : $flowId,
getenv('COLLISION_IGNORE_DURATION') !== false
);
}
}

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Pest\Support;
use Closure;
use ReflectionProperty;
use Throwable;
/**
@ -37,32 +36,4 @@ final class ExceptionTrace
throw $throwable;
}
}
/**
* Removes any item from the stack trace referencing Pest so as not to
* crowd the error log for the end user.
*/
public static function removePestReferences(Throwable $t): void
{
if (! property_exists($t, 'serializableTrace')) {
return;
}
$property = new ReflectionProperty($t, 'serializableTrace');
$property->setAccessible(true);
/** @var array<int, array<string, string>> $trace */
$trace = $property->getValue($t);
$cleanedTrace = [];
foreach ($trace as $item) {
if (array_key_exists('file', $item) && mb_strpos($item['file'], 'vendor/pestphp/pest/') > 0) {
continue;
}
$cleanedTrace[] = $item;
}
$property->setValue($t, $cleanedTrace);
}
}

View File

@ -76,4 +76,9 @@ final class Str
return substr($subject, 0, $pos);
}
public static function after(string $subject, string $search): string
{
return $search === '' ? $subject : array_reverse(explode($search, $subject, 2))[0];
}
}