From 49bf00024f6cd559c40b16d93233587ef2d43605 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 15 Jun 2025 22:43:59 +0100 Subject: [PATCH 01/19] fix: coverage when coverage file is over `2.4gb` on mac os --- composer.json | 4 +- overrides/Report/PHP.php | 98 +++++++++++++++++++++++++++++ src/Bootstrappers/BootOverrides.php | 17 ++--- src/Support/Coverage.php | 9 ++- 4 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 overrides/Report/PHP.php diff --git a/composer.json b/composer.json index cdc534e4..516e62b9 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,11 @@ "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", "pestphp/pest-plugin-profanity": "^4.0.0", - "phpunit/phpunit": "^12.2.1" + "phpunit/phpunit": "^12.2.2" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.2.1", + "phpunit/phpunit": ">12.2.2", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, diff --git a/overrides/Report/PHP.php b/overrides/Report/PHP.php new file mode 100644 index 00000000..0cd7e27b --- /dev/null +++ b/overrides/Report/PHP.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SebastianBergmann\CodeCoverage\Report; + +use const PHP_EOL; + +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Util\Filesystem; +use SebastianBergmann\CodeCoverage\WriteOperationFailedException; + +use function dirname; +use function serialize; +use function str_contains; + +final class PHP +{ + public function process(CodeCoverage $coverage, ?string $target = null): string + { + $coverage->clearCache(); + + $buffer = " */ public const array FILES = [ - '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', - '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php', - 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', - '3bb609b0d3bf6dee8df8d6cd62a3c8ece823c4bb941eaaae39e3cb267171b9d2' => 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', - '8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', - 'b4250fc3ffad5954624cb5e682fd940b874e8d3422fa1ee298bd7225e1aa5fc2' => 'TextUI/TestSuiteFilterProcessor.php', - '8cfcb4999af79463eca51a42058e502ea4ddc776cba5677bf2f8eb6093e21a5c' => 'Event/Value/ThrowableBuilder.php', - '86cd9bcaa53cdd59c5b13e58f30064a015c549501e7629d93b96893d4dee1eb1' => 'Logging/JUnit/JunitXmlLogger.php', + 'Runner/Filter/NameFilterIterator.php', + 'Runner/ResultCache/DefaultResultCache.php', + 'Runner/TestSuiteLoader.php', + 'TextUI/Command/Commands/WarmCodeCoverageCacheCommand.php', + 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php', + 'TextUI/TestSuiteFilterProcessor.php', + 'Event/Value/ThrowableBuilder.php', + 'Logging/JUnit/JunitXmlLogger.php', + 'Report/Php.php', ]; /** diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 955bbfc4..1aa641fb 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -89,9 +89,16 @@ final class Coverage } /** @var CodeCoverage $codeCoverage */ - $codeCoverage = require $reportPath; + $handle = fopen($reportPath, 'r'); + $code = ''; + while (! feof($handle)) { + $code .= fread($handle, 8192); + } + fclose($handle); unlink($reportPath); + $codeCoverage = eval(substr($code, 5)); + $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines(); /** @var Directory $report */ From 8c403a57c262d85ef9de6e7011a12de9b8482f07 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 16 Jun 2025 10:04:00 +0100 Subject: [PATCH 02/19] fix: upper case fix --- src/Bootstrappers/BootOverrides.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index 205a7b0a..c9119219 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -26,7 +26,7 @@ final class BootOverrides implements Bootstrapper 'TextUI/TestSuiteFilterProcessor.php', 'Event/Value/ThrowableBuilder.php', 'Logging/JUnit/JunitXmlLogger.php', - 'Report/Php.php', + 'Report/PHP.php', ]; /** From c3bfdf130e52c26e2ce3e7331fab059e16e7b0fd Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 16 Jun 2025 10:14:04 +0100 Subject: [PATCH 03/19] chore: type checking --- src/Bootstrappers/BootOverrides.php | 2 +- src/Support/Coverage.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index c9119219..a4eef19f 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -15,7 +15,7 @@ final class BootOverrides implements Bootstrapper /** * The list of files to be overridden. * - * @var array + * @var array */ public const array FILES = [ 'Runner/Filter/NameFilterIterator.php', diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 1aa641fb..adcfa40d 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -88,13 +88,16 @@ final class Coverage throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath)); } - /** @var CodeCoverage $codeCoverage */ $handle = fopen($reportPath, 'r'); $code = ''; - while (! feof($handle)) { + while (is_resource($handle) && ! feof($handle)) { $code .= fread($handle, 8192); } - fclose($handle); + + if (is_resource($handle)) { + fclose($handle); + } + unlink($reportPath); $codeCoverage = eval(substr($code, 5)); From 163479ae60d8a45939e64916f8002706794db3c3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 16 Jun 2025 10:14:16 +0100 Subject: [PATCH 04/19] chore: style --- src/Support/Coverage.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index adcfa40d..3f543790 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Pest\Support; use Pest\Exceptions\ShouldNotHappen; -use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Node\Directory; use SebastianBergmann\CodeCoverage\Node\File; use SebastianBergmann\Environment\Runtime; From ef76c04dbe907c65ea2f372630ab8a1327e79d43 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 27 Jun 2025 02:15:28 +0100 Subject: [PATCH 05/19] feat: adds `fixture` --- src/Functions.php | 25 +++++++++++++++++++++++++ src/Pest.php | 2 +- tests/Features/Fixture.php | 11 +++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/Features/Fixture.php diff --git a/src/Functions.php b/src/Functions.php index 1e12fe7e..f17ea15c 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -278,3 +278,28 @@ if (! function_exists('mutates')) { } } } + +if (! function_exists('fixture')) { + /** + * Returns the absolute path to a fixture file. + */ + function fixture(string $file): string + { + $file = implode(DIRECTORY_SEPARATOR, [ + TestSuite::getInstance()->rootPath, + TestSuite::getInstance()->testPath, + 'Fixtures', + str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file), + ]); + + $fileRealPath = realpath($file); + + if ($fileRealPath === false) { + throw new InvalidArgumentException( + 'The fixture file ['.$file.'] does not exist.', + ); + } + + return $fileRealPath; + } +} diff --git a/src/Pest.php b/src/Pest.php index 865c18d0..47af9ee1 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.0.0-alpha.2'; + return '4.0.0-alpha.3'; } function testDirectory(string $file = ''): string diff --git a/tests/Features/Fixture.php b/tests/Features/Fixture.php new file mode 100644 index 00000000..49b4c76a --- /dev/null +++ b/tests/Features/Fixture.php @@ -0,0 +1,11 @@ +toBeString() + ->toBeFile(); +}); + +it('may throw an exception if the file does not exist', function () { + fixture('file-that-does-not-exist.php'); From 7fc69033f895aeae3aa2fda1bcf8233388def7ef Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 27 Jun 2025 02:15:36 +0100 Subject: [PATCH 06/19] chore: adjusts style --- tests/Features/Fixture.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Features/Fixture.php b/tests/Features/Fixture.php index 49b4c76a..1ac538b8 100644 --- a/tests/Features/Fixture.php +++ b/tests/Features/Fixture.php @@ -9,3 +9,4 @@ it('may return a file path', function () { it('may throw an exception if the file does not exist', function () { fixture('file-that-does-not-exist.php'); +})->throws(InvalidArgumentException::class); From a22013a7d367bc7b3c3b52b3832fc236e5f85573 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 28 Jun 2025 12:14:45 +0100 Subject: [PATCH 07/19] fix: `with` types --- src/PendingCalls/TestCall.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 50fef3b6..92723e8d 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -178,10 +178,9 @@ final class TestCall // @phpstan-ignore-line } /** - * Runs the current test multiple times with - * each item of the given `iterable`. + * Runs the current test multiple times with each item of the given `iterable`. * - * @param array<\Closure|iterable|string> $data + * @param Closure|iterable|string $data */ public function with(Closure|iterable|string ...$data): self { From 0bc3219a2b3efe72275e8ed0ccc1dc9765178e65 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 28 Jun 2025 18:18:26 +0100 Subject: [PATCH 08/19] feat: moves `visit` to the core --- composer.json | 3 ++- resources/views/installers/plugin-browser.php | 17 ++++++++++++ src/Configuration.php | 8 ++++++ src/Functions.php | 26 +++++++++++++++++++ src/Installers/PluginBrowser.php | 15 +++++++++++ src/PendingCalls/TestCall.php | 2 +- src/Pest.php | 2 +- 7 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 resources/views/installers/plugin-browser.php create mode 100644 src/Installers/PluginBrowser.php diff --git a/composer.json b/composer.json index 516e62b9..80354710 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "^8.3.0", "brianium/paratest": "^7.10.2", - "nunomaduro/collision": "^8.8.1", + "nunomaduro/collision": "^8.8.2", "nunomaduro/termwind": "^2.3.1", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", @@ -55,6 +55,7 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-browser": "^4.0.0", "pestphp/pest-plugin-type-coverage": "^4.0.0", "symfony/process": "^7.3.0" }, diff --git a/resources/views/installers/plugin-browser.php b/resources/views/installers/plugin-browser.php new file mode 100644 index 00000000..28288ab0 --- /dev/null +++ b/resources/views/installers/plugin-browser.php @@ -0,0 +1,17 @@ +
+

+ Using the visit() function requires the Pest Plugin Browser to be installed. + + Run: +

+ +
+ - + composer require pestphp/pest-plugin-browser:^4.0 --dev +
+ +
+ - + npx playwright install +
+
diff --git a/src/Configuration.php b/src/Configuration.php index c504aa65..fb4f45a4 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -102,6 +102,14 @@ final readonly class Configuration return Configuration\Project::getInstance(); } + /** + * Gets the browser configuration. + */ + public function browser(): Browser\Configuration + { + return new Browser\Configuration; + } + /** * Proxies calls to the uses method. * diff --git a/src/Functions.php b/src/Functions.php index f17ea15c..1702f94a 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,11 +2,14 @@ declare(strict_types=1); +use Pest\Browser\Api\ArrayablePendingAwaitablePage; +use Pest\Browser\Api\PendingAwaitablePage; use Pest\Concerns\Expectable; use Pest\Configuration; use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Expectation; +use Pest\Installers\PluginBrowser; use Pest\Mutate\Contracts\MutationTestRunner; use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\PendingCalls\AfterEachCall; @@ -303,3 +306,26 @@ if (! function_exists('fixture')) { return $fileRealPath; } } + +if (! function_exists('visit')) { + /** + * Browse to the given URL. + * + * @template TUrl of array|string + * + * @param TUrl $url + * @param array $options + * @return (TUrl is array ? ArrayablePendingAwaitablePage : PendingAwaitablePage) + */ + function visit(array|string $url, array $options = []): ArrayablePendingAwaitablePage|PendingAwaitablePage + { + if (! class_exists(\Pest\Browser\Configuration::class)) { + PluginBrowser::install(); + + exit(0); + } + + // @phpstan-ignore-next-line + return test()->visit($url, $options); + } +} diff --git a/src/Installers/PluginBrowser.php b/src/Installers/PluginBrowser.php new file mode 100644 index 00000000..2d36ed3d --- /dev/null +++ b/src/Installers/PluginBrowser.php @@ -0,0 +1,15 @@ +|string $data + * @param Closure|iterable|string $data */ public function with(Closure|iterable|string ...$data): self { diff --git a/src/Pest.php b/src/Pest.php index 47af9ee1..7741ff4d 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.0.0-alpha.3'; + return '4.0.0-alpha.4'; } function testDirectory(string $file = ''): string From 3faeede1ef1e5b23f3d225c0f6a93065b65dab43 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 28 Jun 2025 18:24:19 +0100 Subject: [PATCH 09/19] chore: fixes snapshots --- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- tests/Arch.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index b2f7a23f..e23801fd 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -1,5 +1,5 @@ - Pest Testing Framework 4.0.0-alpha.2. + Pest Testing Framework 4.0.0-alpha.4. USAGE: pest [options] diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap index d72ea0de..90663c17 100644 --- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap @@ -1,3 +1,3 @@ - Pest Testing Framework 4.0.0-alpha.2. + Pest Testing Framework 4.0.0-alpha.4. diff --git a/tests/Arch.php b/tests/Arch.php index d8deb460..905515d2 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -37,6 +37,7 @@ arch('dependencies') 'Termwind', 'ParaTest', 'Pest\Arch', + 'Pest\Browser', 'Pest\Mutate\Contracts\Configuration', 'Pest\Mutate\Decorators\TestCallDecorator', 'Pest\Mutate\Repositories\ConfigurationRepository', From af3fdceddb937ac894b6e59b75921e46f50cf3f0 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 28 Jun 2025 18:31:45 +0100 Subject: [PATCH 10/19] feat: adds phpunit `12.2.5` support --- composer.json | 4 ++-- src/Result.php | 4 ++-- .../visual_snapshot_of_help_command_output.snap | 14 +++++++++++++- tests/.snapshots/success.txt | 6 +++++- tests/Visual/Parallel.php | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 80354710..9bf41f27 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,11 @@ "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", "pestphp/pest-plugin-profanity": "^4.0.0", - "phpunit/phpunit": "^12.2.2" + "phpunit/phpunit": "^12.2.5" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.2.2", + "phpunit/phpunit": ">12.2.5", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, diff --git a/src/Result.php b/src/Result.php index 22e1e895..4dcbe4a1 100644 --- a/src/Result.php +++ b/src/Result.php @@ -40,7 +40,7 @@ final class Result */ public static function exitCode(Configuration $configuration, TestResult $result): int { - if ($result->wasSuccessfulIgnoringPhpunitWarnings()) { + if ($result->wasSuccessful()) { if ($configuration->failOnWarning()) { $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() + count($result->warnings()) @@ -60,7 +60,7 @@ final class Result return self::FAILURE_EXIT; } - if ($result->wasSuccessfulIgnoringPhpunitWarnings()) { + if ($result->wasSuccessful()) { if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) { $returnCode = self::FAILURE_EXIT; } diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index e23801fd..c6234c04 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -53,7 +53,7 @@ --disallow-test-output ................. Be strict about output during tests --enforce-time-limit ................. Enforce time limit based on test size --default-time-limit [sec] Timeout in seconds for tests that have no declared size - --dont-report-useless-tests .. Do not report tests that do not test anything + --do-not-report-useless-tests Do not report tests that do not test anything --stop-on-defect ... Stop after first error, failure, warning, or risky test --stop-on-error ..................................... Stop after first error --stop-on-failure ................................. Stop after first failure @@ -69,10 +69,21 @@ --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered --fail-on-phpunit-notice Signal failure using shell exit code when a PHPUnit notice was triggered + --fail-on-phpunit-warning Signal failure using shell exit code when a PHPUnit warning was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete --fail-on-all-issues Signal failure using shell exit code when an issue is triggered + --do-not-fail-on-empty-test-suite Do not signal failure using shell exit code when no tests were run + --do-not-fail-on-warning Do not signal failure using shell exit code when a warning was triggered + --do-not-fail-on-risky Do not signal failure using shell exit code when a test was considered risky + --do-not-fail-on-deprecation Do not signal failure using shell exit code when a deprecation was triggered + --do-not-fail-on-phpunit-deprecation Do not signal failure using shell exit code when a PHPUnit deprecation was triggered + --do-not-fail-on-phpunit-notice Do not signal failure using shell exit code when a PHPUnit notice was triggered + --do-not-fail-on-phpunit-warning Do not signal failure using shell exit code when a PHPUnit warning was triggered + --do-not-fail-on-notice Do not signal failure using shell exit code when a notice was triggered + --do-not-fail-on-skipped Do not signal failure using shell exit code when a test was skipped + --do-not-fail-on-incomplete Do not signal failure using shell exit code when a test was marked incomplete --cache-result ............................ Write test results to cache file --do-not-cache-result .............. Do not write test results to cache file --order-by [order] Run tests in order: default|defects|depends|duration|no-depends|random|reverse|size @@ -106,6 +117,7 @@ LOGGING OPTIONS: --log-junit [file] .......... Write test results in JUnit XML format to file --log-otr [file] Write test results in Open Test Reporting XML format to file + --include-git-information Include Git information in Open Test Reporting XML logfile --log-teamcity [file] ........ Write test results in TeamCity format to file --testdox-html [file] .. Write test results in TestDox format (HTML) to file --testdox-text [file] Write test results in TestDox format (plain text) to file diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 5245e72c..7b78f425 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1040,6 +1040,10 @@ ✓ it may fail ✓ it may fail with the given message + PASS Tests\Features\Fixture + ✓ it may return a file path + ✓ it may throw an exception if the file does not exist + WARN Tests\Features\Helpers ✓ it can set/get properties on $this ! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw @@ -1708,4 +1712,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1144 passed (2736 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1146 passed (2739 assertions) \ No newline at end of file diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 313f8208..01492414 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,7 +16,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1134 passed (2712 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1136 passed (2715 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From 3f27352560010fb4c16046761af0fb805bf5be58 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 30 Jun 2025 22:15:07 +0100 Subject: [PATCH 11/19] feat: adds `--shard` --- composer.json | 9 ++-- src/Plugins/Shard.php | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/Plugins/Shard.php diff --git a/composer.json b/composer.json index 9bf41f27..df8febf7 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,15 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.10.2", + "brianium/paratest": "^7.10.3", "nunomaduro/collision": "^8.8.2", "nunomaduro/termwind": "^2.3.1", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", "pestphp/pest-plugin-profanity": "^4.0.0", - "phpunit/phpunit": "^12.2.5" + "phpunit/phpunit": "^12.2.5", + "symfony/process": "^7.3" }, "conflict": { "filp/whoops": "<2.16.0", @@ -56,8 +57,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0", - "symfony/process": "^7.3.0" + "pestphp/pest-plugin-type-coverage": "^4.0.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -114,6 +114,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php new file mode 100644 index 00000000..187e11f5 --- /dev/null +++ b/src/Plugins/Shard.php @@ -0,0 +1,108 @@ +hasArgument('--shard', $arguments)) { + return $arguments; + } + + // @phpstan-ignore-next-line + $input = new ArgvInput($arguments); + + if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { + $shard = $input->getParameterOption('--'.self::SHARD_OPTION); + } else { + $shard = null; + } + + if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + [$index, $total] = explode('/', $shard); + + if (! is_numeric($index) || ! is_numeric($total)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + if ($index <= 0 || $total <= 0 || $index > $total) { + throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); + } + + $index = (int) $index; + $total = (int) $total; + + $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument( + "$index/$total", + $arguments, + ))); + + /** @phpstan-ignore-next-line */ + $tests = $this->allTests($arguments); + $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? []; + + return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)]; + } + + /** + * Returns all tests that the test suite would run. + * + * @param list $arguments + * @return list + */ + private function allTests(array $arguments): array + { + $output = (new Process([ + 'php', + ...$this->removeParallelArguments($arguments), + '--list-tests', + ]))->mustRun()->getOutput(); + + preg_match_all('/ - (?:P\\\)?(.+)::/', $output, $matches); + + return array_values(array_unique($matches[1])); + } + + /** + * @param array $arguments + * @return array + */ + private function removeParallelArguments(array $arguments): array + { + return array_filter($arguments, fn (string $argument): bool => ! in_array($argument, ['--parallel', '-p'], strict: true)); + } + + /** + * Builds the filter argument for the given tests to run. + */ + private function buildFilterArgument(mixed $testsToRun): string + { + return addslashes(implode('|', $testsToRun)); + } +} From 2ff471396833f84f354ad36b7ca99934aabffe1a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 30 Jun 2025 22:26:09 +0100 Subject: [PATCH 12/19] ci: fix missing dep --- .github/workflows/static.yml | 1 + .github/workflows/tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 491cd60d..5b9b0df5 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -27,6 +27,7 @@ jobs: php-version: 8.3 tools: composer:v2 coverage: none + extensions: sockets - name: Install Dependencies run: composer update --prefer-stable --no-interaction --no-progress --ansi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index abafbbbb..9cb30b0b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,7 @@ jobs: php-version: ${{ matrix.php }} tools: composer:v2 coverage: none + extensions: sockets - name: Setup Problem Matches run: | From d8e1b27491cee648fefdcf1cf581a16e9beada7b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 30 Jun 2025 22:50:57 +0100 Subject: [PATCH 13/19] ci: fix included version --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9cb30b0b..1a1ea7eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] - symfony: ['7.1'] + symfony: ['7.3'] php: ['8.3', '8.4'] dependency_version: [prefer-lowest, prefer-stable] From 5def62018b19b8a9fc6bdc410f6a48925a1b9d51 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 1 Jul 2025 11:00:05 +0100 Subject: [PATCH 14/19] fix: shard regex --- src/Plugins/Shard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php index 187e11f5..c73601e0 100644 --- a/src/Plugins/Shard.php +++ b/src/Plugins/Shard.php @@ -84,7 +84,7 @@ final class Shard implements HandlesArguments '--list-tests', ]))->mustRun()->getOutput(); - preg_match_all('/ - (?:P\\\)?(.+)::/', $output, $matches); + preg_match_all('/ - (?:P\\\\)?(Tests\\\\[^:]+)::/', $output, $matches); return array_values(array_unique($matches[1])); } From 73bf579da32e1c9c6b820a5adf85fc69f76549fc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 2 Jul 2025 00:26:15 +0100 Subject: [PATCH 15/19] chore: code refactor --- src/Plugins/Shard.php | 60 +++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php index c73601e0..9ac2c436 100644 --- a/src/Plugins/Shard.php +++ b/src/Plugins/Shard.php @@ -7,6 +7,7 @@ namespace Pest\Plugins; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Exceptions\InvalidOption; use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Process\Process; /** @@ -35,28 +36,7 @@ final class Shard implements HandlesArguments // @phpstan-ignore-next-line $input = new ArgvInput($arguments); - if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { - $shard = $input->getParameterOption('--'.self::SHARD_OPTION); - } else { - $shard = null; - } - - if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { - throw new InvalidOption('The [--shard] option must be in the format "index/total".'); - } - - [$index, $total] = explode('/', $shard); - - if (! is_numeric($index) || ! is_numeric($total)) { - throw new InvalidOption('The [--shard] option must be in the format "index/total".'); - } - - if ($index <= 0 || $total <= 0 || $index > $total) { - throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); - } - - $index = (int) $index; - $total = (int) $total; + ['index' => $index, 'total' => $total] = self::getShard($input); $arguments = $this->popArgument("--shard=$index/$total", $this->popArgument('--shard', $this->popArgument( "$index/$total", @@ -105,4 +85,40 @@ final class Shard implements HandlesArguments { return addslashes(implode('|', $testsToRun)); } + + /** + * Returns the shard information. + * + * @return array{index: int, total: int} + */ + public static function getShard(InputInterface $input): array + { + if ($input->hasParameterOption('--'.self::SHARD_OPTION)) { + $shard = $input->getParameterOption('--'.self::SHARD_OPTION); + } else { + $shard = null; + } + + if (! is_string($shard) || ! preg_match('/^\d+\/\d+$/', $shard)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + [$index, $total] = explode('/', $shard); + + if (! is_numeric($index) || ! is_numeric($total)) { + throw new InvalidOption('The [--shard] option must be in the format "index/total".'); + } + + if ($index <= 0 || $total <= 0 || $index > $total) { + throw new InvalidOption('The [--shard] option index must be a non-negative integer less than the total number of shards.'); + } + + $index = (int) $index; + $total = (int) $total; + + return [ + 'index' => $index, + 'total' => $total, + ]; + } } From 0f1e87c726c74659c673ac652c7923d4bd8b7607 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 5 Jul 2025 15:43:43 +0100 Subject: [PATCH 16/19] Adds output about sharding --- composer.json | 4 +-- src/Plugins/Shard.php | 59 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index df8febf7..21f36c73 100644 --- a/composer.json +++ b/composer.json @@ -73,9 +73,9 @@ ], "scripts": { "refacto": "rector", - "lint": "pint", + "lint": "pint --parallel", "test:refacto": "rector --dry-run", - "test:lint": "pint --test", + "test:lint": "pint --parallel --test", "test:profanity": "php bin/pest --profanity --compact --language=en", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", diff --git a/src/Plugins/Shard.php b/src/Plugins/Shard.php index 9ac2c436..f48260bb 100644 --- a/src/Plugins/Shard.php +++ b/src/Plugins/Shard.php @@ -4,25 +4,43 @@ declare(strict_types=1); namespace Pest\Plugins; +use Pest\Contracts\Plugins\AddsOutput; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Exceptions\InvalidOption; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; /** * @internal */ -final class Shard implements HandlesArguments +final class Shard implements AddsOutput, HandlesArguments { use Concerns\HandleArguments; private const string SHARD_OPTION = 'shard'; /** - * The total number of tests. + * The shard index and total number of shards. + * + * @var array{ + * index: int, + * total: int, + * testsRan: int, + * testsCount: int + * }|null */ - public static int $testsCount = 0; + private static ?array $shard = null; + + /** + * Creates a new Plugin instance. + */ + public function __construct( + private readonly OutputInterface $output, + ) { + // + } /** * {@inheritDoc} @@ -47,6 +65,13 @@ final class Shard implements HandlesArguments $tests = $this->allTests($arguments); $testsToRun = (array_chunk($tests, max(1, (int) ceil(count($tests) / $total))))[$index - 1] ?? []; + self::$shard = [ + 'index' => $index, + 'total' => $total, + 'testsRan' => count($testsToRun), + 'testsCount' => count($tests), + ]; + return [...$arguments, '--filter', $this->buildFilterArgument($testsToRun)]; } @@ -86,6 +111,34 @@ final class Shard implements HandlesArguments return addslashes(implode('|', $testsToRun)); } + /** + * Adds output after the Test Suite execution. + */ + public function addOutput(int $exitCode): int + { + if (self::$shard === null) { + return $exitCode; + } + + [ + 'index' => $index, + 'total' => $total, + 'testsRan' => $testsRan, + 'testsCount' => $testsCount, + ] = self::$shard; + + $this->output->writeln(sprintf( + ' Shard: %d of %d — %d file%s ran, out of %d.', + $index, + $total, + $testsRan, + $testsRan === 1 ? '' : 's', + $testsCount, + )); + + return $exitCode; + } + /** * Returns the shard information. * From 0d148c2a67c3a2bf2758bbb9a400512010beaca7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 5 Jul 2025 15:46:03 +0100 Subject: [PATCH 17/19] chroe: bumps phpunit --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 21f36c73..62df89fb 100644 --- a/composer.json +++ b/composer.json @@ -25,12 +25,12 @@ "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", "pestphp/pest-plugin-profanity": "^4.0.0", - "phpunit/phpunit": "^12.2.5", - "symfony/process": "^7.3" + "phpunit/phpunit": "^12.2.6", + "symfony/process": "^7.3.0" }, "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.2.5", + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.2.6", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, From 9d0410ee0b0cec36bbc6e857feefceba01770b7c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 6 Jul 2025 13:45:33 +0100 Subject: [PATCH 18/19] feat: adjusts only for browser debug --- src/Plugins/Only.php | 46 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Plugins/Only.php b/src/Plugins/Only.php index 7c2809f1..fd1001de 100644 --- a/src/Plugins/Only.php +++ b/src/Plugins/Only.php @@ -5,7 +5,10 @@ declare(strict_types=1); namespace Pest\Plugins; use Pest\Contracts\Plugins\Terminable; +use Pest\Factories\Attribute; +use Pest\Factories\TestCaseMethodFactory; use Pest\PendingCalls\TestCall; +use PHPUnit\Framework\Attributes\Group; /** * @internal @@ -23,28 +26,19 @@ final class Only implements Terminable .DIRECTORY_SEPARATOR .'.temp'; - /** - * {@inheritDoc} - */ - public function terminate(): void - { - if (Parallel::isWorker()) { - return; - } - - $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; - - if (file_exists($lockFile)) { - unlink($lockFile); - } - } - /** * Creates the lock file. */ - public static function enable(TestCall $testCall, string $group = '__pest_only'): void + public static function enable(TestCall|TestCaseMethodFactory $testCall, string $group = '__pest_only'): void { - $testCall->group($group); + if ($testCall instanceof TestCall) { + $testCall->group($group); + } else { + $testCall->attributes[] = new Attribute( + Group::class, + [$group], + ); + } if (Environment::name() === Environment::CI || Parallel::isWorker()) { return; @@ -88,4 +82,20 @@ final class Only implements Terminable return file_get_contents($lockFile) ?: '__pest_only'; // @phpstan-ignore-line } + + /** + * {@inheritDoc} + */ + public function terminate(): void + { + if (Parallel::isWorker()) { + return; + } + + $lockFile = self::TEMPORARY_FOLDER.DIRECTORY_SEPARATOR.'only.lock'; + + if (file_exists($lockFile)) { + unlink($lockFile); + } + } } From 0355119afc7c8014f6ccdfdd7c777a18ee3df9c1 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 6 Jul 2025 14:29:18 +0100 Subject: [PATCH 19/19] fix: snapshots feedback --- src/Concerns/Testable.php | 10 +--------- src/Pest.php | 2 +- .../Help/visual_snapshot_of_help_command_output.snap | 2 +- .../visual_snapshot_of_help_command_output.snap | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 9918eb83..f233b6ac 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -438,15 +438,7 @@ trait Testable return; } - if (count($this->__snapshotChanges) === 1) { - $this->markTestIncomplete($this->__snapshotChanges[0]); - - return; - } - - $messages = implode(PHP_EOL, array_map(static fn (string $message): string => '- $message', $this->__snapshotChanges)); - - $this->markTestIncomplete($messages); + $this->markTestIncomplete(implode('. ', $this->__snapshotChanges)); } /** diff --git a/src/Pest.php b/src/Pest.php index 7741ff4d..7ddec711 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.0.0-alpha.4'; + return '4.0.0-alpha.5'; } function testDirectory(string $file = ''): string diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index c6234c04..8a174927 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -1,5 +1,5 @@ - Pest Testing Framework 4.0.0-alpha.4. + Pest Testing Framework 4.0.0-alpha.5. USAGE: pest [options] diff --git a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap index 90663c17..f54244da 100644 --- a/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap @@ -1,3 +1,3 @@ - Pest Testing Framework 4.0.0-alpha.4. + Pest Testing Framework 4.0.0-alpha.5.