diff --git a/devTools/phpstan-src-baseline.neon b/devTools/phpstan-src-baseline.neon index 91cb14cb7..687b28cb7 100644 --- a/devTools/phpstan-src-baseline.neon +++ b/devTools/phpstan-src-baseline.neon @@ -305,6 +305,11 @@ parameters: count: 1 path: ../src/TestFramework/Coverage/XmlReport/XPathFactory.php + - + message: "#^Method Infection\\\\TestFramework\\\\PhpUnit\\\\Adapter\\\\PestAdapterFactory\\:\\:create\\(\\) has parameter \\$sourceDirectories with no value type specified in iterable type array\\.$#" + count: 1 + path: ../src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php + - message: "#^Only booleans are allowed in an if condition, int given\\.$#" count: 3 diff --git a/src/TestFramework/Factory.php b/src/TestFramework/Factory.php index a30985e22..68f5fc7ed 100644 --- a/src/TestFramework/Factory.php +++ b/src/TestFramework/Factory.php @@ -41,6 +41,7 @@ use Infection\Configuration\Configuration; use Infection\FileSystem\Finder\TestFrameworkFinder; use Infection\TestFramework\Config\TestFrameworkConfigLocatorInterface; +use Infection\TestFramework\PhpUnit\Adapter\PestAdapterFactory; use Infection\TestFramework\PhpUnit\Adapter\PhpUnitAdapterFactory; use InvalidArgumentException; use function is_a; @@ -105,7 +106,25 @@ public function create(string $adapterName, bool $skipCoverage): TestFrameworkAd ); } - $availableTestFrameworks = [TestFrameworkTypes::PHPUNIT]; + if ($adapterName === TestFrameworkTypes::PEST) { + $pestConfigPath = $this->configLocator->locate(TestFrameworkTypes::PHPUNIT); + + return PestAdapterFactory::create( + $this->testFrameworkFinder->find( + TestFrameworkTypes::PEST, + (string) $this->infectionConfig->getPhpUnit()->getCustomPath() + ), + $this->tmpDir, + $pestConfigPath, + (string) $this->infectionConfig->getPhpUnit()->getConfigDir(), + $this->jUnitFilePath, + $this->projectDir, + $this->infectionConfig->getSourceDirectories(), + $skipCoverage + ); + } + + $availableTestFrameworks = [TestFrameworkTypes::PHPUNIT, TestFrameworkTypes::PEST]; foreach ($this->installedExtensions as $installedExtension) { $factory = $installedExtension['extra']['class']; diff --git a/src/TestFramework/PhpUnit/Adapter/PestAdapter.php b/src/TestFramework/PhpUnit/Adapter/PestAdapter.php new file mode 100644 index 000000000..3fb1dc642 --- /dev/null +++ b/src/TestFramework/PhpUnit/Adapter/PestAdapter.php @@ -0,0 +1,122 @@ +phpUnitAdapter = $phpUnitAdapter; + } + + public function getName(): string + { + return self::NAME; + } + + public function testsPass(string $output): bool + { + // Tests: 7 failed + if (preg_match('/Tests:\s+(.*?)(\d+\sfailed)/i', $output) === 1) { + return false; + } + + // Tests: 4 passed + $isOk = preg_match('/Tests:\s+(.*?)(\d+\spassed)/', $output) === 1; + + // Tests: 1 risked + $isOkRisked = preg_match('/Tests:\s+(.*?)(\d+\srisked)/', $output) === 1; + + return $isOk || $isOkRisked; + } + + public function hasJUnitReport(): bool + { + return $this->phpUnitAdapter->hasJUnitReport(); + } + + public function getInitialTestRunCommandLine(string $extraOptions, array $phpExtraArgs, bool $skipCoverage): array + { + return $this->phpUnitAdapter->getInitialTestRunCommandLine($extraOptions, $phpExtraArgs, $skipCoverage); + } + + public function getMutantCommandLine(array $coverageTests, string $mutatedFilePath, string $mutationHash, string $mutationOriginalFilePath, string $extraOptions): array + { + return $this->phpUnitAdapter->getMutantCommandLine( + $coverageTests, + $mutatedFilePath, + $mutationHash, + $mutationOriginalFilePath, + sprintf('--colors=never %s', $extraOptions) + ); + } + + public function getVersion(): string + { + return $this->phpUnitAdapter->getVersion(); + } + + public function getInitialTestsFailRecommendations(string $commandLine): string + { + return $this->phpUnitAdapter->getInitialTestsFailRecommendations($commandLine); + } + + public function getMemoryUsed(string $output): float + { + return $this->phpUnitAdapter->getMemoryUsed($output); + } + + /** + * @return string[] + */ + public function getInitialRunOnlyOptions(): array + { + return $this->phpUnitAdapter->getInitialRunOnlyOptions(); + } +} diff --git a/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php new file mode 100644 index 000000000..591b5d6f9 --- /dev/null +++ b/src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php @@ -0,0 +1,117 @@ + + + + + ./tests/ + + + + + + ./src/ + + + diff --git a/tests/e2e/PestTestFramework/run_tests.bash b/tests/e2e/PestTestFramework/run_tests.bash new file mode 100644 index 000000000..949873a44 --- /dev/null +++ b/tests/e2e/PestTestFramework/run_tests.bash @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +readonly INFECTION="../../../bin/infection --test-framework=pest" + +set -e pipefail + +if [ "$DRIVER" = "phpdbg" ] +then + phpdbg -qrr $INFECTION +else + php $INFECTION +fi + +diff -w expected-output.txt infection.log diff --git a/tests/e2e/PestTestFramework/src/Calculator.php b/tests/e2e/PestTestFramework/src/Calculator.php new file mode 100644 index 000000000..8692051ab --- /dev/null +++ b/tests/e2e/PestTestFramework/src/Calculator.php @@ -0,0 +1,20 @@ +sub(3, 1))->toBe(2); +}); + diff --git a/tests/e2e/PestTestFramework/tests/CalculatorPhpUnitTest.php b/tests/e2e/PestTestFramework/tests/CalculatorPhpUnitTest.php new file mode 100644 index 000000000..c43e14cce --- /dev/null +++ b/tests/e2e/PestTestFramework/tests/CalculatorPhpUnitTest.php @@ -0,0 +1,20 @@ +mul(2, 4)); + } +} diff --git a/tests/e2e/PestTestFramework/tests/Datasets/Floats.php b/tests/e2e/PestTestFramework/tests/Datasets/Floats.php new file mode 100644 index 000000000..b15be7122 --- /dev/null +++ b/tests/e2e/PestTestFramework/tests/Datasets/Floats.php @@ -0,0 +1,6 @@ +hello())->toBe('hello'); +}); + +test('Another test from SourceTest.php', function () { + $sourceClass = new ForPest(); + + expect($sourceClass->hello())->toBe('hello'); +}); + +test('Third test from SourceTest.php', function () { + $sourceClass = new ForPest(); + + expect($sourceClass->add(5, 3))->toBeGreaterThan(0); +}); diff --git a/tests/e2e/PestTestFramework/tests/ForPestWithDataProviderTest.php b/tests/e2e/PestTestFramework/tests/ForPestWithDataProviderTest.php new file mode 100644 index 000000000..4dfd85156 --- /dev/null +++ b/tests/e2e/PestTestFramework/tests/ForPestWithDataProviderTest.php @@ -0,0 +1,17 @@ +div($a, $b))->toBe($expectedResult); +})->with([ + [2.0, 4.0, 0.5] +]); + +test('tests division with shared dataset', function (float $a, float $b, float $expectedResult) { + $sourceClass = new ForPestWithDataProvider(); + + expect($sourceClass->div($a, $b))->toBe($expectedResult); +})->with('floats'); diff --git a/tests/e2e/PestTestFramework/tests/ForPhpUnitTest.php b/tests/e2e/PestTestFramework/tests/ForPhpUnitTest.php new file mode 100644 index 000000000..3eeab791e --- /dev/null +++ b/tests/e2e/PestTestFramework/tests/ForPhpUnitTest.php @@ -0,0 +1,20 @@ +getArray()); + } +} diff --git a/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterFactoryTest.php b/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterFactoryTest.php new file mode 100644 index 000000000..43066aa5b --- /dev/null +++ b/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterFactoryTest.php @@ -0,0 +1,71 @@ +assertSame('Pest', $adapter->getName()); + } + + public function test_it_has_a_name(): void + { + $this->assertSame('pest', PestAdapterFactory::getAdapterName()); + } + + public function test_it_has_an_executable(): void + { + $this->assertSame('pest', PestAdapterFactory::getExecutableName()); + } +} diff --git a/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterTest.php b/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterTest.php new file mode 100644 index 000000000..b46876f9a --- /dev/null +++ b/tests/phpunit/TestFramework/PhpUnit/Adapter/PestAdapterTest.php @@ -0,0 +1,288 @@ +pcovDirectoryProvider = $this->createMock(PCOVDirectoryProvider::class); + $this->initialConfigBuilder = $this->createMock(InitialConfigBuilder::class); + $this->mutationConfigBuilder = $this->createMock(MutationConfigBuilder::class); + $this->cliArgumentsBuilder = $this->createMock(CommandLineArgumentsAndOptionsBuilder::class); + $this->commandLineBuilder = $this->createMock(CommandLineBuilder::class); + + $this->adapter = new PestAdapter( + new PhpUnitAdapter( + '/path/to/pest', + '/tmp', + '/tmp/infection/junit.xml', + $this->pcovDirectoryProvider, + $this->initialConfigBuilder, + $this->mutationConfigBuilder, + $this->cliArgumentsBuilder, + new VersionParser(), + $this->commandLineBuilder, + '1.1.0' + ) + ); + } + + public function test_it_has_a_name(): void + { + $this->assertSame('Pest', $this->adapter->getName()); + } + + public function test_it_supports_junit_reports(): void + { + $this->assertTrue($this->adapter->hasJUnitReport()); + } + + /** + * @dataProvider outputProvider + */ + public function test_it_can_tell_the_outcome_of_the_tests_from_the_output( + string $output, + bool $expected + ): void { + $actual = $this->adapter->testsPass($output); + + $this->assertSame($expected, $actual); + } + + /** + * @dataProvider memoryReportProvider + */ + public function test_it_can_tell_the_memory_usage_from_the_output(string $output, float $expectedResult): void + { + $result = $this->adapter->getMemoryUsed($output); + + $this->assertSame($expectedResult, $result); + } + + public function test_it_provides_initial_run_only_options(): void + { + $options = $this->adapter->getInitialRunOnlyOptions(); + + $this->assertSame( + ['--configuration', '--filter', '--testsuite'], + $options + ); + } + + /** + * @group integration + */ + public function test_it_provides_initial_test_run_command_line_when_no_coverage_is_expected(): void + { + $this->cliArgumentsBuilder + ->expects($this->once()) + ->method('build') + ->with('', '--group=default') + ; + + $this->commandLineBuilder + ->expects($this->once()) + ->method('build') + ->with('/path/to/pest', ['-d', 'memory_limit=-1'], []) + ->willReturn(['/path/to/pest', '--dummy-argument']) + ; + + $this->pcovDirectoryProvider + ->expects($this->never()) + ->method($this->anything()) + ; + + $initialTestRunCommandLine = $this->adapter->getInitialTestRunCommandLine('--group=default', ['-d', 'memory_limit=-1'], true); + + $this->assertSame( + [ + '/path/to/pest', + '--dummy-argument', + ], + $initialTestRunCommandLine + ); + } + + /** + * @group integration + */ + public function test_it_provides_initial_test_run_command_line_when_coverage_report_is_requested(): void + { + $this->cliArgumentsBuilder + ->expects($this->once()) + ->method('build') + ->with('', '--group=default --coverage-xml=/tmp/coverage-xml --log-junit=/tmp/infection/junit.xml') + ->willReturn([ + '--group=default', '--coverage-xml=/tmp/coverage-xml', '--log-junit=/tmp/infection/junit.xml', + ]) + ; + + $this->commandLineBuilder + ->expects($this->once()) + ->method('build') + ->with('/path/to/pest', ['-d', 'memory_limit=-1'], [ + '--group=default', '--coverage-xml=/tmp/coverage-xml', '--log-junit=/tmp/infection/junit.xml', + ]) + ->willReturn([ + '/path/to/pest', + '--group=default', + '--coverage-xml=/tmp/coverage-xml', + '--log-junit=/tmp/infection/junit.xml', + ]) + ; + + $this->pcovDirectoryProvider + ->expects($this->once()) + ->method('shallProvide') + ->willReturn(false) + ; + + $this->pcovDirectoryProvider + ->expects($this->never()) + ->method('getDirectory') + ; + + $initialTestRunCommandLine = $this->adapter->getInitialTestRunCommandLine('--group=default', ['-d', 'memory_limit=-1'], false); + + $this->assertSame( + [ + '/path/to/pest', + '--group=default', + '--coverage-xml=/tmp/coverage-xml', + '--log-junit=/tmp/infection/junit.xml', + ], + $initialTestRunCommandLine + ); + } + + /** + * @group integration + */ + public function test_it_provides_initial_test_run_command_line_when_coverage_report_is_requested_and_pcov_is_in_use(): void + { + $this->cliArgumentsBuilder + ->expects($this->once()) + ->method('build') + ->with('', '--group=default --coverage-xml=/tmp/coverage-xml --log-junit=/tmp/infection/junit.xml') + ->willReturn([ + '--group=default', '--coverage-xml=/tmp/coverage-xml', '--log-junit=/tmp/infection/junit.xml', + ]) + ; + + $this->commandLineBuilder + ->expects($this->once()) + ->method('build') + ->with('/path/to/pest', [ + '-d', + 'memory_limit=-1', + '-d', + '\\' === DIRECTORY_SEPARATOR ? 'pcov.directory="."' : "pcov.directory='.'", + ], [ + '--group=default', '--coverage-xml=/tmp/coverage-xml', '--log-junit=/tmp/infection/junit.xml', + ]) + ->willReturn([ + '/path/to/pest', + '--group=default', + '--coverage-xml=/tmp/coverage-xml', + '--log-junit=/tmp/infection/junit.xml', + ]) + ; + + $this->pcovDirectoryProvider + ->expects($this->once()) + ->method('shallProvide') + ->willReturn(true) + ; + + $this->pcovDirectoryProvider + ->expects($this->once()) + ->method('getDirectory') + ->willReturn('.') + ; + + $initialTestRunCommandLine = $this->adapter->getInitialTestRunCommandLine('--group=default', ['-d', 'memory_limit=-1'], false); + + $this->assertSame( + [ + '/path/to/pest', + '--group=default', + '--coverage-xml=/tmp/coverage-xml', + '--log-junit=/tmp/infection/junit.xml', + ], + $initialTestRunCommandLine + ); + } + + public function outputProvider(): iterable + { + yield ['Tests: 1 risked', true]; + + yield ['Tests: 9 passed', true]; + + yield ['Tests: 1 failed, 8 passed', false]; + } + + public function memoryReportProvider(): iterable + { + yield ['Memory: 8.00MB', 8.0]; + + yield ['Memory: 68.00MB', 68.0]; + + yield ['Memory: 68.00 MB', 68.0]; + + yield ['Time: 2.51 seconds', -1.0]; + } +}