From 8442b9a6e441eaa479e33c6283685d9759f0c5da Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 13 May 2023 23:24:31 +0100 Subject: [PATCH 01/38] feat(describe): fixes missing `beforeAll` exception --- src/Repositories/BeforeAllRepository.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Repositories/BeforeAllRepository.php b/src/Repositories/BeforeAllRepository.php index 26e18c5f..51736b41 100644 --- a/src/Repositories/BeforeAllRepository.php +++ b/src/Repositories/BeforeAllRepository.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; -use Pest\Exceptions\BeforeEachAlreadyExist; +use Pest\Exceptions\BeforeAllAlreadyExist; use Pest\Support\NullClosure; use Pest\Support\Reflection; @@ -39,7 +39,7 @@ final class BeforeAllRepository $filename = Reflection::getFileNameFromClosure($closure); if (array_key_exists($filename, $this->state)) { - throw new BeforeEachAlreadyExist($filename); + throw new BeforeAllAlreadyExist($filename); } $this->state[$filename] = $closure; From 9c0e5ddfc6085de05547aa399d9a0b4dc6f76060 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 13 May 2023 23:25:20 +0100 Subject: [PATCH 02/38] feat(describe): adds missing `beforeAll` exception class --- src/Exceptions/BeforeAllAlreadyExist.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Exceptions/BeforeAllAlreadyExist.php diff --git a/src/Exceptions/BeforeAllAlreadyExist.php b/src/Exceptions/BeforeAllAlreadyExist.php new file mode 100644 index 00000000..a21389fe --- /dev/null +++ b/src/Exceptions/BeforeAllAlreadyExist.php @@ -0,0 +1,24 @@ + Date: Wed, 24 May 2023 23:21:15 +0100 Subject: [PATCH 03/38] feat(describe): improves logic around hooks --- src/Concerns/Testable.php | 19 ++-- src/Exceptions/AfterAllWithinDescribe.php | 24 +++++ src/Exceptions/BeforeAllWithinDescribe.php | 24 +++++ src/Factories/TestCaseMethodFactory.php | 6 ++ src/Functions.php | 36 +++++++ src/PendingCalls.php | 115 +++++++++++++++++++++ src/PendingCalls/AfterEachCall.php | 27 +++-- src/PendingCalls/BeforeEachCall.php | 39 +++++-- src/PendingCalls/Concerns/Describable.php | 13 +++ src/PendingCalls/DescribeCall.php | 46 +++++++++ src/PendingCalls/TestCall.php | 16 ++- src/Repositories/AfterEachRepository.php | 13 ++- src/Repositories/BeforeEachRepository.php | 13 ++- src/Support/ChainableClosure.php | 16 +++ tests/Features/Describe.php | 61 +++++++++++ 15 files changed, 433 insertions(+), 35 deletions(-) create mode 100644 src/Exceptions/AfterAllWithinDescribe.php create mode 100644 src/Exceptions/BeforeAllWithinDescribe.php create mode 100644 src/PendingCalls.php create mode 100644 src/PendingCalls/Concerns/Describable.php create mode 100644 src/PendingCalls/DescribeCall.php create mode 100644 tests/Features/Describe.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index ce020c3a..48da241f 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -23,14 +23,19 @@ use Throwable; trait Testable { /** - * Test method description. + * Test method's test description. */ - private string $__description; + private string $__testDescription; + + /** + * Test method's describe description, if any. + */ + public ?string $__describeDescription = null; /** * Test "latest" method description. */ - private static string $__latestDescription; + private static string $__latestTestDescription; /** * The Test Case "test" closure. @@ -77,7 +82,7 @@ trait Testable if ($test->hasMethod($name)) { $method = $test->getMethod($name); - $this->__description = self::$__latestDescription = $method->description; + $this->__testDescription = self::$__latestTestDescription = $method->description; $this->__test = $method->getClosure($this); } } @@ -230,7 +235,7 @@ trait Testable { $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $this->__description = self::$__latestDescription = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; + $this->__testDescription = self::$__latestTestDescription = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); @@ -315,7 +320,7 @@ trait Testable */ public function getPrintableTestCaseMethodName(): string { - return $this->__description; + return $this->__testDescription; } /** @@ -323,6 +328,6 @@ trait Testable */ public static function getLatestPrintableTestCaseMethodName(): string { - return self::$__latestDescription; + return self::$__latestTestDescription; } } diff --git a/src/Exceptions/AfterAllWithinDescribe.php b/src/Exceptions/AfterAllWithinDescribe.php new file mode 100644 index 00000000..4cdf2c2b --- /dev/null +++ b/src/Exceptions/AfterAllWithinDescribe.php @@ -0,0 +1,24 @@ +tests->get($this->filename); + $concrete->__describeDescription = $this->describing; // @phpstan-ignore-line $testCase->factoryProxies->proxy($concrete); $this->factoryProxies->proxy($concrete); diff --git a/src/Functions.php b/src/Functions.php index 0145219e..d9b15fdf 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,9 +2,13 @@ declare(strict_types=1); +use Pest\Exceptions\AfterAllWithinDescribe; +use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Expectation; +use Pest\PendingCalls; use Pest\PendingCalls\AfterEachCall; use Pest\PendingCalls\BeforeEachCall; +use Pest\PendingCalls\DescribeCall; use Pest\PendingCalls\TestCall; use Pest\PendingCalls\UsesCall; use Pest\Repositories\DatasetsRepository; @@ -35,6 +39,12 @@ if (! function_exists('beforeAll')) { */ function beforeAll(Closure $closure): void { + if (! is_null(PendingCalls::$describing)) { + $filename = Backtrace::file(); + + throw new BeforeAllWithinDescribe($filename); + } + TestSuite::getInstance()->beforeAll->set($closure); } } @@ -67,6 +77,26 @@ if (! function_exists('dataset')) { } } +if (! function_exists('describe')) { + /** + * Adds the given closure as a group of tests. The first argument + * is the group description; the second argument is a closure + * that contains the group tests. + * + * @return HigherOrderTapProxy|TestCall|TestCase|mixed + */ + function describe(string $description, Closure $tests): DescribeCall + { + $filename = Backtrace::testFile(); + + PendingCalls::startDescribe( + $describeCall = new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests), + ); + + return $describeCall; + } +} + if (! function_exists('uses')) { /** * The uses function binds the given @@ -159,6 +189,12 @@ if (! function_exists('afterAll')) { */ function afterAll(Closure $closure): void { + if (! is_null(PendingCalls::$describing)) { + $filename = Backtrace::file(); + + throw new AfterAllWithinDescribe($filename); + } + TestSuite::getInstance()->afterAll->set($closure); } } diff --git a/src/PendingCalls.php b/src/PendingCalls.php new file mode 100644 index 00000000..1fba1c55 --- /dev/null +++ b/src/PendingCalls.php @@ -0,0 +1,115 @@ + + */ + public static array $beforeEachCalls = []; + + /** + * The list of test pending calls. + * + * @var array + */ + public static array $testCalls = []; + + /** + * The list of after each pending calls. + * + * @var array + */ + public static array $afterEachCalls = []; + + /** + * Sets the current describe call. + */ + public static function startDescribe(DescribeCall $describeCall): void + { + self::$describing = $describeCall->description; + + ($describeCall->tests)(); + } + + /** + * Adds a new before each call. + */ + public static function beforeEach(BeforeEachCall $beforeEachCall, Closure $setter): void + { + $setter($beforeEachCall->describing = self::$describing); + } + + /** + * Adds a new test call. + */ + public static function test(TestCall $testCall, Closure $setter): void + { + if (! is_null($testCall->describing = self::$describing)) { + self::$testCalls[] = [$testCall, $setter]; + } else { + $setter(); + } + } + + /** + * Adds a new before each call. + */ + public static function afterEach(AfterEachCall $afterEachCall, Closure $setter): void + { + if (! is_null(self::$describing)) { + $afterEachCall->describing = self::$describing; + + self::$afterEachCalls[] = [$afterEachCall, $setter]; + } else { + $setter(); + } + } + + public static function endDescribe(DescribeCall $describeCall): void + { + $describing = self::$describing; + + self::$describing = null; + + foreach (self::$beforeEachCalls as [$beforeEachCall, $setter]) { + $setter($describing); + } + + self::$beforeEachCalls = []; + + foreach (self::$testCalls as [$testCall, $setter]) { + /** @var TestCall $testCall */ + $testCall->testCaseMethod->description = '`'.$describeCall->description.'` '.$testCall->testCaseMethod->description; + + $setter($describing); + } + + self::$testCalls = []; + + foreach (self::$afterEachCalls as [$afterEachCall, $setter]) { + $setter($describing); + } + + self::$afterEachCalls = []; + } +} diff --git a/src/PendingCalls/AfterEachCall.php b/src/PendingCalls/AfterEachCall.php index 585fa197..55da4381 100644 --- a/src/PendingCalls/AfterEachCall.php +++ b/src/PendingCalls/AfterEachCall.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; +use Pest\PendingCalls; +use Pest\PendingCalls\Concerns\Describable; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; use Pest\Support\HigherOrderMessageCollection; @@ -16,6 +18,8 @@ use Pest\TestSuite; */ final class AfterEachCall { + use Describable; + /** * The "afterEach" closure. */ @@ -44,14 +48,23 @@ final class AfterEachCall */ public function __destruct() { - $proxies = $this->proxies; + PendingCalls::afterEach($this, function (string $describing = null) { + $proxies = $this->proxies; + + $afterEachTestCase = ChainableClosure::when( + fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + ChainableClosure::from(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + )->bindTo($this, self::class); + + assert($afterEachTestCase instanceof Closure); + + $this->testSuite->afterEach->set( + $this->filename, + $this, + $afterEachTestCase, + ); + }); - $this->testSuite->afterEach->set( - $this->filename, - ChainableClosure::from(function () use ($proxies): void { - $proxies->chain($this); - }, $this->closure) - ); } /** diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index 2a2fb5f8..44aa85b3 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; +use Pest\PendingCalls; +use Pest\PendingCalls\Concerns\Describable; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; use Pest\Support\HigherOrderMessageCollection; @@ -16,6 +18,8 @@ use Pest\TestSuite; */ final class BeforeEachCall { + use Describable; + /** * Holds the before each closure. */ @@ -35,7 +39,7 @@ final class BeforeEachCall * Creates a new Pending Call. */ public function __construct( - private readonly TestSuite $testSuite, + public readonly TestSuite $testSuite, private readonly string $filename, Closure $closure = null ) { @@ -50,17 +54,29 @@ final class BeforeEachCall */ public function __destruct() { - $testCaseProxies = $this->testCaseProxies; + PendingCalls::beforeEach($this, function (string $describing = null) { + $testCaseProxies = $this->testCaseProxies; - $this->testSuite->beforeEach->set( - $this->filename, - function (TestCall $testCall): void { - $this->testCallProxies->chain($testCall); - }, - ChainableClosure::from(function () use ($testCaseProxies): void { - $testCaseProxies->chain($this); - }, $this->closure), - ); + $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { + if (is_null($describing) || ($this->describing === $testCall->describing && $testCall->describing === $describing)) { + $this->testCallProxies->chain($testCall); + } + }; + + $beforeEachTestCase = ChainableClosure::when( + fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + ChainableClosure::from(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + )->bindTo($this, self::class); + + assert($beforeEachTestCase instanceof Closure); + + $this->testSuite->beforeEach->set( + $this->filename, + $this, + $beforeEachTestCall, + $beforeEachTestCase, + ); + }); } /** @@ -70,6 +86,7 @@ final class BeforeEachCall */ public function __call(string $name, array $arguments): self { + if (method_exists(TestCall::class, $name)) { $this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments); diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php new file mode 100644 index 00000000..4eac9d2d --- /dev/null +++ b/src/PendingCalls/Concerns/Describable.php @@ -0,0 +1,13 @@ + $arguments + */ + public function __call(string $name, array $arguments): self + { + foreach (PendingCalls::$testCalls as [$testCall]) { + $testCall->{$name}(...$arguments); // @phpstan-ignore-line + } + + return $this; + } +} diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 99e45291..cb0359ad 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -10,6 +10,8 @@ use Pest\Factories\Covers\CoversClass; use Pest\Factories\Covers\CoversFunction; use Pest\Factories\Covers\CoversNothing; use Pest\Factories\TestCaseMethodFactory; +use Pest\PendingCalls; +use Pest\PendingCalls\Concerns\Describable; use Pest\Plugins\Only; use Pest\Support\Backtrace; use Pest\Support\Exporter; @@ -25,10 +27,12 @@ use PHPUnit\Framework\TestCase; */ final class TestCall { + use Describable; + /** * The Test Case Factory. */ - private readonly TestCaseMethodFactory $testCaseMethod; + public readonly TestCaseMethodFactory $testCaseMethod; /** * If test call is descriptionLess. @@ -48,7 +52,7 @@ final class TestCall $this->descriptionLess = $description === null; - $this->testSuite->beforeEach->get($filename)[0]($this); + $this->testSuite->beforeEach->get($this->filename)[0]($this); } /** @@ -316,12 +320,14 @@ final class TestCall private function addChain(string $file, int $line, string $name, array $arguments = null): self { $exporter = Exporter::default(); + $this->testCaseMethod ->chains ->add($file, $line, $name, $arguments); if ($this->descriptionLess) { Exporter::default(); + if ($this->testCaseMethod->description !== null) { $this->testCaseMethod->description .= ' → '; } @@ -338,6 +344,10 @@ final class TestCall */ public function __destruct() { - $this->testSuite->tests->set($this->testCaseMethod); + PendingCalls::test($this, function () { + $this->testCaseMethod->describing = $this->describing; + + $this->testSuite->tests->set($this->testCaseMethod); + }); } } diff --git a/src/Repositories/AfterEachRepository.php b/src/Repositories/AfterEachRepository.php index affc4493..b5a55135 100644 --- a/src/Repositories/AfterEachRepository.php +++ b/src/Repositories/AfterEachRepository.php @@ -6,7 +6,7 @@ namespace Pest\Repositories; use Closure; use Mockery; -use Pest\Exceptions\AfterEachAlreadyExist; +use Pest\PendingCalls\AfterEachCall; use Pest\Support\ChainableClosure; use Pest\Support\NullClosure; @@ -23,13 +23,18 @@ final class AfterEachRepository /** * Sets a after each closure. */ - public function set(string $filename, Closure $closure): void + public function set(string $filename, AfterEachCall $afterEachCall, Closure $afterEachTestCase): void { if (array_key_exists($filename, $this->state)) { - throw new AfterEachAlreadyExist($filename); + $fromAfterEachTestCase = $this->state[$filename]; + + $afterEachTestCase = ChainableClosure::from($fromAfterEachTestCase, $afterEachTestCase) + ->bindTo($afterEachCall, $afterEachCall::class); } - $this->state[$filename] = $closure; + assert($afterEachTestCase instanceof Closure); + + $this->state[$filename] = $afterEachTestCase; } /** diff --git a/src/Repositories/BeforeEachRepository.php b/src/Repositories/BeforeEachRepository.php index e74f78ed..d5454d32 100644 --- a/src/Repositories/BeforeEachRepository.php +++ b/src/Repositories/BeforeEachRepository.php @@ -5,7 +5,8 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; -use Pest\Exceptions\BeforeEachAlreadyExist; +use Pest\PendingCalls\BeforeEachCall; +use Pest\Support\ChainableClosure; use Pest\Support\NullClosure; /** @@ -21,12 +22,18 @@ final class BeforeEachRepository /** * Sets a before each closure. */ - public function set(string $filename, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void + public function set(string $filename, BeforeEachCall $beforeEachCall, Closure $beforeEachTestCall, Closure $beforeEachTestCase): void { if (array_key_exists($filename, $this->state)) { - throw new BeforeEachAlreadyExist($filename); + [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename]; + + $beforeEachTestCall = ChainableClosure::from($fromBeforeEachTestCall, $beforeEachTestCall)->bindTo($beforeEachCall, $beforeEachCall::class); + $beforeEachTestCase = ChainableClosure::from($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); } + assert($beforeEachTestCall instanceof Closure); + assert($beforeEachTestCase instanceof Closure); + $this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase]; } diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 55dcca3a..7acc0953 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -12,6 +12,22 @@ use Pest\Exceptions\ShouldNotHappen; */ final class ChainableClosure { + /** + * Calls the given `$closure` when the given condition is true. + */ + public static function when(Closure $condition, Closure $next): Closure + { + return function () use ($condition, $next): void { + if (! is_object($this)) { // @phpstan-ignore-line + throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); + } + + if (\Pest\Support\Closure::bind($condition, $this, self::class)(...func_get_args())) { + \Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args()); + } + }; + } + /** * Calls the given `$closure` and chains the `$next` closure. */ diff --git a/tests/Features/Describe.php b/tests/Features/Describe.php new file mode 100644 index 00000000..f3740abe --- /dev/null +++ b/tests/Features/Describe.php @@ -0,0 +1,61 @@ + $this->count = 1); + +test('before each', function () { + expect($this->count)->toBe(1); +}); + +describe('describable', function () { + beforeEach(function () { + $this->count++; + }); + + test('basic', function () { + expect(true)->toBeTrue(); + }); + + test('before each', function () { + expect($this->count)->toBe(2); + }); + + afterEach(function () { + expect($this->count)->toBe(2); + }); +}); + +describe('another describable', function () { + beforeEach(function () { + $this->count++; + }); + + test('basic', function () { + expect(true)->toBeTrue(); + }); + + test('before each', function () { + expect($this->count)->toBe(2); + }); + + afterEach(function () { + expect($this->count)->toBe(2); + }); +}); + +test('should not fail')->todo()->shouldNotRun(); + +test('previous describable before each does not get applied here', function () { + expect($this->count)->toBe(1); +}); + +afterEach(fn () => expect($this->count)->toBe(is_null($this->__describeDescription) ? 1 : 2)); + +describe('todo', function () { + beforeEach()->todo(); + + test('should not fail')->shouldNotRun(); +}); + +describe('todo after describe', function () { + test('should not fail')->shouldNotRun(); +})->todo(); From 3e8616ec647ce0e101892cc3d31d4e6ffc758b10 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 19:29:46 +0100 Subject: [PATCH 04/38] feat(describe): continues work around hooks --- src/Concerns/Testable.php | 6 +- src/Functions.php | 11 +-- src/PendingCalls.php | 91 +---------------------- src/PendingCalls/AfterEachCall.php | 29 ++++---- src/PendingCalls/BeforeEachCall.php | 41 +++++----- src/PendingCalls/DescribeCall.php | 38 ++++++++-- src/PendingCalls/TestCall.php | 10 ++- src/Repositories/AfterEachRepository.php | 4 +- src/Repositories/BeforeEachRepository.php | 4 +- src/Support/ChainableClosure.php | 13 +++- tests/Features/Describe.php | 57 +++++++++----- 11 files changed, 132 insertions(+), 172 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 48da241f..9c1f716d 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -141,7 +141,7 @@ trait Testable } $this->{$property} = ($this->{$property} instanceof Closure) - ? ChainableClosure::from($this->{$property}, $hook) + ? ChainableClosure::fromSameObject($this->{$property}, $hook) : $hook; } @@ -189,7 +189,7 @@ trait Testable $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1]; if ($this->__beforeEach instanceof Closure) { - $beforeEach = ChainableClosure::from($this->__beforeEach, $beforeEach); + $beforeEach = ChainableClosure::fromSameObject($this->__beforeEach, $beforeEach); } $this->__callClosure($beforeEach, func_get_args()); @@ -203,7 +203,7 @@ trait Testable $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); if ($this->__afterEach instanceof Closure) { - $afterEach = ChainableClosure::from($this->__afterEach, $afterEach); + $afterEach = ChainableClosure::fromSameObject($this->__afterEach, $afterEach); } $this->__callClosure($afterEach, func_get_args()); diff --git a/src/Functions.php b/src/Functions.php index d9b15fdf..03fa778f 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -5,7 +5,6 @@ declare(strict_types=1); use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Expectation; -use Pest\PendingCalls; use Pest\PendingCalls\AfterEachCall; use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\DescribeCall; @@ -39,7 +38,7 @@ if (! function_exists('beforeAll')) { */ function beforeAll(Closure $closure): void { - if (! is_null(PendingCalls::$describing)) { + if (! is_null(DescribeCall::describing())) { $filename = Backtrace::file(); throw new BeforeAllWithinDescribe($filename); @@ -89,11 +88,7 @@ if (! function_exists('describe')) { { $filename = Backtrace::testFile(); - PendingCalls::startDescribe( - $describeCall = new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests), - ); - - return $describeCall; + return new DescribeCall(TestSuite::getInstance(), $filename, $description, $tests); } } @@ -189,7 +184,7 @@ if (! function_exists('afterAll')) { */ function afterAll(Closure $closure): void { - if (! is_null(PendingCalls::$describing)) { + if (! is_null(DescribeCall::describing())) { $filename = Backtrace::file(); throw new AfterAllWithinDescribe($filename); diff --git a/src/PendingCalls.php b/src/PendingCalls.php index 1fba1c55..886166bf 100644 --- a/src/PendingCalls.php +++ b/src/PendingCalls.php @@ -4,11 +4,7 @@ declare(strict_types=1); namespace Pest; -use Closure; -use Pest\PendingCalls\AfterEachCall; -use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\DescribeCall; -use Pest\PendingCalls\TestCall; /** * @internal @@ -20,96 +16,11 @@ final class PendingCalls */ public static ?string $describing = null; - /** - * The list of before each pending calls. - * - * @var array - */ - public static array $beforeEachCalls = []; - - /** - * The list of test pending calls. - * - * @var array - */ - public static array $testCalls = []; - - /** - * The list of after each pending calls. - * - * @var array - */ - public static array $afterEachCalls = []; - /** * Sets the current describe call. */ - public static function startDescribe(DescribeCall $describeCall): void + public static function describe(DescribeCall $describeCall): void { - self::$describing = $describeCall->description; - ($describeCall->tests)(); - } - - /** - * Adds a new before each call. - */ - public static function beforeEach(BeforeEachCall $beforeEachCall, Closure $setter): void - { - $setter($beforeEachCall->describing = self::$describing); - } - - /** - * Adds a new test call. - */ - public static function test(TestCall $testCall, Closure $setter): void - { - if (! is_null($testCall->describing = self::$describing)) { - self::$testCalls[] = [$testCall, $setter]; - } else { - $setter(); - } - } - - /** - * Adds a new before each call. - */ - public static function afterEach(AfterEachCall $afterEachCall, Closure $setter): void - { - if (! is_null(self::$describing)) { - $afterEachCall->describing = self::$describing; - - self::$afterEachCalls[] = [$afterEachCall, $setter]; - } else { - $setter(); - } - } - - public static function endDescribe(DescribeCall $describeCall): void - { - $describing = self::$describing; - - self::$describing = null; - - foreach (self::$beforeEachCalls as [$beforeEachCall, $setter]) { - $setter($describing); - } - - self::$beforeEachCalls = []; - - foreach (self::$testCalls as [$testCall, $setter]) { - /** @var TestCall $testCall */ - $testCall->testCaseMethod->description = '`'.$describeCall->description.'` '.$testCall->testCaseMethod->description; - - $setter($describing); - } - - self::$testCalls = []; - - foreach (self::$afterEachCalls as [$afterEachCall, $setter]) { - $setter($describing); - } - - self::$afterEachCalls = []; } } diff --git a/src/PendingCalls/AfterEachCall.php b/src/PendingCalls/AfterEachCall.php index 55da4381..5388eedf 100644 --- a/src/PendingCalls/AfterEachCall.php +++ b/src/PendingCalls/AfterEachCall.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; -use Pest\PendingCalls; use Pest\PendingCalls\Concerns\Describable; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; @@ -41,6 +40,8 @@ final class AfterEachCall $this->closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->proxies = new HigherOrderMessageCollection(); + + $this->describing = DescribeCall::describing(); } /** @@ -48,22 +49,22 @@ final class AfterEachCall */ public function __destruct() { - PendingCalls::afterEach($this, function (string $describing = null) { - $proxies = $this->proxies; + $describing = $this->describing; - $afterEachTestCase = ChainableClosure::when( - fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line - ChainableClosure::from(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line - )->bindTo($this, self::class); + $proxies = $this->proxies; - assert($afterEachTestCase instanceof Closure); + $afterEachTestCase = ChainableClosure::when( + fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + ChainableClosure::fromSameObject(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + )->bindTo($this, self::class); - $this->testSuite->afterEach->set( - $this->filename, - $this, - $afterEachTestCase, - ); - }); + assert($afterEachTestCase instanceof Closure); + + $this->testSuite->afterEach->set( + $this->filename, + $this, + $afterEachTestCase, + ); } diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index 44aa85b3..b9c88f8f 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; -use Pest\PendingCalls; use Pest\PendingCalls\Concerns\Describable; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; @@ -47,6 +46,8 @@ final class BeforeEachCall $this->testCallProxies = new HigherOrderMessageCollection(); $this->testCaseProxies = new HigherOrderMessageCollection(); + + $this->describing = DescribeCall::describing(); } /** @@ -54,29 +55,28 @@ final class BeforeEachCall */ public function __destruct() { - PendingCalls::beforeEach($this, function (string $describing = null) { - $testCaseProxies = $this->testCaseProxies; + $describing = $this->describing; + $testCaseProxies = $this->testCaseProxies; - $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { - if (is_null($describing) || ($this->describing === $testCall->describing && $testCall->describing === $describing)) { - $this->testCallProxies->chain($testCall); - } - }; + $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { + if ($describing === $this->describing && $describing === $testCall->describing) { + $this->testCallProxies->chain($testCall); + } + }; - $beforeEachTestCase = ChainableClosure::when( - fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line - ChainableClosure::from(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line - )->bindTo($this, self::class); + $beforeEachTestCase = ChainableClosure::when( + fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + ChainableClosure::fromSameObject(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + )->bindTo($this, self::class); - assert($beforeEachTestCase instanceof Closure); + assert($beforeEachTestCase instanceof Closure); - $this->testSuite->beforeEach->set( - $this->filename, - $this, - $beforeEachTestCall, - $beforeEachTestCase, - ); - }); + $this->testSuite->beforeEach->set( + $this->filename, + $this, + $beforeEachTestCall, + $beforeEachTestCase, + ); } /** @@ -86,7 +86,6 @@ final class BeforeEachCall */ public function __call(string $name, array $arguments): self { - if (method_exists(TestCall::class, $name)) { $this->testCallProxies->add(Backtrace::file(), Backtrace::line(), $name, $arguments); diff --git a/src/PendingCalls/DescribeCall.php b/src/PendingCalls/DescribeCall.php index 0f1f217b..f1e503ee 100644 --- a/src/PendingCalls/DescribeCall.php +++ b/src/PendingCalls/DescribeCall.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; -use Pest\PendingCalls; +use Pest\Support\Backtrace; use Pest\TestSuite; /** @@ -13,6 +13,11 @@ use Pest\TestSuite; */ final class DescribeCall { + /** + * The current describe call. + */ + private static ?string $describing = null; + /** * Creates a new Pending Call. */ @@ -25,9 +30,26 @@ final class DescribeCall // } + /** + * What is the current describing. + */ + public static function describing(): ?string + { + return self::$describing; + } + + /** + * Creates the Call. + */ public function __destruct() { - PendingCalls::endDescribe($this); + self::$describing = $this->description; + + try { + ($this->tests)(); + } finally { + self::$describing = null; + } } /** @@ -35,12 +57,14 @@ final class DescribeCall * * @param array $arguments */ - public function __call(string $name, array $arguments): self + public function __call(string $name, array $arguments): BeforeEachCall { - foreach (PendingCalls::$testCalls as [$testCall]) { - $testCall->{$name}(...$arguments); // @phpstan-ignore-line - } + $filename = Backtrace::file(); - return $this; + $beforeEachCall = new BeforeEachCall(TestSuite::getInstance(), $filename); + + $beforeEachCall->describing = $this->description; + + return $beforeEachCall->{$name}(...$arguments); } } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index cb0359ad..649cdffd 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -10,7 +10,6 @@ use Pest\Factories\Covers\CoversClass; use Pest\Factories\Covers\CoversFunction; use Pest\Factories\Covers\CoversNothing; use Pest\Factories\TestCaseMethodFactory; -use Pest\PendingCalls; use Pest\PendingCalls\Concerns\Describable; use Pest\Plugins\Only; use Pest\Support\Backtrace; @@ -52,6 +51,8 @@ final class TestCall $this->descriptionLess = $description === null; + $this->describing = DescribeCall::describing(); + $this->testSuite->beforeEach->get($this->filename)[0]($this); } @@ -344,10 +345,11 @@ final class TestCall */ public function __destruct() { - PendingCalls::test($this, function () { + if ($this->describing) { + $this->testCaseMethod->description = '`'.$this->describing.'` '.$this->testCaseMethod->description; $this->testCaseMethod->describing = $this->describing; + } - $this->testSuite->tests->set($this->testCaseMethod); - }); + $this->testSuite->tests->set($this->testCaseMethod); } } diff --git a/src/Repositories/AfterEachRepository.php b/src/Repositories/AfterEachRepository.php index b5a55135..a3e823b2 100644 --- a/src/Repositories/AfterEachRepository.php +++ b/src/Repositories/AfterEachRepository.php @@ -28,7 +28,7 @@ final class AfterEachRepository if (array_key_exists($filename, $this->state)) { $fromAfterEachTestCase = $this->state[$filename]; - $afterEachTestCase = ChainableClosure::from($fromAfterEachTestCase, $afterEachTestCase) + $afterEachTestCase = ChainableClosure::fromSameObject($fromAfterEachTestCase, $afterEachTestCase) ->bindTo($afterEachCall, $afterEachCall::class); } @@ -44,7 +44,7 @@ final class AfterEachRepository { $afterEach = $this->state[$filename] ?? NullClosure::create(); - return ChainableClosure::from(function (): void { + return ChainableClosure::fromSameObject(function (): void { if (class_exists(Mockery::class)) { if ($container = Mockery::getContainer()) { /* @phpstan-ignore-next-line */ diff --git a/src/Repositories/BeforeEachRepository.php b/src/Repositories/BeforeEachRepository.php index d5454d32..02fcc8a0 100644 --- a/src/Repositories/BeforeEachRepository.php +++ b/src/Repositories/BeforeEachRepository.php @@ -27,8 +27,8 @@ final class BeforeEachRepository if (array_key_exists($filename, $this->state)) { [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename]; - $beforeEachTestCall = ChainableClosure::from($fromBeforeEachTestCall, $beforeEachTestCall)->bindTo($beforeEachCall, $beforeEachCall::class); - $beforeEachTestCase = ChainableClosure::from($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); + $beforeEachTestCall = ChainableClosure::fromDifferentObjects($fromBeforeEachTestCall, $beforeEachTestCall); + $beforeEachTestCase = ChainableClosure::fromSameObject($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); } assert($beforeEachTestCall instanceof Closure); diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 7acc0953..f5f27c66 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -31,7 +31,7 @@ final class ChainableClosure /** * Calls the given `$closure` and chains the `$next` closure. */ - public static function from(Closure $closure, Closure $next): Closure + public static function fromSameObject(Closure $closure, Closure $next): Closure { return function () use ($closure, $next): void { if (! is_object($this)) { // @phpstan-ignore-line @@ -43,6 +43,17 @@ final class ChainableClosure }; } + /** + * Calls the given `$closure` and chains the `$next` closure. + */ + public static function fromDifferentObjects(Closure $closure, Closure $next): Closure + { + return function () use ($closure, $next): void { + $closure(...func_get_args()); + $next(...func_get_args()); + }; + } + /** * Call the given static `$closure` and chains the `$next` closure. */ diff --git a/tests/Features/Describe.php b/tests/Features/Describe.php index f3740abe..6173a4dd 100644 --- a/tests/Features/Describe.php +++ b/tests/Features/Describe.php @@ -6,56 +6,73 @@ test('before each', function () { expect($this->count)->toBe(1); }); -describe('describable', function () { +describe('hooks', function () { beforeEach(function () { $this->count++; }); - test('basic', function () { - expect(true)->toBeTrue(); - }); - - test('before each', function () { + test('value', function () { expect($this->count)->toBe(2); + $this->count++; }); afterEach(function () { - expect($this->count)->toBe(2); + expect($this->count)->toBe(3); }); }); -describe('another describable', function () { +describe('hooks in different orders', function () { beforeEach(function () { $this->count++; }); - test('basic', function () { - expect(true)->toBeTrue(); - }); - - test('before each', function () { - expect($this->count)->toBe(2); + test('value', function () { + expect($this->count)->toBe(3); + $this->count++; }); afterEach(function () { - expect($this->count)->toBe(2); + expect($this->count)->toBe(4); + }); + + beforeEach(function () { + $this->count++; }); }); -test('should not fail')->todo()->shouldNotRun(); +test('todo')->todo()->shouldNotRun(); test('previous describable before each does not get applied here', function () { expect($this->count)->toBe(1); }); -afterEach(fn () => expect($this->count)->toBe(is_null($this->__describeDescription) ? 1 : 2)); - -describe('todo', function () { +describe('todo on hook', function () { beforeEach()->todo(); test('should not fail')->shouldNotRun(); + test('should run')->expect(true)->toBeTrue(); }); -describe('todo after describe', function () { +describe('todo on describe', function () { test('should not fail')->shouldNotRun(); + + test('should run')->expect(true)->toBeTrue(); })->todo(); + +test('should run')->expect(true)->toBeTrue(); + +test('with', fn ($foo) => expect($foo)->toBe(1))->with([1]); + +describe('with on hook', function () { + beforeEach()->with([2]); + + test('value', function ($foo) { + expect($foo)->toBe(2); + }); +}); + +describe('with on describe', function () { + test('value', function ($foo) { + expect($foo)->toBe(3); + }); +})->with([3]); From 68ea2c7d7e7980d19939cde0202844c7021b067c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 19:56:10 +0100 Subject: [PATCH 05/38] feat(describe): refactor --- src/Concerns/Testable.php | 31 ++++++++++++----------- src/Exceptions/AfterEachAlreadyExist.php | 24 ------------------ src/Exceptions/BeforeEachAlreadyExist.php | 24 ------------------ src/Factories/TestCaseMethodFactory.php | 17 ++++++------- src/PendingCalls.php | 8 ------ src/PendingCalls/AfterEachCall.php | 2 +- src/PendingCalls/BeforeEachCall.php | 12 ++++++--- 7 files changed, 33 insertions(+), 85 deletions(-) delete mode 100644 src/Exceptions/AfterEachAlreadyExist.php delete mode 100644 src/Exceptions/BeforeEachAlreadyExist.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 9c1f716d..1b248cc1 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -23,42 +23,42 @@ use Throwable; trait Testable { /** - * Test method's test description. + * The test's description. */ - private string $__testDescription; + private string $__description; /** - * Test method's describe description, if any. + * The test's latest description. */ - public ?string $__describeDescription = null; + private static string $__latestDescription; /** - * Test "latest" method description. + * The test's describing, if any. */ - private static string $__latestTestDescription; + public ?string $__describing = null; /** - * The Test Case "test" closure. + * The test's test closure. */ private Closure $__test; /** - * The Test Case "setUp" closure. + * The test's before each closure. */ private ?Closure $__beforeEach = null; /** - * The Test Case "tearDown" closure. + * The test's after each closure. */ private ?Closure $__afterEach = null; /** - * The Test Case "setUpBeforeClass" closure. + * The test's before all closure. */ private static ?Closure $__beforeAll = null; /** - * The test "tearDownAfterClass" closure. + * The test's after all closure. */ private static ?Closure $__afterAll = null; @@ -82,7 +82,8 @@ trait Testable if ($test->hasMethod($name)) { $method = $test->getMethod($name); - $this->__testDescription = self::$__latestTestDescription = $method->description; + $this->__description = self::$__latestDescription = $method->description; + $this->__describing = $method->describing; $this->__test = $method->getClosure($this); } } @@ -235,7 +236,7 @@ trait Testable { $method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name()); - $this->__testDescription = self::$__latestTestDescription = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; + $this->__description = self::$__latestDescription = $this->dataName() ? $method->description.' with '.$this->dataName() : $method->description; $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); @@ -320,7 +321,7 @@ trait Testable */ public function getPrintableTestCaseMethodName(): string { - return $this->__testDescription; + return $this->__description; } /** @@ -328,6 +329,6 @@ trait Testable */ public static function getLatestPrintableTestCaseMethodName(): string { - return self::$__latestTestDescription; + return self::$__latestDescription; } } diff --git a/src/Exceptions/AfterEachAlreadyExist.php b/src/Exceptions/AfterEachAlreadyExist.php deleted file mode 100644 index cbdd4a7c..00000000 --- a/src/Exceptions/AfterEachAlreadyExist.php +++ /dev/null @@ -1,24 +0,0 @@ -|string> */ public array $datasets = []; /** - * The Test Case depends, if any. + * The test's dependencies. * * @var array */ public array $depends = []; /** - * The Test Case groups, if any. + * The test's groups. * * @var array */ public array $groups = []; /** - * The covered classes and functions, if any. + * The covered classes and functions. * * @var array */ public array $covers = []; /** - * Creates a new Factory instance. + * Creates a new test case method factory instance. */ public function __construct( public string $filename, @@ -75,7 +75,7 @@ final class TestCaseMethodFactory } /** - * Makes the Test Case classes. + * Creates the test's closure. */ public function getClosure(TestCase $concrete): Closure { @@ -89,7 +89,6 @@ final class TestCaseMethodFactory $testCase = TestSuite::getInstance()->tests->get($this->filename); - $concrete->__describeDescription = $this->describing; // @phpstan-ignore-line $testCase->factoryProxies->proxy($concrete); $this->factoryProxies->proxy($concrete); diff --git a/src/PendingCalls.php b/src/PendingCalls.php index 886166bf..b5cbf04d 100644 --- a/src/PendingCalls.php +++ b/src/PendingCalls.php @@ -15,12 +15,4 @@ final class PendingCalls * The current describe call. */ public static ?string $describing = null; - - /** - * Sets the current describe call. - */ - public static function describe(DescribeCall $describeCall): void - { - - } } diff --git a/src/PendingCalls/AfterEachCall.php b/src/PendingCalls/AfterEachCall.php index 5388eedf..32ec937d 100644 --- a/src/PendingCalls/AfterEachCall.php +++ b/src/PendingCalls/AfterEachCall.php @@ -54,7 +54,7 @@ final class AfterEachCall $proxies = $this->proxies; $afterEachTestCase = ChainableClosure::when( - fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line ChainableClosure::fromSameObject(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index b9c88f8f..7543cf39 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -58,14 +58,18 @@ final class BeforeEachCall $describing = $this->describing; $testCaseProxies = $this->testCaseProxies; - $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { - if ($describing === $this->describing && $describing === $testCall->describing) { - $this->testCallProxies->chain($testCall); + $beforeEachTestCall = function (TestCall $testCall) use ($describing) : void { + if ($describing !== $this->describing) { + return; } + if ($describing !== $testCall->describing) { + return; + } + $this->testCallProxies->chain($testCall); }; $beforeEachTestCase = ChainableClosure::when( - fn () => is_null($describing) || $this->__describeDescription === $describing, // @phpstan-ignore-line + fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line ChainableClosure::fromSameObject(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); From 551fa0141547423246f45dcabbe3c1ea0e724554 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 20:01:55 +0100 Subject: [PATCH 06/38] feat(describe): more refactor --- src/Concerns/Testable.php | 6 +++--- src/PendingCalls.php | 18 ------------------ src/PendingCalls/AfterEachCall.php | 2 +- src/PendingCalls/BeforeEachCall.php | 4 ++-- src/Repositories/AfterEachRepository.php | 4 ++-- src/Repositories/BeforeEachRepository.php | 4 ++-- src/Support/ChainableClosure.php | 8 ++++---- 7 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 src/PendingCalls.php diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 1b248cc1..5c9e8b1f 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -142,7 +142,7 @@ trait Testable } $this->{$property} = ($this->{$property} instanceof Closure) - ? ChainableClosure::fromSameObject($this->{$property}, $hook) + ? ChainableClosure::bound($this->{$property}, $hook) : $hook; } @@ -190,7 +190,7 @@ trait Testable $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename)[1]; if ($this->__beforeEach instanceof Closure) { - $beforeEach = ChainableClosure::fromSameObject($this->__beforeEach, $beforeEach); + $beforeEach = ChainableClosure::bound($this->__beforeEach, $beforeEach); } $this->__callClosure($beforeEach, func_get_args()); @@ -204,7 +204,7 @@ trait Testable $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); if ($this->__afterEach instanceof Closure) { - $afterEach = ChainableClosure::fromSameObject($this->__afterEach, $afterEach); + $afterEach = ChainableClosure::bound($this->__afterEach, $afterEach); } $this->__callClosure($afterEach, func_get_args()); diff --git a/src/PendingCalls.php b/src/PendingCalls.php deleted file mode 100644 index b5cbf04d..00000000 --- a/src/PendingCalls.php +++ /dev/null @@ -1,18 +0,0 @@ - is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line - ChainableClosure::fromSameObject(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); assert($afterEachTestCase instanceof Closure); diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index 7543cf39..b51dafd2 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -58,7 +58,7 @@ final class BeforeEachCall $describing = $this->describing; $testCaseProxies = $this->testCaseProxies; - $beforeEachTestCall = function (TestCall $testCall) use ($describing) : void { + $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { if ($describing !== $this->describing) { return; } @@ -70,7 +70,7 @@ final class BeforeEachCall $beforeEachTestCase = ChainableClosure::when( fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line - ChainableClosure::fromSameObject(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line + ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); assert($beforeEachTestCase instanceof Closure); diff --git a/src/Repositories/AfterEachRepository.php b/src/Repositories/AfterEachRepository.php index a3e823b2..ef88f375 100644 --- a/src/Repositories/AfterEachRepository.php +++ b/src/Repositories/AfterEachRepository.php @@ -28,7 +28,7 @@ final class AfterEachRepository if (array_key_exists($filename, $this->state)) { $fromAfterEachTestCase = $this->state[$filename]; - $afterEachTestCase = ChainableClosure::fromSameObject($fromAfterEachTestCase, $afterEachTestCase) + $afterEachTestCase = ChainableClosure::bound($fromAfterEachTestCase, $afterEachTestCase) ->bindTo($afterEachCall, $afterEachCall::class); } @@ -44,7 +44,7 @@ final class AfterEachRepository { $afterEach = $this->state[$filename] ?? NullClosure::create(); - return ChainableClosure::fromSameObject(function (): void { + return ChainableClosure::bound(function (): void { if (class_exists(Mockery::class)) { if ($container = Mockery::getContainer()) { /* @phpstan-ignore-next-line */ diff --git a/src/Repositories/BeforeEachRepository.php b/src/Repositories/BeforeEachRepository.php index 02fcc8a0..3a923df7 100644 --- a/src/Repositories/BeforeEachRepository.php +++ b/src/Repositories/BeforeEachRepository.php @@ -27,8 +27,8 @@ final class BeforeEachRepository if (array_key_exists($filename, $this->state)) { [$fromBeforeEachTestCall, $fromBeforeEachTestCase] = $this->state[$filename]; - $beforeEachTestCall = ChainableClosure::fromDifferentObjects($fromBeforeEachTestCall, $beforeEachTestCall); - $beforeEachTestCase = ChainableClosure::fromSameObject($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); + $beforeEachTestCall = ChainableClosure::unbound($fromBeforeEachTestCall, $beforeEachTestCall); + $beforeEachTestCase = ChainableClosure::bound($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); } assert($beforeEachTestCall instanceof Closure); diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index f5f27c66..b0257d11 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -29,9 +29,9 @@ final class ChainableClosure } /** - * Calls the given `$closure` and chains the `$next` closure. + * Calls the given `$closure` and chains the `$next` closure, "bound" to the same object. */ - public static function fromSameObject(Closure $closure, Closure $next): Closure + public static function bound(Closure $closure, Closure $next): Closure { return function () use ($closure, $next): void { if (! is_object($this)) { // @phpstan-ignore-line @@ -44,9 +44,9 @@ final class ChainableClosure } /** - * Calls the given `$closure` and chains the `$next` closure. + * Calls the given `$closure` and chains the `$next` closure, "unbound" of any object. */ - public static function fromDifferentObjects(Closure $closure, Closure $next): Closure + public static function unbound(Closure $closure, Closure $next): Closure { return function () use ($closure, $next): void { $closure(...func_get_args()); From 0ae08876659e31365fb79216664d7adb1d11a300 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 20:04:50 +0100 Subject: [PATCH 07/38] feat(describe): more refactors --- src/Concerns/Testable.php | 8 ++++---- src/PendingCalls/AfterEachCall.php | 2 +- src/PendingCalls/BeforeEachCall.php | 2 +- src/Support/ChainableClosure.php | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 5c9e8b1f..273dfb75 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -98,7 +98,7 @@ trait Testable } self::$__beforeAll = (self::$__beforeAll instanceof Closure) - ? ChainableClosure::fromStatic(self::$__beforeAll, $hook) + ? ChainableClosure::boundStatically(self::$__beforeAll, $hook) : $hook; } @@ -112,7 +112,7 @@ trait Testable } self::$__afterAll = (self::$__afterAll instanceof Closure) - ? ChainableClosure::fromStatic(self::$__afterAll, $hook) + ? ChainableClosure::boundStatically(self::$__afterAll, $hook) : $hook; } @@ -156,7 +156,7 @@ trait Testable $beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); if (self::$__beforeAll instanceof Closure) { - $beforeAll = ChainableClosure::fromStatic(self::$__beforeAll, $beforeAll); + $beforeAll = ChainableClosure::boundStatically(self::$__beforeAll, $beforeAll); } call_user_func(Closure::bind($beforeAll, null, self::class)); @@ -170,7 +170,7 @@ trait Testable $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); if (self::$__afterAll instanceof Closure) { - $afterAll = ChainableClosure::fromStatic(self::$__afterAll, $afterAll); + $afterAll = ChainableClosure::boundStatically(self::$__afterAll, $afterAll); } call_user_func(Closure::bind($afterAll, null, self::class)); diff --git a/src/PendingCalls/AfterEachCall.php b/src/PendingCalls/AfterEachCall.php index 78c5a3c0..9a22258e 100644 --- a/src/PendingCalls/AfterEachCall.php +++ b/src/PendingCalls/AfterEachCall.php @@ -53,7 +53,7 @@ final class AfterEachCall $proxies = $this->proxies; - $afterEachTestCase = ChainableClosure::when( + $afterEachTestCase = ChainableClosure::boundWhen( fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index b51dafd2..200c4fa9 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -68,7 +68,7 @@ final class BeforeEachCall $this->testCallProxies->chain($testCall); }; - $beforeEachTestCase = ChainableClosure::when( + $beforeEachTestCase = ChainableClosure::boundWhen( fn (): bool => is_null($describing) || $this->__describing === $describing, // @phpstan-ignore-line ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), // @phpstan-ignore-line )->bindTo($this, self::class); diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index b0257d11..b012e907 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -13,9 +13,9 @@ use Pest\Exceptions\ShouldNotHappen; final class ChainableClosure { /** - * Calls the given `$closure` when the given condition is true. + * Calls the given `$closure` when the given condition is true, "bound" to the same object. */ - public static function when(Closure $condition, Closure $next): Closure + public static function boundWhen(Closure $condition, Closure $next): Closure { return function () use ($condition, $next): void { if (! is_object($this)) { // @phpstan-ignore-line @@ -55,9 +55,9 @@ final class ChainableClosure } /** - * Call the given static `$closure` and chains the `$next` closure. + * Call the given static `$closure` and chains the `$next` closure, "bound" to the same object statically. */ - public static function fromStatic(Closure $closure, Closure $next): Closure + public static function boundStatically(Closure $closure, Closure $next): Closure { return static function () use ($closure, $next): void { \Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args()); From bc951787d3ff391a128da7338f00423801140c74 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 20:18:52 +0100 Subject: [PATCH 08/38] feat(describe): snapshots --- src/PendingCalls/DescribeCall.php | 2 +- src/PendingCalls/TestCall.php | 4 ++-- src/Repositories/BeforeEachRepository.php | 4 +--- tests/.snapshots/todo.txt | 9 ++++++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/PendingCalls/DescribeCall.php b/src/PendingCalls/DescribeCall.php index f1e503ee..dc8e5e4f 100644 --- a/src/PendingCalls/DescribeCall.php +++ b/src/PendingCalls/DescribeCall.php @@ -65,6 +65,6 @@ final class DescribeCall $beforeEachCall->describing = $this->description; - return $beforeEachCall->{$name}(...$arguments); + return $beforeEachCall->{$name}(...$arguments); // @phpstan-ignore-line } } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 649cdffd..76de51f5 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -345,9 +345,9 @@ final class TestCall */ public function __destruct() { - if ($this->describing) { - $this->testCaseMethod->description = '`'.$this->describing.'` '.$this->testCaseMethod->description; + if (! is_null($this->describing)) { $this->testCaseMethod->describing = $this->describing; + $this->testCaseMethod->description = sprintf('`%s` > %s', $this->describing, $this->testCaseMethod->description); } $this->testSuite->tests->set($this->testCaseMethod); diff --git a/src/Repositories/BeforeEachRepository.php b/src/Repositories/BeforeEachRepository.php index 3a923df7..5f6072e7 100644 --- a/src/Repositories/BeforeEachRepository.php +++ b/src/Repositories/BeforeEachRepository.php @@ -29,11 +29,9 @@ final class BeforeEachRepository $beforeEachTestCall = ChainableClosure::unbound($fromBeforeEachTestCall, $beforeEachTestCall); $beforeEachTestCase = ChainableClosure::bound($fromBeforeEachTestCase, $beforeEachTestCase)->bindTo($beforeEachCall, $beforeEachCall::class); + assert($beforeEachTestCase instanceof Closure); } - assert($beforeEachTestCall instanceof Closure); - assert($beforeEachTestCase instanceof Closure); - $this->state[$filename] = [$beforeEachTestCall, $beforeEachTestCase]; } diff --git a/tests/.snapshots/todo.txt b/tests/.snapshots/todo.txt index 1d506b46..84a117aa 100644 --- a/tests/.snapshots/todo.txt +++ b/tests/.snapshots/todo.txt @@ -7,6 +7,13 @@ TODO Tests\Features\DatasetsTests - 1 todo ↓ forbids to define tests in Datasets dirs and Datasets.php files + TODO Tests\Features\Describe - 5 todos + ↓ todo + ↓ todo on hook > should not fail + ↓ todo on hook > should run + ↓ todo on describe > should not fail + ↓ todo on describe > should run + TODO Tests\Features\Todo - 3 todos ↓ something todo later ↓ something todo later chained @@ -15,4 +22,4 @@ PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed - Tests: 8 todos, 1 passed (1 assertions) + Tests: 13 todos, 1 passed (1 assertions) From a90b90ad29039386e7659a81884515e0893512e8 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 26 May 2023 20:47:02 +0100 Subject: [PATCH 09/38] docs: package description --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f5caffb6..c223f0fb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "pestphp/pest", - "description": "An elegant PHP Testing Framework.", + "description": "The elegant PHP Testing Framework.", "keywords": [ "php", "framework", From 26a6e7d7124a97e4d1e3d1945ef6b009029f08ec Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 May 2023 14:08:38 +0100 Subject: [PATCH 10/38] More tests --- src/PendingCalls/TestCall.php | 2 +- src/Support/Backtrace.php | 32 +++++++++++++++---- tests/.snapshots/todo.txt | 8 ++--- tests/Features/AfterEach.php | 10 +++++- tests/Features/BeforeEach.php | 16 ++++++++-- tests/Features/Covers.php | 8 +++++ ...gherOrderTests.php => DescriptionLess.php} | 6 ++++ tests/Features/Incompleted.php | 4 +++ tests/Features/It.php | 8 +++++ tests/Features/Notices.php | 8 +++++ tests/Features/ThrowsNoExceptions.php | 6 ++++ tests/Features/Warnings.php | 8 +++++ 12 files changed, 101 insertions(+), 15 deletions(-) rename tests/Features/{PendingHigherOrderTests.php => DescriptionLess.php} (71%) diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 76de51f5..74d88377 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -347,7 +347,7 @@ final class TestCall { if (! is_null($this->describing)) { $this->testCaseMethod->describing = $this->describing; - $this->testCaseMethod->description = sprintf('`%s` > %s', $this->describing, $this->testCaseMethod->description); + $this->testCaseMethod->description = sprintf('`%s` → %s', $this->describing, $this->testCaseMethod->description); } $this->testSuite->tests->set($this->testCaseMethod); diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php index 8f683ff7..729bf08d 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -78,9 +78,7 @@ final class Backtrace */ public static function file(): string { - $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; - - assert(array_key_exists(self::FILE, $trace)); + $trace = self::backtrace(); return $trace[self::FILE]; } @@ -90,9 +88,7 @@ final class Backtrace */ public static function dirname(): string { - $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; - - assert(array_key_exists(self::FILE, $trace)); + $trace = self::backtrace(); return dirname($trace[self::FILE]); } @@ -102,8 +98,30 @@ final class Backtrace */ public static function line(): int { - $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1]; + $trace = self::backtrace(); return $trace['line'] ?? 0; } + + /** + * @return array{function: string, line?: int, file: string, class?: class-string, type?: string, args?: mixed[], object?: object} + */ + private static function backtrace(): array + { + $backtrace = debug_backtrace(self::BACKTRACE_OPTIONS); + + foreach ($backtrace as $trace) { + if (! isset($trace['file'])) { + continue; + } + + if (str_contains($trace['file'], 'pest/src')) { + continue; + } + + return $trace; + } + + throw ShouldNotHappen::fromMessage('Backtrace not found.'); + } } diff --git a/tests/.snapshots/todo.txt b/tests/.snapshots/todo.txt index 84a117aa..1d3687a5 100644 --- a/tests/.snapshots/todo.txt +++ b/tests/.snapshots/todo.txt @@ -9,10 +9,10 @@ TODO Tests\Features\Describe - 5 todos ↓ todo - ↓ todo on hook > should not fail - ↓ todo on hook > should run - ↓ todo on describe > should not fail - ↓ todo on describe > should run + ↓ todo on hook → should not fail + ↓ todo on hook → should run + ↓ todo on describe → should not fail + ↓ todo on describe → should run TODO Tests\Features\Todo - 3 todos ↓ something todo later diff --git a/tests/Features/AfterEach.php b/tests/Features/AfterEach.php index cdaed1d3..47d1fb60 100644 --- a/tests/Features/AfterEach.php +++ b/tests/Features/AfterEach.php @@ -7,7 +7,11 @@ beforeEach(function () use ($state) { }); afterEach(function () { - $this->state->bar = 2; + $this->state->bar = 1; +}); + +afterEach(function () { + unset($this->state->bar); }); it('does not get executed before the test', function () { @@ -18,3 +22,7 @@ it('gets executed after the test', function () { expect($this->state)->toHaveProperty('bar'); expect($this->state->bar)->toBe(2); }); + +afterEach(function () { + $this->state->bar = 2; +}); diff --git a/tests/Features/BeforeEach.php b/tests/Features/BeforeEach.php index a2e70d61..7ef6144b 100644 --- a/tests/Features/BeforeEach.php +++ b/tests/Features/BeforeEach.php @@ -4,12 +4,24 @@ beforeEach(function () { $this->bar = 2; }); +beforeEach(function () { + $this->bar++; +}); + +beforeEach(function () { + $this->bar = 0; +}); + it('gets executed before each test', function () { - expect($this->bar)->toBe(2); + expect($this->bar)->toBe(1); $this->bar = 'changed'; }); it('gets executed before each test once again', function () { - expect($this->bar)->toBe(2); + expect($this->bar)->toBe(1); +}); + +beforeEach(function () { + $this->bar++; }); diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php index 12ec4ac5..31074e09 100644 --- a/tests/Features/Covers.php +++ b/tests/Features/Covers.php @@ -68,3 +68,11 @@ it('throws exception if no class nor method has been found', function () { $testCall->covers('fakeName'); })->throws(InvalidArgumentException::class, 'No class or method named "fakeName" has been found.'); + +describe('a "describe" group of tests', function () { + it('does not append CoversNothing to method attributes', function () { + $phpDoc = (new ReflectionClass($this))->getMethod($this->name()); + + expect(str_contains($phpDoc->getDocComment(), '* @coversNothing'))->toBeTrue(); + }); +})->coversNothing(); diff --git a/tests/Features/PendingHigherOrderTests.php b/tests/Features/DescriptionLess.php similarity index 71% rename from tests/Features/PendingHigherOrderTests.php rename to tests/Features/DescriptionLess.php index a22cf027..971d5cd3 100644 --- a/tests/Features/PendingHigherOrderTests.php +++ b/tests/Features/DescriptionLess.php @@ -29,3 +29,9 @@ trait Gettable get('foo'); // not incomplete because closure is created... get('foo')->get('bar')->expect(true)->toBeTrue(); get('foo')->expect(true)->toBeTrue(); + +describe('a "describe" group of tests', function () { + get('foo'); // not incomplete because closure is created... + get('foo')->get('bar')->expect(true)->toBeTrue(); + get('foo')->expect(true)->toBeTrue(); +}); diff --git a/tests/Features/Incompleted.php b/tests/Features/Incompleted.php index b32a1954..c55c1c2b 100644 --- a/tests/Features/Incompleted.php +++ b/tests/Features/Incompleted.php @@ -15,3 +15,7 @@ it('is not incompleted because of assert')->assertTrue(true); it('is not incompleted because of test with assertions', function () { expect(true)->toBeTrue(); }); + +describe('a "describe" group of tests', function () { + it('is incompleted'); +}); diff --git a/tests/Features/It.php b/tests/Features/It.php index dc483a2e..a9582b2a 100644 --- a/tests/Features/It.php +++ b/tests/Features/It.php @@ -5,3 +5,11 @@ it('is a test', function () { }); it('is a higher order message test')->expect(true)->toBeTrue(); + +describe('a "describe" group of tests', function () { + it('is a test', function () { + expect(['key' => 'foo'])->toHaveKey('key')->key->toBeString(); + }); + + it('is a higher order message test')->expect(true)->toBeTrue(); +}); diff --git a/tests/Features/Notices.php b/tests/Features/Notices.php index dd245450..fb94fcf9 100644 --- a/tests/Features/Notices.php +++ b/tests/Features/Notices.php @@ -5,3 +5,11 @@ test('notice', function () { expect(true)->toBeTrue(); }); + +describe('a "describe" group of tests', function () { + test('notice', function () { + trigger_error('This is a notice description', E_USER_NOTICE); + + expect(true)->toBeTrue(); + }); +}); diff --git a/tests/Features/ThrowsNoExceptions.php b/tests/Features/ThrowsNoExceptions.php index 98f38574..5ae0422c 100644 --- a/tests/Features/ThrowsNoExceptions.php +++ b/tests/Features/ThrowsNoExceptions.php @@ -9,3 +9,9 @@ it('allows access to the underlying expectNotToPerformAssertions method', functi it('allows performing no expectations without being risky', function () { $result = 1 + 1; })->throwsNoExceptions(); + +describe('a "describe" group of tests', function () { + it('allows performing no expectations without being risky', function () { + $result = 1 + 1; + }); +})->throwsNoExceptions(); diff --git a/tests/Features/Warnings.php b/tests/Features/Warnings.php index e638b3f0..76a51f2c 100644 --- a/tests/Features/Warnings.php +++ b/tests/Features/Warnings.php @@ -11,3 +11,11 @@ test('user warning', function () { expect(true)->toBeTrue(); }); + +describe('a "describe" group of tests', function () { + test('user warning', function () { + trigger_error('This is a warning description', E_USER_WARNING); + + expect(true)->toBeTrue(); + }); +}); From be9056f9789a819870a01c686352957d89df264f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 May 2023 20:22:36 +0100 Subject: [PATCH 11/38] feat: `toUseStrictTypes` --- src/Expectation.php | 11 +++++++++++ src/Expectations/OppositeExpectation.php | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Expectation.php b/src/Expectation.php index cb9b0361..8579da04 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -13,6 +13,7 @@ use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; +use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -369,6 +370,16 @@ final class Expectation return ToUse::make($this, $targets); } + /** + * Asserts that the given expectation target use the "declare(strict_types=1)" declaration. + * + * @param array|string $targets + */ + public function toUseStrictTypes(): ArchExpectation + { + return ToUseStrictTypes::make($this); + } + /** * Asserts that the given expectation target "only" use on the given dependencies. * diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 327676db..57e0193d 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -5,15 +5,19 @@ declare(strict_types=1); namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Exceptions\ArchExpectationFailedException; +use Pest\Arch\Expectations\NotToUseStrictTypes; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToUse; +use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; use Pest\Support\Exporter; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\ExpectationFailedException; /** @@ -71,6 +75,14 @@ final class OppositeExpectation ), is_string($targets) ? [$targets] : $targets)); } + /** + * Asserts that the given expectation target does not use the "declare(strict_types=1)" declaration. + */ + public function toUseStrictTypes(): ArchExpectation + { + return ToUseStrictTypes::make($this->original, false); + } + /** * @param array|string $targets */ @@ -128,7 +140,7 @@ final class OppositeExpectation try { /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException|AssertionFailedError) { return $this->original; } From 0de1ce053a36e7ce23797dd452b09117ef2b6fd6 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 May 2023 20:31:21 +0100 Subject: [PATCH 12/38] feat: `toBeFinal` --- src/Expectation.php | 11 +++++++++-- src/Expectations/OppositeExpectation.php | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 8579da04..701eadc1 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,6 +7,7 @@ namespace Pest; use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToOnlyBeUsedIn; @@ -372,14 +373,20 @@ final class Expectation /** * Asserts that the given expectation target use the "declare(strict_types=1)" declaration. - * - * @param array|string $targets */ public function toUseStrictTypes(): ArchExpectation { return ToUseStrictTypes::make($this); } + /** + * Asserts that the given expectation target is final. + */ + public function toBeFinal(): ArchExpectation + { + return ToBeFinal::make($this); + } + /** * Asserts that the given expectation target "only" use on the given dependencies. * diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 57e0193d..b2932167 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -7,6 +7,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Exceptions\ArchExpectationFailedException; use Pest\Arch\Expectations\NotToUseStrictTypes; +use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToUse; @@ -83,6 +84,14 @@ final class OppositeExpectation return ToUseStrictTypes::make($this->original, false); } + /** + * Asserts that the given expectation target is final. + */ + public function toBeFinal(): ArchExpectation + { + return ToBeFinal::make($this->original, false); + } + /** * @param array|string $targets */ From ee2f4eedbde2e4e01dcdf2a56a746b1f87197e65 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 28 May 2023 02:03:10 +0100 Subject: [PATCH 13/38] feat: more reflection based expectations --- src/Expectation.php | 124 ++++++++++++++++++++++- src/Expectations/OppositeExpectation.php | 124 ++++++++++++++++++++++- 2 files changed, 241 insertions(+), 7 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 701eadc1..927998bb 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,14 +7,13 @@ namespace Pest; use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; -use Pest\Arch\Expectations\ToBeFinal; +use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; -use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -26,6 +25,7 @@ use Pest\Expectations\HigherOrderExpectation; use Pest\Expectations\OppositeExpectation; use Pest\Matchers\Any; use Pest\Support\ExpectationPipeline; +use PHPUnit\Architecture\Elements\ObjectDescription; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; @@ -376,7 +376,11 @@ final class Expectation */ public function toUseStrictTypes(): ArchExpectation { - return ToUseStrictTypes::make($this); + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), + 'to use strict types', + ); } /** @@ -384,7 +388,119 @@ final class Expectation */ public function toBeFinal(): ArchExpectation { - return ToBeFinal::make($this); + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isFinal(), + 'to be final', + ); + } + + /** + * Asserts that the given expectation target is readonly. + */ + public function toBeReadonly(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly(), + 'to be readonly', + ); + } + + /** + * Asserts that the given expectation target is trait. + */ + public function toBeTrait(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), + 'to be trait', + ); + } + + /** + * Asserts that the given expectation target is abstract. + */ + public function toBeAbstract(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), + 'to be abstract', + ); + } + + /** + * Asserts that the given expectation target is enum. + */ + public function toBeEnum(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), + 'to be enum', + ); + } + + /** + * Asserts that the given expectation target is interface. + */ + public function toBeInterface(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), + 'to be interface', + ); + } + + /** + * Asserts that the given expectation target to be subclass of the given class. + * + * @param class-string $class + */ + public function toExtend(string $class): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->isSubclassOf($class), + sprintf("to extend '%s'", $class), + ); + } + + /** + * Asserts that the given expectation target to be have a parent class. + */ + public function toExtendNothing(): ArchExpectation + { + return ToBe::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, + "to extend nothing", + ); + } + + /** + * Asserts that the given expectation target to implement the given interfaces. + */ + public function toImplement(array|string $interfaces): ArchExpectation + { + $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; + + return ToBe::make( + $this, + function (ObjectDescription $object) use ($interfaces) : bool { + foreach ($interfaces as $interface) { + if (! $object->reflectionClass->implementsInterface($interface)) { + return false; + } + } + + return true; + }, + "to implement '".implode("', '", (array) $interfaces)."'", + ); } /** diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index b2932167..bf2f49fe 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -7,6 +7,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Exceptions\ArchExpectationFailedException; use Pest\Arch\Expectations\NotToUseStrictTypes; +use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; @@ -18,6 +19,7 @@ use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; use Pest\Support\Exporter; +use PHPUnit\Architecture\Elements\ObjectDescription; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\ExpectationFailedException; @@ -81,15 +83,131 @@ final class OppositeExpectation */ public function toUseStrictTypes(): ArchExpectation { - return ToUseStrictTypes::make($this->original, false); + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), + 'not to use strict types', + ); } /** - * Asserts that the given expectation target is final. + * Asserts that the given expectation target is not final. */ public function toBeFinal(): ArchExpectation { - return ToBeFinal::make($this->original, false); + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isFinal(), + 'not to be final', + ); + } + + /** + * Asserts that the given expectation target is not readonly. + */ + public function toBeReadonly(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly(), + 'not to be readonly', + ); + } + + /** + * Asserts that the given expectation target is not trait. + */ + public function toBeTrait(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), + 'not to be trait', + ); + } + + /** + * Asserts that the given expectation target is not abstract. + */ + public function toBeAbstract(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), + 'not to be abstract', + ); + } + + /** + * Asserts that the given expectation target is not enum. + */ + public function toBeEnum(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), + 'not to be enum', + ); + } + + /** + * Asserts that the given expectation target is not interface. + */ + public function toBeInterface(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), + 'not to be interface', + ); + } + + /** + * Asserts that the given expectation target to be subclass of the given class. + * + * @param class-string $class + */ + public function toExtend(string $class): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), + sprintf("not to extend '%s'", $class), + ); + } + + /** + * Asserts that the given expectation target to be not have any parent class. + */ + public function toExtendNothing(): ArchExpectation + { + return ToBe::make( + $this->original, + fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, + "to extend a class", + ); + } + + /** + * Asserts that the given expectation target not to implement the given interfaces. + */ + public function toImplement(array|string $interfaces): ArchExpectation + { + $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; + + return ToBe::make( + $this->original, + function (ObjectDescription $object) use ($interfaces) : bool { + foreach ($interfaces as $interface) { + if ($object->reflectionClass->implementsInterface($interface)) { + return false; + } + } + + return true; + }, + "not to implement '".implode("', '", (array) $interfaces)."'", + ); } /** From 5802bbc1dd74c15c3478aeeab77b4e58aae4502a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 29 May 2023 22:25:37 +0100 Subject: [PATCH 14/38] feat: `toHavePrefix`, `toHaveSuffix`, `toOnlyImplement`, `toImplementNothing` --- src/Expectation.php | 115 +++++++++++++++++++++-- src/Expectations/OppositeExpectation.php | 86 +++++++++++++++-- 2 files changed, 181 insertions(+), 20 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 927998bb..7943f6d0 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,6 +7,7 @@ namespace Pest; use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\Targeted; use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; @@ -14,6 +15,7 @@ use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; +use Pest\Arch\Support\FileLineFinder; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -376,10 +378,13 @@ final class Expectation */ public function toUseStrictTypes(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'to use strict types', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, ' $object->reflectionClass->isFinal(), 'to be final', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -400,10 +408,13 @@ final class Expectation */ public function toBeReadonly(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly(), 'to be readonly', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -412,10 +423,13 @@ final class Expectation */ public function toBeTrait(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), 'to be trait', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -424,10 +438,13 @@ final class Expectation */ public function toBeAbstract(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), 'to be abstract', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -436,10 +453,13 @@ final class Expectation */ public function toBeEnum(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), 'to be enum', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -448,10 +468,13 @@ final class Expectation */ public function toBeInterface(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), 'to be interface', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -462,10 +485,13 @@ final class Expectation */ public function toExtend(string $class): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isSubclassOf($class), sprintf("to extend '%s'", $class), + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -474,10 +500,76 @@ final class Expectation */ public function toExtendNothing(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, "to extend nothing", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to not implement any interfaces. + */ + public function toImplementNothing(): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], + "to implement nothing", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to only implement the given interfaces. + */ + public function toOnlyImplement(array|string $interfaces): ArchExpectation + { + $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; + + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) + && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], + "to only implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to have the given suffix. + */ + public function toHaveSuffix(string $suffix): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix), + "to have suffix '{$suffix}'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to have the given suffix. + */ + public function toHavePrefix(string $suffix): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getName(), $suffix), + "to have prefix '{$suffix}'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -488,7 +580,7 @@ final class Expectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; - return ToBe::make( + return Targeted::make( $this, function (ObjectDescription $object) use ($interfaces) : bool { foreach ($interfaces as $interface) { @@ -500,6 +592,9 @@ final class Expectation return true; }, "to implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index bf2f49fe..310ad3ba 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -7,6 +7,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Exceptions\ArchExpectationFailedException; use Pest\Arch\Expectations\NotToUseStrictTypes; +use Pest\Arch\Expectations\Targeted; use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; @@ -15,6 +16,7 @@ use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; +use Pest\Arch\Support\FileLineFinder; use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; @@ -83,10 +85,13 @@ final class OppositeExpectation */ public function toUseStrictTypes(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'not to use strict types', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isFinal(), 'not to be final', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -107,10 +115,13 @@ final class OppositeExpectation */ public function toBeReadonly(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly(), 'not to be readonly', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -119,10 +130,13 @@ final class OppositeExpectation */ public function toBeTrait(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), 'not to be trait', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -131,10 +145,13 @@ final class OppositeExpectation */ public function toBeAbstract(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), 'not to be abstract', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -143,10 +160,13 @@ final class OppositeExpectation */ public function toBeEnum(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), 'not to be enum', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -155,10 +175,13 @@ final class OppositeExpectation */ public function toBeInterface(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), 'not to be interface', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -169,10 +192,13 @@ final class OppositeExpectation */ public function toExtend(string $class): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -181,10 +207,13 @@ final class OppositeExpectation */ public function toExtendNothing(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, "to extend a class", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -195,7 +224,7 @@ final class OppositeExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; - return ToBe::make( + return Targeted::make( $this->original, function (ObjectDescription $object) use ($interfaces) : bool { foreach ($interfaces as $interface) { @@ -207,9 +236,45 @@ final class OppositeExpectation return true; }, "not to implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } + /** + * Asserts that the given expectation target to not implement any interfaces. + */ + public function toImplementNothing(): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], + "to implement an interface", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to not only implement the given interfaces. + */ + public function toOnlyImplement(array|string $interfaces): ArchExpectation + { + throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); + } + + public function toHavePrefix(string $suffix): never + { + throw InvalidExpectation::fromMethods(['not', 'toHavePrefix']); + } + + public function toHaveSuffix(string $suffix): never + { + throw InvalidExpectation::fromMethods(['not', 'toHaveSuffix']); + } + /** * @param array|string $targets */ @@ -218,6 +283,7 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } + public function toUseNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); From 80129f2e2367a6389b647324eee5a8c140d9ccb3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 30 May 2023 00:32:07 +0100 Subject: [PATCH 15/38] chore: asserts style --- src/Expectations/OppositeExpectation.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 310ad3ba..9fb0df27 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -257,9 +257,6 @@ final class OppositeExpectation ); } - /** - * Asserts that the given expectation target to not only implement the given interfaces. - */ public function toOnlyImplement(array|string $interfaces): ArchExpectation { throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); @@ -283,7 +280,6 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } - public function toUseNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); From e2ccc9deac743c912d77439b0a088647e6b5ba0f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 31 May 2023 01:40:03 +0100 Subject: [PATCH 16/38] chore: style changes --- src/Expectation.php | 7 +++---- src/Expectations/OppositeExpectation.php | 11 +++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 7943f6d0..0cecfedf 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -8,7 +8,6 @@ use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\Targeted; -use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToOnlyBeUsedIn; @@ -503,7 +502,7 @@ final class Expectation return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, - "to extend nothing", + 'to extend nothing', FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), @@ -518,7 +517,7 @@ final class Expectation return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], - "to implement nothing", + 'to implement nothing', FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), @@ -582,7 +581,7 @@ final class Expectation return Targeted::make( $this, - function (ObjectDescription $object) use ($interfaces) : bool { + function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if (! $object->reflectionClass->implementsInterface($interface)) { return false; diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 9fb0df27..3f85ad3b 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -5,15 +5,10 @@ declare(strict_types=1); namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; -use Pest\Arch\Exceptions\ArchExpectationFailedException; -use Pest\Arch\Expectations\NotToUseStrictTypes; use Pest\Arch\Expectations\Targeted; -use Pest\Arch\Expectations\ToBe; -use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToUse; -use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Arch\Support\FileLineFinder; @@ -210,7 +205,7 @@ final class OppositeExpectation return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, - "to extend a class", + 'to extend a class', FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), @@ -226,7 +221,7 @@ final class OppositeExpectation return Targeted::make( $this->original, - function (ObjectDescription $object) use ($interfaces) : bool { + function (ObjectDescription $object) use ($interfaces): bool { foreach ($interfaces as $interface) { if ($object->reflectionClass->implementsInterface($interface)) { return false; @@ -250,7 +245,7 @@ final class OppositeExpectation return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], - "to implement an interface", + 'to implement an interface', FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), From ac13a288fb2ef37f65b8f32e2f0de101f53c7c7d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 3 Jun 2023 00:37:28 +0100 Subject: [PATCH 17/38] feat: improve grammar --- src/Expectation.php | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 0cecfedf..df7d095b 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -432,6 +432,14 @@ final class Expectation ); } + /** + * Asserts that the given expectation targets are traits. + */ + public function toBeTraits(): ArchExpectation + { + return $this->toBeTrait(); + } + /** * Asserts that the given expectation target is abstract. */ @@ -462,6 +470,14 @@ final class Expectation ); } + /** + * Asserts that the given expectation targets are enums. + */ + public function toBeEnums(): ArchExpectation + { + return $this->toBeEnum(); + } + /** * Asserts that the given expectation target is interface. */ @@ -477,6 +493,14 @@ final class Expectation ); } + /** + * Asserts that the given expectation targets are interfaces. + */ + public function toBeInterfaces(): ArchExpectation + { + return $this->toBeInterface(); + } + /** * Asserts that the given expectation target to be subclass of the given class. * @@ -526,6 +550,8 @@ final class Expectation /** * Asserts that the given expectation target to only implement the given interfaces. + * + * @param array|class-string $interfaces */ public function toOnlyImplement(array|string $interfaces): ArchExpectation { @@ -535,7 +561,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], - "to only implement '".implode("', '", (array) $interfaces)."'", + "to only implement '".implode("', '", $interfaces)."'", FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), @@ -574,6 +600,8 @@ final class Expectation /** * Asserts that the given expectation target to implement the given interfaces. + * + * @param array|class-string $interfaces */ public function toImplement(array|string $interfaces): ArchExpectation { @@ -590,7 +618,7 @@ final class Expectation return true; }, - "to implement '".implode("', '", (array) $interfaces)."'", + "to implement '".implode("', '", $interfaces)."'", FileLineFinder::where(function (string $line): bool { return str_contains($line, 'class'); }), From 4f3796ed2e0158773889a143ab430d4e0cdae209 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Jun 2023 20:43:33 +0200 Subject: [PATCH 18/38] feat: improves VS Code auto-complete --- src/Functions.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Functions.php b/src/Functions.php index 03fa778f..465f3add 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Pest\Concerns\Expectable; use Pest\Exceptions\AfterAllWithinDescribe; use Pest\Exceptions\BeforeAllWithinDescribe; use Pest\Expectation; @@ -52,7 +53,7 @@ if (! function_exists('beforeEach')) { /** * Runs the given closure before each test in the current file. * - * @return HigherOrderTapProxy|TestCall|TestCase|mixed + * @return HigherOrderTapProxy|Expectable|TestCall|TestCase|mixed */ function beforeEach(Closure $closure = null): BeforeEachCall { @@ -82,7 +83,7 @@ if (! function_exists('describe')) { * is the group description; the second argument is a closure * that contains the group tests. * - * @return HigherOrderTapProxy|TestCall|TestCase|mixed + * @return HigherOrderTapProxy|Expectable|TestCall|TestCase|mixed */ function describe(string $description, Closure $tests): DescribeCall { @@ -113,7 +114,7 @@ if (! function_exists('test')) { * is the test description; the second argument is * a closure that contains the test expectations. * - * @return TestCall|TestCase|mixed + * @return Expectable|TestCall|TestCase|mixed */ function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall { @@ -133,7 +134,7 @@ if (! function_exists('it')) { * is the test description; the second argument is * a closure that contains the test expectations. * - * @return TestCall|TestCase|mixed + * @return Expectable|TestCall|TestCase|mixed */ function it(string $description, Closure $closure = null): TestCall { @@ -152,7 +153,7 @@ if (! function_exists('todo')) { * is marked as incomplete. Yet, Collision, Pest's * printer, will display it as a "todo" test. * - * @return TestCall|TestCase|mixed + * @return Expectable|TestCall|TestCase|mixed */ function todo(string $description): TestCall { @@ -168,7 +169,7 @@ if (! function_exists('afterEach')) { /** * Runs the given closure after each test in the current file. * - * @return HigherOrderTapProxy|TestCall|mixed + * @return Expectable|HigherOrderTapProxy|TestCall|mixed */ function afterEach(Closure $closure = null): AfterEachCall { From 54f4ee57ad3d1e98b85827079d5fc9a62aba11b7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 15 Jun 2023 15:16:04 +0200 Subject: [PATCH 19/38] refacto: 100% type coverage --- composer.json | 4 +- src/Expectation.php | 56 ++++---------- src/Expectations/OppositeExpectation.php | 77 ++++++++++--------- src/Factories/TestCaseFactory.php | 4 +- src/Factories/TestCaseMethodFactory.php | 4 +- src/PendingCalls/UsesCall.php | 4 +- src/Plugins/Coverage.php | 2 +- src/Plugins/Parallel.php | 12 +-- src/Plugins/Parallel/Handlers/Parallel.php | 2 +- .../Parallel/Paratest/WrapperRunner.php | 6 +- src/Repositories/DatasetsRepository.php | 8 +- src/Support/Coverage.php | 3 +- src/Support/ExceptionTrace.php | 4 +- src/Support/ExpectationPipeline.php | 2 +- src/Support/Reflection.php | 19 ++--- .../GitDirtyTestCaseFilter.php | 11 ++- tests/Visual/Parallel.php | 2 +- 17 files changed, 96 insertions(+), 124 deletions(-) diff --git a/composer.json b/composer.json index c223f0fb..a32a3185 100644 --- a/composer.json +++ b/composer.json @@ -50,9 +50,11 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.12.0", + "pestphp/pest-plugin-type-coverage": "^2.0.0", "symfony/process": "^6.3.0" }, - "minimum-stability": "stable", + "minimum-stability": "dev", + "prefer-stable": true, "config": { "sort-packages": true, "preferred-install": "dist", diff --git a/src/Expectation.php b/src/Expectation.php index df7d095b..893cc049 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -381,9 +381,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'to use strict types', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, ' str_contains($line, ' $object->reflectionClass->isFinal(), 'to be final', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -411,9 +407,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly(), 'to be readonly', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -426,9 +420,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), 'to be trait', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -449,9 +441,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), 'to be abstract', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -464,9 +454,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), 'to be enum', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -487,9 +475,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), 'to be interface', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -512,9 +498,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isSubclassOf($class), sprintf("to extend '%s'", $class), - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -527,9 +511,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, 'to extend nothing', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -542,9 +524,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], 'to implement nothing', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -562,9 +542,7 @@ final class Expectation fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], "to only implement '".implode("', '", $interfaces)."'", - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -577,9 +555,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix), "to have suffix '{$suffix}'", - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -592,9 +568,7 @@ final class Expectation $this, fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getName(), $suffix), "to have prefix '{$suffix}'", - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -619,9 +593,7 @@ final class Expectation return true; }, "to implement '".implode("', '", $interfaces)."'", - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 3f85ad3b..d84c00f2 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -84,9 +84,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'not to use strict types', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, ' str_contains($line, 'original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isFinal(), 'not to be final', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -114,9 +110,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly(), 'not to be readonly', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -129,9 +123,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), 'not to be trait', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -144,9 +136,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), 'not to be abstract', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -159,9 +149,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), 'not to be enum', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -174,9 +162,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), 'not to be interface', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -191,9 +177,7 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -206,14 +190,14 @@ final class OppositeExpectation $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, 'to extend a class', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } /** * Asserts that the given expectation target not to implement the given interfaces. + * + * @param array|string $interfaces */ public function toImplement(array|string $interfaces): ArchExpectation { @@ -230,10 +214,8 @@ final class OppositeExpectation return true; }, - "not to implement '".implode("', '", (array) $interfaces)."'", - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + "not to implement '".implode("', '", $interfaces)."'", + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } @@ -243,31 +225,42 @@ final class OppositeExpectation public function toImplementNothing(): ArchExpectation { return Targeted::make( - $this, + $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], 'to implement an interface', - FileLineFinder::where(function (string $line): bool { - return str_contains($line, 'class'); - }), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); } - public function toOnlyImplement(array|string $interfaces): ArchExpectation + /** + * Not supported. + * + * @param array|string $interfaces + */ + public function toOnlyImplement(array|string $interfaces): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); } + /** + * Not supported. + */ public function toHavePrefix(string $suffix): never { throw InvalidExpectation::fromMethods(['not', 'toHavePrefix']); } + /** + * Not supported. + */ public function toHaveSuffix(string $suffix): never { throw InvalidExpectation::fromMethods(['not', 'toHaveSuffix']); } /** + * Not supported. + * * @param array|string $targets */ public function toOnlyUse(array|string $targets): never @@ -275,6 +268,9 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } + /** + * Not supported. + */ public function toUseNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); @@ -358,8 +354,13 @@ final class OppositeExpectation $exporter = Exporter::default(); - $toString = fn ($argument): string => $exporter->shortenedExport($argument); + $toString = fn (mixed $argument): string => $exporter->shortenedExport($argument); - throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(? $toString($argument), $arguments)))); + throw new ExpectationFailedException(sprintf( + 'Expecting %s not %s %s.', + $toString($this->original->value), + strtolower((string) preg_replace('/(? $toString($argument), $arguments)), + )); } } diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 0b5b66f4..cbe50985 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -98,7 +98,7 @@ final class TestCaseFactory { if ('\\' === DIRECTORY_SEPARATOR) { // In case Windows, strtolower drive name, like in UsesCall. - $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename); + $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', static fn (array $match): string => strtolower($match['drive']), $filename); } $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); @@ -134,7 +134,7 @@ final class TestCaseFactory $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $traitsCode = sprintf('use %s;', implode(', ', array_map( - static fn ($trait): string => sprintf('\%s', $trait), $this->traits)) + static fn (string $trait): string => sprintf('\%s', $trait), $this->traits)) ); $partsFQN = explode('\\', $classFQN); diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index abeebffb..b4fce58c 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -147,11 +147,11 @@ final class TestCaseMethodFactory } $annotations = implode('', array_map( - static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations, + static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations, )); $attributes = implode('', array_map( - static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes, + static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes, )); return << 0) { - $path = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', fn ($match): string => strtolower($match['drive']), $path); + $path = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', fn (array $match): string => strtolower($match['drive']), $path); $startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__)); } diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index 07b21232..668ff09f 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -50,7 +50,7 @@ final class Coverage implements AddsOutput, HandlesArguments */ public function handleArguments(array $originals): array { - $arguments = [...[''], ...array_values(array_filter($originals, function ($original): bool { + $arguments = [...[''], ...array_values(array_filter($originals, function (string $original): bool { foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) { if ($original === sprintf('--%s', $option)) { return true; diff --git a/src/Plugins/Parallel.php b/src/Plugins/Parallel.php index 1e610c5f..3354b743 100644 --- a/src/Plugins/Parallel.php +++ b/src/Plugins/Parallel.php @@ -115,13 +115,13 @@ final class Parallel implements HandlesArguments private function runTestSuiteInParallel(array $arguments): int { $handlers = array_filter( - array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), - fn ($handler): bool => $handler instanceof HandlesArguments, + array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), + fn (object|string $handler): bool => $handler instanceof HandlesArguments, ); $filteredArguments = array_reduce( $handlers, - fn ($arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments), + fn (array $arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments), $arguments ); @@ -139,13 +139,13 @@ final class Parallel implements HandlesArguments private function runWorkerHandlers(array $arguments): array { $handlers = array_filter( - array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), - fn ($handler): bool => $handler instanceof HandlersWorkerArguments, + array_map(fn (string $handler): object|string => Container::getInstance()->get($handler), self::HANDLERS), + fn (object|string $handler): bool => $handler instanceof HandlersWorkerArguments, ); return array_reduce( $handlers, - fn ($arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments), + fn (array $arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments), $arguments ); } diff --git a/src/Plugins/Parallel/Handlers/Parallel.php b/src/Plugins/Parallel/Handlers/Parallel.php index 2e87a521..76a59af6 100644 --- a/src/Plugins/Parallel/Handlers/Parallel.php +++ b/src/Plugins/Parallel/Handlers/Parallel.php @@ -30,7 +30,7 @@ final class Parallel implements HandlesArguments */ public function handleArguments(array $arguments): array { - $args = array_reduce(self::ARGS_TO_REMOVE, fn ($args, $arg): array => $this->popArgument($arg, $args), $arguments); + $args = array_reduce(self::ARGS_TO_REMOVE, fn (array $args, string $arg): array => $this->popArgument($arg, $args), $arguments); return $this->pushArgument('--runner='.WrapperRunner::class, $args); } diff --git a/src/Plugins/Parallel/Paratest/WrapperRunner.php b/src/Plugins/Parallel/Paratest/WrapperRunner.php index c45d6a5a..2a1db6bb 100644 --- a/src/Plugins/Parallel/Paratest/WrapperRunner.php +++ b/src/Plugins/Parallel/Paratest/WrapperRunner.php @@ -23,6 +23,7 @@ use ParaTest\WrapperRunner\WrapperWorker; use Pest\Result; use Pest\TestSuite; use PHPUnit\Event\Facade as EventFacade; +use PHPUnit\Event\TestRunner\WarningTriggered; use PHPUnit\Runner\CodeCoverage; use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade; use PHPUnit\TestRunner\TestResult\TestResult; @@ -317,7 +318,10 @@ final class WrapperRunner implements RunnerInterface $testResultSum->testTriggeredPhpunitErrorEvents(), $testResultSum->testTriggeredPhpunitWarningEvents(), $testResultSum->testRunnerTriggeredDeprecationEvents(), - array_values(array_filter($testResultSum->testRunnerTriggeredWarningEvents(), fn ($event): bool => ! str_contains($event->message(), 'No tests found'))), + array_values(array_filter( + $testResultSum->testRunnerTriggeredWarningEvents(), + fn (WarningTriggered $event): bool => ! str_contains($event->message(), 'No tests found') + )), ); $this->printer->printResults( diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 07c4a42c..90ff9e3f 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -66,11 +66,11 @@ final class DatasetsRepository } /** - * @return Closure|array|never + * @return Closure|array * * @throws ShouldNotHappen */ - public static function get(string $filename, string $description) + public static function get(string $filename, string $description): Closure|array { $dataset = self::$withs[$filename.self::SEPARATOR.$description]; @@ -138,7 +138,7 @@ final class DatasetsRepository /** * @param array|string> $datasets - * @return array> + * @return array> */ private static function processDatasets(array $datasets, string $currentTestFile): array { @@ -193,7 +193,7 @@ final class DatasetsRepository $closestScopeDatasetKey = array_reduce( array_keys($matchingDatasets), - fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB + fn (string|int|null $keyA, string|int|null $keyB): string|int|null => $keyA !== null && strlen((string) $keyA) > strlen((string) $keyB) ? $keyA : $keyB ); if ($closestScopeDatasetKey === null) { diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 526b15fa..6d61b68d 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -159,10 +159,9 @@ final class Coverage * ['11', '20..25', '50', '60..80']; * ``` * - * @param File $file * @return array */ - public static function getMissingCoverage($file): array + public static function getMissingCoverage(File $file): array { $shouldBeNewLine = true; diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 8d57f1db..0f6dc10b 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -18,11 +18,9 @@ final class ExceptionTrace /** * Ensures the given closure reports the good execution context. * - * @return mixed - * * @throws Throwable */ - public static function ensure(Closure $closure) + public static function ensure(Closure $closure): mixed { try { return $closure(); diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index 1df555f8..c9d3f6ed 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -84,6 +84,6 @@ final class ExpectationPipeline */ public function carry(): Closure { - return fn ($stack, $pipe): Closure => fn () => $pipe($stack, ...$this->passables); + return fn (mixed $stack, callable $pipe): Closure => fn () => $pipe($stack, ...$this->passables); } } diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 18a71aa6..3a6f2377 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -24,9 +24,8 @@ final class Reflection * Calls the given method with args on the given object. * * @param array $args - * @return mixed */ - public static function call(object $object, string $method, array $args = []) + public static function call(object $object, string $method, array $args = []): mixed { $reflectionClass = new ReflectionClass($object); @@ -53,9 +52,8 @@ final class Reflection * Bind a callable to the TestCase and return the result. * * @param array $args - * @return mixed */ - public static function bindCallable(callable $callable, array $args = []) + public static function bindCallable(callable $callable, array $args = []): mixed { return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args); } @@ -63,10 +61,8 @@ final class Reflection /** * Bind a callable to the TestCase and return the result, * passing in the current dataset values as arguments. - * - * @return mixed */ - public static function bindCallableWithData(callable $callable) + public static function bindCallableWithData(callable $callable): mixed { $test = TestSuite::getInstance()->test; @@ -87,10 +83,8 @@ final class Reflection /** * Gets the property value from of the given object. - * - * @return mixed */ - public static function getPropertyValue(object $object, string $property) + public static function getPropertyValue(object $object, string $property): mixed { $reflectionClass = new ReflectionClass($object); @@ -206,10 +200,7 @@ final class Reflection return $arguments; } - /** - * @return mixed - */ - public static function getFunctionVariable(Closure $function, string $key) + public static function getFunctionVariable(Closure $function, string $key): mixed { return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; } diff --git a/src/TestCaseFilters/GitDirtyTestCaseFilter.php b/src/TestCaseFilters/GitDirtyTestCaseFilter.php index f881d9a3..dc4f20b3 100644 --- a/src/TestCaseFilters/GitDirtyTestCaseFilter.php +++ b/src/TestCaseFilters/GitDirtyTestCaseFilter.php @@ -59,13 +59,18 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter $dirtyFiles[substr($dirtyFile, 3)] = trim(substr($dirtyFile, 0, 3)); } - $dirtyFiles = array_filter($dirtyFiles, fn ($status): bool => $status !== 'D'); + $dirtyFiles = array_filter($dirtyFiles, fn (string $status): bool => $status !== 'D'); - $dirtyFiles = array_map(fn ($file, $status): string => in_array($status, ['R', 'RM'], true) ? explode(' -> ', $file)[1] : $file, array_keys($dirtyFiles), $dirtyFiles); + $dirtyFiles = array_map( + fn (string $file, string $status): string => in_array($status, ['R', 'RM'], true) + ? explode(' -> ', $file)[1] + : $file, array_keys($dirtyFiles), $dirtyFiles, + ); $dirtyFiles = array_filter( $dirtyFiles, - fn ($file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) || str_starts_with($file, TestSuite::getInstance()->testPath) + fn (string $file): bool => str_starts_with('.'.DIRECTORY_SEPARATOR.$file, TestSuite::getInstance()->testPath) + || str_starts_with($file, TestSuite::getInstance()->testPath) ); $dirtyFiles = array_values($dirtyFiles); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index e8ff77d1..b79aa537 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -18,7 +18,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 1 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 15 skipped, 707 passed (1720 assertions)') + ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 720 passed (1740 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From d5334f96a458d876beef90c42af1dd39da40ea8e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 15 Jun 2023 18:18:42 +0200 Subject: [PATCH 20/38] chore: increase deps --- src/Expectation.php | 15 ++++++++++++++- src/Expectations/OppositeExpectation.php | 11 ++++++++++- src/Support/Coverage.php | 4 +++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 893cc049..7a8090f0 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -14,6 +14,7 @@ use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; +use Pest\Arch\PendingArchExpectation; use Pest\Arch\Support\FileLineFinder; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; @@ -39,6 +40,7 @@ use PHPUnit\Framework\ExpectationFailedException; * @property EachExpectation $each Creates an expectation on each element on the traversable value. * * @mixin Mixins\Expectation + * @mixin PendingArchExpectation */ final class Expectation { @@ -290,9 +292,15 @@ final class Expectation * @param array $parameters * @return Expectation|HigherOrderExpectation, TValue> */ - public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation + public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation|PendingArchExpectation { if (! self::hasMethod($method)) { + if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $method)) { + $pendingArchExpectation = new PendingArchExpectation($this, []); + + return $pendingArchExpectation->$method(...$parameters); // @phpstan-ignore-line + } + /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); } @@ -336,6 +344,11 @@ final class Expectation public function __get(string $name) { if (! self::hasMethod($name)) { + if (! is_object($this->value) && method_exists(PendingArchExpectation::class, $name)) { + /* @phpstan-ignore-next-line */ + return $this->{$name}(); + } + /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index d84c00f2..ceec8337 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -10,6 +10,7 @@ use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; use Pest\Arch\Expectations\ToUse; use Pest\Arch\GroupArchExpectation; +use Pest\Arch\PendingArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Arch\Support\FileLineFinder; use Pest\Exceptions\InvalidExpectation; @@ -318,6 +319,10 @@ final class OppositeExpectation public function __call(string $name, array $arguments): Expectation { try { + if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { + throw InvalidExpectation::fromMethods(['not', $name]); + } + /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); } catch (ExpectationFailedException|AssertionFailedError) { @@ -335,8 +340,12 @@ final class OppositeExpectation public function __get(string $name): Expectation { try { + if (! is_object($this->original->value) && method_exists(PendingArchExpectation::class, $name)) { + throw InvalidExpectation::fromMethods(['not', $name]); + } + $this->original->{$name}; // @phpstan-ignore-line - } catch (ExpectationFailedException) { // @phpstan-ignore-line + } catch (ExpectationFailedException) { return $this->original; } diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 6d61b68d..525e67e2 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -159,9 +159,11 @@ final class Coverage * ['11', '20..25', '50', '60..80']; * ``` * + * + * @param File $file * @return array */ - public static function getMissingCoverage(File $file): array + public static function getMissingCoverage($file): array { $shouldBeNewLine = true; From c98d8ca26a5ed7d5a4c0b338ddb66d2f0eee369d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 15 Jun 2023 18:22:23 +0200 Subject: [PATCH 21/38] feat: more expectations --- src/Expectation.php | 21 +++++++++++++++++++++ tests/Arch.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Expectation.php b/src/Expectation.php index 7a8090f0..d237b567 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -479,6 +479,27 @@ final class Expectation return $this->toBeEnum(); } + /** + * Asserts that the given expectation targets is an class. + */ + public function toBeClass(): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => class_exists($object->name), + 'to be class', + FileLineFinder::where(fn (string $line): bool => true), + ); + } + + /** + * Asserts that the given expectation targets are classes. + */ + public function toBeClasses(): ArchExpectation + { + return $this->toBeClass(); + } + /** * Asserts that the given expectation target is interface. */ diff --git a/tests/Arch.php b/tests/Arch.php index eb47a06e..a46c37b2 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -30,4 +30,4 @@ test('contracts') 'NunoMaduro\Collision\Contracts', 'Pest\Factories\TestCaseMethodFactory', 'Symfony\Component\Console', - ]); + ])->toBeInterfaces(); From 17db4bd616d8140f718f5ed4df50b99a7b797543 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 16 Jun 2023 09:43:38 +0100 Subject: [PATCH 22/38] chore: missing properties --- src/Expectation.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Expectation.php b/src/Expectation.php index d237b567..bd8524a3 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -38,6 +38,10 @@ use PHPUnit\Framework\ExpectationFailedException; * * @property OppositeExpectation $not Creates the opposite expectation. * @property EachExpectation $each Creates an expectation on each element on the traversable value. + * @property PendingArchExpectation $classes + * @property PendingArchExpectation $traits + * @property PendingArchExpectation $interfaces + * @property PendingArchExpectation $enums * * @mixin Mixins\Expectation * @mixin PendingArchExpectation From 36b585835d15aaf4751069868f606fc85da2a3a6 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 13:26:16 +0100 Subject: [PATCH 23/38] feat: adds snapshot testing --- composer.json | 7 +- src/Expectation.php | 8 ++ src/Mixins/Expectation.php | 38 +++++ src/Plugins/Snapshot.php | 30 ++++ src/Repositories/SnapshotRepository.php | 133 ++++++++++++++++++ src/Support/Coverage.php | 2 +- src/TestSuite.php | 9 +- ...tas_set_value______my_datas_set_value__.snap | 7 + .../Expect/toMatchSnapshot/failures.snap | 7 + .../failures_with_custom_message.snap | 7 + .../Expect/toMatchSnapshot/not_failures.snap | 7 + .../Features/Expect/toMatchSnapshot/pass.snap | 7 + .../pass_with______toString_.snap | 7 + .../toMatchSnapshot/pass_with__toString_.snap | 7 + ...s_set_value______my_datas_set_value__.snap | 7 + tests/Features/Expect/toMatchSnapshot.php | 94 +++++++++++++ tests/Visual/Parallel.php | 2 +- 17 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 src/Plugins/Snapshot.php create mode 100644 src/Repositories/SnapshotRepository.php create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures_with_custom_message.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/not_failures.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString_.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString_.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap create mode 100644 tests/Features/Expect/toMatchSnapshot.php diff --git a/composer.json b/composer.json index a32a3185..52ccdba9 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,8 @@ "lint": "pint", "test:refacto": "rector --dry-run", "test:lint": "pint --test", - "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", + "test:type:check": "phpstan analyse --ansi --memory-limit=-1 --debug", + "test:type:coverage": "php 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=10", @@ -79,7 +80,8 @@ "test": [ "@test:refacto", "@test:lint", - "@test:types", + "@test:type:check", + "@test:type:coverage", "@test:unit", "@test:parallel", "@test:integration" @@ -100,6 +102,7 @@ "Pest\\Plugins\\ProcessIsolation", "Pest\\Plugins\\Profile", "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Version", "Pest\\Plugins\\Parallel" ] diff --git a/src/Expectation.php b/src/Expectation.php index bd8524a3..ad0da961 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -305,6 +305,14 @@ final class Expectation return $pendingArchExpectation->$method(...$parameters); // @phpstan-ignore-line } + if (! is_object($this->value)) { + throw new BadMethodCallException(sprintf( + 'Method "%s" does not exist in %s.', + $method, + gettype($this->value) + )); + } + /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); } diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 385b0e95..e9da32dc 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -14,11 +14,14 @@ use Pest\Matchers\Any; use Pest\Support\Arr; use Pest\Support\Exporter; use Pest\Support\NullClosure; +use Pest\TestSuite; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; use ReflectionFunction; use ReflectionNamedType; +use Stringable; use Throwable; /** @@ -794,6 +797,41 @@ final class Expectation return $this; } + /** + * Asserts that the value "stringable" matches the given snapshot.. + * + * @return self + */ + public function toMatchSnapshot(string $message = ''): self + { + $string = match (true) { + is_string($this->value) => $this->value, + is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), + is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), + default => InvalidExpectationValue::expected('Stringable|string'), + }; + + $testCase = TestSuite::getInstance()->test; + assert($testCase instanceof TestCase); + $snapshots = TestSuite::getInstance()->snapshots; + + if ($snapshots->has($testCase, $string)) { + [$filename, $content] = $snapshots->get($testCase, $string); + + Assert::assertSame( + $content, + $string, + $message === '' ? "Failed asserting that the string value matches its snapshot ($filename)." : $message + ); + } else { + $filename = $snapshots->save($testCase, $string); + + $testCase::markTestIncomplete('Snapshot created at ['.$filename.'].'); + } + + return $this; + } + /** * Asserts that the value matches a regular expression. * diff --git a/src/Plugins/Snapshot.php b/src/Plugins/Snapshot.php new file mode 100644 index 00000000..3f9cb850 --- /dev/null +++ b/src/Plugins/Snapshot.php @@ -0,0 +1,30 @@ +hasArgument('--update-snapshots', $arguments)) { + return $arguments; + } + + TestSuite::getInstance()->snapshots->flush(); + + return $this->popArgument('--update-snapshots', $arguments); + } +} diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php new file mode 100644 index 00000000..7725cc13 --- /dev/null +++ b/src/Repositories/SnapshotRepository.php @@ -0,0 +1,133 @@ +getFilenameAndDescription($testCase); + + return file_exists($this->getSnapshotFilename($filename, $description)); + } + + /** + * Gets the snapshot. + * + * @return array{0: string, 1: string} + * + * @throws ShouldNotHappen + */ + public function get(TestCase $testCase, string $description): array + { + [$filename, $description] = $this->getFilenameAndDescription($testCase); + + $contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename($filename, $description)); + + if ($contents === false) { + throw ShouldNotHappen::fromMessage('Snapshot file could not be read.'); + } + + return [$snapshotFilename, $contents]; + } + + /** + * Saves the given snapshot for the given test case. + */ + public function save(TestCase $testCase, string $snapshot): string + { + [$filename, $description] = $this->getFilenameAndDescription($testCase); + + $snapshotFilename = $this->getSnapshotFilename($filename, $description); + + if (! file_exists(dirname($snapshotFilename))) { + mkdir(dirname($snapshotFilename), 0755, true); + } + + file_put_contents($snapshotFilename, $snapshot); + + return str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); + } + + /** + * Flushes the snapshots. + */ + public function flush(): void + { + $absoluteSnapshotsPath = $this->testsPath.'/'.$this->snapshotsPath; + + $deleteDirectory = function (string $path) use (&$deleteDirectory): void { + if (file_exists($path)) { + $scannedDir = scandir($path); + assert(is_array($scannedDir)); + + $files = array_diff($scannedDir, ['.', '..']); + + foreach ($files as $file) { + if (is_dir($path.'/'.$file)) { + $deleteDirectory($path.'/'.$file); + } else { + unlink($path.'/'.$file); + } + } + + rmdir($path); + } + }; + + if (file_exists($absoluteSnapshotsPath)) { + $deleteDirectory($absoluteSnapshotsPath); + } + } + + /** + * Gets the snapshot's "filename" and "description". + * + * @return array{0: string, 1: string} + */ + private function getFilenameAndDescription(TestCase $testCase): array + { + $filename = (fn () => self::$__filename)->call($testCase, $testCase::class); // @phpstan-ignore-line + + $description = str_replace('__pest_evaluable_', '', $testCase->name()); + $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($testCase->dataSetAsStringWithData())); + + $description = str_replace(' ', '_', $description.$datasetAsString); + + return [$filename, $description]; + } + + /** + * Gets the snapshot's "filename". + */ + private function getSnapshotFilename(string $filename, string $description): string + { + $relativePath = str_replace($this->testsPath, '', $filename); + + // remove extension from filename + $relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.')); + + return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description); + } +} diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 525e67e2..eccff9d3 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -163,7 +163,7 @@ final class Coverage * @param File $file * @return array */ - public static function getMissingCoverage($file): array + public static function getMissingCoverage(mixed $file): array { $shouldBeNewLine = true; diff --git a/src/TestSuite.php b/src/TestSuite.php index 1c4297f6..0baf4586 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -9,6 +9,7 @@ use Pest\Repositories\AfterAllRepository; use Pest\Repositories\AfterEachRepository; use Pest\Repositories\BeforeAllRepository; use Pest\Repositories\BeforeEachRepository; +use Pest\Repositories\SnapshotRepository; use Pest\Repositories\TestRepository; use PHPUnit\Framework\TestCase; @@ -47,6 +48,11 @@ final class TestSuite */ public AfterAllRepository $afterAll; + /** + * Holds the snapshots repository. + */ + public SnapshotRepository $snapshots; + /** * Holds the root path. */ @@ -69,8 +75,9 @@ final class TestSuite $this->tests = new TestRepository(); $this->afterEach = new AfterEachRepository(); $this->afterAll = new AfterAllRepository(); - $this->rootPath = (string) realpath($rootPath); + + $this->snapshots = new SnapshotRepository($this->rootPath.'/'.$this->testPath, '.pest/snapshots'); } /** diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/_within_describe__→_pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures_with_custom_message.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures_with_custom_message.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/failures_with_custom_message.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/not_failures.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/not_failures.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/not_failures.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString_.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString_.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString_.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString_.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php new file mode 100644 index 00000000..8887e2e1 --- /dev/null +++ b/tests/Features/Expect/toMatchSnapshot.php @@ -0,0 +1,94 @@ +snapshotable = <<<'HTML' +
+
+
+

Snapshot

+
+
+
+ HTML; +}); + +test('pass', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + expect($this->snapshotable)->toMatchSnapshot(); +}); + +test('pass with `__toString`', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + $object = new class($this->snapshotable) + { + public function __construct(protected string $snapshotable) + { + } + + public function __toString() + { + return $this->snapshotable; + } + }; + + expect($object)->toMatchSnapshot()->toMatchSnapshot(); +}); + +test('pass with `toString`', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + $object = new class($this->snapshotable) + { + public function __construct(protected string $snapshotable) + { + } + + public function toString() + { + return $this->snapshotable; + } + }; + + expect($object)->toMatchSnapshot()->toMatchSnapshot(); +}); + +test('pass with dataset', function ($data) { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get($this, $this->snapshotable); + + expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') + ->and($this->snapshotable)->toMatchSnapshot(); +})->with(['my-datas-set-value']); + +describe('within describe', function () { + test('pass with dataset', function ($data) { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get($this, $this->snapshotable); + + expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') + ->and($this->snapshotable)->toMatchSnapshot(); + }); +})->with(['my-datas-set-value']); + +test('failures', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + expect('contain that does not match snapshot')->toMatchSnapshot(); +})->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical.'); + +test('failures with custom message', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + expect('contain that does not match snapshot')->toMatchSnapshot('oh no'); +})->throws(ExpectationFailedException::class, 'oh no'); + +test('not failures', function () { + TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + + expect($this->snapshotable)->not->toMatchSnapshot(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index b79aa537..e7eb527e 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -18,7 +18,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 720 passed (1740 assertions)') + ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 728 passed (1767 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From b1c6f247e058a0fb3a997f89aff24b2c926b45bb Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 13:36:39 +0100 Subject: [PATCH 24/38] chore: uses snapshot testing in some visual testing --- composer.json | 2 +- src/Plugins/Help.php | 4 +++ ...n_with_data_set___________array_____.snap} | 0 ...__parallel______array____parallel___.snap} | 0 ...sual_snapshot_of_help_command_output.snap} | 1 + tests/Visual/Collision.php | 26 +++++-------------- tests/Visual/Help.php | 10 +------ tests/Visual/Parallel.php | 2 -- 8 files changed, 14 insertions(+), 31 deletions(-) rename tests/{.snapshots/collision.txt => .pest/snapshots/Visual/Collision/collision_with_data_set___________array_____.snap} (100%) rename tests/{.snapshots/collision-parallel.txt => .pest/snapshots/Visual/Collision/collision_with_data_set_______parallel______array____parallel___.snap} (100%) rename tests/{.snapshots/help-command.txt => .pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap} (98%) diff --git a/composer.json b/composer.json index 52ccdba9..25153077 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:parallel": "php bin/pest --colors=always --exclude-group=integration --parallel --processes=10", "test:integration": "php bin/pest --colors=always --group=integration", - "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", + "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always --update-snapshots", "test": [ "@test:refacto", "@test:lint", diff --git a/src/Plugins/Help.php b/src/Plugins/Help.php index cbbb13a4..b0643461 100644 --- a/src/Plugins/Help.php +++ b/src/Plugins/Help.php @@ -107,6 +107,10 @@ final class Help implements HandlesArguments 'arg' => '--parallel', 'desc' => 'Run tests in parallel', ], + [ + 'arg' => '--update-snapshots', + 'desc' => 'Update snapshots for tests using the "toMatchSnapshot" expectation', + ], ], ...$content['Execution']]; $content['Selection'] = [[ diff --git a/tests/.snapshots/collision.txt b/tests/.pest/snapshots/Visual/Collision/collision_with_data_set___________array_____.snap similarity index 100% rename from tests/.snapshots/collision.txt rename to tests/.pest/snapshots/Visual/Collision/collision_with_data_set___________array_____.snap diff --git a/tests/.snapshots/collision-parallel.txt b/tests/.pest/snapshots/Visual/Collision/collision_with_data_set_______parallel______array____parallel___.snap similarity index 100% rename from tests/.snapshots/collision-parallel.txt rename to tests/.pest/snapshots/Visual/Collision/collision_with_data_set_______parallel______array____parallel___.snap diff --git a/tests/.snapshots/help-command.txt b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap similarity index 98% rename from tests/.snapshots/help-command.txt rename to tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index f32c248b..c2eb7a74 100644 --- a/tests/.snapshots/help-command.txt +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -34,6 +34,7 @@ EXECUTION OPTIONS: --parallel ........................................... Run tests in parallel + --update-snapshots Update snapshots for tests using the "toMatchSnapshot" expectation --process-isolation ................ Run each test in a separate PHP process --globals-backup ................. Backup and restore $GLOBALS for each test --static-backup ......... Backup and restore static properties for each test diff --git a/tests/Visual/Collision.php b/tests/Visual/Collision.php index de6dad47..23f9b815 100644 --- a/tests/Visual/Collision.php +++ b/tests/Visual/Collision.php @@ -1,12 +1,6 @@ getOutput()); }; - if (getenv('REBUILD_SNAPSHOTS')) { - $outputContent = explode("\n", $output()); + $outputContent = explode("\n", $output()); + array_pop($outputContent); + array_pop($outputContent); + array_pop($outputContent); + + if (in_array('--parallel', $arguments)) { array_pop($outputContent); array_pop($outputContent); - array_pop($outputContent); - - if (in_array('--parallel', $arguments)) { - array_pop($outputContent); - array_pop($outputContent); - } - - file_put_contents($snapshot, implode("\n", $outputContent)); - - $this->markTestSkipped('Snapshot rebuilt.'); } - expect($output())->toContain(file_get_contents($snapshot)); + expect(implode("\n", $outputContent))->toMatchSnapshot(); })->with([ [['']], [['--parallel']], diff --git a/tests/Visual/Help.php b/tests/Visual/Help.php index a3a77a87..4f48171d 100644 --- a/tests/Visual/Help.php +++ b/tests/Visual/Help.php @@ -1,8 +1,6 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); @@ -11,11 +9,5 @@ test('visual snapshot of help command output', function () { return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); }; - if (getenv('REBUILD_SNAPSHOTS')) { - file_put_contents($snapshot, $output()); - - $this->markTestSkipped('Snapshot rebuilt.'); - } - - expect($output())->toContain(file_get_contents($snapshot)); + expect($output())->toMatchSnapshot(); })->skipOnWindows(); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index e7eb527e..e27fb3b7 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -11,8 +11,6 @@ $run = function () { $process->run(); - // expect($process->getExitCode())->toBe(0); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); }; From e541ee86fcea8e4c6d842c7976d487de431bd726 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 13:48:32 +0100 Subject: [PATCH 25/38] feat: adds "Illuminate\Testing\TestResponse" behavior to snapshot testing --- src/Mixins/Expectation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index e9da32dc..da6c2107 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -808,6 +808,7 @@ final class Expectation is_string($this->value) => $this->value, is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), + $this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line default => InvalidExpectationValue::expected('Stringable|string'), }; From 1f6970a5b3944c9f15abae9163fae8662c8d2cac Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 14:01:48 +0100 Subject: [PATCH 26/38] fix: returns relative path from snapshot --- src/Repositories/SnapshotRepository.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php index 7725cc13..89a4135e 100644 --- a/src/Repositories/SnapshotRepository.php +++ b/src/Repositories/SnapshotRepository.php @@ -49,7 +49,9 @@ final class SnapshotRepository throw ShouldNotHappen::fromMessage('Snapshot file could not be read.'); } - return [$snapshotFilename, $contents]; + $snapshot = str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); + + return [$snapshot, $contents]; } /** From 9426881cf6ef3eae251092b9ac793b29d5bd35dc Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 16:43:30 +0100 Subject: [PATCH 27/38] fix: avoids usage of `--update-snapshots` in parallel --- src/Plugins/Snapshot.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Plugins/Snapshot.php b/src/Plugins/Snapshot.php index 3f9cb850..262eadcc 100644 --- a/src/Plugins/Snapshot.php +++ b/src/Plugins/Snapshot.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Plugins; +use Pest\Exceptions\InvalidOption; use Pest\Contracts\Plugins\HandlesArguments; use Pest\TestSuite; @@ -23,6 +24,10 @@ final class Snapshot implements HandlesArguments return $arguments; } + if ($this->hasArgument('--parallel', $arguments)) { + throw new InvalidOption('The [--update-snapshots] option is not supported when running in parallel.'); + } + TestSuite::getInstance()->snapshots->flush(); return $this->popArgument('--update-snapshots', $arguments); From 25e15e76e0ea7c7024aacf1fa2099611b96b1885 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 19:43:15 +0100 Subject: [PATCH 28/38] tests: updates snapshots --- tests/.snapshots/Failure.php.inc | 30 ------------------------------ tests/Visual/Parallel.php | 2 +- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/tests/.snapshots/Failure.php.inc b/tests/.snapshots/Failure.php.inc index 058d1e26..e69de29b 100644 --- a/tests/.snapshots/Failure.php.inc +++ b/tests/.snapshots/Failure.php.inc @@ -1,30 +0,0 @@ -##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234'] -##teamcity[testCount count='8' flowId='1234'] -##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234'] -##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234'] -##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234'] -##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234'] -##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234'] -##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234'] -##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234'] -##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234'] -##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234'] -##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234'] -##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234'] -##teamcity[testFinished name='it can fail' duration='100000' flowId='1234'] -##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234'] -##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234'] -##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234'] -##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234'] -##teamcity[testIgnored name='it is not done yet' message='This test was ignored.' details='' flowId='1234'] -##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234'] -##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234'] -##teamcity[testIgnored name='build this one.' message='This test was ignored.' details='' flowId='1234'] -##teamcity[testFinished name='build this one.' duration='100000' flowId='1234'] -##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234'] -##teamcity[testFinished name='it is passing' duration='100000' flowId='1234'] -##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234'] - - Tests: 3 failed, 1 risky, 2 todos, 1 skipped, 1 passed (3 assertions) - Duration: 1.00s - diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index e27fb3b7..d44a5de3 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: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 728 passed (1767 assertions)') + ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 730 passed (1769 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); From 7f1135eeac160bf3945e4d17aa81efae7a5e1bab Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 19:48:33 +0100 Subject: [PATCH 29/38] chore: adjusts workflows --- .github/workflows/static.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 49528668..dfddaaf5 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -30,8 +30,11 @@ jobs: - name: Install Dependencies run: composer update --prefer-stable --no-interaction --no-progress --ansi - - name: Types - run: composer test:types + - name: Type Check + run: composer test:type:check + + - name: Type Coverage + run: composer test:type:coverage - name: Refacto run: composer test:refacto From fb443e0fa0571d824d2a6bb94031cec57314126c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 20:13:32 +0100 Subject: [PATCH 30/38] chore: fixes type checking --- src/Expectation.php | 2 +- src/Expectations/OppositeExpectation.php | 2 +- src/Plugins/Snapshot.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index ad0da961..7eef6fee 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -430,7 +430,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly(), + fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line, 'to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index ceec8337..cd672e81 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -109,7 +109,7 @@ final class OppositeExpectation { return Targeted::make( $this->original, - fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly(), + fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly() && assert(true), // @phpstan-ignore-line 'not to be readonly', FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); diff --git a/src/Plugins/Snapshot.php b/src/Plugins/Snapshot.php index 262eadcc..717512c2 100644 --- a/src/Plugins/Snapshot.php +++ b/src/Plugins/Snapshot.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Pest\Plugins; -use Pest\Exceptions\InvalidOption; use Pest\Contracts\Plugins\HandlesArguments; +use Pest\Exceptions\InvalidOption; use Pest\TestSuite; /** From 7e815cc985ac8fda5a72c07e146dc2f372b71fce Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 17 Jun 2023 20:56:23 +0100 Subject: [PATCH 31/38] tests: updates snapshots --- tests/.snapshots/success.txt | 47 +++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 80b07807..4927d35c 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -53,6 +53,7 @@ ✓ it appends CoversNothing to method attributes ✓ it does not append CoversNothing to other methods ✓ it throws exception if no class nor method has been found + ✓ a "describe" group of tests → it does not append CoversNothing to method attributes PASS Tests\Features\DatasetsTests - 1 todo ✓ it throws exception if dataset does not exist @@ -172,6 +173,29 @@ ! deprecated → str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated // tests/Features/Deprecated.php:6 ! user deprecated → Since foo 1.0: This is a deprecation description // vendor/symfony/deprecation-contracts/function.php:25 + PASS Tests\Features\Describe - 5 todos + ✓ before each + ✓ hooks → value + ✓ hooks in different orders → value + ↓ todo + ✓ previous describable before each does not get applied here + ↓ todo on hook → should not fail + ↓ todo on hook → should run + ↓ todo on describe → should not fail + ↓ todo on describe → should run + ✓ should run + ✓ with with (1) + ✓ with on hook → value with (2) + ✓ with on describe → value with (3) + + PASS Tests\Features\DescriptionLess + ✓ get 'foo' + ✓ get 'foo' → get 'bar' → expect true → toBeTrue + ✓ get 'foo' → expect true → toBeTrue + ✓ a "describe" group of tests → get 'foo' + ✓ a "describe" group of tests → get 'foo' → get 'bar' → expect true → toBeTrue + ✓ a "describe" group of tests → get 'foo' → expect true → toBeTrue + PASS Tests\Features\Exceptions ✓ it gives access the the underlying expectException ✓ it catch exceptions @@ -641,6 +665,16 @@ ✓ pass with class ✓ failures ✓ failures with custom message + ✓ not failures + + PASS Tests\Features\Expect\toMatchSnapshot + ✓ pass + ✓ pass with __toString + ✓ pass with toString + ✓ pass with dataset with ('my-datas-set-value') + ✓ within describe → pass with dataset with ('my-datas-set-value') + ✓ failures + ✓ failures with custom message ✓ not failures PASS Tests\Features\Expect\toStartWith @@ -714,18 +748,17 @@ ✓ it is not incompleted because of expect ✓ it is not incompleted because of assert ✓ it is not incompleted because of test with assertions + … a "describe" group of tests → it is incompleted PASS Tests\Features\It ✓ it is a test ✓ it is a higher order message test + ✓ a "describe" group of tests → it is a test + ✓ a "describe" group of tests → it is a higher order message test NOTI Tests\Features\Notices ! notice → This is a notice description // tests/Features/Notices.php:4 - - PASS Tests\Features\PendingHigherOrderTests - ✓ get 'foo' - ✓ get 'foo' → get 'bar' → expect true → toBeTrue - ✓ get 'foo' → expect true → toBeTrue + ! a "describe" group of tests → notice → This is a notice description // tests/Features/Notices.php:11 PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile ✓ uses dataset with (1) @@ -787,6 +820,7 @@ PASS Tests\Features\ThrowsNoExceptions ✓ it allows access to the underlying expectNotToPerformAssertions method ✓ it allows performing no expectations without being risky + ✓ a "describe" group of tests → it allows performing no expectations without being risky PASS Tests\Features\Todo - 3 todos ↓ something todo later @@ -797,6 +831,7 @@ WARN Tests\Features\Warnings ! warning → Undefined property: P\Tests\Features\Warnings::$fooqwdfwqdfqw ! user warning → This is a warning description + ! a "describe" group of tests → user warning → This is a warning description WARN Tests\Fixtures\CollisionTest - error @@ -1047,4 +1082,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 2 deprecated, 3 warnings, 4 incomplete, 1 notice, 8 todos, 18 skipped, 718 passed (1735 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 18 skipped, 741 passed (1784 assertions) \ No newline at end of file From 355a2349aff52f9241b8bf71864580baed5ea0b4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 18 Jun 2023 01:01:29 +0100 Subject: [PATCH 32/38] feat: allows array formats --- src/Mixins/Expectation.php | 8 ++- .../toMatchSnapshot/pass_with__toArray_.snap | 3 + .../toMatchSnapshot/pass_with_array.snap | 3 + ...sual_snapshot_of_help_command_output.snap} | 0 tests/.snapshots/coverage.txt | 60 ------------------- tests/.snapshots/success.txt | 8 ++- tests/Features/Expect/toMatchSnapshot.php | 28 +++++++++ tests/Visual/Parallel.php | 2 +- tests/Visual/Version.php | 12 +--- 9 files changed, 49 insertions(+), 75 deletions(-) create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray_.snap create mode 100644 tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array.snap rename tests/{.snapshots/version-command.txt => .pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap} (100%) delete mode 100644 tests/.snapshots/coverage.txt diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index da6c2107..c6876530 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -9,6 +9,7 @@ use Closure; use DateTimeInterface; use Error; use InvalidArgumentException; +use JsonSerializable; use Pest\Exceptions\InvalidExpectationValue; use Pest\Matchers\Any; use Pest\Support\Arr; @@ -23,6 +24,7 @@ use ReflectionFunction; use ReflectionNamedType; use Stringable; use Throwable; +use Traversable; /** * @internal @@ -809,7 +811,11 @@ final class Expectation is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), is_object($this->value) && method_exists($this->value, 'toString') => $this->value->toString(), $this->value instanceof \Illuminate\Testing\TestResponse => $this->value->getContent(), // @phpstan-ignore-line - default => InvalidExpectationValue::expected('Stringable|string'), + is_array($this->value) => json_encode($this->value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), + $this->value instanceof Traversable => json_encode(iterator_to_array($this->value), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), + $this->value instanceof JsonSerializable => json_encode($this->value->jsonSerialize(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), + is_object($this->value) && method_exists($this->value, 'toArray') => json_encode($this->value->toArray(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), + default => InvalidExpectationValue::expected('array|object|string'), }; $testCase = TestSuite::getInstance()->test; diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray_.snap new file mode 100644 index 00000000..afd4f5f9 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray_.snap @@ -0,0 +1,3 @@ +{ + "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" +} \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array.snap new file mode 100644 index 00000000..afd4f5f9 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array.snap @@ -0,0 +1,3 @@ +{ + "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" +} \ No newline at end of file diff --git a/tests/.snapshots/version-command.txt b/tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap similarity index 100% rename from tests/.snapshots/version-command.txt rename to tests/.pest/snapshots/Visual/Version/visual_snapshot_of_help_command_output.snap diff --git a/tests/.snapshots/coverage.txt b/tests/.snapshots/coverage.txt deleted file mode 100644 index e7cf7549..00000000 --- a/tests/.snapshots/coverage.txt +++ /dev/null @@ -1,60 +0,0 @@ - - PASS Tests\Playground - ✓ basic - - Tests: 1 passed - Time: 0.20s - Cov: 6.49% - - Actions/AddsDefaults ........................................... 0.0 % - Actions/AddsTests .............................................. 0.0 % - Actions/LoadStructure .......................................... 0.0 % - Actions/ValidatesConfiguration ................................. 0.0 % - Actions/ValidatesEnvironment ................................... 0.0 % - Concerns/TestCase 40..54, 71..88, 123..126, 147 ............... 44.4 % - Console/Command ................................................ 0.0 % - Contracts/HasPrintableTestCaseName ............................. 0.0 % - Contracts/Plugins/AddsOutput ................................ 100.0 % - Contracts/Plugins/HandlesArguments .......................... 100.0 % - Datasets ....................................................... 0.0 % - Exceptions/AfterAllAlreadyExist ................................ 0.0 % - Exceptions/AfterEachAlreadyExist ............................... 0.0 % - Exceptions/AttributeNotSupportedYet ............................ 0.0 % - Exceptions/BeforeEachAlreadyExist .............................. 0.0 % - Exceptions/DatasetAlreadyExist ................................. 0.0 % - Exceptions/DatasetDoesNotExist ................................. 0.0 % - Exceptions/FileOrFolderNotFound ................................ 0.0 % - Exceptions/InvalidConsoleArgument .............................. 0.0 % - Exceptions/InvalidPestCommand .................................. 0.0 % - Exceptions/InvalidUsesPath ..................................... 0.0 % - Exceptions/ShouldNotHappen ..................................... 0.0 % - Exceptions/TestAlreadyExist .................................... 0.0 % - Exceptions/TestCaseAlreadyInUse ................................ 0.0 % - Exceptions/TestCaseClassOrTraitNotFound ........................ 0.0 % - Factories/TestCaseFactory 111..133, 141..204 ................... 8.2 % - Laravel/Commands/PestDatasetCommand ............................ 0.0 % - Laravel/Commands/PestInstallCommand ............................ 0.0 % - Laravel/Commands/PestTestCommand ............................... 0.0 % - Laravel/PestServiceProvider .................................... 0.0 % - PendingObjects/AfterEachCall ................................... 0.0 % - PendingObjects/BeforeEachCall .................................. 0.0 % - PendingObjects/TestCall ........................................ 0.0 % - PendingObjects/UsesCall ........................................ 0.0 % - Plugin ......................................................... 0.0 % - Repositories/AfterAllRepository ................................ 0.0 % - Repositories/AfterEachRepository 28..33 ....................... 60.0 % - Repositories/BeforeAllRepository ............................... 0.0 % - Repositories/BeforeEachRepository 26..31 ...................... 20.0 % - Repositories/TestRepository .................................... 0.0 % - Support/Backtrace .............................................. 0.0 % - Support/ChainableClosure .................................... 100.0 % - Support/Container .............................................. 0.0 % - Support/ExceptionTrace 25..32 ................................. 28.6 % - Support/HigherOrderMessage ..................................... 0.0 % - Support/HigherOrderMessageCollection 24..25, 33, 43 ........... 50.0 % - Support/HigherOrderTapProxy .................................... 0.0 % - Support/NullClosure ......................................... 100.0 % - Support/Reflection ............................................. 0.0 % - Support/Str .................................................... 0.0 % - TestSuite 80..87, 95..101, 105 ................................ 20.0 % - globals ........................................................ 0.0 % diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 4927d35c..45efb415 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -673,6 +673,8 @@ ✓ pass with toString ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') + ✓ pass with toArray + ✓ pass with array ✓ failures ✓ failures with custom message ✓ not failures @@ -1079,7 +1081,7 @@ ✓ todo ✓ todo in parallel - PASS Tests\Visual\Version - ✓ visual snapshot of help command output + WARN Tests\Visual\Version + - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 18 skipped, 741 passed (1784 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 19 skipped, 742 passed (1787 assertions) \ No newline at end of file diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php index 8887e2e1..29a63a59 100644 --- a/tests/Features/Expect/toMatchSnapshot.php +++ b/tests/Features/Expect/toMatchSnapshot.php @@ -75,6 +75,34 @@ describe('within describe', function () { }); })->with(['my-datas-set-value']); +test('pass with `toArray`', function () { + TestSuite::getInstance()->snapshots->save($this, json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); + + $object = new class($this->snapshotable) + { + public function __construct(protected string $snapshotable) + { + } + + public function toArray() + { + return [ + 'key' => $this->snapshotable, + ]; + } + }; + + expect($object)->toMatchSnapshot()->toMatchSnapshot(); +}); + +test('pass with array', function () { + TestSuite::getInstance()->snapshots->save($this, json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); + + expect([ + 'key' => $this->snapshotable, + ])->toMatchSnapshot()->toMatchSnapshot(); +}); + test('failures', function () { TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index d44a5de3..eff61789 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: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 730 passed (1769 assertions)') + ->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 732 passed (1773 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows(); diff --git a/tests/Visual/Version.php b/tests/Visual/Version.php index 7db09e3c..50b156b8 100644 --- a/tests/Visual/Version.php +++ b/tests/Visual/Version.php @@ -1,8 +1,6 @@ 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'])); @@ -11,11 +9,5 @@ test('visual snapshot of help command output', function () { return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); }; - if (getenv('REBUILD_SNAPSHOTS')) { - file_put_contents($snapshot, $output()); - - $this->markTestSkipped('Snapshot rebuilt.'); - } - - expect($output())->toContain(file_get_contents($snapshot)); -})->skipOnWindows(); + expect($output())->toMatchSnapshot(); +})->skipOnWindows()->skip(! getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE')); From cee5b9feb99527cc1035b9fe4f9962f36dd5a90c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 19 Jun 2023 17:12:50 +0100 Subject: [PATCH 33/38] fix: arguments keys --- src/Plugins/Concerns/HandleArguments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/Concerns/HandleArguments.php b/src/Plugins/Concerns/HandleArguments.php index 4734a045..13bfbbbc 100644 --- a/src/Plugins/Concerns/HandleArguments.php +++ b/src/Plugins/Concerns/HandleArguments.php @@ -44,6 +44,6 @@ trait HandleArguments unset($arguments[$argument]); - return array_flip($arguments); + return array_values(array_flip($arguments)); } } From 19e75d1070de99d3f009c445aa047d5f27e57b30 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 27 Jun 2023 21:37:20 +0100 Subject: [PATCH 34/38] chore: coding style --- src/Mixins/Expectation.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index c6876530..a6fc2215 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -22,7 +22,6 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use ReflectionFunction; use ReflectionNamedType; -use Stringable; use Throwable; use Traversable; From 4d0dffafd3aab44f15272665e7d6b6edd46df709 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 30 Jun 2023 10:09:17 +0100 Subject: [PATCH 35/38] tests: adjusts snapshots --- tests/.snapshots/Failure.php.inc | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/.snapshots/Failure.php.inc b/tests/.snapshots/Failure.php.inc index e69de29b..058d1e26 100644 --- a/tests/.snapshots/Failure.php.inc +++ b/tests/.snapshots/Failure.php.inc @@ -0,0 +1,30 @@ +##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234'] +##teamcity[testCount count='8' flowId='1234'] +##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234'] +##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234'] +##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234'] +##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234'] +##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234'] +##teamcity[testFinished name='it can be ignored because of no assertions' duration='100000' flowId='1234'] +##teamcity[testStarted name='it can be ignored because it is skipped' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because it is skipped' flowId='1234'] +##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234'] +##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234'] +##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234'] +##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18' flowId='1234'] +##teamcity[testFinished name='it can fail' duration='100000' flowId='1234'] +##teamcity[testStarted name='it throws exception' locationHint='pest_qn://tests/.tests/Failure.php::it throws exception' flowId='1234'] +##teamcity[testFailed name='it throws exception' message='Exception: test error' details='at tests/.tests/Failure.php:22' flowId='1234'] +##teamcity[testFinished name='it throws exception' duration='100000' flowId='1234'] +##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234'] +##teamcity[testIgnored name='it is not done yet' message='This test was ignored.' details='' flowId='1234'] +##teamcity[testFinished name='it is not done yet' duration='100000' flowId='1234'] +##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234'] +##teamcity[testIgnored name='build this one.' message='This test was ignored.' details='' flowId='1234'] +##teamcity[testFinished name='build this one.' duration='100000' flowId='1234'] +##teamcity[testStarted name='it is passing' locationHint='pest_qn://tests/.tests/Failure.php::it is passing' flowId='1234'] +##teamcity[testFinished name='it is passing' duration='100000' flowId='1234'] +##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234'] + + Tests: 3 failed, 1 risky, 2 todos, 1 skipped, 1 passed (3 assertions) + Duration: 1.00s + From aa4fb3bba2834a3ca67544547ba66c9c8787915d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 8 Jul 2023 13:42:37 +0100 Subject: [PATCH 36/38] chore: bumps arch plugin --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 25153077..ec6159d5 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "nunomaduro/collision": "^7.7.0", "nunomaduro/termwind": "^1.15.1", "pestphp/pest-plugin": "^2.0.1", - "pestphp/pest-plugin-arch": "^2.2.1", + "pestphp/pest-plugin-arch": "^2.2.2", "phpunit/phpunit": "^10.2.3" }, "conflict": { From 21a04fefcfc98bd5737e231adf2f8ff631f4a2ff Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 8 Jul 2023 17:54:21 +0100 Subject: [PATCH 37/38] fix: same class on `toExtend` --- src/Expectation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expectation.php b/src/Expectation.php index 7eef6fee..65de1239 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -542,7 +542,7 @@ final class Expectation { return Targeted::make( $this, - fn (ObjectDescription $object): bool => $object->reflectionClass->isSubclassOf($class), + fn (ObjectDescription $object): bool => $class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class), sprintf("to extend '%s'", $class), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), ); From c7e6df7c95320c4d70d5e935252e35fdbe181291 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 15 Jul 2023 15:11:03 +0100 Subject: [PATCH 38/38] chore: coding style --- overrides/Runner/ResultCache/DefaultResultCache.php | 2 +- src/Concerns/Pipeable.php | 2 +- src/Logging/TeamCity/Converter.php | 2 +- src/Logging/TeamCity/ServiceMessage.php | 4 ++-- src/Logging/TeamCity/TeamCityLogger.php | 2 +- src/Mixins/Expectation.php | 2 +- src/PendingCalls/Concerns/Describable.php | 2 +- src/Repositories/DatasetsRepository.php | 2 +- src/Support/Closure.php | 2 +- src/TestCaseFilters/GitDirtyTestCaseFilter.php | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/overrides/Runner/ResultCache/DefaultResultCache.php b/overrides/Runner/ResultCache/DefaultResultCache.php index 640c3971..40134011 100644 --- a/overrides/Runner/ResultCache/DefaultResultCache.php +++ b/overrides/Runner/ResultCache/DefaultResultCache.php @@ -89,7 +89,7 @@ final class DefaultResultCache implements ResultCache */ private array $times = []; - public function __construct(?string $filepath = null) + public function __construct(string $filepath = null) { if ($filepath !== null && is_dir($filepath)) { $filepath .= DIRECTORY_SEPARATOR.self::DEFAULT_RESULT_CACHE_FILENAME; diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php index 15102870..6d889f16 100644 --- a/src/Concerns/Pipeable.php +++ b/src/Concerns/Pipeable.php @@ -36,7 +36,7 @@ trait Pipeable /** * Register an interceptor that should replace an existing expectation. * - * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter + * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter */ public function intercept(string $name, string|Closure $filter, Closure $handler): void { diff --git a/src/Logging/TeamCity/Converter.php b/src/Logging/TeamCity/Converter.php index 336dc76b..2a2cce4f 100644 --- a/src/Logging/TeamCity/Converter.php +++ b/src/Logging/TeamCity/Converter.php @@ -153,7 +153,7 @@ final class Converter /** * Gets the test suite location. */ - public function getTestSuiteLocation(TestSuite $testSuite): string|null + public function getTestSuiteLocation(TestSuite $testSuite): ?string { $tests = $testSuite->tests()->asArray(); diff --git a/src/Logging/TeamCity/ServiceMessage.php b/src/Logging/TeamCity/ServiceMessage.php index e27f8013..1f3f2edf 100644 --- a/src/Logging/TeamCity/ServiceMessage.php +++ b/src/Logging/TeamCity/ServiceMessage.php @@ -9,7 +9,7 @@ namespace Pest\Logging\TeamCity; */ final class ServiceMessage { - private static int|null $flowId = null; + private static ?int $flowId = null; /** * @param array $parameters @@ -32,7 +32,7 @@ final class ServiceMessage return "##teamcity[$this->type$paramsToString]"; } - public static function testSuiteStarted(string $name, string|null $location): self + public static function testSuiteStarted(string $name, ?string $location): self { return new self('testSuiteStarted', [ 'name' => $name, diff --git a/src/Logging/TeamCity/TeamCityLogger.php b/src/Logging/TeamCity/TeamCityLogger.php index 5bbde518..92d35daf 100644 --- a/src/Logging/TeamCity/TeamCityLogger.php +++ b/src/Logging/TeamCity/TeamCityLogger.php @@ -60,7 +60,7 @@ final class TeamCityLogger public function __construct( private readonly OutputInterface $output, private readonly Converter $converter, - private readonly int|null $flowId, + private readonly ?int $flowId, private readonly bool $withoutDuration, ) { $this->registerSubscribers(); diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index a6fc2215..b90e601a 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -37,7 +37,7 @@ final class Expectation /** * The exporter instance, if any. */ - private Exporter|null $exporter = null; + private ?Exporter $exporter = null; /** * Creates a new expectation. diff --git a/src/PendingCalls/Concerns/Describable.php b/src/PendingCalls/Concerns/Describable.php index 4eac9d2d..bb013681 100644 --- a/src/PendingCalls/Concerns/Describable.php +++ b/src/PendingCalls/Concerns/Describable.php @@ -9,5 +9,5 @@ namespace Pest\PendingCalls\Concerns; */ trait Describable { - public string|null $describing = null; + public ?string $describing = null; } diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 90ff9e3f..a43f1a84 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -89,7 +89,7 @@ final class DatasetsRepository * @param array|string> $dataset * @return array|null */ - public static function resolve(array $dataset, string $currentTestFile): array|null + public static function resolve(array $dataset, string $currentTestFile): ?array { if ($dataset === []) { return null; diff --git a/src/Support/Closure.php b/src/Support/Closure.php index ebc8c769..a7b75f31 100644 --- a/src/Support/Closure.php +++ b/src/Support/Closure.php @@ -18,7 +18,7 @@ final class Closure * * @throws ShouldNotHappen */ - public static function bind(BaseClosure|null $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure + public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure { if ($closure == null) { throw ShouldNotHappen::fromMessage('Could not bind null closure.'); diff --git a/src/TestCaseFilters/GitDirtyTestCaseFilter.php b/src/TestCaseFilters/GitDirtyTestCaseFilter.php index dc4f20b3..a4620a91 100644 --- a/src/TestCaseFilters/GitDirtyTestCaseFilter.php +++ b/src/TestCaseFilters/GitDirtyTestCaseFilter.php @@ -16,7 +16,7 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter /** * @var string[]|null */ - private array|null $changedFiles = null; + private ?array $changedFiles = null; public function __construct(private readonly string $projectRoot) {