From 75f17bb1183b9a2567362b907c10c49d10cfab9c Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Tue, 28 Jul 2020 09:07:47 +0200 Subject: [PATCH 1/5] feat(teamcity): Add basic team city output format --- src/Actions/AddsDefaults.php | 5 + src/Concerns/TestCase.php | 5 + src/TeamCity.php | 192 +++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/TeamCity.php diff --git a/src/Actions/AddsDefaults.php b/src/Actions/AddsDefaults.php index 799eda6e..b13cdc52 100644 --- a/src/Actions/AddsDefaults.php +++ b/src/Actions/AddsDefaults.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest\Actions; use NunoMaduro\Collision\Adapters\Phpunit\Printer; +use Pest\TeamCity; /** * @internal @@ -24,6 +25,10 @@ final class AddsDefaults $arguments['printer'] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); } + if ($arguments['printer'] === \PHPUnit\Util\Log\TeamCity::class) { + $arguments['printer'] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); + } + return $arguments; } } diff --git a/src/Concerns/TestCase.php b/src/Concerns/TestCase.php index 6c1d98ce..01b5b00a 100644 --- a/src/Concerns/TestCase.php +++ b/src/Concerns/TestCase.php @@ -64,6 +64,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..e7e5acd1 --- /dev/null +++ b/src/TeamCity.php @@ -0,0 +1,192 @@ +phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity( + $out, + $verbose, + $colors, + $debug, + $numberOfColumns, + $reverse + ); + } + + public function printResult(TestResult $result): void + { + $this->printHeader($result); + $this->printFooter($result); + } + + 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)) { + $this->printEvent('testSuiteStarted', [ + 'name' => $suiteName, + 'locationHint' => self::PROTOCOL . $suiteName, + ]); + + return; + } + + $fileName = $suite->getName()::__getFileName(); + + $this->printEvent('testSuiteStarted', [ + 'name' => substr($suiteName, 2), + 'locationHint' => self::PROTOCOL . $fileName, + ]); + } + + public function endTestSuite(TestSuite $suite): void + { + $suiteName = $suite->getName(); + + if (file_exists($suiteName)) { + $this->printEvent('testSuiteFinished', [ + 'name' => $suiteName, + 'locationHint' => self::PROTOCOL . $suiteName, + ]); + + return; + } + + $this->printEvent('testSuiteFinished', [ + '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', [ + 'name' => $test->getName(), + 'locationHint' => 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', [ + 'name' => $test->getName(), + 'duration' => self::toMilliseconds($time), + ]); + } + + /** + * @param Test|TestCase $test + */ + public function addError(Test $test, \Throwable $t, float $time): void + { + $this->printEvent('testFailed', [ + 'name' => $test->getName(), + 'message' => $t->getMessage(), + 'details' => $t->getTraceAsString(), + 'duration' => self::toMilliseconds($time), + ]); + } + + public function addWarning(Test $test, Warning $e, float $time): void + { + $this->printEvent('testFailed', [ + 'name' => $test->getName(), + 'message' => $e->getMessage(), + 'details' => $e->getTraceAsString(), + '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 + { + } + + private function printEvent(string $eventName, array $params = []): void + { + $this->write("\n##teamcity[{$eventName}"); + + if ($this->flowId) { + $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): bool + { + return in_array(TestCase::class, class_uses($test)); + } +} From fa413aafbb67a1b78de15ef470b0d7dac2894112 Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Tue, 28 Jul 2020 10:51:57 +0200 Subject: [PATCH 2/5] style(teamcity): fix styling --- phpstan.neon | 9 ++++ src/Actions/AddsDefaults.php | 13 ++++-- src/TeamCity.php | 91 ++++++++++++++++++++++++------------ 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index e133d425..546d1683 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -21,3 +21,12 @@ parameters: - "# with null as default value#" - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" + - + 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/src/Actions/AddsDefaults.php b/src/Actions/AddsDefaults.php index b13cdc52..1372b638 100644 --- a/src/Actions/AddsDefaults.php +++ b/src/Actions/AddsDefaults.php @@ -12,6 +12,11 @@ use Pest\TeamCity; */ final class AddsDefaults { + /** + * @var string + */ + private const PRINTER = 'printer'; + /** * Adds default arguments to the given `arguments` array. * @@ -21,12 +26,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'] ?? 'always'); } - if ($arguments['printer'] === \PHPUnit\Util\Log\TeamCity::class) { - $arguments['printer'] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); + if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) { + $arguments[self::PRINTER] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); } return $arguments; diff --git a/src/TeamCity.php b/src/TeamCity.php index e7e5acd1..1432b83d 100644 --- a/src/TeamCity.php +++ b/src/TeamCity.php @@ -1,5 +1,7 @@ phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity( $out, $verbose, $colors, - $debug, - $numberOfColumns, - $reverse + false, + 80, + false ); } @@ -40,6 +50,7 @@ class TeamCity extends DefaultResultPrinter $this->printFooter($result); } + /** @phpstan-ignore-next-line */ public function startTestSuite(TestSuite $suite): void { $this->flowId = getmypid(); @@ -56,8 +67,8 @@ class TeamCity extends DefaultResultPrinter if (file_exists($suiteName)) { $this->printEvent('testSuiteStarted', [ - 'name' => $suiteName, - 'locationHint' => self::PROTOCOL . $suiteName, + self::NAME => $suiteName, + self::LOCATION_HINT => self::PROTOCOL . $suiteName, ]); return; @@ -66,26 +77,27 @@ class TeamCity extends DefaultResultPrinter $fileName = $suite->getName()::__getFileName(); $this->printEvent('testSuiteStarted', [ - 'name' => substr($suiteName, 2), - 'locationHint' => self::PROTOCOL . $fileName, + 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)) { $this->printEvent('testSuiteFinished', [ - 'name' => $suiteName, - 'locationHint' => self::PROTOCOL . $suiteName, + self::NAME => $suiteName, + self::LOCATION_HINT => self::PROTOCOL . $suiteName, ]); return; } $this->printEvent('testSuiteFinished', [ - 'name' => substr($suiteName, 2), + self::NAME => substr($suiteName, 2), ]); } @@ -101,8 +113,9 @@ class TeamCity extends DefaultResultPrinter } $this->printEvent('testStarted', [ - 'name' => $test->getName(), - 'locationHint' => self::PROTOCOL . $test->toString(), + self::NAME => $test->getName(), + /* @phpstan-ignore-next-line */ + self::LOCATION_HINT => self::PROTOCOL . $test->toString(), ]); } @@ -118,8 +131,8 @@ class TeamCity extends DefaultResultPrinter } $this->printEvent('testFinished', [ - 'name' => $test->getName(), - 'duration' => self::toMilliseconds($time), + self::NAME => $test->getName(), + self::DURATION => self::toMilliseconds($time), ]); } @@ -128,21 +141,38 @@ class TeamCity extends DefaultResultPrinter */ public function addError(Test $test, \Throwable $t, float $time): void { + if (!TeamCity::isPestTest($test)) { + $this->phpunitTeamCity->addError($test, $t, $time); + + return; + } + $this->printEvent('testFailed', [ - 'name' => $test->getName(), - 'message' => $t->getMessage(), - 'details' => $t->getTraceAsString(), - 'duration' => self::toMilliseconds($time), + 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('testFailed', [ - 'name' => $test->getName(), - 'message' => $e->getMessage(), - 'details' => $e->getTraceAsString(), - 'duration' => self::toMilliseconds($time), + self::NAME => $test->getName(), + 'message' => $e->getMessage(), + 'details' => $e->getTraceAsString(), + self::DURATION => self::toMilliseconds($time), ]); } @@ -155,11 +185,14 @@ class TeamCity extends DefaultResultPrinter { } + /** + * @param array $params + */ private function printEvent(string $eventName, array $params = []): void { $this->write("\n##teamcity[{$eventName}"); - if ($this->flowId) { + if ($this->flowId !== 0) { $params['flowId'] = $this->flowId; } @@ -185,8 +218,8 @@ class TeamCity extends DefaultResultPrinter return (int) round($time * 1000); } - private static function isPestTest($test): bool + private static function isPestTest(Test $test): bool { - return in_array(TestCase::class, class_uses($test)); + return in_array(TestCase::class, class_uses($test), true); } } From 0c16942d379e29c02754def677754a8168d576ab Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Tue, 28 Jul 2020 11:04:08 +0200 Subject: [PATCH 3/5] refactor(teamcity): Small cleanup --- src/Actions/AddsDefaults.php | 8 +++----- src/TeamCity.php | 35 ++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Actions/AddsDefaults.php b/src/Actions/AddsDefaults.php index 1372b638..d711641c 100644 --- a/src/Actions/AddsDefaults.php +++ b/src/Actions/AddsDefaults.php @@ -6,15 +6,13 @@ namespace Pest\Actions; use NunoMaduro\Collision\Adapters\Phpunit\Printer; use Pest\TeamCity; +use PHPUnit\TextUI\DefaultResultPrinter; /** * @internal */ final class AddsDefaults { - /** - * @var string - */ private const PRINTER = 'printer'; /** @@ -27,11 +25,11 @@ final class AddsDefaults public static function to(array $arguments): array { if (!array_key_exists(self::PRINTER, $arguments)) { - $arguments[self::PRINTER] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); + $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(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always'); + $arguments[self::PRINTER] = new TeamCity($arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS); } return $arguments; diff --git a/src/TeamCity.php b/src/TeamCity.php index 1432b83d..c70aa7f5 100644 --- a/src/TeamCity.php +++ b/src/TeamCity.php @@ -17,10 +17,13 @@ use function str_replace; final class TeamCity extends DefaultResultPrinter { - private const PROTOCOL = 'pest_qn://'; - private const NAME = 'name'; - private const LOCATION_HINT = 'locationHint'; - private const DURATION = 'duration'; + private const PROTOCOL = 'pest_qn://'; + private const NAME = 'name'; + private const LOCATION_HINT = 'locationHint'; + private const DURATION = 'duration'; + private const TEST_SUITE_STARTED = 'testSuiteStarted'; + private const TEST_SUITE_FINISHED = 'testSuiteFinished'; + private const TEST_FAILED = 'testFailed'; /** @var int */ private $flowId; @@ -31,11 +34,11 @@ final class TeamCity extends DefaultResultPrinter /** @var \PHPUnit\Util\Log\TeamCity */ private $phpunitTeamCity; - public function __construct($out, bool $verbose, string $colors) + public function __construct(bool $verbose, string $colors) { - parent::__construct($out, $verbose, $colors, false, 80, false); + parent::__construct(null, $verbose, $colors, false, 80, false); $this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity( - $out, + null, $verbose, $colors, false, @@ -66,7 +69,8 @@ final class TeamCity extends DefaultResultPrinter $suiteName = $suite->getName(); if (file_exists($suiteName)) { - $this->printEvent('testSuiteStarted', [ + $this->printEvent( + self::TEST_SUITE_STARTED, [ self::NAME => $suiteName, self::LOCATION_HINT => self::PROTOCOL . $suiteName, ]); @@ -76,7 +80,8 @@ final class TeamCity extends DefaultResultPrinter $fileName = $suite->getName()::__getFileName(); - $this->printEvent('testSuiteStarted', [ + $this->printEvent( + self::TEST_SUITE_STARTED, [ self::NAME => substr($suiteName, 2), self::LOCATION_HINT => self::PROTOCOL . $fileName, ]); @@ -88,7 +93,8 @@ final class TeamCity extends DefaultResultPrinter $suiteName = $suite->getName(); if (file_exists($suiteName)) { - $this->printEvent('testSuiteFinished', [ + $this->printEvent( + self::TEST_SUITE_FINISHED, [ self::NAME => $suiteName, self::LOCATION_HINT => self::PROTOCOL . $suiteName, ]); @@ -96,7 +102,8 @@ final class TeamCity extends DefaultResultPrinter return; } - $this->printEvent('testSuiteFinished', [ + $this->printEvent( + self::TEST_SUITE_FINISHED, [ self::NAME => substr($suiteName, 2), ]); } @@ -147,7 +154,8 @@ final class TeamCity extends DefaultResultPrinter return; } - $this->printEvent('testFailed', [ + $this->printEvent( + self::TEST_FAILED, [ self::NAME => $test->getName(), 'message' => $t->getMessage(), 'details' => $t->getTraceAsString(), @@ -168,7 +176,8 @@ final class TeamCity extends DefaultResultPrinter return; } - $this->printEvent('testFailed', [ + $this->printEvent( + self::TEST_FAILED, [ self::NAME => $test->getName(), 'message' => $e->getMessage(), 'details' => $e->getTraceAsString(), From cc1abe7f06dd835dc6ecdb3571cf54bc397674cc Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Fri, 31 Jul 2020 10:25:23 +0200 Subject: [PATCH 4/5] fix(teamcity): Fixed a bug when running phpunit tests together with pest tests --- src/TeamCity.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TeamCity.php b/src/TeamCity.php index c70aa7f5..a2ca06f5 100644 --- a/src/TeamCity.php +++ b/src/TeamCity.php @@ -68,7 +68,7 @@ final class TeamCity extends DefaultResultPrinter $suiteName = $suite->getName(); - if (file_exists($suiteName)) { + if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) { $this->printEvent( self::TEST_SUITE_STARTED, [ self::NAME => $suiteName, @@ -78,7 +78,7 @@ final class TeamCity extends DefaultResultPrinter return; } - $fileName = $suite->getName()::__getFileName(); + $fileName = $suiteName::__getFileName(); $this->printEvent( self::TEST_SUITE_STARTED, [ @@ -92,7 +92,7 @@ final class TeamCity extends DefaultResultPrinter { $suiteName = $suite->getName(); - if (file_exists($suiteName)) { + if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) { $this->printEvent( self::TEST_SUITE_FINISHED, [ self::NAME => $suiteName, From bcc206d1837649e87ab5570ae41195f1601f5c61 Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Sat, 15 Aug 2020 08:53:08 +0200 Subject: [PATCH 5/5] chore(teamcity): static analysis fix --- rector.php | 6 +++++- src/TeamCity.php | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) 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/TeamCity.php b/src/TeamCity.php index a2ca06f5..b5dec5ab 100644 --- a/src/TeamCity.php +++ b/src/TeamCity.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Warning; use PHPUnit\TextUI\DefaultResultPrinter; use function round; use function str_replace; +use Throwable; final class TeamCity extends DefaultResultPrinter { @@ -146,7 +147,7 @@ final class TeamCity extends DefaultResultPrinter /** * @param Test|TestCase $test */ - public function addError(Test $test, \Throwable $t, float $time): void + public function addError(Test $test, Throwable $t, float $time): void { if (!TeamCity::isPestTest($test)) { $this->phpunitTeamCity->addError($test, $t, $time);