From bdc8416b41f3bedea846257ea8e82caf19036167 Mon Sep 17 00:00:00 2001 From: Filippo Tessarotto Date: Tue, 21 Jun 2022 08:07:54 +0200 Subject: [PATCH] Add PHPStorm integration (#672) Co-authored-by: Alexander Shvets --- Makefile | 1 + README.md | 13 +++ bin/paratest_for_phpstorm | 10 ++ phpcs.xml | 2 +- src/Runners/PHPUnit/Options.php | 22 +++++ src/Runners/PHPUnit/ResultPrinter.php | 23 ++++- src/Runners/PHPUnit/SuiteLoader.php | 6 +- src/Util/PhpstormHelper.php | 40 ++++++++ test/Unit/ResultTester.php | 28 +++++- test/Unit/Runners/PHPUnit/OptionsTest.php | 16 +++ .../Runners/PHPUnit/ResultPrinterTest.php | 24 ++++- test/Unit/Runners/PHPUnit/RunnerTestCase.php | 12 +++ test/Unit/Util/PhpstormHelperTest.php | 98 +++++++++++++++++++ 13 files changed, 286 insertions(+), 9 deletions(-) create mode 100755 bin/paratest_for_phpstorm create mode 100644 src/Util/PhpstormHelper.php create mode 100644 test/Unit/Util/PhpstormHelperTest.php diff --git a/Makefile b/Makefile index caffc7e7..714e42b8 100644 --- a/Makefile +++ b/Makefile @@ -54,5 +54,6 @@ code-coverage: coverage/junit.xml --skip-initial-tests \ --coverage=coverage \ --show-mutations \ + --verbose \ --min-msi=100 \ $(INFECTION_ARGS) diff --git a/README.md b/README.md index c1406c7f..cd626bd9 100644 --- a/README.md +++ b/README.md @@ -250,3 +250,16 @@ if (getenv('TEST_TOKEN') !== false) { // Using paratest Before creating a Pull Request be sure to run all the necessary checks with `make` command. For an example of ParaTest out in the wild check out the [example](https://github.com/brianium/paratest-selenium). + +# Integration with PHPStorm + +ParaTest provides a dedicated binary to work with PHPStorm; follow these steps to have ParaTest working within it: + +1. Be sure you have PHPUnit already configured in PHPStorm: https://www.jetbrains.com/help/phpstorm/using-phpunit-framework.html#php_test_frameworks_phpunit_integrate +2. Go to `Run` -> `Edit configurations...` +3. Select `Add new Configuration`, select the `PHPUnit` type and name it `ParaTest` +4. In the `Command Line` -> `Interpreter options` add `./vendor/bin/paratest_for_phpstorm` +5. Any additional ParaTest options you want to pass to ParaTest should go within the `Test runner` -> `Test runner options` section + +You should now have a `ParaTest` run within your configurations list. +It should natively work with the `Rerun failed tests` and `Toggle auto-test` buttons of the `Run` overlay. \ No newline at end of file diff --git a/bin/paratest_for_phpstorm b/bin/paratest_for_phpstorm new file mode 100755 index 00000000..cb6f91bb --- /dev/null +++ b/bin/paratest_for_phpstorm @@ -0,0 +1,10 @@ +#!/usr/bin/env php + - + diff --git a/src/Runners/PHPUnit/Options.php b/src/Runners/PHPUnit/Options.php index 9bec1d84..f7d88cc4 100644 --- a/src/Runners/PHPUnit/Options.php +++ b/src/Runners/PHPUnit/Options.php @@ -209,6 +209,8 @@ final class Options private $cwd; /** @var string|null */ private $logJunit; + /** @var bool */ + private $teamcity; /** @var string|null */ private $logTeamcity; /** @var string|null */ @@ -249,6 +251,7 @@ private function __construct( bool $functional, array $group, ?string $logJunit, + bool $teamcity, ?string $logTeamcity, ?int $maxBatchSize, bool $noCoverage, @@ -287,6 +290,7 @@ private function __construct( $this->functional = $functional; $this->group = $group; $this->logJunit = $logJunit; + $this->teamcity = $teamcity; $this->logTeamcity = $logTeamcity; $this->maxBatchSize = $maxBatchSize; $this->noCoverage = $noCoverage; @@ -326,6 +330,7 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel assert($options['filter'] === null || is_string($options['filter'])); assert(is_bool($options['functional'])); assert($options['log-junit'] === null || is_string($options['log-junit'])); + assert(is_bool($options['teamcity'])); assert($options['log-teamcity'] === null || is_string($options['log-teamcity'])); assert(is_bool($options['no-coverage'])); assert(is_bool($options['no-test-tokens'])); @@ -541,6 +546,7 @@ public static function fromConsoleInput(InputInterface $input, string $cwd): sel $options['functional'], $group, $options['log-junit'], + $options['teamcity'], $options['log-teamcity'], (int) $options['max-batch-size'], $options['no-coverage'], @@ -782,6 +788,12 @@ public static function setInputDefinition(InputDefinition $inputDefinition): voi InputOption::VALUE_NONE, 'Don\'t start any more processes after a failure.' ), + new InputOption( + 'teamcity', + null, + InputOption::VALUE_NONE, + 'Output test results in Teamcity format.' + ), new InputOption( 'testsuite', null, @@ -1085,6 +1097,11 @@ public function logJunit(): ?string return $this->logJunit; } + public function teamcity(): bool + { + return $this->teamcity; + } + public function logTeamcity(): ?string { return $this->logTeamcity; @@ -1095,6 +1112,11 @@ public function hasLogTeamcity(): bool return $this->logTeamcity !== null; } + public function needsTeamcity(): bool + { + return $this->teamcity() || $this->hasLogTeamcity(); + } + public function tmpDir(): string { return $this->tmpDir; diff --git a/src/Runners/PHPUnit/ResultPrinter.php b/src/Runners/PHPUnit/ResultPrinter.php index 762d5e25..b3b78ae9 100644 --- a/src/Runners/PHPUnit/ResultPrinter.php +++ b/src/Runners/PHPUnit/ResultPrinter.php @@ -114,6 +114,10 @@ final class ResultPrinter private $output; /** @var Options */ private $options; + /** @var bool */ + private $needsTeamcity; + /** @var bool */ + private $printsTeamcity; /** @var resource|null */ private $teamcityLogFileHandle; @@ -123,6 +127,9 @@ public function __construct(LogInterpreter $results, OutputInterface $output, Op $this->output = $output; $this->options = $options; + $this->printsTeamcity = $this->options->teamcity(); + $this->needsTeamcity = $this->options->needsTeamcity(); + if (($teamcityLogFile = $this->options->logTeamcity()) === null) { return; } @@ -235,7 +242,7 @@ public function printFeedback(ExecutableTest $test): Reader ); } - if ($this->teamcityLogFileHandle !== null) { + if ($this->needsTeamcity) { $teamcityLogFile = $test->getTeamcityTempFile(); if (filesize($teamcityLogFile) === 0) { @@ -244,11 +251,21 @@ public function printFeedback(ExecutableTest $test): Reader $result = file_get_contents($teamcityLogFile); assert($result !== false); - fwrite($this->teamcityLogFileHandle, $result); + + if ($this->teamcityLogFileHandle !== null) { + fwrite($this->teamcityLogFileHandle, $result); + } + + if ($this->printsTeamcity) { + $this->output->write($result); + } } $this->results->addReader($reader); - $this->processReaderFeedback($reader, $test->getTestCount()); + + if (! $this->printsTeamcity) { + $this->processReaderFeedback($reader, $test->getTestCount()); + } return $reader; } diff --git a/src/Runners/PHPUnit/SuiteLoader.php b/src/Runners/PHPUnit/SuiteLoader.php index 67a1fb59..f1c10776 100644 --- a/src/Runners/PHPUnit/SuiteLoader.php +++ b/src/Runners/PHPUnit/SuiteLoader.php @@ -226,7 +226,7 @@ private function executableTests(string $path, ParsedClass $class): array $path, $methodBatch, $this->options->hasCoverage(), - $this->options->hasLogTeamcity(), + $this->options->needsTeamcity(), $this->options->tmpDir() ); } @@ -399,7 +399,7 @@ private function createSuite(string $path, ParsedClass $class): Suite $class ), $this->options->hasCoverage(), - $this->options->hasLogTeamcity(), + $this->options->needsTeamcity(), $this->options->tmpDir() ); } @@ -409,7 +409,7 @@ private function createFullSuite(string $suiteName): FullSuite return new FullSuite( $suiteName, $this->options->hasCoverage(), - $this->options->hasLogTeamcity(), + $this->options->needsTeamcity(), $this->options->tmpDir() ); } diff --git a/src/Util/PhpstormHelper.php b/src/Util/PhpstormHelper.php new file mode 100644 index 00000000..c271e4b5 --- /dev/null +++ b/src/Util/PhpstormHelper.php @@ -0,0 +1,40 @@ + $argv + */ + public static function handleArgvFromPhpstorm(array &$argv, string $paratestBinary): string + { + if (! in_array('--filter', $argv, true)) { + unset($argv[1]); + + return $paratestBinary; + } + + unset($argv[0]); + $phpunitBinary = $argv[1]; + foreach ($argv as $index => $value) { + if ($value === '--configuration' || $value === '--bootstrap') { + break; + } + + unset($argv[$index]); + } + + array_unshift($argv, $phpunitBinary); + + return $phpunitBinary; + } +} diff --git a/test/Unit/ResultTester.php b/test/Unit/ResultTester.php index 2cfadca8..4f5b9844 100644 --- a/test/Unit/ResultTester.php +++ b/test/Unit/ResultTester.php @@ -13,6 +13,27 @@ abstract class ResultTester extends TestBase { + protected const SINGLE_PASSING_TEAMCITY_OUTPUT = <<<'EOF' +##teamcity[testCount count='3' flowId='118852'] + +##teamcity[testSuiteStarted name='ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest' locationHint='php_qn:///repos/paratest/test/fixtures/passing_tests/level1/AnotherUnitTestInSubLevelTest.php::\ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest' flowId='118852'] + +##teamcity[testStarted name='testTruth' locationHint='php_qn:///repos/paratest/test/fixtures/passing_tests/level1/AnotherUnitTestInSubLevelTest.php::\ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest::testTruth' flowId='118852'] + +##teamcity[testFinished name='testTruth' duration='1' flowId='118852'] + +##teamcity[testStarted name='testFalsehood' locationHint='php_qn:///repos/paratest/test/fixtures/passing_tests/level1/AnotherUnitTestInSubLevelTest.php::\ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest::testFalsehood' flowId='118852'] + +##teamcity[testFinished name='testFalsehood' duration='0' flowId='118852'] + +##teamcity[testStarted name='testArrayLength' locationHint='php_qn:///repos/paratest/test/fixtures/passing_tests/level1/AnotherUnitTestInSubLevelTest.php::\ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest::testArrayLength' flowId='118852'] + +##teamcity[testFinished name='testArrayLength' duration='0' flowId='118852'] + +##teamcity[testSuiteFinished name='ParaTest\Tests\fixtures\passthru_tests\level1\AnotherUnitTestInSubLevelTest' flowId='118852'] +EOF; + + /** @var Suite */ protected $failureSuite; /** @var Suite */ @@ -58,7 +79,12 @@ final protected function getSuiteWithResult(string $result, int $methodCount): S $suite = new Suite('', $functions, false, true, $this->tmpDir); file_put_contents($suite->getTempFile(), (string) file_get_contents(FIXTURES . DS . 'results' . DS . $result)); - file_put_contents($suite->getTeamcityTempFile(), 'no data'); + $teamcityData = 'no data'; + if ($result === 'single-passing.xml') { + $teamcityData = self::SINGLE_PASSING_TEAMCITY_OUTPUT; + } + + file_put_contents($suite->getTeamcityTempFile(), $teamcityData); return $suite; } diff --git a/test/Unit/Runners/PHPUnit/OptionsTest.php b/test/Unit/Runners/PHPUnit/OptionsTest.php index c68c0f81..7242aceb 100644 --- a/test/Unit/Runners/PHPUnit/OptionsTest.php +++ b/test/Unit/Runners/PHPUnit/OptionsTest.php @@ -263,7 +263,9 @@ public function testDefaultOptions(): void static::assertNull($options->whitelist()); static::assertSame(Options::ORDER_DEFAULT, $options->orderBy()); static::assertSame(0, $options->randomOrderSeed()); + static::assertFalse($options->teamcity()); static::assertFalse($options->hasLogTeamcity()); + static::assertFalse($options->needsTeamcity()); static::assertFalse($options->hasCoverage()); static::assertSame(0, $options->repeat()); } @@ -288,6 +290,7 @@ public function testProvidedOptions(): void '--functional' => true, '--group' => 'GROUP', '--log-junit' => 'LOG-JUNIT', + '--teamcity' => true, '--log-teamcity' => 'LOG-TEAMCITY', '--max-batch-size' => 5, '--no-test-tokens' => true, @@ -326,7 +329,9 @@ public function testProvidedOptions(): void static::assertTrue($options->functional()); static::assertSame(['GROUP'], $options->group()); static::assertSame('LOG-JUNIT', $options->logJunit()); + static::assertTrue($options->teamcity()); static::assertSame('LOG-TEAMCITY', $options->logTeamcity()); + static::assertTrue($options->needsTeamcity()); static::assertSame(5, $options->maxBatchSize()); static::assertTrue($options->noTestTokens()); static::assertTrue($options->parallelSuite()); @@ -450,4 +455,15 @@ public function testFillEnvWithTokens(): void static::assertArrayNotHasKey(Options::ENV_KEY_TOKEN, $env); static::assertArrayNotHasKey(Options::ENV_KEY_UNIQUE_TOKEN, $env); } + + public function testNeedsTeamcityGetsActivatedBothByLogTeamcityAndTeamcityFlags(): void + { + $options = $this->createOptionsFromArgv(['--teamcity' => true], __DIR__); + + self::assertTrue($options->needsTeamcity()); + + $options = $this->createOptionsFromArgv(['--log-teamcity' => 'LOG-TEAMCITY'], __DIR__); + + self::assertTrue($options->needsTeamcity()); + } } diff --git a/test/Unit/Runners/PHPUnit/ResultPrinterTest.php b/test/Unit/Runners/PHPUnit/ResultPrinterTest.php index 3c848f33..ff07806b 100644 --- a/test/Unit/Runners/PHPUnit/ResultPrinterTest.php +++ b/test/Unit/Runners/PHPUnit/ResultPrinterTest.php @@ -14,7 +14,9 @@ use Symfony\Component\Console\Output\BufferedOutput; use function defined; +use function file_get_contents; use function file_put_contents; +use function preg_match_all; use function sprintf; use function str_repeat; use function uniqid; @@ -553,7 +555,7 @@ public function testTeamcityEmptyLogFileRaiseException(): void $this->printer->printFeedback($test); } - public function testTeamcityFeedback(): void + public function testTeamcityFeedbackOnFile(): void { $teamcityLog = $this->tmpDir . DS . 'teamcity2.log'; @@ -567,6 +569,26 @@ public function testTeamcityFeedback(): void static::assertStringContainsString('OK', $this->output->fetch()); static::assertFileExists($teamcityLog); + + $logContent = file_get_contents($teamcityLog); + + self::assertNotFalse($logContent); + self::assertSame(9, preg_match_all('/^##teamcity/m', $logContent)); + } + + public function testTeamcityFeedbackOnStdout(): void + { + $this->options = $this->createOptionsFromArgv(['--teamcity' => true]); + $this->printer = new ResultPrinter($this->interpreter, $this->output, $this->options); + $this->printer->addTest($this->passingSuite); + + $this->printer->start(); + $this->printer->printFeedback($this->passingSuite); + $this->printer->printResults(); + + $output = $this->output->fetch(); + static::assertStringContainsString('OK', $output); + self::assertSame(9, preg_match_all('/^##teamcity/m', $output)); } private function getStartOutput(): string diff --git a/test/Unit/Runners/PHPUnit/RunnerTestCase.php b/test/Unit/Runners/PHPUnit/RunnerTestCase.php index adfc6674..8dc47aca 100644 --- a/test/Unit/Runners/PHPUnit/RunnerTestCase.php +++ b/test/Unit/Runners/PHPUnit/RunnerTestCase.php @@ -413,6 +413,18 @@ final public function testTeamcityLog(): void self::assertSame(66, preg_match_all('/^##teamcity/m', $content)); } + final public function testTeamcityOutput(): void + { + $this->bareOptions = [ + '--configuration' => $this->fixture('phpunit-passing.xml'), + '--teamcity' => true, + ]; + + $result = $this->runRunner(); + + self::assertSame(66, preg_match_all('/^##teamcity/m', $result->getOutput())); + } + /** * @requires OSFAMILY Linux */ diff --git a/test/Unit/Util/PhpstormHelperTest.php b/test/Unit/Util/PhpstormHelperTest.php new file mode 100644 index 00000000..da8ed402 --- /dev/null +++ b/test/Unit/Util/PhpstormHelperTest.php @@ -0,0 +1,98 @@ + $argv + * @param array $expectedArgv + * + * @dataProvider providePhpstormCases + */ + public function testWithoutFilterRunParaTest( + array $argv, + array $expectedArgv, + string $paratestBinary, + string $expectedBinary + ): void { + $actualBinary = PhpstormHelper::handleArgvFromPhpstorm($argv, $paratestBinary); + $argv = array_values($argv); + + static::assertSame($expectedArgv, $argv); + self::assertSame($expectedBinary, $actualBinary); + } + + public function providePhpstormCases(): Generator + { + $paratestBinary = uniqid('paratest_'); + $phpunitBinary = uniqid('phpunit_'); + + $argv = []; + $argv[] = $paratestBinary; + $argv[] = $phpunitBinary; + $argv[] = '--runner'; + $argv[] = 'WrapperRunner'; + $argv[] = '--no-coverage'; + $argv[] = '--configuration'; + $argv[] = '/home/user/repos/test/phpunit.xml'; + $argv[] = '--teamcity'; + + $expected = []; + $expected[] = $paratestBinary; + $expected[] = '--runner'; + $expected[] = 'WrapperRunner'; + $expected[] = '--no-coverage'; + $expected[] = '--configuration'; + $expected[] = '/home/user/repos/test/phpunit.xml'; + $expected[] = '--teamcity'; + + yield 'without --filter run ParaTest' => [ + $argv, + $expected, + $paratestBinary, + $paratestBinary, + ]; + + $argv = []; + $argv[] = $paratestBinary; + $argv[] = $phpunitBinary; + $argv[] = '--runner'; + $argv[] = 'WrapperRunner'; + $argv[] = '--no-coverage'; + $argv[] = '--configuration'; + $argv[] = '/home/user/repos/test/phpunit.xml'; + $argv[] = '--filter'; + $argv[] = '"/MyTests\\MyTest::testFalse( .*)?$$/"'; + $argv[] = '--teamcity'; + + $expected = []; + $expected[] = $phpunitBinary; + $expected[] = '--configuration'; + $expected[] = '/home/user/repos/test/phpunit.xml'; + $expected[] = '--filter'; + $expected[] = '"/MyTests\\MyTest::testFalse( .*)?$$/"'; + $expected[] = '--teamcity'; + + yield 'with --filter run PHPUnit' => [ + $argv, + $expected, + $paratestBinary, + $phpunitBinary, + ]; + } +}