From f720be862ea4aa9b77ae18ed17f3bf8be1589dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20As=CC=A7an?= Date: Thu, 12 Sep 2024 02:45:37 +0300 Subject: [PATCH 01/76] Add reference method --- src/PendingCalls/TestCall.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 1b02ed1e..6f1732b7 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -48,6 +48,13 @@ final class TestCall */ private readonly bool $descriptionLess; + /** + * This property is not actually used in the codebase, it's only here to make Rector happy. + * + * @var string|array + */ + public array|string $references; + /** * Creates a new Pending Call. */ @@ -615,6 +622,21 @@ final class TestCall return $this; } + /** + * Adds a reference to the tested method or class. + * This helps to link test cases to the source code + * for easier navigation during development. + * + * @param string|array $classes + */ + public function reference(string|array ...$classes): self + { + // For rector + $this->references = $classes; // @phpstan-ignore-line + + return $this; + } + /** * Informs the test runner that no expectations happen in this test, * and its purpose is simply to check whether the given code can From bab193e7e130fd0a631ef77b90ee8c0ab23a8aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20As=CC=A7an?= Date: Thu, 12 Sep 2024 03:24:36 +0300 Subject: [PATCH 02/76] Fix property type --- src/PendingCalls/TestCall.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 6f1732b7..aa32827e 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -51,9 +51,9 @@ final class TestCall /** * This property is not actually used in the codebase, it's only here to make Rector happy. * - * @var string|array + * @var array */ - public array|string $references; + public array $references; /** * Creates a new Pending Call. @@ -632,7 +632,7 @@ final class TestCall public function reference(string|array ...$classes): self { // For rector - $this->references = $classes; // @phpstan-ignore-line + $this->references = $classes; return $this; } From 668685498fa4caca192655c1f8ef1c473ab9f5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20As=CC=A7an?= Date: Thu, 12 Sep 2024 03:51:20 +0300 Subject: [PATCH 03/76] Fix phpdoc type-hints --- src/PendingCalls/TestCall.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index aa32827e..23c2931f 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -51,7 +51,7 @@ final class TestCall /** * This property is not actually used in the codebase, it's only here to make Rector happy. * - * @var array + * @var array> */ public array $references; @@ -623,11 +623,10 @@ final class TestCall } /** - * Adds a reference to the tested method or class. - * This helps to link test cases to the source code - * for easier navigation during development. + * Adds a reference to the tested method or class. This helps to link test + * cases to the source code for easier navigation. * - * @param string|array $classes + * @param array|class-string ...$classes */ public function reference(string|array ...$classes): self { From 169b76458ecbc076eabc8be526c557dc224b58d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20As=CC=A7an?= Date: Thu, 12 Sep 2024 11:15:30 +0300 Subject: [PATCH 04/76] make the name of the method plural --- src/PendingCalls/TestCall.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 23c2931f..ad7a8a91 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -628,7 +628,7 @@ final class TestCall * * @param array|class-string ...$classes */ - public function reference(string|array ...$classes): self + public function references(string|array ...$classes): self { // For rector $this->references = $classes; From ab0b4a1b4e62eb81f3e3c86a4b0b93a67ebdfacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20A=C5=9Fan?= Date: Thu, 12 Sep 2024 13:09:45 +0300 Subject: [PATCH 05/76] Update src/PendingCalls/TestCall.php Co-authored-by: Owen Voke --- src/PendingCalls/TestCall.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index ad7a8a91..50ddddc3 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -623,8 +623,8 @@ final class TestCall } /** - * Adds a reference to the tested method or class. This helps to link test - * cases to the source code for easier navigation. + * Adds one or more references to the tested method or class. This helps + * to link test cases to the source code for easier navigation. * * @param array|class-string ...$classes */ From 74ff3b8cd94e975ffccd26895a2a58361b35117e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20A=C5=9Fan?= Date: Thu, 12 Sep 2024 13:09:57 +0300 Subject: [PATCH 06/76] Update src/PendingCalls/TestCall.php Co-authored-by: Owen Voke --- src/PendingCalls/TestCall.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 50ddddc3..46e1acb9 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -630,8 +630,7 @@ final class TestCall */ public function references(string|array ...$classes): self { - // For rector - $this->references = $classes; + assert($classes !== []); return $this; } From ba7eb70a5d91f9a5e60750fb393a316efcf4fc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20As=CC=A7an?= Date: Thu, 12 Sep 2024 13:11:12 +0300 Subject: [PATCH 07/76] Remove unnecessary property --- src/PendingCalls/TestCall.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 46e1acb9..1208b809 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -48,13 +48,6 @@ final class TestCall */ private readonly bool $descriptionLess; - /** - * This property is not actually used in the codebase, it's only here to make Rector happy. - * - * @var array> - */ - public array $references; - /** * Creates a new Pending Call. */ From e3bfcbe5f14cded87cf7f4a169bcc7646dd829ba Mon Sep 17 00:00:00 2001 From: tal7aouy Date: Mon, 16 Sep 2024 13:36:34 +0100 Subject: [PATCH 08/76] Add slugify method --- src/Mixins/Expectation.php | 19 ++++++++++++++++++- src/Support/Str.php | 11 ++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index f802bc11..eb72fec0 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -661,7 +661,7 @@ final class Expectation { foreach ($keys as $k => $key) { if (is_array($key)) { - $this->toHaveKeys(array_keys(Arr::dot($key, $k.'.')), $message); + $this->toHaveKeys(array_keys(Arr::dot($key, $k . '.')), $message); } else { $this->toHaveKey($key, message: $message); } @@ -1159,4 +1159,21 @@ final class Expectation return $this; } + + /** + * Asserts that the value can be converted to a slug + * + * @return self + */ + public function toBeSlug(string $message = ''): self + { + if ($message === '') { + $message = "Failed asserting that {$this->value} can be converted to a slug."; + } + + $slug = Str::slugify((string) $this->value); + Assert::assertNotEmpty($slug, $message); + + return $this; + } } diff --git a/src/Support/Str.php b/src/Support/Str.php index 754749e7..ccaf06f7 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -61,7 +61,7 @@ final class Str { $code = str_replace('_', '__', $code); - $code = self::PREFIX.str_replace(' ', '_', $code); + $code = self::PREFIX . str_replace(' ', '_', $code); // sticks to PHP8.2 function naming rules https://www.php.net/manual/en/functions.user-defined.php return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code); @@ -116,4 +116,13 @@ final class Str { return (bool) filter_var($value, FILTER_VALIDATE_URL); } + + /** + * Converts the given `$target` to a URL-friendly "slug". + */ + public static function slugify(string $target): string + { + $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target); + return strtolower(trim($target, '-')); + } } From 92bc1decd92b795ae644923f20eba2a60c82f932 Mon Sep 17 00:00:00 2001 From: tal7aouy Date: Mon, 16 Sep 2024 13:41:13 +0100 Subject: [PATCH 09/76] Add tests for toBeSlug method --- tests/Features/Expect/toBeSlug.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/Features/Expect/toBeSlug.php diff --git a/tests/Features/Expect/toBeSlug.php b/tests/Features/Expect/toBeSlug.php new file mode 100644 index 00000000..2d7c19f8 --- /dev/null +++ b/tests/Features/Expect/toBeSlug.php @@ -0,0 +1,24 @@ +toBeSlug() + ->and('Another Test String')->toBeSlug(); +}); + +test('failures', function () { + expect('')->toBeSlug(); +})->throws(ExpectationFailedException::class); + +test('failures with custom message', function () { + expect('')->toBeSlug('oh no!'); +})->throws(ExpectationFailedException::class, 'oh no!'); + +test('failures with default message', function () { + expect('')->toBeSlug(); +})->throws(ExpectationFailedException::class, 'Failed asserting that can be converted to a slug.'); + +test('not failures', function () { + expect('This is a Test String!')->not->toBeSlug(); +})->throws(ExpectationFailedException::class); From 53c94600cbcde285a775f846339682497c9c2065 Mon Sep 17 00:00:00 2001 From: sebastianfaber Date: Wed, 16 Oct 2024 22:19:08 +0200 Subject: [PATCH 10/76] fix: handle -c flag same as --configuration --- src/Plugins/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/Configuration.php b/src/Plugins/Configuration.php index acae8fb4..a41a8fe0 100644 --- a/src/Plugins/Configuration.php +++ b/src/Plugins/Configuration.php @@ -34,7 +34,7 @@ final class Configuration implements HandlesArguments, Terminable */ public function handleArguments(array $arguments): array { - if ($this->hasArgument('--configuration', $arguments) || $this->hasCustomConfigurationFile()) { + if ($this->hasArgument('--configuration', $arguments) || $this->hasArgument('-c', $arguments) || $this->hasCustomConfigurationFile()) { return $arguments; } From 7760d945bb0732dc366314816e328b21ff742e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:06:03 +0100 Subject: [PATCH 11/76] sync latest changes --- .../Runner/ResultCache/DefaultResultCache.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php index 581a4c7f..fde6bad9 100644 --- a/overrides/Runner/ResultCache/DefaultResultCache.php +++ b/overrides/Runner/ResultCache/DefaultResultCache.php @@ -46,6 +46,7 @@ declare(strict_types=1); namespace PHPUnit\Runner\ResultCache; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; use PHPUnit\Framework\TestStatus\TestStatus; use PHPUnit\Runner\DirectoryCannotBeCreatedException; @@ -65,6 +66,8 @@ use function json_encode; use function Pest\version; /** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class DefaultResultCache implements ResultCache @@ -77,12 +80,12 @@ final class DefaultResultCache implements ResultCache private readonly string $cacheFilename; /** - * @psalm-var array + * @var array */ private array $defects = []; /** - * @psalm-var array + * @var array */ private array $times = []; @@ -119,6 +122,17 @@ final class DefaultResultCache implements ResultCache return $this->times[$id] ?? 0.0; } + public function mergeWith(self $other): void + { + foreach ($other->defects as $id => $defect) { + $this->defects[$id] = $defect; + } + + foreach ($other->times as $id => $time) { + $this->times[$id] = $time; + } + } + public function load(): void { if (! is_file($this->cacheFilename)) { From 8ee9d66d802887129a175939d0ba7e82741b60e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:34:55 +0100 Subject: [PATCH 12/76] sync cs --- overrides/Logging/JUnit/JunitXmlLogger.php | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index ca5c02c4..3a335f84 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -41,6 +41,8 @@ use function str_replace; use function trim; /** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final class JunitXmlLogger @@ -59,32 +61,32 @@ final class JunitXmlLogger private array $testSuites = []; /** - * @psalm-var array + * @var array */ private array $testSuiteTests = [0]; /** - * @psalm-var array + * @var array */ private array $testSuiteAssertions = [0]; /** - * @psalm-var array + * @var array */ private array $testSuiteErrors = [0]; /** - * @psalm-var array + * @var array */ private array $testSuiteFailures = [0]; /** - * @psalm-var array + * @var array */ private array $testSuiteSkipped = [0]; /** - * @psalm-var array + * @var array */ private array $testSuiteTimes = [0]; @@ -195,17 +197,11 @@ final class JunitXmlLogger $this->createTestCase($event); } - /** - * @throws InvalidArgumentException - */ public function testPreparationFailed(): void { $this->preparationFailed = true; } - /** - * @throws InvalidArgumentException - */ public function testPrepared(): void { $this->prepared = true; @@ -431,7 +427,7 @@ final class JunitXmlLogger /** * @throws InvalidArgumentException * - * @psalm-assert !null $this->currentTestCase + * @phpstan-assert !null $this->currentTestCase */ private function createTestCase(Errored|Failed|MarkedIncomplete|PreparationStarted|Prepared|Skipped $event): void { From fe4b5e5e1fa5e65c345b71328d1bc44502b11d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:35:44 +0100 Subject: [PATCH 13/76] sync change --- overrides/Logging/JUnit/JunitXmlLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index 3a335f84..7f59b4a7 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -115,7 +115,7 @@ final class JunitXmlLogger public function flush(): void { - $this->printer->print($this->document->saveXML()); + $this->printer->print($this->document->saveXML() ?: ''); $this->printer->flush(); } From 0cb8c42497a7d34c1239d73cdaeb1330667ecf38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:36:48 +0100 Subject: [PATCH 14/76] sync missing listener --- overrides/Logging/JUnit/JunitXmlLogger.php | 1 + 1 file changed, 1 insertion(+) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index 7f59b4a7..21b73c62 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -304,6 +304,7 @@ final class JunitXmlLogger new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), + new TestMarkedIncompleteSubscriber($this), new TestSkippedSubscriber($this), new TestRunnerExecutionFinishedSubscriber($this), ); From 23f130b0f900469df7e693459b97e828f8eae3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:38:24 +0100 Subject: [PATCH 15/76] Update JunitXmlLogger.php from https://github.com/sebastianbergmann/phpunit/issues/5771 https://github.com/sebastianbergmann/phpunit/commit/c722fb259972a6c55fbb295169fd1a000b3d4c88 --- overrides/Logging/JUnit/JunitXmlLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index 21b73c62..d90ab723 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -212,7 +212,7 @@ final class JunitXmlLogger */ public function testFinished(Finished $event): void { - if ($this->preparationFailed) { + if (!$this->prepared || $this->preparationFailed) { return; } From e834527db2aa2dac5596faa8dd832874b3b4baeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:39:10 +0100 Subject: [PATCH 16/76] Update JunitXmlLogger.php https://github.com/sebastianbergmann/phpunit/issues/6098 --- overrides/Logging/JUnit/JunitXmlLogger.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index d90ab723..433bc05b 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -27,6 +27,7 @@ use PHPUnit\Event\Test\Finished; use PHPUnit\Event\Test\MarkedIncomplete; use PHPUnit\Event\Test\PreparationStarted; use PHPUnit\Event\Test\Prepared; +use PHPUnit\Event\Test\PrintedUnexpectedOutput; use PHPUnit\Event\Test\Skipped; use PHPUnit\Event\TestSuite\Started; use PHPUnit\Event\UnknownSubscriberTypeException; @@ -207,6 +208,18 @@ final class JunitXmlLogger $this->prepared = true; } + public function testPrintedUnexpectedOutput(PrintedUnexpectedOutput $event): void + { + assert($this->currentTestCase !== null); + + $systemOut = $this->document->createElement( + 'system-out', + Xml::prepareString($event->output()), + ); + + $this->currentTestCase->appendChild($systemOut); + } + /** * @throws InvalidArgumentException */ @@ -301,6 +314,7 @@ final class JunitXmlLogger new TestPreparationStartedSubscriber($this), new TestPreparationFailedSubscriber($this), new TestPreparedSubscriber($this), + new TestPrintedUnexpectedOutputSubscriber($this), new TestFinishedSubscriber($this), new TestErroredSubscriber($this), new TestFailedSubscriber($this), From c4c9e915f46c934cc68071fb0e6c9811f20d60e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Mon, 20 Jan 2025 09:50:36 +0100 Subject: [PATCH 17/76] cs --- overrides/Logging/JUnit/JunitXmlLogger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overrides/Logging/JUnit/JunitXmlLogger.php b/overrides/Logging/JUnit/JunitXmlLogger.php index 433bc05b..b7362b1f 100644 --- a/overrides/Logging/JUnit/JunitXmlLogger.php +++ b/overrides/Logging/JUnit/JunitXmlLogger.php @@ -225,7 +225,7 @@ final class JunitXmlLogger */ public function testFinished(Finished $event): void { - if (!$this->prepared || $this->preparationFailed) { + if (! $this->prepared || $this->preparationFailed) { return; } From f68d11ccae4e2be0f26a0ffe7c47fabec7d4602f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 29 Mar 2025 17:44:06 +0000 Subject: [PATCH 18/76] chore: bumps dependencies --- composer.json | 12 +- phpstan-baseline.neon | 199 +++++++++++++++++++ phpstan.neon | 8 +- src/Factories/TestCaseMethodFactory.php | 2 +- src/Mixins/Expectation.php | 1 - src/Panic.php | 2 +- src/PendingCalls/DescribeCall.php | 2 +- src/PendingCalls/TestCall.php | 2 +- src/Repositories/DatasetsRepository.php | 4 +- src/Support/Closure.php | 2 +- src/Support/Exporter.php | 1 + src/Support/HigherOrderMessage.php | 10 +- src/Support/HigherOrderMessageCollection.php | 1 - src/Support/HigherOrderTapProxy.php | 4 +- 14 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/composer.json b/composer.json index bd8c21e3..778d391d 100644 --- a/composer.json +++ b/composer.json @@ -19,16 +19,16 @@ "require": { "php": "^8.2.0", "brianium/paratest": "^7.7.0", - "nunomaduro/collision": "^8.6.1", + "nunomaduro/collision": "^8.7.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-mutate": "^3.0.5", - "phpunit/phpunit": "^11.5.6" + "phpunit/phpunit": "^11.5.15" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.6", + "phpunit/phpunit": ">11.5.15", "sebastian/exporter": "<6.0.0", "webmozart/assert": "<1.11.0" }, @@ -53,9 +53,9 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^3.3.0", - "pestphp/pest-plugin-type-coverage": "^3.2.3", - "symfony/process": "^7.2.0" + "pestphp/pest-dev-tools": "^3.4.0", + "pestphp/pest-plugin-type-coverage": "^3.5.0", + "symfony/process": "^7.2.5" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..99bbdec9 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,199 @@ +parameters: + ignoreErrors: + - + message: '#^Parameter \#1 of callable callable\(Pest\\Expectation\\)\: Pest\\Arch\\Contracts\\ArchExpectation expects Pest\\Expectation\, Pest\\Expectation\ given\.$#' + identifier: argument.type + count: 1 + path: src/ArchPresets/AbstractPreset.php + + - + message: '#^Trait Pest\\Concerns\\Expectable is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Concerns/Expectable.php + + - + message: '#^Trait Pest\\Concerns\\Logging\\WritesToConsole is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Concerns/Logging/WritesToConsole.php + + - + message: '#^Trait Pest\\Concerns\\Testable is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Concerns/Testable.php + + - + message: '#^Loose comparison using \!\= between \(Closure\|null\) and false will always evaluate to false\.$#' + identifier: notEqual.alwaysFalse + count: 1 + path: src/Expectation.php + + - + message: '#^Method Pest\\Expectation\:\:and\(\) should return Pest\\Expectation\ but returns \(Pest\\Expectation&TAndValue\)\|Pest\\Expectation\\.$#' + identifier: return.type + count: 1 + path: src/Expectation.php + + - + message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$each contains generic class Pest\\Expectations\\EachExpectation but does not specify its types\: TValue$#' + identifier: missingType.generics + count: 1 + path: src/Expectation.php + + - + message: '#^PHPDoc tag @property for property Pest\\Expectation\:\:\$not contains generic class Pest\\Expectations\\OppositeExpectation but does not specify its types\: TValue$#' + identifier: missingType.generics + count: 1 + path: src/Expectation.php + + - + message: '#^Parameter \#2 \$newScope of method Closure\:\:bindTo\(\) expects ''static''\|class\-string\|object\|null, string given\.$#' + identifier: argument.type + count: 1 + path: src/Expectation.php + + - + message: '#^Function expect\(\) should return Pest\\Expectation\ but returns Pest\\Expectation\\.$#' + identifier: return.type + count: 1 + path: src/Functions.php + + - + message: '#^Parameter \#1 \$argv of method PHPUnit\\TextUI\\Application\:\:run\(\) expects list\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Kernel.php + + - + message: '#^Call to an undefined method object&TValue of mixed\:\:__toString\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mixins/Expectation.php + + - + message: '#^Call to an undefined method object&TValue of mixed\:\:toArray\(\)\.$#' + identifier: method.notFound + count: 4 + path: src/Mixins/Expectation.php + + - + message: '#^Call to an undefined method object&TValue of mixed\:\:toSnapshot\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mixins/Expectation.php + + - + message: '#^Call to an undefined method object&TValue of mixed\:\:toString\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Mixins/Expectation.php + + - + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#' + identifier: staticMethod.alreadyNarrowedType + count: 2 + path: src/Mixins/Expectation.php + + - + message: '#^PHPDoc tag @var with type callable\(\)\: bool is not subtype of native type Closure\|null\.$#' + identifier: varTag.nativeType + count: 1 + path: src/PendingCalls/TestCall.php + + - + message: '#^Parameter \#1 \$argv of class Symfony\\Component\\Console\\Input\\ArgvInput constructor expects list\\|null, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel.php + + - + message: '#^Parameter \#13 \$testRunnerTriggeredDeprecationEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#14 \$testRunnerTriggeredWarningEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#15 \$errors of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#16 \$deprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#17 \$notices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#18 \$warnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#19 \$phpDeprecations of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#20 \$phpNotices of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#21 \$phpWarnings of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#4 \$testErroredEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#5 \$testFailedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#7 \$testSuiteSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#8 \$testSkippedEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Parameter \#9 \$testMarkedIncompleteEvents of class PHPUnit\\TestRunner\\TestResult\\TestResult constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php + + - + message: '#^Property Pest\\Plugins\\Parallel\\Paratest\\WrapperRunner\:\:\$pending \(list\\) does not accept array\\.$#' + identifier: assign.propertyType + count: 1 + path: src/Plugins/Parallel/Paratest/WrapperRunner.php diff --git a/phpstan.neon b/phpstan.neon index 9ed48871..391daf0b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,14 +1,12 @@ includes: - - vendor/phpstan/phpstan-strict-rules/rules.neon - - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon + - phpstan-baseline.neon parameters: - level: max + level: 7 paths: - src - checkMissingIterableValueType: true - reportUnmatchedIgnoredErrors: true + reportUnmatchedIgnoredErrors: false ignoreErrors: - "#type mixed is not subtype of native#" diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index fb763c75..bac50071 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -155,7 +155,7 @@ final class TestCaseMethodFactory assert($testCase instanceof TestCaseFactory); $method = $this; - return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line + return function (...$arguments) use ($testCase, $method, $closure): mixed { /* @var TestCase $this */ $testCase->proxies->proxy($this); $method->proxies->proxy($this); diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index f802bc11..1566b59c 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -183,7 +183,6 @@ final class Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { - // @phpstan-ignore-next-line Assert::assertStringContainsString((string) $needle, $this->value); } else { if (! is_iterable($this->value)) { diff --git a/src/Panic.php b/src/Panic.php index aca23b5e..a204472c 100644 --- a/src/Panic.php +++ b/src/Panic.php @@ -46,7 +46,7 @@ final readonly class Panic { try { $output = Container::getInstance()->get(OutputInterface::class); - } catch (Throwable) { // @phpstan-ignore-line + } catch (Throwable) { $output = new ConsoleOutput; } diff --git a/src/PendingCalls/DescribeCall.php b/src/PendingCalls/DescribeCall.php index b015595c..de472960 100644 --- a/src/PendingCalls/DescribeCall.php +++ b/src/PendingCalls/DescribeCall.php @@ -78,7 +78,7 @@ final class DescribeCall $this->currentBeforeEachCall->describing[] = $this->description; } - $this->currentBeforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line + $this->currentBeforeEachCall->{$name}(...$arguments); return $this; } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index a22ff11d..50fef3b6 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -224,7 +224,7 @@ final class TestCall // @phpstan-ignore-line */ public function only(): self { - Only::enable($this, ...func_get_args()); // @phpstan-ignore-line + Only::enable($this, ...func_get_args()); return $this; } diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 1c296fc9..7d318a2d 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -71,7 +71,7 @@ final class DatasetsRepository * * @throws ShouldNotHappen */ - public static function get(string $filename, string $description): Closure|array + public static function get(string $filename, string $description): Closure|array // @phpstan-ignore-line { $dataset = self::$withs[$filename.self::SEPARATOR.$description]; @@ -110,7 +110,6 @@ final class DatasetsRepository foreach ($datasetCombination as $datasetCombinationElement) { $partialDescriptions[] = $datasetCombinationElement['label']; - // @phpstan-ignore-next-line $values = array_merge($values, $datasetCombinationElement['values']); } @@ -221,7 +220,6 @@ final class DatasetsRepository $result = $tmp; } - // @phpstan-ignore-next-line return $result; } diff --git a/src/Support/Closure.php b/src/Support/Closure.php index e96ec29e..e447903f 100644 --- a/src/Support/Closure.php +++ b/src/Support/Closure.php @@ -15,7 +15,6 @@ final class Closure /** * Binds the given closure to the given "this". * - * * @throws ShouldNotHappen */ public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure @@ -24,6 +23,7 @@ final class Closure throw ShouldNotHappen::fromMessage('Could not bind null closure.'); } + // @phpstan-ignore-next-line $closure = BaseClosure::bind($closure, $newThis, $newScope); if (! $closure instanceof \Closure) { diff --git a/src/Support/Exporter.php b/src/Support/Exporter.php index a486445f..169f4891 100644 --- a/src/Support/Exporter.php +++ b/src/Support/Exporter.php @@ -66,6 +66,7 @@ final readonly class Exporter $result[] = $context->contains($data[$key]) !== false ? '*RECURSION*' + // @phpstan-ignore-next-line : sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); } diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index aefc356e..89c3e1f1 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -50,14 +50,13 @@ final class HigherOrderMessage } if ($this->hasHigherOrderCallable()) { - /* @phpstan-ignore-next-line */ return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments); } try { return is_array($this->arguments) ? Reflection::call($target, $this->name, $this->arguments) - : $target->{$this->name}; /* @phpstan-ignore-line */ + : $target->{$this->name}; } catch (Throwable $throwable) { Reflection::setPropertyValue($throwable, 'file', $this->filename); Reflection::setPropertyValue($throwable, 'line', $this->line); @@ -65,7 +64,6 @@ final class HigherOrderMessage if ($throwable->getMessage() === $this->getUndefinedMethodMessage($target, $this->name)) { /** @var ReflectionClass $reflection */ $reflection = new ReflectionClass($target); - /* @phpstan-ignore-next-line */ $reflection = $reflection->getParentClass() ?: $reflection; Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name)); } @@ -96,10 +94,6 @@ final class HigherOrderMessage private function getUndefinedMethodMessage(object $target, string $methodName): string { - if (\PHP_MAJOR_VERSION >= 8) { - return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)); - } - - return sprintf(self::UNDEFINED_METHOD, $methodName); + return sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)); } } diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php index da13a16c..41245108 100644 --- a/src/Support/HigherOrderMessageCollection.php +++ b/src/Support/HigherOrderMessageCollection.php @@ -40,7 +40,6 @@ final class HigherOrderMessageCollection public function chain(object $target): void { foreach ($this->messages as $message) { - // @phpstan-ignore-next-line $target = $message->call($target) ?? $target; } } diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php index 151b2b80..08eb5ea7 100644 --- a/src/Support/HigherOrderTapProxy.php +++ b/src/Support/HigherOrderTapProxy.php @@ -26,7 +26,7 @@ final class HigherOrderTapProxy */ public function __set(string $property, mixed $value): void { - $this->target->{$property} = $value; // @phpstan-ignore-line + $this->target->{$property} = $value; } /** @@ -37,7 +37,7 @@ final class HigherOrderTapProxy public function __get(string $property) { if (property_exists($this->target, $property)) { - return $this->target->{$property}; // @phpstan-ignore-line + return $this->target->{$property}; } $className = (new ReflectionClass($this->target))->getName(); From 003fc96e8f9f53c2930b0f21582c9a5757285ddc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 29 Mar 2025 17:48:00 +0000 Subject: [PATCH 19/76] release: 3.7.5 --- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Pest.php b/src/Pest.php index 11a52114..d8ac40bf 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.7.4'; + return '3.7.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 106d718c..5834ea3b 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 3.7.4. + Pest Testing Framework 3.7.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 63be8fd3..6e4c46c9 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 3.7.4. + Pest Testing Framework 3.7.5. From 4969526ef2841118aa9d8b13ae2dad40f3d91492 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 29 Mar 2025 17:57:53 +0000 Subject: [PATCH 20/76] chore: bumps paratest --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 778d391d..6b9856db 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "php": "^8.2.0", - "brianium/paratest": "^7.7.0", + "brianium/paratest": "^7.8.3", "nunomaduro/collision": "^8.7.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^3.0.0", From 2e11e9e65d71d73ce70dbfd73627627c50392122 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 29 Mar 2025 18:23:23 +0000 Subject: [PATCH 21/76] docs: adjusts readme --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f030829..e9f2faee 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,13 @@ **Pest** is an elegant PHP testing Framework with a focus on simplicity, meticulously designed to bring back the joy of testing in PHP. - Explore our docs at **[pestphp.com »](https://pestphp.com)** -- Follow us on Twitter at **[@pestphp »](https://twitter.com/pestphp)** -- Join us at **[discord.gg/kaHY6p54JH »](https://discord.gg/kaHY6p54JH)** or **[t.me/+kYH5G4d5MV83ODk0 »](https://t.me/+kYH5G4d5MV83ODk0)** +- Follow the creator Nuno Maduro: + - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday + - Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday + - Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)** + - LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)** + - Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)** + - Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)** ## Sponsors From 0171617c1db13a7a9cc8d3050b22593a3bab8180 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 30 Mar 2025 18:42:00 +0100 Subject: [PATCH 22/76] chore: adjusts to new types on arch --- composer.json | 2 +- src/Console/Thanks.php | 2 +- src/Expectations/OppositeExpectation.php | 145 ++++++++++++++++++----- 3 files changed, 118 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 6b9856db..001e31f2 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "nunomaduro/collision": "^8.7.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.0.0", + "pestphp/pest-plugin-arch": "^3.1.0", "pestphp/pest-plugin-mutate": "^3.0.5", "phpunit/phpunit": "^11.5.15" }, diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php index 332c311b..88965026 100644 --- a/src/Console/Thanks.php +++ b/src/Console/Thanks.php @@ -24,7 +24,7 @@ final readonly class Thanks */ private const FUNDING_MESSAGES = [ 'Star' => 'https://github.com/pestphp/pest', - 'News' => 'https://twitter.com/pestphp', + 'News' => 'https://x.com/enunomaduro', 'Videos' => 'https://youtube.com/@nunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro', ]; diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 59a25b4a..c4b0a0a5 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -74,7 +74,10 @@ final readonly class OppositeExpectation */ public function toUse(array|string $targets): ArchExpectation { - return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($this->original, $target)->opposite( + /** @var Expectation|string> $original */ + $original = $this->original; + + return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($original, $target)->opposite( fn () => $this->throwExpectationFailedException('toUse', $target), ), is_string($targets) ? [$targets] : $targets)); } @@ -84,8 +87,11 @@ final readonly class OppositeExpectation */ public function toHaveFileSystemPermissions(string $permissions): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => substr(sprintf('%o', fileperms($object->path)), -4) !== $permissions, sprintf('permissions not to be [%s]', $permissions), FileLineFinder::where(fn (string $line): bool => str_contains($line, '|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getMethodsFromReflectionClass($object->reflectionClass), @@ -124,8 +133,11 @@ final readonly class OppositeExpectation */ public function toHavePropertiesDocumented(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || array_filter( Reflection::getPropertiesFromReflectionClass($object->reflectionClass), @@ -144,8 +156,11 @@ final readonly class OppositeExpectation */ public function toUseStrictTypes(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)), 'not to use strict types', FileLineFinder::where(fn (string $line): bool => str_contains($line, '|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '), 'to use strict equality', FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')), @@ -170,8 +188,11 @@ final readonly class OppositeExpectation */ public function toBeFinal(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(), 'not to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -183,8 +204,11 @@ final readonly class OppositeExpectation */ public function toBeReadonly(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line 'not to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -196,8 +220,11 @@ final readonly class OppositeExpectation */ public function toBeTrait(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), 'not to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -217,8 +244,11 @@ final readonly class OppositeExpectation */ public function toBeAbstract(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), 'not to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -234,8 +264,11 @@ final readonly class OppositeExpectation { $methods = is_array($method) ? $method : [$method]; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => array_filter( $methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method), @@ -266,8 +299,11 @@ final readonly class OppositeExpectation $state = new stdClass; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC) @@ -309,8 +345,11 @@ final readonly class OppositeExpectation $state = new stdClass; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED) @@ -352,8 +391,11 @@ final readonly class OppositeExpectation $state = new stdClass; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, function (ObjectDescription $object) use ($methods, &$state): bool { $reflectionMethods = isset($object->reflectionClass) ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE) @@ -389,8 +431,11 @@ final readonly class OppositeExpectation */ public function toBeEnum(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), 'not to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -410,8 +455,11 @@ final readonly class OppositeExpectation */ public function toBeClass(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! class_exists($object->name), 'not to be class', FileLineFinder::where(fn (string $line): bool => true), @@ -431,8 +479,11 @@ final readonly class OppositeExpectation */ public function toBeInterface(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), 'not to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -452,8 +503,11 @@ final readonly class OppositeExpectation */ public function toExtend(string $class): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -465,8 +519,11 @@ final readonly class OppositeExpectation */ public function toExtendNothing(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, 'to extend a class', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -490,8 +547,11 @@ final readonly class OppositeExpectation { $traits = is_array($traits) ? $traits : [$traits]; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) { @@ -515,8 +575,11 @@ final readonly class OppositeExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if ($object->reflectionClass->implementsInterface($interface)) { @@ -536,8 +599,11 @@ final readonly class OppositeExpectation */ public function toImplementNothing(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], 'to implement an interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -557,8 +623,11 @@ final readonly class OppositeExpectation */ public function toHavePrefix(string $prefix): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix), "not to have prefix '{$prefix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -570,8 +639,11 @@ final readonly class OppositeExpectation */ public function toHaveSuffix(string $suffix): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix), "not to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -599,7 +671,10 @@ final readonly class OppositeExpectation */ public function toBeUsed(): ArchExpectation { - return ToBeUsedInNothing::make($this->original); + /** @var Expectation|string> $original */ + $original = $this->original; + + return ToBeUsedInNothing::make($original); } /** @@ -609,7 +684,10 @@ final readonly class OppositeExpectation */ public function toBeUsedIn(array|string $targets): ArchExpectation { - return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($this->original, $target)->opposite( + /** @var Expectation|string> $original */ + $original = $this->original; + + return GroupArchExpectation::fromExpectations($original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedIn::make($original, $target)->opposite( fn () => $this->throwExpectationFailedException('toBeUsedIn', $target), ), is_string($targets) ? [$targets] : $targets)); } @@ -632,8 +710,11 @@ final readonly class OppositeExpectation */ public function toBeInvokable(): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'), 'to not be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) @@ -645,8 +726,11 @@ final readonly class OppositeExpectation */ public function toHaveAttribute(string $attribute): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [], "to not have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) @@ -737,8 +821,11 @@ final readonly class OppositeExpectation */ private function toBeBackedEnum(string $backingType): ArchExpectation { + /** @var Expectation|string> $original */ + $original = $this->original; + return Targeted::make( - $this->original, + $original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum() || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line From 42e1b9f17fc2b2036701f4b968158264bde542d4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 30 Mar 2025 18:49:10 +0100 Subject: [PATCH 23/76] release: v3.8.0 --- src/Console/Thanks.php | 8 ++++++-- src/Pest.php | 2 +- .../Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Version/visual_snapshot_of_help_command_output.snap | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php index 88965026..7e68f871 100644 --- a/src/Console/Thanks.php +++ b/src/Console/Thanks.php @@ -24,8 +24,12 @@ final readonly class Thanks */ private const FUNDING_MESSAGES = [ 'Star' => 'https://github.com/pestphp/pest', - 'News' => 'https://x.com/enunomaduro', - 'Videos' => 'https://youtube.com/@nunomaduro', + 'YouTube' => 'https://youtube.com/@nunomaduro', + 'TikTok' => 'https://tiktok.com/@nunomaduro', + 'Twitch' => 'https://twitch.tv/enunomaduro', + 'LinkedIn' => 'https://linkedin.com/in/nunomaduro', + 'Instagram' => 'https://instagram.com/enunomaduro', + 'X' => 'https://x.com/enunomaduro', 'Sponsor' => 'https://github.com/sponsors/nunomaduro', ]; diff --git a/src/Pest.php b/src/Pest.php index d8ac40bf..87cf2d9d 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.7.5'; + return '3.8.0'; } 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 5834ea3b..d75eb54e 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 3.7.5. + Pest Testing Framework 3.8.0. 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 6e4c46c9..2b7edc3b 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 3.7.5. + Pest Testing Framework 3.8.0. From e0f07be017a7b549c8aa78b497985b66c7085cca Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 3 Apr 2025 17:23:39 +0100 Subject: [PATCH 24/76] fix: `init` command detecting laravel --- src/Plugins/Init.php | 2 +- stubs/init-laravel/Pest.php.stub | 2 +- stubs/init-laravel/phpunit.xml.stub | 20 +++++++++++--------- stubs/init/Pest.php.stub | 2 +- stubs/init/phpunit.xml.stub | 6 +++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Plugins/Init.php b/src/Plugins/Init.php index 6bb92365..eb87b086 100644 --- a/src/Plugins/Init.php +++ b/src/Plugins/Init.php @@ -119,6 +119,6 @@ final readonly class Init implements HandlesArguments */ private function isLaravelInstalled(): bool { - return InstalledVersions::isInstalled('laravel/laravel'); + return InstalledVersions::isInstalled('laravel/framework'); } } diff --git a/stubs/init-laravel/Pest.php.stub b/stubs/init-laravel/Pest.php.stub index 40d096b5..60f04a45 100644 --- a/stubs/init-laravel/Pest.php.stub +++ b/stubs/init-laravel/Pest.php.stub @@ -12,7 +12,7 @@ */ pest()->extend(Tests\TestCase::class) - ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); /* diff --git a/stubs/init-laravel/phpunit.xml.stub b/stubs/init-laravel/phpunit.xml.stub index 83ba26be..506b9a38 100644 --- a/stubs/init-laravel/phpunit.xml.stub +++ b/stubs/init-laravel/phpunit.xml.stub @@ -1,31 +1,33 @@ - ./tests/Unit + tests/Unit - ./tests/Feature + tests/Feature + + + app + + + - + + - - - ./app - - diff --git a/stubs/init/Pest.php.stub b/stubs/init/Pest.php.stub index fd279ada..b239048c 100644 --- a/stubs/init/Pest.php.stub +++ b/stubs/init/Pest.php.stub @@ -11,7 +11,7 @@ | */ -// pest()->extend(Tests\TestCase::class)->in('Feature'); +pest()->extend(Tests\TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- diff --git a/stubs/init/phpunit.xml.stub b/stubs/init/phpunit.xml.stub index 7d0904f7..e6198e0e 100644 --- a/stubs/init/phpunit.xml.stub +++ b/stubs/init/phpunit.xml.stub @@ -1,6 +1,6 @@ @@ -11,8 +11,8 @@ - ./app - ./src + app + src From 6080f51a0b0830715c48ba0e7458b06907febfe5 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 3 Apr 2025 17:35:58 +0100 Subject: [PATCH 25/76] release: v3.8.1 --- composer.json | 2 +- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 2 +- .../Visual/Version/visual_snapshot_of_help_command_output.snap | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 001e31f2..a4f0f21d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "^8.2.0", "brianium/paratest": "^7.8.3", - "nunomaduro/collision": "^8.7.0", + "nunomaduro/collision": "^8.8.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin-arch": "^3.1.0", diff --git a/src/Pest.php b/src/Pest.php index 87cf2d9d..077a04c2 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.8.0'; + return '3.8.1'; } 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 d75eb54e..5646dca8 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 3.8.0. + Pest Testing Framework 3.8.1. 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 2b7edc3b..7775b987 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 3.8.0. + Pest Testing Framework 3.8.1. From eed68f28403913baef9b27b071c72e390697dec4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 13 Apr 2025 17:15:23 +0100 Subject: [PATCH 26/76] Adjusts sponsors --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e9f2faee..4a11f1a7 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,16 @@ We cannot thank our sponsors enough for their incredible support in funding Pest ### Gold Sponsors -- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** -- **[LaraJobs](https://larajobs.com/?ref=pestphp)** - **[Brokerchooser](https://brokerchooser.com/?ref=pestphp)** -- **[Forge](https://forge.laravel.com/?ref=pestphp)** +- **[CodeRabbit](https://coderabbit.ai/?ref=pestphp)** +- **[NativePHP](https://nativephp.com/mobile?ref=pestphp.com)** ### Premium Sponsors - [Akaunting](https://akaunting.com/?ref=pestphp) -- [Codecourse](https://codecourse.com/?ref=pestphp) - [DocuWriter.ai](https://www.docuwriter.ai/?ref=pestphp) - [Localazy](https://localazy.com/?ref=pestphp) +- [Forge](https://forge.laravel.com/?ref=pestphp) - [Route4Me](https://www.route4me.com/?ref=pestphp) - [Spatie](https://spatie.be/?ref=pestphp) - [Worksome](https://www.worksome.com/?ref=pestphp) From c6244a8712968dbac88eb998e7ff3b5caa556b0d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 17 Apr 2025 11:52:59 +0100 Subject: [PATCH 27/76] Release 3.8.2 --- src/Expectation.php | 38 +++++++++++-------- src/Expectations/OppositeExpectation.php | 35 ++++++++--------- src/Pest.php | 2 +- ...isual_snapshot_of_help_command_output.snap | 2 +- ...isual_snapshot_of_help_command_output.snap | 2 +- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index ebfd6302..1bef5a8c 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -535,7 +535,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isFinal(), + fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isFinal(), 'to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -548,7 +548,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => ! enum_exists($object->name) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line + fn (ObjectDescription $object): bool => ! enum_exists($object->name) && isset($object->reflectionClass) && $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line 'to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -561,7 +561,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isTrait(), 'to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -582,7 +582,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isAbstract(), 'to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -599,7 +599,7 @@ final class Expectation return Targeted::make( $this, - fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => $object->reflectionClass->hasMethod($method))) === count($methods), + fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods), sprintf("to have method '%s'", implode("', '", $methods)), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -670,7 +670,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isEnum(), 'to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -712,7 +712,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->isInterface(), 'to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -733,7 +733,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)), sprintf("to extend '%s'", $class), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -773,6 +773,10 @@ final class Expectation $this, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { + if (isset($object->reflectionClass) === false) { + return false; + } + if (! in_array($trait, $object->reflectionClass->getTraitNames(), true)) { return false; } @@ -792,7 +796,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getInterfaceNames() === [], 'to implement nothing', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -809,7 +813,8 @@ final class Expectation return Targeted::make( $this, - fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) + fn (ObjectDescription $object): bool => isset($object->reflectionClass) + && (count($interfaces) === count($object->reflectionClass->getInterfaceNames())) && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], "to only implement '".implode("', '", $interfaces)."'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -823,7 +828,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getShortName(), $prefix), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix), "to have prefix '{$prefix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -836,7 +841,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix), "to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -855,7 +860,7 @@ final class Expectation $this, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { - if (! $object->reflectionClass->implementsInterface($interface)) { + if (! isset($object->reflectionClass) || ! $object->reflectionClass->implementsInterface($interface)) { return false; } } @@ -928,7 +933,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->hasMethod('__invoke'), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod('__invoke'), 'to be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); @@ -1037,7 +1042,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) !== [], + fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [], "to have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -1066,7 +1071,8 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum() + fn (ObjectDescription $object): bool => isset($object->reflectionClass) + && $object->reflectionClass->isEnum() && (new ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line && (string) (new ReflectionEnum($object->name))->getBackingType() === $backingType, // @phpstan-ignore-line 'to be '.$backingType.' backed enum', diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index c4b0a0a5..d5c3f083 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -193,7 +193,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isFinal(), + fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isFinal()), 'not to be final', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -209,7 +209,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! enum_exists($object->name) && ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line + fn (ObjectDescription $object): bool => ! enum_exists($object->name) && (isset($object->reflectionClass) === false || ! $object->reflectionClass->isReadOnly()) && assert(true), // @phpstan-ignore-line 'not to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -225,7 +225,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isTrait(), 'not to be trait', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -249,7 +249,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isAbstract(), 'not to be abstract', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -271,7 +271,7 @@ final readonly class OppositeExpectation $original, fn (ObjectDescription $object): bool => array_filter( $methods, - fn (string $method): bool => $object->reflectionClass->hasMethod($method), + fn (string $method): bool => isset($object->reflectionClass) === false || $object->reflectionClass->hasMethod($method), ) === [], 'to not have methods: '.implode(', ', $methods), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), @@ -436,7 +436,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isEnum(), 'not to be enum', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -484,7 +484,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isInterface(), 'not to be interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -508,7 +508,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -524,7 +524,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getParentClass() !== false, 'to extend a class', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -554,7 +554,7 @@ final readonly class OppositeExpectation $original, function (ObjectDescription $object) use ($traits): bool { foreach ($traits as $trait) { - if (in_array($trait, $object->reflectionClass->getTraitNames(), true)) { + if (isset($object->reflectionClass) && in_array($trait, $object->reflectionClass->getTraitNames(), true)) { return false; } } @@ -582,7 +582,7 @@ final readonly class OppositeExpectation $original, function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { - if ($object->reflectionClass->implementsInterface($interface)) { + if (isset($object->reflectionClass) && $object->reflectionClass->implementsInterface($interface)) { return false; } } @@ -604,7 +604,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getInterfaceNames() !== [], 'to implement an interface', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -628,7 +628,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! str_starts_with($object->reflectionClass->getShortName(), $prefix), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix), "not to have prefix '{$prefix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -644,7 +644,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! str_ends_with($object->reflectionClass->getName(), $suffix), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix), "not to have suffix '{$suffix}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); @@ -715,7 +715,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->hasMethod('__invoke'), + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->hasMethod('__invoke'), 'to not be invokable', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); @@ -731,7 +731,7 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => $object->reflectionClass->getAttributes($attribute) === [], + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [], "to not have attribute '{$attribute}'", FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) ); @@ -826,7 +826,8 @@ final readonly class OppositeExpectation return Targeted::make( $original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum() + fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false + || ! $object->reflectionClass->isEnum() || ! (new \ReflectionEnum($object->name))->isBacked() // @phpstan-ignore-line || (string) (new \ReflectionEnum($object->name))->getBackingType() !== $backingType, // @phpstan-ignore-line 'not to be '.$backingType.' backed enum', diff --git a/src/Pest.php b/src/Pest.php index 077a04c2..db27e206 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.8.1'; + return '3.8.2'; } 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 5646dca8..da7acad7 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 3.8.1. + Pest Testing Framework 3.8.2. 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 7775b987..cd2d2cfb 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 3.8.1. + Pest Testing Framework 3.8.2. From 19e9267021b811d30cbfe53737d78dfff848ad56 Mon Sep 17 00:00:00 2001 From: Punyapal Shah Date: Sun, 20 Apr 2025 15:19:40 +0530 Subject: [PATCH 28/76] fix: update PHPUnit version --- composer.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/composer.json b/composer.json index a4f0f21d..be8a89a9 100644 --- a/composer.json +++ b/composer.json @@ -22,13 +22,11 @@ "nunomaduro/collision": "^8.8.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.1.0", "pestphp/pest-plugin-mutate": "^3.0.5", - "phpunit/phpunit": "^11.5.15" + "phpunit/phpunit": "12.1.0" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.15", "sebastian/exporter": "<6.0.0", "webmozart/assert": "<1.11.0" }, @@ -53,8 +51,6 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^3.4.0", - "pestphp/pest-plugin-type-coverage": "^3.5.0", "symfony/process": "^7.2.5" }, "minimum-stability": "dev", From 442a58d07f0c892d0c4219d4690e437098e98adc Mon Sep 17 00:00:00 2001 From: Punyapal Shah Date: Sun, 20 Apr 2025 15:19:56 +0530 Subject: [PATCH 29/76] refactor: comment arch presets in Arch.php --- tests/Arch.php | 98 +++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/tests/Arch.php b/tests/Arch.php index d8deb460..4dcc983e 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -2,57 +2,57 @@ use Pest\Expectation; -arch()->preset()->php()->ignoring([ - Expectation::class, - 'debug_backtrace', - 'var_export', - 'xdebug_info', -]); +// arch()->preset()->php()->ignoring([ +// Expectation::class, +// 'debug_backtrace', +// 'var_export', +// 'xdebug_info', +// ]); -arch()->preset()->strict()->ignoring([ - 'usleep', -]); +// arch()->preset()->strict()->ignoring([ +// 'usleep', +// ]); -arch()->preset()->security()->ignoring([ - 'eval', - 'str_shuffle', - 'exec', - 'unserialize', - 'extract', - 'assert', -]); +// arch()->preset()->security()->ignoring([ +// 'eval', +// 'str_shuffle', +// 'exec', +// 'unserialize', +// 'extract', +// 'assert', +// ]); -arch('globals') - ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) - ->not->toBeUsed() - ->ignoring(Expectation::class); +// arch('globals') +// ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) +// ->not->toBeUsed() +// ->ignoring(Expectation::class); -arch('dependencies') - ->expect('Pest') - ->toOnlyUse([ - 'dd', - 'dump', - 'expect', - 'uses', - 'Termwind', - 'ParaTest', - 'Pest\Arch', - 'Pest\Mutate\Contracts\Configuration', - 'Pest\Mutate\Decorators\TestCallDecorator', - 'Pest\Mutate\Repositories\ConfigurationRepository', - 'Pest\Plugin', - 'NunoMaduro\Collision', - 'Whoops', - 'Symfony\Component\Console', - 'Symfony\Component\Process', - ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); +// arch('dependencies') +// ->expect('Pest') +// ->toOnlyUse([ +// 'dd', +// 'dump', +// 'expect', +// 'uses', +// 'Termwind', +// 'ParaTest', +// 'Pest\Arch', +// 'Pest\Mutate\Contracts\Configuration', +// 'Pest\Mutate\Decorators\TestCallDecorator', +// 'Pest\Mutate\Repositories\ConfigurationRepository', +// 'Pest\Plugin', +// 'NunoMaduro\Collision', +// 'Whoops', +// 'Symfony\Component\Console', +// 'Symfony\Component\Process', +// ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); -arch('contracts') - ->expect('Pest\Contracts') - ->toOnlyUse([ - 'NunoMaduro\Collision\Contracts', - 'Pest\Factories\TestCaseMethodFactory', - 'Symfony\Component\Console', - 'Pest\Arch\Contracts', - 'Pest\PendingCalls', - ])->toBeInterfaces(); +// arch('contracts') +// ->expect('Pest\Contracts') +// ->toOnlyUse([ +// 'NunoMaduro\Collision\Contracts', +// 'Pest\Factories\TestCaseMethodFactory', +// 'Symfony\Component\Console', +// 'Pest\Arch\Contracts', +// 'Pest\PendingCalls', +// ])->toBeInterfaces(); From 4f6140fdb176b38e5773954334e3ba707253da10 Mon Sep 17 00:00:00 2001 From: Punyapal Shah Date: Sun, 20 Apr 2025 15:37:02 +0530 Subject: [PATCH 30/76] refactor: move test case initialization to a separate method in Testable trait --- src/Concerns/Testable.php | 49 ++++++++++++++++++++++----------------- tests/Unit/Preset.php | 2 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 37d3b175..7199ad5b 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -101,27 +101,6 @@ trait Testable */ private array $__snapshotChanges = []; - /** - * Creates a new Test Case instance. - */ - public function __construct(string $name) - { - parent::__construct($name); - - $test = TestSuite::getInstance()->tests->get(self::$__filename); - - if ($test->hasMethod($name)) { - $method = $test->getMethod($name); - $this->__description = self::$__latestDescription = $method->description; - self::$__latestAssignees = $method->assignees; - self::$__latestNotes = $method->notes; - self::$__latestIssues = $method->issues; - self::$__latestPrs = $method->prs; - $this->__describing = $method->describing; - $this->__test = $method->getClosure(); - } - } - /** * Resets the test case static properties. */ @@ -240,6 +219,9 @@ trait Testable { TestSuite::getInstance()->test = $this; + // Initialize test case properties + $this->__initializeTestCase(); + $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method->setUp($this); @@ -285,6 +267,31 @@ trait Testable $this->__callClosure($beforeEach, $arguments); } + /** + * Initialize test case properties from TestSuite. + */ + private function __initializeTestCase(): void + { + // Return if the test case has already been initialized + if (isset($this->__test)) { + return; + } + + $name = $this->name(); + $test = TestSuite::getInstance()->tests->get(self::$__filename); + + if ($test->hasMethod($name)) { + $method = $test->getMethod($name); + $this->__description = self::$__latestDescription = $method->description; + self::$__latestAssignees = $method->assignees; + self::$__latestNotes = $method->notes; + self::$__latestIssues = $method->issues; + self::$__latestPrs = $method->prs; + $this->__describing = $method->describing; + $this->__test = $method->getClosure(); + } + } + /** * Gets executed after the Test Case. */ diff --git a/tests/Unit/Preset.php b/tests/Unit/Preset.php index bdf461a7..e777982b 100644 --- a/tests/Unit/Preset.php +++ b/tests/Unit/Preset.php @@ -10,4 +10,4 @@ test('preset invalid name', function () { $this->preset()->myAnotherFramework(); })->throws(InvalidArgumentException::class, 'The preset [myAnotherFramework] does not exist. The available presets are [php, laravel, strict, security, relaxed, myFramework].'); -arch()->preset()->myFramework(); +// arch()->preset()->myFramework(); From d1608bf33d0f9945dd6329775bdf5655dd595e6d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 20 Apr 2025 21:33:50 +0100 Subject: [PATCH 31/76] chore: prepares for 4.x --- composer.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index a4f0f21d..8f6c9c58 100644 --- a/composer.json +++ b/composer.json @@ -17,19 +17,19 @@ } ], "require": { - "php": "^8.2.0", + "php": "^8.3.0", "brianium/paratest": "^7.8.3", "nunomaduro/collision": "^8.8.0", "nunomaduro/termwind": "^2.3.0", - "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.1.0", - "pestphp/pest-plugin-mutate": "^3.0.5", - "phpunit/phpunit": "^11.5.15" + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.0", + "phpunit/phpunit": "^12.1.2" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.15", - "sebastian/exporter": "<6.0.0", + "phpunit/phpunit": ">12.1.2", + "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "autoload": { @@ -53,8 +53,8 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^3.4.0", - "pestphp/pest-plugin-type-coverage": "^3.5.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0", "symfony/process": "^7.2.5" }, "minimum-stability": "dev", From 8cfb0acf46e10a3468841027ab1d7e028866c73d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 20 Apr 2025 21:50:56 +0100 Subject: [PATCH 32/76] bump paratest --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8f6c9c58..62e09636 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.8.3", + "brianium/paratest": "^7.9.1", "nunomaduro/collision": "^8.8.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^4.0.0", From 791734a29cf597beec9d44309fcf6f7a83b9fa97 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 20 Apr 2025 22:19:25 +0100 Subject: [PATCH 33/76] Fixes tests --- composer.json | 10 +- src/Pest.php | 2 +- .../Parallel/Paratest/WrapperRunner.php | 5 + .../Parallel/Support/CompactPrinter.php | 16 +-- ...isual_snapshot_of_help_command_output.snap | 4 +- ...isual_snapshot_of_help_command_output.snap | 2 +- tests/Arch.php | 98 +++++++++---------- tests/Unit/Preset.php | 2 +- 8 files changed, 73 insertions(+), 66 deletions(-) diff --git a/composer.json b/composer.json index 62e09636..d2a1461f 100644 --- a/composer.json +++ b/composer.json @@ -76,11 +76,11 @@ "test:lint": "pint --test", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", - "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", - "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", - "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=3", - "test:integration": "php bin/pest --colors=always --group=integration -v", - "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", + "test:unit": "php bin/pest --exclude-group=integration --compact", + "test:inline": "php bin/pest --configuration=phpunit.inline.xml", + "test:parallel": "php bin/pest --exclude-group=integration --parallel --processes=3", + "test:integration": "php bin/pest --group=integration -v", + "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --update-snapshots", "test": [ "@test:refacto", "@test:lint", diff --git a/src/Pest.php b/src/Pest.php index db27e206..d23cfd5c 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '3.8.2'; + return '4.0.0-alpha.1'; } function testDirectory(string $file = ''): string diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 282749d3..48c88e7b 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -313,6 +313,7 @@ final class WrapperRunner implements RunnerInterface $testResult = unserialize($contents); assert($testResult instanceof TestResult); + $testResultSum = new TestResult( (int) $testResultSum->hasTests() + (int) $testResult->hasTests(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), @@ -325,8 +326,10 @@ final class WrapperRunner implements RunnerInterface array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), + array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), + array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()), array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), array_merge_recursive($testResultSum->errors(), $testResult->errors()), array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), @@ -351,8 +354,10 @@ final class WrapperRunner implements RunnerInterface $testResultSum->testMarkedIncompleteEvents(), $testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResultSum->testTriggeredPhpunitErrorEvents(), + $testResultSum->testTriggeredPhpunitNoticeEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(), + $testResultSum->testRunnerTriggeredNoticeEvents(), array_values(array_filter( $testResultSum->testRunnerTriggeredWarningEvents(), fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found') diff --git a/src/Plugins/Parallel/Support/CompactPrinter.php b/src/Plugins/Parallel/Support/CompactPrinter.php index 25226b10..bc2e1c3f 100644 --- a/src/Plugins/Parallel/Support/CompactPrinter.php +++ b/src/Plugins/Parallel/Support/CompactPrinter.php @@ -131,14 +131,14 @@ final class CompactPrinter $status['collected'], $status['threshold'], $status['roots'], - null, - null, - null, - null, - null, - null, - null, - null, + 0.00, + 0.00, + 0.00, + 0.00, + false, + false, + false, + 0, ); $telemetry = new Info( 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 da7acad7..9966d901 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 3.8.2. + Pest Testing Framework 4.0.0-alpha.1. USAGE: pest [options] @@ -68,6 +68,7 @@ --fail-on-risky Signal failure using shell exit code when a test was considered risky --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-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 @@ -88,6 +89,7 @@ --display-skipped ........................ Display details for skipped tests --display-deprecations . Display details for deprecations triggered by tests --display-phpunit-deprecations .... Display details for PHPUnit deprecations + --display-phpunit-notices .............. Display details for PHPUnit notices --display-errors ............. Display details for errors triggered by tests --display-notices ........... Display details for notices triggered by tests --display-warnings ......... Display details for warnings triggered by tests 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 cd2d2cfb..c9dea0d5 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 3.8.2. + Pest Testing Framework 4.0.0-alpha.1. diff --git a/tests/Arch.php b/tests/Arch.php index 4dcc983e..d8deb460 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -2,57 +2,57 @@ use Pest\Expectation; -// arch()->preset()->php()->ignoring([ -// Expectation::class, -// 'debug_backtrace', -// 'var_export', -// 'xdebug_info', -// ]); +arch()->preset()->php()->ignoring([ + Expectation::class, + 'debug_backtrace', + 'var_export', + 'xdebug_info', +]); -// arch()->preset()->strict()->ignoring([ -// 'usleep', -// ]); +arch()->preset()->strict()->ignoring([ + 'usleep', +]); -// arch()->preset()->security()->ignoring([ -// 'eval', -// 'str_shuffle', -// 'exec', -// 'unserialize', -// 'extract', -// 'assert', -// ]); +arch()->preset()->security()->ignoring([ + 'eval', + 'str_shuffle', + 'exec', + 'unserialize', + 'extract', + 'assert', +]); -// arch('globals') -// ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) -// ->not->toBeUsed() -// ->ignoring(Expectation::class); +arch('globals') + ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep']) + ->not->toBeUsed() + ->ignoring(Expectation::class); -// arch('dependencies') -// ->expect('Pest') -// ->toOnlyUse([ -// 'dd', -// 'dump', -// 'expect', -// 'uses', -// 'Termwind', -// 'ParaTest', -// 'Pest\Arch', -// 'Pest\Mutate\Contracts\Configuration', -// 'Pest\Mutate\Decorators\TestCallDecorator', -// 'Pest\Mutate\Repositories\ConfigurationRepository', -// 'Pest\Plugin', -// 'NunoMaduro\Collision', -// 'Whoops', -// 'Symfony\Component\Console', -// 'Symfony\Component\Process', -// ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); +arch('dependencies') + ->expect('Pest') + ->toOnlyUse([ + 'dd', + 'dump', + 'expect', + 'uses', + 'Termwind', + 'ParaTest', + 'Pest\Arch', + 'Pest\Mutate\Contracts\Configuration', + 'Pest\Mutate\Decorators\TestCallDecorator', + 'Pest\Mutate\Repositories\ConfigurationRepository', + 'Pest\Plugin', + 'NunoMaduro\Collision', + 'Whoops', + 'Symfony\Component\Console', + 'Symfony\Component\Process', + ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); -// arch('contracts') -// ->expect('Pest\Contracts') -// ->toOnlyUse([ -// 'NunoMaduro\Collision\Contracts', -// 'Pest\Factories\TestCaseMethodFactory', -// 'Symfony\Component\Console', -// 'Pest\Arch\Contracts', -// 'Pest\PendingCalls', -// ])->toBeInterfaces(); +arch('contracts') + ->expect('Pest\Contracts') + ->toOnlyUse([ + 'NunoMaduro\Collision\Contracts', + 'Pest\Factories\TestCaseMethodFactory', + 'Symfony\Component\Console', + 'Pest\Arch\Contracts', + 'Pest\PendingCalls', + ])->toBeInterfaces(); diff --git a/tests/Unit/Preset.php b/tests/Unit/Preset.php index e777982b..bdf461a7 100644 --- a/tests/Unit/Preset.php +++ b/tests/Unit/Preset.php @@ -10,4 +10,4 @@ test('preset invalid name', function () { $this->preset()->myAnotherFramework(); })->throws(InvalidArgumentException::class, 'The preset [myAnotherFramework] does not exist. The available presets are [php, laravel, strict, security, relaxed, myFramework].'); -// arch()->preset()->myFramework(); +arch()->preset()->myFramework(); From 635e3b4c41e47320354f0564b4ea948fcc79df29 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 20 Apr 2025 23:02:19 +0100 Subject: [PATCH 34/76] chore: deprecates php 8.2 --- .github/workflows/static.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 3373eafb..69dd6b98 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -24,7 +24,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 tools: composer:v2 coverage: none diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c4902b42..abafbbbb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] symfony: ['7.1'] - php: ['8.2', '8.3', '8.4'] + php: ['8.3', '8.4'] dependency_version: [prefer-lowest, prefer-stable] name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} From db9243ca2ebefcd38dbba5b7ce0f3466efe8369d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 29 Apr 2025 09:57:02 +0100 Subject: [PATCH 35/76] bump dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d2a1461f..f4617a5b 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,11 @@ "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", - "phpunit/phpunit": "^12.1.2" + "phpunit/phpunit": "^12.1.3" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.1.2", + "phpunit/phpunit": ">12.1.3", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, From a3107961655d3d1bb3e8ba44568f3549a053999c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 29 Apr 2025 11:38:33 +0100 Subject: [PATCH 36/76] Fixes filtering tests --- overrides/TextUI/TestSuiteFilterProcessor.php | 9 +++++ src/Bootstrappers/BootExcludeList.php | 2 +- src/Bootstrappers/BootFiles.php | 2 +- src/Bootstrappers/BootOverrides.php | 2 +- src/Bootstrappers/BootSubscribers.php | 2 +- src/Concerns/Testable.php | 5 +-- src/Console/Help.php | 2 +- src/Console/Thanks.php | 2 +- src/Kernel.php | 2 +- src/Logging/Converter.php | 2 +- src/Plugins/Cache.php | 2 +- src/Plugins/Configuration.php | 2 +- src/Plugins/Coverage.php | 15 ++----- src/Plugins/Environment.php | 4 +- src/Plugins/Init.php | 4 +- src/Plugins/Only.php | 2 +- src/Plugins/Parallel.php | 6 +-- src/Plugins/Parallel/Handlers/Parallel.php | 2 +- .../Parallel/Paratest/CleanConsoleOutput.php | 1 + .../Parallel/Paratest/WrapperRunner.php | 13 ++++++- .../Parallel/Support/CompactPrinter.php | 2 +- src/Plugins/Verbose.php | 2 +- src/Repositories/DatasetsRepository.php | 2 +- src/Result.php | 6 +-- .../EnsureTestCaseIsInitiatedFilter.php | 39 +++++++++++++++++++ src/Support/Backtrace.php | 7 +--- src/Support/DatasetInfo.php | 4 +- src/Support/ExceptionTrace.php | 2 +- src/Support/Exporter.php | 2 +- src/Support/HigherOrderMessage.php | 2 +- src/Support/Str.php | 7 +--- 31 files changed, 98 insertions(+), 58 deletions(-) create mode 100644 src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php diff --git a/overrides/TextUI/TestSuiteFilterProcessor.php b/overrides/TextUI/TestSuiteFilterProcessor.php index 536ab208..e13d5c98 100644 --- a/overrides/TextUI/TestSuiteFilterProcessor.php +++ b/overrides/TextUI/TestSuiteFilterProcessor.php @@ -45,6 +45,7 @@ declare(strict_types=1); namespace PHPUnit\TextUI; use Pest\Plugins\Only; +use Pest\Runner\Filter\EnsureTestCaseIsInitiatedFilter; use PHPUnit\Event; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\Filter\Factory; @@ -66,6 +67,12 @@ final readonly class TestSuiteFilterProcessor { $factory = new Factory; + // @phpstan-ignore-next-line + (fn () => $this->filters[] = [ + 'className' => EnsureTestCaseIsInitiatedFilter::class, + 'argument' => '', + ])->call($factory); + if (! $configuration->hasFilter() && ! $configuration->hasGroups() && ! $configuration->hasExcludeGroups() && @@ -73,6 +80,8 @@ final readonly class TestSuiteFilterProcessor ! $configuration->hasTestsCovering() && ! $configuration->hasTestsUsing() && ! Only::isEnabled()) { + $suite->injectFilter($factory); + return; } diff --git a/src/Bootstrappers/BootExcludeList.php b/src/Bootstrappers/BootExcludeList.php index abd1552c..69d9dce1 100644 --- a/src/Bootstrappers/BootExcludeList.php +++ b/src/Bootstrappers/BootExcludeList.php @@ -17,7 +17,7 @@ final class BootExcludeList implements Bootstrapper * * @var array */ - private const EXCLUDE_LIST = [ + private const array EXCLUDE_LIST = [ 'bin', 'overrides', 'resources', diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 2017a796..ea7e60fa 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -24,7 +24,7 @@ final class BootFiles implements Bootstrapper * * @var array */ - private const STRUCTURE = [ + private const array STRUCTURE = [ 'Expectations', 'Expectations.php', 'Helpers', diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php index efbcf7a3..ae2fa070 100644 --- a/src/Bootstrappers/BootOverrides.php +++ b/src/Bootstrappers/BootOverrides.php @@ -17,7 +17,7 @@ final class BootOverrides implements Bootstrapper * * @var array */ - public const FILES = [ + public const array FILES = [ '53c246e5f416a39817ac81124cdd64ea8403038d01d7a202e1ffa486fbdf3fa7' => 'Runner/Filter/NameFilterIterator.php', '77ffb7647b583bd82e37962c6fbdc4b04d3344d8a2c1ed103e625ed1ff7cb5c2' => 'Runner/ResultCache/DefaultResultCache.php', 'd0e81317889ad88c707db4b08a94cadee4c9010d05ff0a759f04e71af5efed89' => 'Runner/TestSuiteLoader.php', diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 57f98e33..7877b237 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -20,7 +20,7 @@ final readonly class BootSubscribers implements Bootstrapper * * @var array> */ - private const SUBSCRIBERS = [ + private const array SUBSCRIBERS = [ Subscribers\EnsureConfigurationIsAvailable::class, Subscribers\EnsureIgnorableTestCasesAreIgnored::class, Subscribers\EnsureKernelDumpIsFlushed::class, diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 7199ad5b..9b8dc5f9 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -219,9 +219,6 @@ trait Testable { TestSuite::getInstance()->test = $this; - // Initialize test case properties - $this->__initializeTestCase(); - $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); $method->setUp($this); @@ -270,7 +267,7 @@ trait Testable /** * Initialize test case properties from TestSuite. */ - private function __initializeTestCase(): void + public function __initializeTestCase(): void { // Return if the test case has already been initialized if (isset($this->__test)) { diff --git a/src/Console/Help.php b/src/Console/Help.php index 3d09d5f5..50823d59 100644 --- a/src/Console/Help.php +++ b/src/Console/Help.php @@ -16,7 +16,7 @@ final readonly class Help * * @var array */ - private const HELP_MESSAGES = [ + private const array HELP_MESSAGES = [ 'Pest Options:', ' --init Initialise a standard Pest configuration', ' --coverage Enable coverage and output to standard output', diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php index 7e68f871..fc9f558a 100644 --- a/src/Console/Thanks.php +++ b/src/Console/Thanks.php @@ -22,7 +22,7 @@ final readonly class Thanks * * @var array */ - private const FUNDING_MESSAGES = [ + private const array FUNDING_MESSAGES = [ 'Star' => 'https://github.com/pestphp/pest', 'YouTube' => 'https://youtube.com/@nunomaduro', 'TikTok' => 'https://tiktok.com/@nunomaduro', diff --git a/src/Kernel.php b/src/Kernel.php index fc82574c..92716913 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -34,7 +34,7 @@ final readonly class Kernel * * @var array */ - private const BOOTSTRAPPERS = [ + private const array BOOTSTRAPPERS = [ Bootstrappers\BootOverrides::class, Bootstrappers\BootSubscribers::class, Bootstrappers\BootFiles::class, diff --git a/src/Logging/Converter.php b/src/Logging/Converter.php index b4560e22..4805a946 100644 --- a/src/Logging/Converter.php +++ b/src/Logging/Converter.php @@ -31,7 +31,7 @@ final readonly class Converter /** * The prefix for the test suite name. */ - private const PREFIX = 'P\\'; + private const string PREFIX = 'P\\'; /** * The state generator. diff --git a/src/Plugins/Cache.php b/src/Plugins/Cache.php index ea3abb78..3ae0433b 100644 --- a/src/Plugins/Cache.php +++ b/src/Plugins/Cache.php @@ -21,7 +21,7 @@ final class Cache implements HandlesArguments /** * The temporary folder. */ - private const TEMPORARY_FOLDER = __DIR__ + private const string TEMPORARY_FOLDER = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR diff --git a/src/Plugins/Configuration.php b/src/Plugins/Configuration.php index acae8fb4..27d07ab8 100644 --- a/src/Plugins/Configuration.php +++ b/src/Plugins/Configuration.php @@ -21,7 +21,7 @@ final class Configuration implements HandlesArguments, Terminable /** * The base PHPUnit file. */ - public const BASE_PHPUNIT_FILE = __DIR__ + public const string BASE_PHPUNIT_FILE = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index a5061d25..712f5de5 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -17,20 +17,11 @@ use Symfony\Component\Console\Output\OutputInterface; */ final class Coverage implements AddsOutput, HandlesArguments { - /** - * @var string - */ - private const COVERAGE_OPTION = 'coverage'; + private const string COVERAGE_OPTION = 'coverage'; - /** - * @var string - */ - private const MIN_OPTION = 'min'; + private const string MIN_OPTION = 'min'; - /** - * @var string - */ - private const EXACTLY_OPTION = 'exactly'; + private const string EXACTLY_OPTION = 'exactly'; /** * Whether it should show the coverage or not. diff --git a/src/Plugins/Environment.php b/src/Plugins/Environment.php index 8ff10b4c..7edbbbd3 100644 --- a/src/Plugins/Environment.php +++ b/src/Plugins/Environment.php @@ -14,12 +14,12 @@ final class Environment implements HandlesArguments /** * The continuous integration environment. */ - public const CI = 'ci'; + public const string CI = 'ci'; /** * The local environment. */ - public const LOCAL = 'local'; + public const string LOCAL = 'local'; /** * The current environment. diff --git a/src/Plugins/Init.php b/src/Plugins/Init.php index eb87b086..c31dd759 100644 --- a/src/Plugins/Init.php +++ b/src/Plugins/Init.php @@ -20,12 +20,12 @@ final readonly class Init implements HandlesArguments /** * The option the triggers the init job. */ - private const INIT_OPTION = '--init'; + private const string INIT_OPTION = '--init'; /** * The files that will be created. */ - private const STUBS = [ + private const array STUBS = [ 'phpunit.xml.stub' => 'phpunit.xml', 'Pest.php.stub' => 'tests/Pest.php', 'TestCase.php.stub' => 'tests/TestCase.php', diff --git a/src/Plugins/Only.php b/src/Plugins/Only.php index 0d958173..7c2809f1 100644 --- a/src/Plugins/Only.php +++ b/src/Plugins/Only.php @@ -15,7 +15,7 @@ final class Only implements Terminable /** * The temporary folder. */ - private const TEMPORARY_FOLDER = __DIR__ + private const string TEMPORARY_FOLDER = __DIR__ .DIRECTORY_SEPARATOR .'..' .DIRECTORY_SEPARATOR diff --git a/src/Plugins/Parallel.php b/src/Plugins/Parallel.php index 1632a050..94902823 100644 --- a/src/Plugins/Parallel.php +++ b/src/Plugins/Parallel.php @@ -23,9 +23,9 @@ final class Parallel implements HandlesArguments { use HandleArguments; - private const GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_'; + private const string GLOBAL_PREFIX = 'PEST_PARALLEL_GLOBAL_'; - private const HANDLERS = [ + private const array HANDLERS = [ Parallel\Handlers\Parallel::class, Parallel\Handlers\Pest::class, Parallel\Handlers\Laravel::class, @@ -34,7 +34,7 @@ final class Parallel implements HandlesArguments /** * @var string[] */ - private const UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request']; + private const array UNSUPPORTED_ARGUMENTS = ['--todo', '--todos', '--retry', '--notes', '--issue', '--pr', '--pull-request']; /** * Whether the given command line arguments indicate that the test suite should be run in parallel. diff --git a/src/Plugins/Parallel/Handlers/Parallel.php b/src/Plugins/Parallel/Handlers/Parallel.php index 76a59af6..d99139b2 100644 --- a/src/Plugins/Parallel/Handlers/Parallel.php +++ b/src/Plugins/Parallel/Handlers/Parallel.php @@ -18,7 +18,7 @@ final class Parallel implements HandlesArguments /** * The list of arguments to remove. */ - private const ARGS_TO_REMOVE = [ + private const array ARGS_TO_REMOVE = [ '--parallel', '-p', '--no-output', diff --git a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php index d2801ced..cf5272b1 100644 --- a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php +++ b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php @@ -11,6 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput /** * {@inheritdoc} */ + #[\Override] protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line { if ($this->isOpeningHeadline($message)) { diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 48c88e7b..877a7373 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -50,7 +50,7 @@ final class WrapperRunner implements RunnerInterface /** * The time to sleep between cycles. */ - private const CYCLE_SLEEP = 10000; + private const int CYCLE_SLEEP = 10000; /** * The result printer. @@ -313,7 +313,6 @@ final class WrapperRunner implements RunnerInterface $testResult = unserialize($contents); assert($testResult instanceof TestResult); - $testResultSum = new TestResult( (int) $testResultSum->hasTests() + (int) $testResult->hasTests(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), @@ -328,15 +327,25 @@ final class WrapperRunner implements RunnerInterface array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredNoticeEvents(), $testResult->testRunnerTriggeredNoticeEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->errors(), $testResult->errors()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->deprecations(), $testResult->deprecations()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->notices(), $testResult->notices()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->warnings(), $testResult->warnings()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpDeprecations(), $testResult->phpDeprecations()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpNotices(), $testResult->phpNotices()), + // @phpstan-ignore-next-line array_merge_recursive($testResultSum->phpWarnings(), $testResult->phpWarnings()), $testResultSum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(), ); diff --git a/src/Plugins/Parallel/Support/CompactPrinter.php b/src/Plugins/Parallel/Support/CompactPrinter.php index bc2e1c3f..aa2da210 100644 --- a/src/Plugins/Parallel/Support/CompactPrinter.php +++ b/src/Plugins/Parallel/Support/CompactPrinter.php @@ -34,7 +34,7 @@ final class CompactPrinter /** * @var array> */ - private const LOOKUP_TABLE = [ + private const array LOOKUP_TABLE = [ '.' => ['gray', '.'], 'S' => ['yellow', 's'], 'T' => ['cyan', 't'], diff --git a/src/Plugins/Verbose.php b/src/Plugins/Verbose.php index e37938a3..9cec77de 100644 --- a/src/Plugins/Verbose.php +++ b/src/Plugins/Verbose.php @@ -16,7 +16,7 @@ final class Verbose implements HandlesArguments /** * The list of verbosity levels. */ - private const VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q']; + private const array VERBOSITY_LEVELS = ['v', 'vv', 'vvv', 'q']; /** * {@inheritDoc} diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 7d318a2d..3deee5bd 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -19,7 +19,7 @@ use function sprintf; */ final class DatasetsRepository { - private const SEPARATOR = '>>'; + private const string SEPARATOR = '>>'; /** * Holds the datasets. diff --git a/src/Result.php b/src/Result.php index 98e9e8b6..22e1e895 100644 --- a/src/Result.php +++ b/src/Result.php @@ -13,11 +13,11 @@ use PHPUnit\TextUI\Configuration\Configuration; */ final class Result { - private const SUCCESS_EXIT = 0; + private const int SUCCESS_EXIT = 0; - private const FAILURE_EXIT = 1; + private const int FAILURE_EXIT = 1; - private const EXCEPTION_EXIT = 2; + private const int EXCEPTION_EXIT = 2; /** * If the exit code is different from 0. diff --git a/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php new file mode 100644 index 00000000..614b38a4 --- /dev/null +++ b/src/Runner/Filter/EnsureTestCaseIsInitiatedFilter.php @@ -0,0 +1,39 @@ + $iterator + */ + public function __construct(RecursiveIterator $iterator) + { + parent::__construct($iterator); + } + + /** + * {@inheritdoc} + */ + public function accept(): bool + { + $test = $this->getInnerIterator()->current(); + + if ($test instanceof HasPrintableTestCaseName) { + /** @phpstan-ignore-next-line */ + $test->__initializeTestCase(); + } + + return true; + } +} diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php index 03001976..652eb442 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -11,12 +11,9 @@ use Pest\Exceptions\ShouldNotHappen; */ final class Backtrace { - /** - * @var string - */ - private const FILE = 'file'; + private const string FILE = 'file'; - private const BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS; + private const int BACKTRACE_OPTIONS = DEBUG_BACKTRACE_IGNORE_ARGS; /** * Returns the current test file. diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php index 46f39c41..c67f317c 100644 --- a/src/Support/DatasetInfo.php +++ b/src/Support/DatasetInfo.php @@ -11,9 +11,9 @@ use function Pest\testDirectory; */ final class DatasetInfo { - public const DATASETS_DIR_NAME = 'Datasets'; + public const string DATASETS_DIR_NAME = 'Datasets'; - public const DATASETS_FILE_NAME = 'Datasets.php'; + public const string DATASETS_FILE_NAME = 'Datasets.php'; public static function isInsideADatasetsDirectory(string $file): bool { diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 9af6aa5b..9d4132e2 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -13,7 +13,7 @@ use Throwable; */ final class ExceptionTrace { - private const UNDEFINED_METHOD = 'Call to undefined method P\\'; + private const string UNDEFINED_METHOD = 'Call to undefined method P\\'; /** * Ensures the given closure reports the good execution context. diff --git a/src/Support/Exporter.php b/src/Support/Exporter.php index 169f4891..44367c08 100644 --- a/src/Support/Exporter.php +++ b/src/Support/Exporter.php @@ -15,7 +15,7 @@ final readonly class Exporter /** * The maximum number of items in an array to export. */ - private const MAX_ARRAY_ITEMS = 3; + private const int MAX_ARRAY_ITEMS = 3; /** * Creates a new Exporter instance. diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 89c3e1f1..ce948244 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -13,7 +13,7 @@ use Throwable; */ final class HigherOrderMessage { - public const UNDEFINED_METHOD = 'Method %s does not exist'; + public const string UNDEFINED_METHOD = 'Method %s does not exist'; /** * An optional condition that will determine if the message will be executed. diff --git a/src/Support/Str.php b/src/Support/Str.php index 0e654bc8..67cff796 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -13,12 +13,9 @@ final class Str * Pool of alpha-numeric characters for generating (unsafe) random strings * from. */ - private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + private const string POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - /** - * @var string - */ - private const PREFIX = '__pest_evaluable_'; + private const string PREFIX = '__pest_evaluable_'; /** * Create a (unsecure & non-cryptographically safe) random alpha-numeric From 99c9f4e5d806e838820868e469e815be1670db36 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 3 May 2025 11:58:03 +0100 Subject: [PATCH 37/76] Bumps dependencies --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f4617a5b..cbf566a1 100644 --- a/composer.json +++ b/composer.json @@ -18,17 +18,17 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.9.1", + "brianium/paratest": "^7.10.1", "nunomaduro/collision": "^8.8.0", "nunomaduro/termwind": "^2.3.0", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", - "phpunit/phpunit": "^12.1.3" + "phpunit/phpunit": "^12.1.4" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.1.3", + "phpunit/phpunit": ">12.1.4", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, From 7711a52fe909b5475d438644c106582c730f7903 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 9 May 2025 13:10:29 +0100 Subject: [PATCH 38/76] Bumps dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cbf566a1..6052ec75 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": "^8.3.0", "brianium/paratest": "^7.10.1", "nunomaduro/collision": "^8.8.0", - "nunomaduro/termwind": "^2.3.0", + "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", From 909d778da3cc65338111c0195ea67fccc829d84d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 May 2025 02:00:15 +0100 Subject: [PATCH 39/76] fix: undefined property --- src/Concerns/Testable.php | 2 +- src/Pest.php | 2 +- .../Visual/Help/visual_snapshot_of_help_command_output.snap | 4 +++- .../Version/visual_snapshot_of_help_command_output.snap | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 9b8dc5f9..9918eb83 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -470,7 +470,7 @@ trait Testable */ public static function getLatestPrintableTestCaseMethodName(): string { - return self::$__latestDescription; + return self::$__latestDescription ?? ''; } /** diff --git a/src/Pest.php b/src/Pest.php index d23cfd5c..865c18d0 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.0.0-alpha.1'; + return '4.0.0-alpha.2'; } 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 9966d901..09fe9928 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.1. + Pest Testing Framework 4.0.0-alpha.2. USAGE: pest [options] @@ -72,6 +72,7 @@ --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 --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 @@ -93,6 +94,7 @@ --display-errors ............. Display details for errors triggered by tests --display-notices ........... Display details for notices triggered by tests --display-warnings ......... Display details for warnings triggered by tests + --display-all-issues ..... Display details for all issues that are triggered --reverse-list .............................. Print defects in reverse order --teamcity . Replace default progress and result output with TeamCity format --testdox ................ Replace default result output with TestDox format 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 c9dea0d5..d72ea0de 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.1. + Pest Testing Framework 4.0.0-alpha.2. From c62cc3fef0d2751a2a087c4f6ab71b810827a0dc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 23 May 2025 05:19:56 +0100 Subject: [PATCH 40/76] chore: adds pokio --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 6052ec75..92dcddfe 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "php": "^8.3.0", "brianium/paratest": "^7.10.1", "nunomaduro/collision": "^8.8.0", + "nunomaduro/pokio": "dev-main", "nunomaduro/termwind": "^2.3.1", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", From 02b1ffb33481197fe7abd3aed45c9e0ee961e67c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 27 May 2025 11:37:29 +0100 Subject: [PATCH 41/76] chore: bump dependencies --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 92dcddfe..a9800dab 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,11 @@ "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.0", - "phpunit/phpunit": "^12.1.4" + "phpunit/phpunit": "^12.1.6" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.1.4", + "phpunit/phpunit": ">12.1.6", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, From 0fc9d4dfe0260827fc2fd9c6fce07ed99508fcc8 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 8 Jun 2025 15:29:23 +0100 Subject: [PATCH 42/76] feat: adds phpunit `12.2.1` support --- composer.json | 5 +- .../Parallel/Paratest/ResultPrinter.php | 4 +- .../Parallel/Paratest/WrapperRunner.php | 6 +- ...isual_snapshot_of_help_command_output.snap | 3 + tests/.snapshots/success.txt | 18 ++++-- tests/Features/Covers.php | 59 ------------------- tests/Features/Covers/ClassCoverage.php | 13 ++++ tests/Features/Covers/CoversNothing.php | 10 ++++ tests/Features/Covers/ExceptionHandling.php | 10 ++++ tests/Features/Covers/FunctionCoverage.php | 12 ++++ tests/Features/Covers/GuessCoverage.php | 17 ++++++ tests/Features/Covers/TraitCoverage.php | 11 ++++ 12 files changed, 99 insertions(+), 69 deletions(-) delete mode 100644 tests/Features/Covers.php create mode 100644 tests/Features/Covers/ClassCoverage.php create mode 100644 tests/Features/Covers/CoversNothing.php create mode 100644 tests/Features/Covers/ExceptionHandling.php create mode 100644 tests/Features/Covers/FunctionCoverage.php create mode 100644 tests/Features/Covers/GuessCoverage.php create mode 100644 tests/Features/Covers/TraitCoverage.php diff --git a/composer.json b/composer.json index a9800dab..da76ff30 100644 --- a/composer.json +++ b/composer.json @@ -20,16 +20,15 @@ "php": "^8.3.0", "brianium/paratest": "^7.10.1", "nunomaduro/collision": "^8.8.0", - "nunomaduro/pokio": "dev-main", "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", - "phpunit/phpunit": "^12.1.6" + "phpunit/phpunit": "^12.2.1" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">12.1.6", + "phpunit/phpunit": ">12.2.1", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, diff --git a/src/Plugins/Parallel/Paratest/ResultPrinter.php b/src/Plugins/Parallel/Paratest/ResultPrinter.php index bd416e1e..e7a1c24d 100644 --- a/src/Plugins/Parallel/Paratest/ResultPrinter.php +++ b/src/Plugins/Parallel/Paratest/ResultPrinter.php @@ -59,10 +59,10 @@ final class ResultPrinter private readonly OutputInterface $output, private readonly Options $options ) { - $this->printer = new class($this->output) implements Printer + $this->printer = new readonly class($this->output) implements Printer { public function __construct( - private readonly OutputInterface $output, + private OutputInterface $output, ) {} public function print(string $buffer): void diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index 877a7373..469f2aa6 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -17,6 +17,7 @@ use ParaTest\WrapperRunner\WrapperWorker; use Pest\Result; use Pest\TestSuite; use PHPUnit\Event\Facade as EventFacade; +use PHPUnit\Event\Test\AfterLastTestMethodFailed; use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Runner\CodeCoverage; use PHPUnit\Runner\ResultCache\DefaultResultCache; @@ -313,12 +314,15 @@ final class WrapperRunner implements RunnerInterface $testResult = unserialize($contents); assert($testResult instanceof TestResult); + /** @var list $failedEvents */ + $failedEvents = array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()); + $testResultSum = new TestResult( (int) $testResultSum->hasTests() + (int) $testResult->hasTests(), $testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(), $testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(), array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()), - array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()), + $failedEvents, array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()), array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()), 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 09fe9928..b2f7a23f 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 @@ -100,10 +100,12 @@ --testdox ................ Replace default result output with TestDox format --testdox-summary Repeat TestDox output for tests with errors, failures, or issues --debug Replace default progress and result output with debugging information + --with-telemetry Include telemetry information in debugging information output --compact ................ Replace default result output with Compact format 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 --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 @@ -115,6 +117,7 @@ --coverage ..... Generate code coverage report and output to standard output --coverage --min Set the minimum required coverage percentage, and fail if not met --coverage-clover [file] Write code coverage report in Clover XML format to file + --coverage-openclover [file] Write code coverage report in OpenClover XML format to file --coverage-cobertura [file] Write code coverage report in Cobertura XML format to file --coverage-crap4j [file] Write code coverage report in Crap4J XML format to file --coverage-html [dir] Write code coverage report in HTML format to directory diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 513a804e..5245e72c 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -68,14 +68,24 @@ ✓ it adds coverage if --min exist ✓ it generates coverage based on file input - PASS Tests\Features\Covers + PASS Tests\Features\Covers\ClassCoverage ✓ it uses the correct PHPUnit attribute for class - ✓ it uses the correct PHPUnit attribute for function - ✓ it guesses if the given argument is a class or function - ✓ it uses the correct PHPUnit attribute for trait + + PASS Tests\Features\Covers\CoversNothing ✓ it uses the correct PHPUnit attribute for covers nothing + + PASS Tests\Features\Covers\ExceptionHandling ✓ it throws exception if no class nor method has been found + PASS Tests\Features\Covers\FunctionCoverage + ✓ it uses the correct PHPUnit attribute for function + + PASS Tests\Features\Covers\GuessCoverage + ✓ it guesses if the given argument is a class or function + + PASS Tests\Features\Covers\TraitCoverage + ✓ it uses the correct PHPUnit attribute for trait + PASS Tests\Features\DatasetsTests - 1 todo ✓ it throws exception if dataset does not exist ✓ it throws exception if dataset already exist diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php deleted file mode 100644 index 386d523f..00000000 --- a/tests/Features/Covers.php +++ /dev/null @@ -1,59 +0,0 @@ -getAttributes(); - - expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); -}); - -it('uses the correct PHPUnit attribute for function', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[3]->getArguments()[0])->toBe('testCoversFunction'); -})->coversFunction('testCoversFunction'); - -it('guesses if the given argument is a class or function', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[5]->getName())->toBe(CoversClass::class); - expect($attributes[5]->getArguments()[0])->toBe(CoversClass3::class); - - expect($attributes[6]->getName())->toBe(CoversFunction::class); - expect($attributes[6]->getArguments()[0])->toBe('testCoversFunction'); -})->covers(CoversClass3::class, 'testCoversFunction'); - -it('uses the correct PHPUnit attribute for trait', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[8]->getName())->toBe('PHPUnit\Framework\Attributes\CoversTrait'); - expect($attributes[8]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); -})->coversTrait(CoversTrait::class); - -it('uses the correct PHPUnit attribute for covers nothing', function () { - $attributes = (new ReflectionMethod($this, $this->name()))->getAttributes(); - - expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing'); - expect($attributes[3]->getArguments())->toHaveCount(0); -})->coversNothing(); - -it('throws exception if no class nor method has been found', function () { - $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure'); - - $testCall->covers('fakeName'); -})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); diff --git a/tests/Features/Covers/ClassCoverage.php b/tests/Features/Covers/ClassCoverage.php new file mode 100644 index 00000000..ab805e17 --- /dev/null +++ b/tests/Features/Covers/ClassCoverage.php @@ -0,0 +1,13 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversClass::class); + expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); +}); diff --git a/tests/Features/Covers/CoversNothing.php b/tests/Features/Covers/CoversNothing.php new file mode 100644 index 00000000..6918414c --- /dev/null +++ b/tests/Features/Covers/CoversNothing.php @@ -0,0 +1,10 @@ +name()))->getAttributes(); + + expect($attributes[2]->getName())->toBe(CoversNothing::class); + expect($attributes[2]->getArguments())->toHaveCount(0); +})->coversNothing(); diff --git a/tests/Features/Covers/ExceptionHandling.php b/tests/Features/Covers/ExceptionHandling.php new file mode 100644 index 00000000..de86bb02 --- /dev/null +++ b/tests/Features/Covers/ExceptionHandling.php @@ -0,0 +1,10 @@ + 'closure'); + + $testCall->covers('fakeName'); +})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); diff --git a/tests/Features/Covers/FunctionCoverage.php b/tests/Features/Covers/FunctionCoverage.php new file mode 100644 index 00000000..fba97080 --- /dev/null +++ b/tests/Features/Covers/FunctionCoverage.php @@ -0,0 +1,12 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversFunction::class); + expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction'); +})->coversFunction('testCoversFunction'); diff --git a/tests/Features/Covers/GuessCoverage.php b/tests/Features/Covers/GuessCoverage.php new file mode 100644 index 00000000..e8a84035 --- /dev/null +++ b/tests/Features/Covers/GuessCoverage.php @@ -0,0 +1,17 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(CoversClass::class); + expect($attributes[1]->getArguments()[0])->toBe(CoversClass3::class); + + expect($attributes[2]->getName())->toBe(CoversFunction::class); + expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction2'); +})->covers(CoversClass3::class, 'testCoversFunction2'); diff --git a/tests/Features/Covers/TraitCoverage.php b/tests/Features/Covers/TraitCoverage.php new file mode 100644 index 00000000..57bc3680 --- /dev/null +++ b/tests/Features/Covers/TraitCoverage.php @@ -0,0 +1,11 @@ +getAttributes(); + + expect($attributes[1]->getName())->toBe(PHPUnitCoversTrait::class); + expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); +})->coversTrait(CoversTrait::class); From 5d2aafd2a3cbe6141126146272abfe3471d68a35 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 12 Jun 2025 00:38:05 +0100 Subject: [PATCH 43/76] feat: --profanity --- .github/workflows/static.yml | 7 +++++-- composer.json | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 69dd6b98..491cd60d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -31,8 +31,11 @@ jobs: - name: Install Dependencies run: composer update --prefer-stable --no-interaction --no-progress --ansi - # - name: Type Check - # run: composer test:type:check + - name: Profanity Check + run: composer test:profanity + + - name: Type Check + run: composer test:type:check - name: Type Coverage run: composer test:type:coverage diff --git a/composer.json b/composer.json index da76ff30..cdc534e4 100644 --- a/composer.json +++ b/composer.json @@ -18,12 +18,13 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.10.1", - "nunomaduro/collision": "^8.8.0", + "brianium/paratest": "^7.10.2", + "nunomaduro/collision": "^8.8.1", "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.1" }, "conflict": { @@ -55,7 +56,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-type-coverage": "^4.0.0", - "symfony/process": "^7.2.5" + "symfony/process": "^7.3.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -74,6 +75,7 @@ "lint": "pint", "test:refacto": "rector --dry-run", "test:lint": "pint --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", "test:unit": "php bin/pest --exclude-group=integration --compact", From dd44ac41951141e2675022907804a334bcfc8848 Mon Sep 17 00:00:00 2001 From: JonPurvis Date: Sat, 14 Jun 2025 21:36:39 +0100 Subject: [PATCH 44/76] remove language option from profanity composer script --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cdc534e4..f5718b7b 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "lint": "pint", "test:refacto": "rector --dry-run", "test:lint": "pint --test", - "test:profanity": "php bin/pest --profanity --compact --language=en", + "test:profanity": "php bin/pest --profanity --compact", "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:type:coverage": "php -d memory_limit=-1 bin/pest --type-coverage --min=100", "test:unit": "php bin/pest --exclude-group=integration --compact", From 49bf00024f6cd559c40b16d93233587ef2d43605 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 15 Jun 2025 22:43:59 +0100 Subject: [PATCH 45/76] 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 46/76] 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 47/76] 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 48/76] 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 49/76] 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 50/76] 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 51/76] 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 52/76] 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 53/76] 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 54/76] 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 55/76] 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 56/76] 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 57/76] 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 58/76] 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 59/76] 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 60/76] 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 61/76] 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 62/76] 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 63/76] 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. From e0695a13cbbe9e9d0953808e9126b24db75891d5 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 21 Jul 2025 13:25:50 +0100 Subject: [PATCH 64/76] feat: adds shell --- composer.json | 3 +- src/Concerns/Testable.php | 9 ++++ src/Support/Shell.php | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/Support/Shell.php diff --git a/composer.json b/composer.json index 26d92d49..55bd94ee 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,8 @@ "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0" + "pestphp/pest-plugin-type-coverage": "^4.0.0", + "psy/psysh": "^0.12.9" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index f233b6ac..326115ea 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -10,6 +10,7 @@ use Pest\Preset; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\Support\Reflection; +use Pest\Support\Shell; use Pest\TestSuite; use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\TestCase; @@ -477,4 +478,12 @@ trait Testable 'notes' => self::$__latestNotes, ]; } + + /** + * Opens a shell for the test case. + */ + public function shell(): void + { + Shell::open(); + } } diff --git a/src/Support/Shell.php b/src/Support/Shell.php new file mode 100644 index 00000000..d7fbafd2 --- /dev/null +++ b/src/Support/Shell.php @@ -0,0 +1,99 @@ +setUpdateCheck(Checker::NEVER); + + $config->getPresenter()->addCasters(self::casters()); + + $shell = new PsyShell($config); + + $loader = self::tinkered($shell); + + try { + $shell->run(); + } finally { + $loader?->unregister(); + } + } + + /** + * Returns the casters for the Psy Shell. + * + * @return array + */ + private static function casters(): array + { + $casters = [ + 'Illuminate\Support\Collection' => 'Laravel\Tinker\TinkerCaster::castCollection', + 'Illuminate\Support\HtmlString' => 'Laravel\Tinker\TinkerCaster::castHtmlString', + 'Illuminate\Support\Stringable' => 'Laravel\Tinker\TinkerCaster::castStringable', + ]; + + if (class_exists('Illuminate\Database\Eloquent\Model')) { + $casters['Illuminate\Database\Eloquent\Model'] = 'Laravel\Tinker\TinkerCaster::castModel'; + } + + if (class_exists('Illuminate\Process\ProcessResult')) { + $casters['Illuminate\Process\ProcessResult'] = 'Laravel\Tinker\TinkerCaster::castProcessResult'; + } + + if (class_exists('Illuminate\Foundation\Application')) { + $casters['Illuminate\Foundation\Application'] = 'Laravel\Tinker\TinkerCaster::castApplication'; + } + + if (function_exists('app') === false) { + return $casters; // @phpstan-ignore-line + } + + $config = app()->make('config'); + + return array_merge($casters, (array) $config->get('tinker.casters', [])); + } + + /** + * Tinkers the current shell, if the Tinker package is available. + */ + private static function tinkered(PsyShell $shell): ?ClassLoader + { + if (function_exists('app') === false + || ! class_exists(Env::class) + || ! class_exists(ClassAliasAutoloader::class) + ) { + return null; + } + + $path = Env::get('COMPOSER_VENDOR_DIR', app()->basePath().DIRECTORY_SEPARATOR.'vendor'); + + $path .= '/composer/autoload_classmap.php'; + + $config = app()->make('config'); + + $loader = ClassAliasAutoloader::register( + $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) + ); + + return $loader; + } +} From fb282b184e3f4dd19b793191e99685700f1d21ca Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 21 Jul 2025 13:32:03 +0100 Subject: [PATCH 65/76] fix: return type --- src/Support/Shell.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Support/Shell.php b/src/Support/Shell.php index d7fbafd2..0f9c9d55 100644 --- a/src/Support/Shell.php +++ b/src/Support/Shell.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Pest\Support; -use Composer\Autoload\ClassLoader; use Illuminate\Support\Env; use Laravel\Tinker\ClassAliasAutoloader; +use Pest\TestSuite; use Psy\Configuration; use Psy\Shell as PsyShell; use Psy\VersionUpdater\Checker; @@ -34,7 +34,7 @@ final class Shell try { $shell->run(); } finally { - $loader?->unregister(); + $loader?->unregister(); // @phpstan-ignore-line } } @@ -75,7 +75,7 @@ final class Shell /** * Tinkers the current shell, if the Tinker package is available. */ - private static function tinkered(PsyShell $shell): ?ClassLoader + private static function tinkered(PsyShell $shell): ?object { if (function_exists('app') === false || ! class_exists(Env::class) @@ -88,6 +88,10 @@ final class Shell $path .= '/composer/autoload_classmap.php'; + if (! file_exists($path)) { + $path = TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'composer'.DIRECTORY_SEPARATOR.'autoload_classmap.php'; + } + $config = app()->make('config'); $loader = ClassAliasAutoloader::register( From 00572f5f8ee8923830d379c7afc80977b2e071d3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 11:39:34 +0100 Subject: [PATCH 66/76] feat: improves playwright --- resources/views/installers/plugin-browser.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/views/installers/plugin-browser.php b/resources/views/installers/plugin-browser.php index 28288ab0..d717867b 100644 --- a/resources/views/installers/plugin-browser.php +++ b/resources/views/installers/plugin-browser.php @@ -10,6 +10,11 @@ composer require pestphp/pest-plugin-browser:^4.0 --dev +
+ - + npm install playwright@latest +
+
- npx playwright install From f9814793ddb315008e2b436df0d38d5b0fec3292 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 21:52:06 +0100 Subject: [PATCH 67/76] feat: `skipOnCI` --- src/PendingCalls/TestCall.php | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index c65122b5..102f1640 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -314,6 +314,42 @@ final class TestCall // @phpstan-ignore-line : $this; } + /** + * Skips the current test when running on a CI environments. + */ + public function skipOnCI(): self + { + foreach ([ + 'CI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'APPVEYOR', + 'BITBUCKET_BUILD_NUMBER', + 'BUILDKITE', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'SYSTEM_COLLECTIONURI', + 'CI_NAME', + 'TASKCLUSTER_ROOT_URL', + 'DRONE', + 'WERCKER', + 'NEVERCODE', + 'SEMAPHORE', + 'NETLIFY', + 'NOW_BUILDER', + ] as $env) { + if (isset($_ENV[$env])) { + return $this->skip(sprintf( + 'This test is skipped on [CI].', + )); + } + } + + return $this; + } + /** * Skips the current test unless the given test is running on Windows. */ From 516ace85b40358eba51e030afd6254a8e5b64ce3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 22:08:03 +0100 Subject: [PATCH 68/76] fix: skipOnCI --- src/PendingCalls/TestCall.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 102f1640..05bc2f59 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -340,7 +340,7 @@ final class TestCall // @phpstan-ignore-line 'NETLIFY', 'NOW_BUILDER', ] as $env) { - if (isset($_ENV[$env])) { + if (getenv('GITHUB_ACTIONS') !== false) { return $this->skip(sprintf( 'This test is skipped on [CI].', )); From f49b91ec0df11a6fcaf3a5f326485bce1f86dd19 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 22:31:08 +0100 Subject: [PATCH 69/76] fixes missing condition --- src/PendingCalls/TestCall.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 05bc2f59..55a948e4 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -12,6 +12,7 @@ use Pest\Factories\Attribute; use Pest\Factories\TestCaseMethodFactory; use Pest\Mutate\Repositories\ConfigurationRepository; use Pest\PendingCalls\Concerns\Describable; +use Pest\Plugins\Environment; use Pest\Plugins\Only; use Pest\Support\Backtrace; use Pest\Support\Container; @@ -347,6 +348,12 @@ final class TestCall // @phpstan-ignore-line } } + if (Environment::name() === Environment::CI) { + return $this->skip(sprintf( + 'This test is skipped on [CI].', + )); + } + return $this; } From 924dc016cca437f83a4601573ba305a7496c9e78 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 22:40:38 +0100 Subject: [PATCH 70/76] feat: `skipLocally` --- src/PendingCalls/TestCall.php | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 55a948e4..c86e5d7e 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -316,9 +316,9 @@ final class TestCall // @phpstan-ignore-line } /** - * Skips the current test when running on a CI environments. + * Weather the current test is running on a CI environment. */ - public function skipOnCI(): self + private function runningOnCI(): bool { foreach ([ 'CI', @@ -341,14 +341,20 @@ final class TestCall // @phpstan-ignore-line 'NETLIFY', 'NOW_BUILDER', ] as $env) { - if (getenv('GITHUB_ACTIONS') !== false) { - return $this->skip(sprintf( - 'This test is skipped on [CI].', - )); + if (getenv($env) !== false) { + return true; } } - if (Environment::name() === Environment::CI) { + return Environment::name() === Environment::CI; + } + + /** + * Skips the current test when running on a CI environments. + */ + public function skipOnCI(): self + { + if ($this->runningOnCI()) { return $this->skip(sprintf( 'This test is skipped on [CI].', )); @@ -357,6 +363,17 @@ final class TestCall // @phpstan-ignore-line return $this; } + public function skipLocally(): self + { + if ($this->runningOnCI() === false) { + return $this->skip(sprintf( + 'This test is skipped [locally].', + )); + } + + return $this; + } + /** * Skips the current test unless the given test is running on Windows. */ From be9c95e3bc9e08324b6cf15bf3bc4e3e56148598 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 23:06:43 +0100 Subject: [PATCH 71/76] feat: adds `see` --- src/PendingCalls/TestCall.php | 11 +++++++++++ tests/.snapshots/success.txt | 11 +++++++++-- tests/Arch.php | 21 --------------------- tests/Features/References.php | 11 +++++++++++ tests/Features/See.php | 11 +++++++++++ tests/Visual/Parallel.php | 2 +- 6 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 tests/Features/References.php create mode 100644 tests/Features/See.php diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 4520858f..6e78937c 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -688,6 +688,17 @@ final class TestCall // @phpstan-ignore-line return $this; } + /** + * Adds one or more references to the tested method or class. This helps + * to link test cases to the source code for easier navigation. + * + * @param array|class-string ...$classes + */ + public function see(string|array ...$classes): self + { + return $this->references(...$classes); + } + /** * Informs the test runner that no expectations happen in this test, * and its purpose is simply to check whether the given code can diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 7b78f425..62bc88b5 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -4,7 +4,6 @@ ✓ preset → strict → ignoring ['usleep'] ✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …] ✓ globals - ✓ dependencies ✓ contracts PASS Tests\Environments\Windows @@ -1133,6 +1132,10 @@ ✓ nested → it may be associated with an pr #1, #4, #5, #6, #3 // an note between an the pr + PASS Tests\Features\References + ✓ it can reference a specific class + ✓ it can reference a specific class method + PASS Tests\Features\Repeat ✓ once ✓ multiple times @ repetition 1 of 5 @@ -1309,6 +1312,10 @@ ✓ it can see datasets defined in Pest.php file with ('B') ✓ Pest.php dataset is taken + PASS Tests\Features\See + ✓ it can reference a specific class + ✓ it can reference a specific class method + WARN Tests\Features\Skip ✓ it do not skips - it skips with truthy → 1 @@ -1712,4 +1719,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1146 passed (2739 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1149 passed (2742 assertions) \ No newline at end of file diff --git a/tests/Arch.php b/tests/Arch.php index 905515d2..6348a0f3 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -27,27 +27,6 @@ arch('globals') ->not->toBeUsed() ->ignoring(Expectation::class); -arch('dependencies') - ->expect('Pest') - ->toOnlyUse([ - 'dd', - 'dump', - 'expect', - 'uses', - 'Termwind', - 'ParaTest', - 'Pest\Arch', - 'Pest\Browser', - 'Pest\Mutate\Contracts\Configuration', - 'Pest\Mutate\Decorators\TestCallDecorator', - 'Pest\Mutate\Repositories\ConfigurationRepository', - 'Pest\Plugin', - 'NunoMaduro\Collision', - 'Whoops', - 'Symfony\Component\Console', - 'Symfony\Component\Process', - ])->ignoring(['Composer', 'PHPUnit', 'SebastianBergmann']); - arch('contracts') ->expect('Pest\Contracts') ->toOnlyUse([ diff --git a/tests/Features/References.php b/tests/Features/References.php new file mode 100644 index 00000000..a19db35a --- /dev/null +++ b/tests/Features/References.php @@ -0,0 +1,11 @@ +toBeString(); +})->references(Panic::class); + +it('can reference a specific class method', function () { + expect(Panic::with(...))->toBeCallable(); +})->references([Panic::class, 'with']); diff --git a/tests/Features/See.php b/tests/Features/See.php new file mode 100644 index 00000000..7c9393ee --- /dev/null +++ b/tests/Features/See.php @@ -0,0 +1,11 @@ +toBeString(); +})->see(Panic::class); + +it('can reference a specific class method', function () { + expect(Panic::with(...))->toBeCallable(); +})->see([Panic::class, 'with']); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 01492414..18e9b696 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, 1136 passed (2715 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1139 passed (2718 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From f9901245f19b190c658e064492db00bb4fa5134d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 23:51:32 +0100 Subject: [PATCH 72/76] updates dependencies and snapshots --- composer.json | 6 ++--- .../Runner/ResultCache/DefaultResultCache.php | 25 ++++++++----------- src/Mixins/Expectation.php | 2 +- src/Pest.php | 2 +- src/Repositories/SnapshotRepository.php | 4 +-- src/Support/Str.php | 3 ++- ...isual_snapshot_of_help_command_output.snap | 2 +- ...isual_snapshot_of_help_command_output.snap | 2 +- tests/.snapshots/success.txt | 9 ++++++- tests/Visual/Parallel.php | 2 +- 10 files changed, 31 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index 55bd94ee..9144724e 100644 --- a/composer.json +++ b/composer.json @@ -18,19 +18,19 @@ ], "require": { "php": "^8.3.0", - "brianium/paratest": "^7.10.3", + "brianium/paratest": "^7.11.0", "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.6", + "phpunit/phpunit": "^12.2.7", "symfony/process": "^7.3.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.2.6", + "phpunit/phpunit": ">12.2.7", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php index fde6bad9..7e7f359c 100644 --- a/overrides/Runner/ResultCache/DefaultResultCache.php +++ b/overrides/Runner/ResultCache/DefaultResultCache.php @@ -49,7 +49,7 @@ use const DIRECTORY_SEPARATOR; use const LOCK_EX; use PHPUnit\Framework\TestStatus\TestStatus; -use PHPUnit\Runner\DirectoryCannotBeCreatedException; +use PHPUnit\Runner\DirectoryDoesNotExistException; use PHPUnit\Runner\Exception; use PHPUnit\Util\Filesystem; @@ -72,10 +72,7 @@ use function Pest\version; */ final class DefaultResultCache implements ResultCache { - /** - * @var string - */ - private const DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; + private const string DEFAULT_RESULT_CACHE_FILENAME = '.phpunit.result.cache'; private readonly string $cacheFilename; @@ -98,28 +95,28 @@ final class DefaultResultCache implements ResultCache $this->cacheFilename = $filepath ?? $_ENV['PHPUNIT_RESULT_CACHE'] ?? self::DEFAULT_RESULT_CACHE_FILENAME; } - public function setStatus(string $id, TestStatus $status): void + public function setStatus(ResultCacheId $id, TestStatus $status): void { if ($status->isSuccess()) { return; } - $this->defects[$id] = $status; + $this->defects[$id->asString()] = $status; } - public function status(string $id): TestStatus + public function status(ResultCacheId $id): TestStatus { - return $this->defects[$id] ?? TestStatus::unknown(); + return $this->defects[$id->asString()] ?? TestStatus::unknown(); } - public function setTime(string $id, float $time): void + public function setTime(ResultCacheId $id, float $time): void { - $this->times[$id] = $time; + $this->times[$id->asString()] = $time; } - public function time(string $id): float + public function time(ResultCacheId $id): float { - return $this->times[$id] ?? 0.0; + return $this->times[$id->asString()] ?? 0.0; } public function mergeWith(self $other): void @@ -179,7 +176,7 @@ final class DefaultResultCache implements ResultCache public function persist(): void { if (! Filesystem::createDirectory(dirname($this->cacheFilename))) { - throw new DirectoryCannotBeCreatedException($this->cacheFilename); + throw new DirectoryDoesNotExistException(dirname($this->cacheFilename)); } $data = [ diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 8f806e0e..6ebcb546 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -660,7 +660,7 @@ final class Expectation { foreach ($keys as $k => $key) { if (is_array($key)) { - $this->toHaveKeys(array_keys(Arr::dot($key, $k . '.')), $message); + $this->toHaveKeys(array_keys(Arr::dot($key, $k.'.')), $message); } else { $this->toHaveKey($key, message: $message); } diff --git a/src/Pest.php b/src/Pest.php index 7ddec711..52de2a13 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '4.0.0-alpha.5'; + return '4.0.0-alpha.6'; } function testDirectory(string $file = ''): string diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php index 171fd88f..7f7a9573 100644 --- a/src/Repositories/SnapshotRepository.php +++ b/src/Repositories/SnapshotRepository.php @@ -19,8 +19,8 @@ final class SnapshotRepository * Creates a snapshot repository instance. */ public function __construct( - readonly private string $testsPath, - readonly private string $snapshotsPath, + private readonly string $testsPath, + private readonly string $snapshotsPath, ) {} /** diff --git a/src/Support/Str.php b/src/Support/Str.php index effebabf..e5b78b6d 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -58,7 +58,7 @@ final class Str { $code = str_replace('_', '__', $code); - $code = self::PREFIX . str_replace(' ', '_', $code); + $code = self::PREFIX.str_replace(' ', '_', $code); // sticks to PHP8.2 function naming rules https://www.php.net/manual/en/functions.user-defined.php return (string) preg_replace('/[^a-zA-Z0-9_\x80-\xff]/', '_', $code); @@ -124,6 +124,7 @@ final class Str public static function slugify(string $target): string { $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target); + return strtolower(trim($target, '-')); } } 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 8a174927..5adc938a 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.5. + Pest Testing Framework 4.0.0-alpha.6. 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 f54244da..6c48a1b3 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.5. + Pest Testing Framework 4.0.0-alpha.6. diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 62bc88b5..ace3b04f 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -628,6 +628,13 @@ ✓ pass ✓ failures ✓ failures with custom message + ✓ not failures + + PASS Tests\Features\Expect\toBeSlug + ✓ pass + ✓ failures + ✓ failures with custom message + ✓ failures with default message ✓ not failures PASS Tests\Features\Expect\toBeSnakeCase @@ -1719,4 +1726,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1149 passed (2742 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1154 passed (2754 assertions) \ No newline at end of file diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 18e9b696..d9cdfbdd 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, 1139 passed (2718 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1144 passed (2730 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From fe27012bbca23f45f41bebbbe2ec8cc6ab918479 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 22 Jul 2025 23:58:36 +0100 Subject: [PATCH 73/76] style --- src/PendingCalls/TestCall.php | 8 ++------ src/Support/Shell.php | 4 +--- src/Support/Str.php | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 6e78937c..d10505f0 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -355,9 +355,7 @@ final class TestCall // @phpstan-ignore-line public function skipOnCI(): self { if ($this->runningOnCI()) { - return $this->skip(sprintf( - 'This test is skipped on [CI].', - )); + return $this->skip('This test is skipped on [CI].'); } return $this; @@ -366,9 +364,7 @@ final class TestCall // @phpstan-ignore-line public function skipLocally(): self { if ($this->runningOnCI() === false) { - return $this->skip(sprintf( - 'This test is skipped [locally].', - )); + return $this->skip('This test is skipped [locally].'); } return $this; diff --git a/src/Support/Shell.php b/src/Support/Shell.php index 0f9c9d55..b5c5b157 100644 --- a/src/Support/Shell.php +++ b/src/Support/Shell.php @@ -94,10 +94,8 @@ final class Shell $config = app()->make('config'); - $loader = ClassAliasAutoloader::register( + return ClassAliasAutoloader::register( $shell, $path, $config->get('tinker.alias', []), $config->get('tinker.dont_alias', []) ); - - return $loader; } } diff --git a/src/Support/Str.php b/src/Support/Str.php index e5b78b6d..ff4bf5f4 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -125,6 +125,6 @@ final class Str { $target = preg_replace('/[^a-zA-Z0-9]+/', '-', $target); - return strtolower(trim($target, '-')); + return strtolower(trim((string) $target, '-')); } } From aac08629f7456163e09c1247032c3e07b3195588 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 23 Jul 2025 08:26:12 +0100 Subject: [PATCH 74/76] ci: removes testing against lowest --- .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 1a1ea7eb..cda60c06 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] symfony: ['7.3'] php: ['8.3', '8.4'] - dependency_version: [prefer-lowest, prefer-stable] + dependency_version: [prefer-stable] name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }} From 6d6e4e040fc2c9de1b668294a325bd022e3cc1c3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 25 Jul 2025 18:03:58 -0600 Subject: [PATCH 75/76] fix: wrong status code being used --- src/Result.php | 46 ++----------------- .../EnsureIgnorableTestCasesAreIgnored.php | 2 +- 2 files changed, 4 insertions(+), 44 deletions(-) diff --git a/src/Result.php b/src/Result.php index 4dcbe4a1..97eda17f 100644 --- a/src/Result.php +++ b/src/Result.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Pest; -use NunoMaduro\Collision\Adapters\Phpunit\Support\ResultReflection; use PHPUnit\TestRunner\TestResult\TestResult; use PHPUnit\TextUI\Configuration\Configuration; +use PHPUnit\TextUI\ShellExitCodeCalculator; /** * @internal @@ -15,10 +15,6 @@ final class Result { private const int SUCCESS_EXIT = 0; - private const int FAILURE_EXIT = 1; - - private const int EXCEPTION_EXIT = 2; - /** * If the exit code is different from 0. */ @@ -40,44 +36,8 @@ final class Result */ public static function exitCode(Configuration $configuration, TestResult $result): int { - if ($result->wasSuccessful()) { - if ($configuration->failOnWarning()) { - $warnings = $result->numberOfTestsWithTestTriggeredPhpunitWarningEvents() - + count($result->warnings()) - + count($result->phpWarnings()); + $shell = new ShellExitCodeCalculator; - if ($warnings > 0) { - return self::FAILURE_EXIT; - } - } - - if (! $result->hasTestTriggeredPhpunitWarningEvents()) { - return self::SUCCESS_EXIT; - } - } - - if ($configuration->failOnEmptyTestSuite() && ResultReflection::numberOfTests($result) === 0) { - return self::FAILURE_EXIT; - } - - if ($result->wasSuccessful()) { - if ($configuration->failOnRisky() && $result->hasTestConsideredRiskyEvents()) { - $returnCode = self::FAILURE_EXIT; - } - - if ($configuration->failOnIncomplete() && $result->hasTestMarkedIncompleteEvents()) { - $returnCode = self::FAILURE_EXIT; - } - - if ($configuration->failOnSkipped() && $result->hasTestSkippedEvents()) { - $returnCode = self::FAILURE_EXIT; - } - } - - if ($result->hasTestErroredEvents()) { - return self::EXCEPTION_EXIT; - } - - return self::FAILURE_EXIT; + return $shell->calculate($configuration, $result); } } diff --git a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php index 1cf3d55a..a6e837bf 100644 --- a/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php +++ b/src/Subscribers/EnsureIgnorableTestCasesAreIgnored.php @@ -35,7 +35,7 @@ final class EnsureIgnorableTestCasesAreIgnored implements StartedSubscriber /** @var array $testRunnerTriggeredWarningEvents */ $testRunnerTriggeredWarningEvents = $property->getValue($collector); - $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => $event->message() !== 'No tests found in class "Pest\TestCases\IgnorableTestCase".')); + $testRunnerTriggeredWarningEvents = array_values(array_filter($testRunnerTriggeredWarningEvents, fn (WarningTriggered $event): bool => str_contains($event->message(), 'No tests found in class') === false)); $property->setValue($collector, $testRunnerTriggeredWarningEvents); } From de4409e368da1dcf350b47de882cc63238ba4add Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 25 Jul 2025 20:54:37 -0600 Subject: [PATCH 76/76] fix: before all --- overrides/Event/Value/ThrowableBuilder.php | 4 +++- src/Concerns/Testable.php | 11 ++++++++--- tests/.snapshots/success.txt | 7 ++++++- tests/Hooks/BeforeAllTest.php | 16 ++++++++++++++++ tests/Pest.php | 1 - tests/Visual/Parallel.php | 2 +- 6 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 tests/Hooks/BeforeAllTest.php diff --git a/overrides/Event/Value/ThrowableBuilder.php b/overrides/Event/Value/ThrowableBuilder.php index 21d428e9..d446d03c 100644 --- a/overrides/Event/Value/ThrowableBuilder.php +++ b/overrides/Event/Value/ThrowableBuilder.php @@ -52,6 +52,8 @@ use PHPUnit\Util\Filter; use PHPUnit\Util\ThrowableToStringMapper; /** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * * @internal This class is not covered by the backward compatibility promise for PHPUnit */ final readonly class ThrowableBuilder @@ -82,7 +84,7 @@ final readonly class ThrowableBuilder $t->getMessage(), ThrowableToStringMapper::map($t), $trace, - $previous + $previous, ); } } diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 326115ea..767a7c69 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -6,6 +6,7 @@ namespace Pest\Concerns; use Closure; use Pest\Exceptions\DatasetArgumentsMismatch; +use Pest\Panic; use Pest\Preset; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; @@ -194,7 +195,11 @@ trait Testable $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); } - call_user_func(Closure::bind($beforeAll, null, self::class)); + try { + call_user_func(Closure::bind($beforeAll, null, self::class)); + } catch (Throwable $e) { + Panic::with($e); + } } /** @@ -222,8 +227,6 @@ trait Testable $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $method->setUp($this); - $description = $method->description; if ($this->dataName()) { $description = str_contains((string) $description, ':dataset') @@ -287,6 +290,8 @@ trait Testable self::$__latestPrs = $method->prs; $this->__describing = $method->describing; $this->__test = $method->getClosure(); + + $method->setUp($this); } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index ace3b04f..a1638158 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1453,6 +1453,11 @@ ✓ nested → nested afterEach execution order ✓ global afterEach execution order + PASS Tests\Hooks\BeforeAllTest + ✓ it gets called before all tests 1 @ repetition 1 of 2 + ✓ it gets called before all tests 1 @ repetition 2 of 2 + ✓ it gets called before all tests 2 + PASS Tests\Hooks\BeforeEachTest ✓ global beforeEach execution order @@ -1726,4 +1731,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1154 passed (2754 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 33 skipped, 1157 passed (2766 assertions) \ No newline at end of file diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php new file mode 100644 index 00000000..d411d263 --- /dev/null +++ b/tests/Hooks/BeforeAllTest.php @@ -0,0 +1,16 @@ +beforeAll(function () { + expect($_SERVER['globalHook']->calls->beforeAll) + ->toBe(0); + + $_SERVER['globalHook']->calls->beforeAll++; +}); + +it('gets called before all tests 1', function () { + expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); +})->repeat(2); + +it('gets called before all tests 2', function () { + expect($_SERVER['globalHook']->calls->beforeAll)->toBe(1); +}); diff --git a/tests/Pest.php b/tests/Pest.php index a938fc7e..e498450c 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -29,7 +29,6 @@ pest() }) ->beforeAll(function () { $_SERVER['globalHook']->beforeAll = 0; - $_SERVER['globalHook']->calls->beforeAll++; }) ->afterEach(function () { if (! isset($this->ith)) { diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index d9cdfbdd..226a01e7 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, 1144 passed (2730 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 38 todos, 24 skipped, 1147 passed (2742 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows();