diff --git a/phpstan.neon b/phpstan.neon index 6900f634..81e3bd98 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -22,3 +22,12 @@ parameters: - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" - "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#" + - + message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#' + path: src/TeamCity.php + - + message: '#invalid typehint type Pest\\Concerns\\TestCase#' + path: src/TeamCity.php + - + message: '#is not subtype of native type PHPUnit\\Framework\\Test#' + path: src/TeamCity.php diff --git a/rector.php b/rector.php index 3069da26..4179a721 100644 --- a/rector.php +++ b/rector.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Core\Configuration\Option; +use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector; use Rector\Set\ValueObject\SetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -27,5 +28,8 @@ return static function (ContainerConfigurator $containerConfigurator): void { SetList::SOLID, ]); - $parameters->set(Option::PATHS, [__DIR__.'/src', __DIR__.'/tests']); + $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']); + $parameters->set(Option::EXCLUDE_RECTORS, [ + StaticCallOnNonStaticToInstanceCallRector::class, + ]); }; diff --git a/src/Actions/AddsDefaults.php b/src/Actions/AddsDefaults.php index 799eda6e..d711641c 100644 --- a/src/Actions/AddsDefaults.php +++ b/src/Actions/AddsDefaults.php @@ -5,12 +5,16 @@ declare(strict_types=1); namespace Pest\Actions; use NunoMaduro\Collision\Adapters\Phpunit\Printer; +use Pest\TeamCity; +use PHPUnit\TextUI\DefaultResultPrinter; /** * @internal */ final class AddsDefaults { + private const PRINTER = 'printer'; + /** * Adds default arguments to the given `arguments` array. * @@ -20,8 +24,12 @@ final class AddsDefaults */ public static function to(array $arguments): array { - if (!array_key_exists('printer', $arguments)) { - $arguments['printer'] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); + if (!array_key_exists(self::PRINTER, $arguments)) { + $arguments[self::PRINTER] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS); + } + + if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) { + $arguments[self::PRINTER] = new TeamCity($arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS); } return $arguments; diff --git a/src/Concerns/TestCase.php b/src/Concerns/TestCase.php index 58d17059..86862d9f 100644 --- a/src/Concerns/TestCase.php +++ b/src/Concerns/TestCase.php @@ -65,6 +65,11 @@ trait TestCase return $this->__description; } + public static function __getFileName(): string + { + return self::$__filename; + } + /** * This method is called before the first test of this test class is run. */ diff --git a/src/TeamCity.php b/src/TeamCity.php new file mode 100644 index 00000000..b5dec5ab --- /dev/null +++ b/src/TeamCity.php @@ -0,0 +1,235 @@ +phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity( + null, + $verbose, + $colors, + false, + 80, + false + ); + } + + public function printResult(TestResult $result): void + { + $this->printHeader($result); + $this->printFooter($result); + } + + /** @phpstan-ignore-next-line */ + public function startTestSuite(TestSuite $suite): void + { + $this->flowId = getmypid(); + + if (!$this->isSummaryTestCountPrinted) { + $this->printEvent( + 'testCount', + ['count' => $suite->count()] + ); + $this->isSummaryTestCountPrinted = true; + } + + $suiteName = $suite->getName(); + + if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) { + $this->printEvent( + self::TEST_SUITE_STARTED, [ + self::NAME => $suiteName, + self::LOCATION_HINT => self::PROTOCOL . $suiteName, + ]); + + return; + } + + $fileName = $suiteName::__getFileName(); + + $this->printEvent( + self::TEST_SUITE_STARTED, [ + self::NAME => substr($suiteName, 2), + self::LOCATION_HINT => self::PROTOCOL . $fileName, + ]); + } + + /** @phpstan-ignore-next-line */ + public function endTestSuite(TestSuite $suite): void + { + $suiteName = $suite->getName(); + + if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) { + $this->printEvent( + self::TEST_SUITE_FINISHED, [ + self::NAME => $suiteName, + self::LOCATION_HINT => self::PROTOCOL . $suiteName, + ]); + + return; + } + + $this->printEvent( + self::TEST_SUITE_FINISHED, [ + self::NAME => substr($suiteName, 2), + ]); + } + + /** + * @param Test|TestCase $test + */ + public function startTest(Test $test): void + { + if (!TeamCity::isPestTest($test)) { + $this->phpunitTeamCity->startTest($test); + + return; + } + + $this->printEvent('testStarted', [ + self::NAME => $test->getName(), + /* @phpstan-ignore-next-line */ + self::LOCATION_HINT => self::PROTOCOL . $test->toString(), + ]); + } + + /** + * @param Test|TestCase $test + */ + public function endTest(Test $test, float $time): void + { + if (!TeamCity::isPestTest($test)) { + $this->phpunitTeamCity->endTest($test, $time); + + return; + } + + $this->printEvent('testFinished', [ + self::NAME => $test->getName(), + self::DURATION => self::toMilliseconds($time), + ]); + } + + /** + * @param Test|TestCase $test + */ + public function addError(Test $test, Throwable $t, float $time): void + { + if (!TeamCity::isPestTest($test)) { + $this->phpunitTeamCity->addError($test, $t, $time); + + return; + } + + $this->printEvent( + self::TEST_FAILED, [ + self::NAME => $test->getName(), + 'message' => $t->getMessage(), + 'details' => $t->getTraceAsString(), + self::DURATION => self::toMilliseconds($time), + ]); + } + + /** + * @phpstan-ignore-next-line + * + * @param Test|TestCase $test + */ + public function addWarning(Test $test, Warning $e, float $time): void + { + if (!TeamCity::isPestTest($test)) { + $this->phpunitTeamCity->addWarning($test, $e, $time); + + return; + } + + $this->printEvent( + self::TEST_FAILED, [ + self::NAME => $test->getName(), + 'message' => $e->getMessage(), + 'details' => $e->getTraceAsString(), + self::DURATION => self::toMilliseconds($time), + ]); + } + + public function addFailure(Test $test, AssertionFailedError $e, float $time): void + { + $this->phpunitTeamCity->addFailure($test, $e, $time); + } + + protected function writeProgress(string $progress): void + { + } + + /** + * @param array $params + */ + private function printEvent(string $eventName, array $params = []): void + { + $this->write("\n##teamcity[{$eventName}"); + + if ($this->flowId !== 0) { + $params['flowId'] = $this->flowId; + } + + foreach ($params as $key => $value) { + $escapedValue = self::escapeValue((string) $value); + $this->write(" {$key}='{$escapedValue}'"); + } + + $this->write("]\n"); + } + + private static function escapeValue(string $text): string + { + return str_replace( + ['|', "'", "\n", "\r", ']', '['], + ['||', "|'", '|n', '|r', '|]', '|['], + $text + ); + } + + private static function toMilliseconds(float $time): int + { + return (int) round($time * 1000); + } + + private static function isPestTest(Test $test): bool + { + return in_array(TestCase::class, class_uses($test), true); + } +}