mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 07:47:22 +01:00
Add Initial teamcity support
This commit is contained in:
28
bin/pest
28
bin/pest
@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use Pest\Actions\ValidatesEnvironment;
|
|
||||||
use Pest\ConfigLoader;
|
use Pest\ConfigLoader;
|
||||||
use Pest\Kernel;
|
use Pest\Kernel;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
use Pest\TestCaseFilters\GitDirtyTestCaseFilter;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use Symfony\Component\Console\Input\ArgvInput;
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
@ -20,10 +20,26 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
foreach ($args as $key => $value) {
|
foreach ($args as $key => $value) {
|
||||||
if (str_contains($value, '--compact')) {
|
if (str_contains($value, '--compact')) {
|
||||||
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
|
$_SERVER['COLLISION_PRINTER_COMPACT'] = 'true';
|
||||||
|
unset($args[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains($value, '--profile')) {
|
if (str_contains($value, '--profile')) {
|
||||||
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
|
$_SERVER['COLLISION_PRINTER_PROFILE'] = 'true';
|
||||||
|
unset($args[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--test-directory')) {
|
||||||
|
unset($args[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--dirty')) {
|
||||||
|
unset($args[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($value, '--teamcity')) {
|
||||||
|
unset($args[$key]);
|
||||||
|
$args[] = '--no-output';
|
||||||
|
unset($_SERVER['COLLISION_PRINTER']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,14 +79,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
$container = Container::getInstance();
|
$container = Container::getInstance();
|
||||||
$container->add(TestSuite::class, $testSuite);
|
$container->add(TestSuite::class, $testSuite);
|
||||||
$container->add(OutputInterface::class, $output);
|
$container->add(OutputInterface::class, $output);
|
||||||
|
$container->add(InputInterface::class, $argv);
|
||||||
$argsToUnset = ['--test-directory', '--compact', '--profile', '--dirty'];
|
$container->add(Container::class, $container);
|
||||||
|
|
||||||
foreach ($args as $key => $value) {
|
|
||||||
if (in_array($value, $argsToUnset)) {
|
|
||||||
unset($args[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$kernel = Kernel::boot();
|
$kernel = Kernel::boot();
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"pestphp/pest-plugin": "^2.0.0",
|
"pestphp/pest-plugin": "^2.0.0",
|
||||||
"phpunit/phpunit": "10.0.x-dev"
|
"phpunit/phpunit": "10.0.x-dev"
|
||||||
},
|
},
|
||||||
|
"version": "2.x-dev",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Pest\\": "src/"
|
"Pest\\": "src/"
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="default">
|
<testsuite name="default">
|
||||||
<directory suffix=".php">./tests</directory>
|
<directory suffix=".php">./tests</directory>
|
||||||
|
<exclude>./tests/.snapshots</exclude>
|
||||||
|
<exclude>./tests/.tests</exclude>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<coverage>
|
<coverage>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace Pest\Bootstrappers;
|
|||||||
|
|
||||||
use Pest\Contracts\Bootstrapper;
|
use Pest\Contracts\Bootstrapper;
|
||||||
use Pest\Subscribers;
|
use Pest\Subscribers;
|
||||||
|
use Pest\Support\Container;
|
||||||
use PHPUnit\Event;
|
use PHPUnit\Event;
|
||||||
use PHPUnit\Event\Subscriber;
|
use PHPUnit\Event\Subscriber;
|
||||||
|
|
||||||
@ -25,16 +26,24 @@ final class BootSubscribers implements Bootstrapper
|
|||||||
Subscribers\EnsureRetryRepositoryExists::class,
|
Subscribers\EnsureRetryRepositoryExists::class,
|
||||||
Subscribers\EnsureErroredTestsAreRetryable::class,
|
Subscribers\EnsureErroredTestsAreRetryable::class,
|
||||||
Subscribers\EnsureFailedTestsAreRetryable::class,
|
Subscribers\EnsureFailedTestsAreRetryable::class,
|
||||||
|
Subscribers\EnsureTeamCityEnabled::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Container $container,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boots the Subscribers.
|
* Boots the Subscribers.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
foreach (self::SUBSCRIBERS as $subscriber) {
|
foreach (self::SUBSCRIBERS as $subscriber) {
|
||||||
|
/** @var Subscriber $instance */
|
||||||
|
$instance = $this->container->get($subscriber);
|
||||||
Event\Facade::registerSubscriber(
|
Event\Facade::registerSubscriber(
|
||||||
new $subscriber()
|
$instance
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/Factories/Annotations/AddsAnnotation.php
Normal file
21
src/Factories/Annotations/AddsAnnotation.php
Normal 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;
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ use Pest\Factories\TestCaseMethodFactory;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class CoversNothing
|
final class CoversNothing implements AddsAnnotation
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Adds annotations regarding the "depends" feature.
|
* Adds annotations regarding the "depends" feature.
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use Pest\Support\Str;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Depends
|
final class Depends implements AddsAnnotation
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Adds annotations regarding the "depends" feature.
|
* Adds annotations regarding the "depends" feature.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use Pest\Factories\TestCaseMethodFactory;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class Groups
|
final class Groups implements AddsAnnotation
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Adds annotations regarding the "groups" feature.
|
* Adds annotations regarding the "groups" feature.
|
||||||
|
|||||||
24
src/Factories/Annotations/TestDox.php
Normal file
24
src/Factories/Annotations/TestDox.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ use Pest\Exceptions\DatasetMissing;
|
|||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Exceptions\TestAlreadyExist;
|
use Pest\Exceptions\TestAlreadyExist;
|
||||||
use Pest\Exceptions\TestDescriptionMissing;
|
use Pest\Exceptions\TestDescriptionMissing;
|
||||||
|
use Pest\Factories\Annotations\AddsAnnotation;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
use Pest\Plugins\Environment;
|
use Pest\Plugins\Environment;
|
||||||
use Pest\Support\Reflection;
|
use Pest\Support\Reflection;
|
||||||
@ -29,12 +30,13 @@ final class TestCaseFactory
|
|||||||
/**
|
/**
|
||||||
* The list of annotations.
|
* The list of annotations.
|
||||||
*
|
*
|
||||||
* @var array<int, class-string>
|
* @var array<int, class-string<AddsAnnotation>>
|
||||||
*/
|
*/
|
||||||
private const ANNOTATIONS = [
|
private const ANNOTATIONS = [
|
||||||
Annotations\Depends::class,
|
Annotations\Depends::class,
|
||||||
Annotations\Groups::class,
|
Annotations\Groups::class,
|
||||||
Annotations\CoversNothing::class,
|
Annotations\CoversNothing::class,
|
||||||
|
Annotations\TestDox::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,6 +200,10 @@ final class TestCaseFactory
|
|||||||
|
|
||||||
use Pest\Repositories\DatasetsRepository as __PestDatasets;
|
use Pest\Repositories\DatasetsRepository as __PestDatasets;
|
||||||
use Pest\TestSuite as __PestTestSuite;
|
use Pest\TestSuite as __PestTestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @testdox $filename
|
||||||
|
*/
|
||||||
$classAttributesCode
|
$classAttributesCode
|
||||||
#[\AllowDynamicProperties]
|
#[\AllowDynamicProperties]
|
||||||
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace Pest\Factories;
|
|||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
use Pest\Factories\Annotations\AddsAnnotation;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
use Pest\Plugins\Retry;
|
use Pest\Plugins\Retry;
|
||||||
use Pest\Repositories\DatasetsRepository;
|
use Pest\Repositories\DatasetsRepository;
|
||||||
@ -112,7 +113,7 @@ final class TestCaseMethodFactory
|
|||||||
/**
|
/**
|
||||||
* Creates a PHPUnit method as a string ready for evaluation.
|
* 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
|
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
|
||||||
*/
|
*/
|
||||||
public function buildForEvaluation(string $classFQN, array $annotationsToUse, array $attributesToUse): string
|
public function buildForEvaluation(string $classFQN, array $annotationsToUse, array $attributesToUse): string
|
||||||
@ -134,7 +135,6 @@ final class TestCaseMethodFactory
|
|||||||
$attributes = [];
|
$attributes = [];
|
||||||
|
|
||||||
foreach ($annotationsToUse as $annotation) {
|
foreach ($annotationsToUse as $annotation) {
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
$annotations = (new $annotation())->__invoke($this, $annotations);
|
$annotations = (new $annotation())->__invoke($this, $annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,9 +73,7 @@ final class Kernel
|
|||||||
{
|
{
|
||||||
$argv = (new Plugins\Actions\CallsHandleArguments())->__invoke($argv);
|
$argv = (new Plugins\Actions\CallsHandleArguments())->__invoke($argv);
|
||||||
|
|
||||||
$this->application->run(
|
$this->application->run($argv, false);
|
||||||
$argv, false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (new CallsAddsOutput())->__invoke(
|
return (new CallsAddsOutput())->__invoke(
|
||||||
Result::exitCode(),
|
Result::exitCode(),
|
||||||
|
|||||||
238
src/Logging/TeamCity/Converter.php
Normal file
238
src/Logging/TeamCity/Converter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/Logging/TeamCity/ServiceMessage.php
Normal file
133
src/Logging/TeamCity/ServiceMessage.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Logging/TeamCity/Subscriber/Subscriber.php
Normal file
22
src/Logging/TeamCity/Subscriber/Subscriber.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Logging/TeamCity/Subscriber/TestErroredSubscriber.php
Normal file
19
src/Logging/TeamCity/Subscriber/TestErroredSubscriber.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Logging/TeamCity/Subscriber/TestFailedSubscriber.php
Normal file
19
src/Logging/TeamCity/Subscriber/TestFailedSubscriber.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Logging/TeamCity/Subscriber/TestFinishedSubscriber.php
Normal file
19
src/Logging/TeamCity/Subscriber/TestFinishedSubscriber.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Logging/TeamCity/Subscriber/TestPreparedSubscriber.php
Normal file
19
src/Logging/TeamCity/Subscriber/TestPreparedSubscriber.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Logging/TeamCity/Subscriber/TestSkippedSubscriber.php
Normal file
19
src/Logging/TeamCity/Subscriber/TestSkippedSubscriber.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/Logging/TeamCity/TeamCityLogger.php
Normal file
251
src/Logging/TeamCity/TeamCityLogger.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Subscribers/EnsureTeamCityEnabled.php
Normal file
46
src/Subscribers/EnsureTeamCityEnabled.php
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Support;
|
namespace Pest\Support;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use ReflectionProperty;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,32 +36,4 @@ final class ExceptionTrace
|
|||||||
throw $throwable;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,4 +76,9 @@ final class Str
|
|||||||
|
|
||||||
return substr($subject, 0, $pos);
|
return substr($subject, 0, $pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function after(string $subject, string $search): string
|
||||||
|
{
|
||||||
|
return $search === '' ? $subject : array_reverse(explode($search, $subject, 2))[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
tests/.snapshots/Failure.php.inc
Normal file
24
tests/.snapshots/Failure.php.inc
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
##teamcity[testSuiteStarted name='Tests\tests\Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||||
|
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at src/Mixins/Expectation.php:312|nat src/Support/ExpectationPipeline.php:75|nat src/Support/ExpectationPipeline.php:79|nat src/Expectation.php:300|nat tests/.tests/Failure.php:6|nat src/Factories/TestCaseMethodFactory.php:101|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:76' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||||
|
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18|nat src/Factories/TestCaseMethodFactory.php:101|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:76' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='it is not done yet' message='This test was ignored.' details='' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||||
|
##teamcity[testIgnored name='build this one.' message='This test was ignored.' details='' flowId='1234']
|
||||||
|
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='Tests\tests\Failure' flowId='1234']
|
||||||
|
|
||||||
|
[90mTests:[39m [31;1m2 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m (2 assertions)[39m
|
||||||
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
10
tests/.snapshots/SuccessOnly.php.inc
Normal file
10
tests/.snapshots/SuccessOnly.php.inc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
##teamcity[testSuiteStarted name='Tests\tests\SuccessOnly' locationHint='file://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||||
|
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||||
|
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||||
|
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||||
|
##teamcity[testSuiteFinished name='Tests\tests\SuccessOnly' flowId='1234']
|
||||||
|
|
||||||
|
[90mTests:[39m [32;1m2 passed[39;22m[90m (2 assertions)[39m
|
||||||
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|
||||||
@ -893,9 +893,10 @@
|
|||||||
- visual snapshot of test suite on success
|
- visual snapshot of test suite on success
|
||||||
|
|
||||||
WARN Tests\Visual\TeamCity
|
WARN Tests\Visual\TeamCity
|
||||||
- it is can successfully call all public methods → Not supported yet.
|
- visual snapshot of team city with ('Failure.php')
|
||||||
|
- visual snapshot of team city with ('SuccessOnly.php')
|
||||||
|
|
||||||
PASS Tests\Visual\Version
|
PASS Tests\Visual\Version
|
||||||
✓ visual snapshot of help command output
|
✓ visual snapshot of help command output
|
||||||
|
|
||||||
Tests: 4 incomplete, 4 todos, 17 skipped, 624 passed (1511 assertions)
|
Tests: 4 incomplete, 4 todos, 18 skipped, 624 passed (1511 assertions)
|
||||||
26
tests/.tests/Failure.php
Normal file
26
tests/.tests/Failure.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('can fail with comparison', function () {
|
||||||
|
expect(true)->toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be ignored because of no assertions', function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be ignored because it is skipped', function () {
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
})->skip("this is why");
|
||||||
|
|
||||||
|
it('can fail', function () {
|
||||||
|
$this->fail("oh noo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not done yet', function () {
|
||||||
|
|
||||||
|
})->todo();
|
||||||
|
|
||||||
|
todo("build this one.");
|
||||||
|
|
||||||
11
tests/.tests/SuccessOnly.php
Normal file
11
tests/.tests/SuccessOnly.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('can pass with comparison', function () {
|
||||||
|
expect(true)->toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can also pass', function () {
|
||||||
|
expect("string")->toBeString();
|
||||||
|
});
|
||||||
@ -1,32 +1,42 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Pest\Logging\TeamCity;
|
test('visual snapshot of team city', function (string $testFile) {
|
||||||
use PHPUnit\Framework\AssertionFailedError;
|
$testsPath = dirname(__DIR__)."/.tests/$testFile";
|
||||||
use PHPUnit\Framework\TestResult;
|
|
||||||
use PHPUnit\Framework\TestSuite;
|
|
||||||
use PHPUnit\Framework\Warning;
|
|
||||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
$snapshot = implode(DIRECTORY_SEPARATOR, [
|
||||||
file_put_contents(__DIR__.'/output.txt', '');
|
dirname(__DIR__),
|
||||||
});
|
'.snapshots',
|
||||||
|
"$testFile.inc",
|
||||||
|
]);
|
||||||
|
|
||||||
it('is can successfully call all public methods', function () {
|
$output = function () use ($testsPath) {
|
||||||
$teamCity = new TeamCity(__DIR__.'/output.txt', false, DefaultResultPrinter::COLOR_ALWAYS);
|
$process = (new Symfony\Component\Process\Process(
|
||||||
expect($teamCity::isPestTest($this))->toBeTrue();
|
['php', 'bin/pest', '--teamcity', $testsPath],
|
||||||
$teamCity->startTestSuite(new TestSuite());
|
dirname(__DIR__, levels: 2),
|
||||||
$teamCity->startTest($this);
|
[
|
||||||
$teamCity->addError($this, new Exception('Don\'t worry about this error. Its purposeful.'), 0);
|
'EXCLUDE' => 'integration',
|
||||||
$teamCity->addFailure($this, new AssertionFailedError('Don\'t worry about this error. Its purposeful.'), 0);
|
'REBUILD_SNAPSHOTS' => false,
|
||||||
$teamCity->addWarning($this, new Warning(), 0);
|
'PARATEST' => 0,
|
||||||
$teamCity->addIncompleteTest($this, new Exception(), 0);
|
'COLLISION_IGNORE_DURATION' => 'true',
|
||||||
$teamCity->addRiskyTest($this, new Exception(), 0);
|
'FLOW_ID' => '1234',
|
||||||
$teamCity->addSkippedTest($this, new Exception(), 0);
|
],
|
||||||
$teamCity->endTest($this, 0);
|
));
|
||||||
$teamCity->printResult(new TestResult());
|
|
||||||
$teamCity->endTestSuite(new TestSuite());
|
|
||||||
})->skip('Not supported yet.');
|
|
||||||
|
|
||||||
afterEach(function () {
|
$process->run();
|
||||||
unlink(__DIR__.'/output.txt');
|
|
||||||
});
|
return $process->getOutput();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (getenv('REBUILD_SNAPSHOTS')) {
|
||||||
|
$outputContent = explode("\n", $output());
|
||||||
|
|
||||||
|
file_put_contents($snapshot, implode("\n", $outputContent));
|
||||||
|
} elseif (! getenv('EXCLUDE')) {
|
||||||
|
$output = explode("\n", $output());
|
||||||
|
|
||||||
|
expect(implode("\n", $output))->toEqual(file_get_contents($snapshot));
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
'Failure.php',
|
||||||
|
'SuccessOnly.php',
|
||||||
|
])->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE'));
|
||||||
|
|||||||
Reference in New Issue
Block a user