From cbee6e76b0deba6b6af87e86a001ab5c3a4b1f39 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 19 Sep 2022 17:45:27 +0200 Subject: [PATCH 01/59] [feat] scoped datasets --- src/Bootstrappers/BootFiles.php | 27 +++++++- src/Exceptions/DatasetAlreadyExist.php | 4 +- src/Functions.php | 13 +++- src/Repositories/DatasetsRepository.php | 61 ++++++++++++++----- src/Support/Backtrace.php | 24 ++++++++ src/Support/Str.php | 18 ++++++ tests/.snapshots/success.txt | 48 ++++++++++++++- .../{Datasets.php => DatasetsTests.php} | 18 +++--- .../Directory/Datasets/Scoped.php | 5 ++ .../Directory/NestedDirectory1/Datasets.php | 5 ++ ...tFileInNestedDirectoryWithDatasetsFile.php | 12 ++++ .../TestFileInNestedDirectory.php | 12 ++++ .../TestFileWithLocallyDefinedDataset.php | 16 +++++ .../Directory/TestFileWithScopedDataset.php | 12 ++++ .../ScopedDatasets/TestFileOutOfScope.php | 12 ++++ .../Unit/{Datasets.php => DatasetsTests.php} | 20 +++--- 16 files changed, 267 insertions(+), 40 deletions(-) rename tests/Features/{Datasets.php => DatasetsTests.php} (94%) create mode 100644 tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php create mode 100644 tests/Features/ScopedDatasets/Directory/NestedDirectory1/Datasets.php create mode 100644 tests/Features/ScopedDatasets/Directory/NestedDirectory1/TestFileInNestedDirectoryWithDatasetsFile.php create mode 100644 tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php create mode 100644 tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php create mode 100644 tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php create mode 100644 tests/Features/ScopedDatasets/TestFileOutOfScope.php rename tests/Unit/{Datasets.php => DatasetsTests.php} (81%) diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 765d0843..13ad0504 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -9,6 +9,7 @@ use function Pest\testDirectory; use Pest\TestSuite; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator; /** * @internal @@ -21,8 +22,6 @@ final class BootFiles * @var array */ private const STRUCTURE = [ - 'Datasets', - 'Datasets.php', 'Expectations', 'Expectations.php', 'Helpers', @@ -56,6 +55,8 @@ final class BootFiles $this->load($filename); } } + + $this->bootDatasets($testsPath); } /** @@ -73,4 +74,26 @@ final class BootFiles include_once $filename; } + + private function bootDatasets(string $testsPath): void + { + $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); + + foreach ($files as $fullPath) { + $filename = Str::afterLast($fullPath, DIRECTORY_SEPARATOR); + + if ($filename === 'Datasets.php') { + $this->load($fullPath); + + continue; + } + + $directoryFullPath = Str::beforeLast($fullPath, DIRECTORY_SEPARATOR); + $directory = Str::afterLast($directoryFullPath, DIRECTORY_SEPARATOR); + + if ($directory === 'Datasets') { + $this->load($fullPath); + } + } + } } diff --git a/src/Exceptions/DatasetAlreadyExist.php b/src/Exceptions/DatasetAlreadyExist.php index b5276329..91146997 100644 --- a/src/Exceptions/DatasetAlreadyExist.php +++ b/src/Exceptions/DatasetAlreadyExist.php @@ -17,8 +17,8 @@ final class DatasetAlreadyExist extends InvalidArgumentException implements Exce /** * Creates a new Exception instance. */ - public function __construct(string $name) + public function __construct(string $name, string $scope) { - parent::__construct(sprintf('A dataset with the name `%s` already exist.', $name)); + parent::__construct(sprintf('A dataset with the name `%s` already exist in scope [%s].', $name, $scope)); } } diff --git a/src/Functions.php b/src/Functions.php index 50571727..a1dad4fe 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -10,6 +10,7 @@ use Pest\PendingCalls\UsesCall; use Pest\Repositories\DatasetsRepository; use Pest\Support\Backtrace; use Pest\Support\HigherOrderTapProxy; +use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\TestCase; @@ -60,7 +61,17 @@ if (! function_exists('dataset')) { */ function dataset(string $name, Closure|iterable $dataset): void { - DatasetsRepository::set($name, $dataset); + $file = Backtrace::datasetsFile(); + $filename = Str::afterLast($file, DIRECTORY_SEPARATOR); + $scope = Str::beforeLast($file, DIRECTORY_SEPARATOR); + + if (Str::afterLast($scope, DIRECTORY_SEPARATOR) === 'Datasets') { + $scope = Str::beforeLast($scope, DIRECTORY_SEPARATOR); + } elseif ($filename !== 'Datasets.php') { + $scope = $file; + } + + DatasetsRepository::set($name, $dataset, $scope); } } diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index f6874fc3..d9f06874 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -36,13 +36,15 @@ final class DatasetsRepository * * @param Closure|iterable $data */ - public static function set(string $name, Closure|iterable $data): void + public static function set(string $name, Closure|iterable $data, string $scope): void { - if (array_key_exists($name, self::$datasets)) { - throw new DatasetAlreadyExist($name); + $datasetKey = "$scope>>>$name"; + + if (array_key_exists("$datasetKey", self::$datasets)) { + throw new DatasetAlreadyExist($name, $scope); } - self::$datasets[$name] = $data; + self::$datasets[$datasetKey] = $data; } /** @@ -52,7 +54,7 @@ final class DatasetsRepository */ public static function with(string $filename, string $description, array $with): void { - self::$withs[$filename.'>>>'.$description] = $with; + self::$withs["$filename>>>$description"] = $with; } public static function has(string $filename, string $description): bool @@ -67,9 +69,10 @@ final class DatasetsRepository */ public static function get(string $filename, string $description) { + // dump("requesting file: " . $filename); $dataset = self::$withs[$filename.'>>>'.$description]; - $dataset = self::resolve($description, $dataset); + $dataset = self::resolve($dataset, $filename); if ($dataset === null) { throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); @@ -84,14 +87,14 @@ final class DatasetsRepository * @param array|string> $dataset * @return array|null */ - public static function resolve(string $description, array $dataset): array|null + public static function resolve(array $dataset, string $currentTestFile): array|null { /* @phpstan-ignore-next-line */ if (empty($dataset)) { return null; } - $dataset = self::processDatasets($dataset); + $dataset = self::processDatasets($dataset, $currentTestFile); $datasetCombinations = self::getDatasetsCombinations($dataset); @@ -132,11 +135,45 @@ final class DatasetsRepository return $namedData; } + /** + * @return Closure|iterable + */ + private static function getScopedDataset(string $name, string $currentTestFile) + { + $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { + [$datasetScope, $datasetName] = explode('>>>', $key); + + if ($name !== $datasetName) { + return false; + } + + if (! str_starts_with($currentTestFile, $datasetScope)) { + return false; + } + + return true; + }, ARRAY_FILTER_USE_KEY); + + $closestScopeDatasetKey = array_reduce(array_keys($matchingDatasets), function ($keyA, $keyB) { + if ($keyA === null) { + return $keyB; + } + + return strlen($keyA) > strlen($keyB) ? $keyA : $keyB; + }); + + if ($closestScopeDatasetKey === null) { + throw new DatasetDoesNotExist($name); + } + + return $matchingDatasets[$closestScopeDatasetKey]; + } + /** * @param array|string> $datasets * @return array> */ - private static function processDatasets(array $datasets): array + private static function processDatasets(array $datasets, string $currentTestFile): array { $processedDatasets = []; @@ -144,11 +181,7 @@ final class DatasetsRepository $processedDataset = []; if (is_string($data)) { - if (! array_key_exists($data, self::$datasets)) { - throw new DatasetDoesNotExist($data); - } - - $datasets[$index] = self::$datasets[$data]; + $datasets[$index] = self::getScopedDataset($data, $currentTestFile); } if (is_callable($datasets[$index])) { diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php index 56ac4fc9..beccedac 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -42,6 +42,30 @@ final class Backtrace return $current[self::FILE]; } + /** + * Returns the current datasets file. + */ + public static function datasetsFile(): string + { + $current = null; + + foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) { + assert(array_key_exists(self::FILE, $trace)); + + if (Str::endsWith($trace['file'], 'Bootstrappers/BootFiles.php') || Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) { + break; + } + + $current = $trace; + } + + if ($current === null) { + throw ShouldNotHappen::fromMessage('Dataset file not found.'); + } + + return $current[self::FILE]; + } + /** * Returns the filename that called the current function/method. */ diff --git a/src/Support/Str.php b/src/Support/Str.php index 69522b6a..28d4ef93 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -59,6 +59,24 @@ final class Str return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code); } + /** + * Return the remainder of a string after the last occurrence of a given value. + */ + public static function afterLast(string $subject, string $search): string + { + if ($search === '') { + return $subject; + } + + $position = strrpos($subject, $search); + + if ($position === false) { + return $subject; + } + + return substr($subject, $position + strlen($search)); + } + /** * Get the portion of a string before the last occurrence of a given value. */ diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index c23ef369..50598b85 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -29,7 +29,7 @@ ✓ it does not append CoversNothing to other methods ✓ it throws exception if no class nor method has been found - PASS Tests\Features\Datasets + PASS Tests\Features\DatasetsTests ✓ it throws exception if dataset does not exist ✓ it throws exception if dataset already exist ✓ it sets closures @@ -116,6 +116,7 @@ ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2 ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...)) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...)) + ↓ forbids to define tests in Datasets dirs and Datasets.php files PASS Tests\Features\Depends ✓ first @@ -663,6 +664,47 @@ ✓ get 'foo' → get 'bar' → expect true → toBeTrue ✓ get 'foo' → expect true → toBeTrue + PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/NestedDirector...ts.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory2\TestFileInNestedDirectory + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\TestFileWithLocallyDefinedDataset + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/ScopedDatasets.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\TestFileWithScopedDataset + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\TestFileOutOfScope + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ the right dataset is taken + WARN Tests\Features\Skip ✓ it do not skips - it skips with truthy → 1 @@ -761,7 +803,7 @@ PASS Tests\Unit\Console\Help ✓ it outputs the help information when --help is used - PASS Tests\Unit\Datasets + PASS Tests\Unit\DatasetsTests ✓ it show only the names of named datasets in their description ✓ it show the actual dataset of non-named datasets in their description ✓ it show only the names of multiple named datasets in their description @@ -818,4 +860,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 1 todo, 18 skipped, 562 passed (1460 assertions) + Tests: 4 incomplete, 2 todos, 18 skipped, 593 passed (1503 assertions) \ No newline at end of file diff --git a/tests/Features/Datasets.php b/tests/Features/DatasetsTests.php similarity index 94% rename from tests/Features/Datasets.php rename to tests/Features/DatasetsTests.php index 183b2d3b..9dc72b2b 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/DatasetsTests.php @@ -13,28 +13,28 @@ it('throws exception if dataset does not exist', function () { $this->expectException(DatasetDoesNotExist::class); $this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`."); - DatasetsRepository::resolve('foo', ['first']); + DatasetsRepository::resolve(['first'], __FILE__); }); it('throws exception if dataset already exist', function () { - DatasetsRepository::set('second', [[]]); + DatasetsRepository::set('second', [[]], __DIR__); $this->expectException(DatasetAlreadyExist::class); - $this->expectExceptionMessage('A dataset with the name `second` already exist.'); - DatasetsRepository::set('second', [[]]); + $this->expectExceptionMessage('A dataset with the name `second` already exist in scope ['.__DIR__.'].'); + DatasetsRepository::set('second', [[]], __DIR__); }); it('sets closures', function () { DatasetsRepository::set('foo', function () { yield [1]; - }); + }, __DIR__); - expect(DatasetsRepository::resolve('foo', ['foo']))->toBe(['(1)' => [1]]); + expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]); }); it('sets arrays', function () { - DatasetsRepository::set('bar', [[2]]); + DatasetsRepository::set('bar', [[2]], __DIR__); - expect(DatasetsRepository::resolve('bar', ['bar']))->toBe(['(2)' => [2]]); + expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]); }); it('gets bound to test case object', function ($value) { @@ -304,3 +304,5 @@ it('can correctly resolve a bound dataset that returns an array but wants to be return ['foo', 'bar', 'baz']; }, ]); + +todo('forbids to define tests in Datasets dirs and Datasets.php files'); diff --git a/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php b/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php new file mode 100644 index 00000000..d49e32c3 --- /dev/null +++ b/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php @@ -0,0 +1,5 @@ +text = ''; +test('uses dataset', function ($value) use ($state) { + $state->text .= $value; + expect(true)->toBe(true); +})->with('numbers.array'); + +test('the right dataset is taken', function () use ($state) { + expect($state->text)->toBe('12345ScopedDatasets/NestedDirectory1/Datasets.php'); +}); diff --git a/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php b/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php new file mode 100644 index 00000000..a86b9d23 --- /dev/null +++ b/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php @@ -0,0 +1,12 @@ +text = ''; +test('uses dataset', function ($value) use ($state) { + $state->text .= $value; + expect(true)->toBe(true); +})->with('numbers.array'); + +test('the right dataset is taken', function () use ($state) { + expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php'); +}); diff --git a/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php b/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php new file mode 100644 index 00000000..0bf23b8c --- /dev/null +++ b/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php @@ -0,0 +1,16 @@ +text = ''; +test('uses dataset', function ($value) use ($state) { + $state->text .= $value; + expect(true)->toBe(true); +})->with('numbers.array'); + +test('the right dataset is taken', function () use ($state) { + expect($state->text)->toBe('12345ScopedDatasets/ScopedDatasets.php'); +}); diff --git a/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php b/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php new file mode 100644 index 00000000..a86b9d23 --- /dev/null +++ b/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php @@ -0,0 +1,12 @@ +text = ''; +test('uses dataset', function ($value) use ($state) { + $state->text .= $value; + expect(true)->toBe(true); +})->with('numbers.array'); + +test('the right dataset is taken', function () use ($state) { + expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php'); +}); diff --git a/tests/Features/ScopedDatasets/TestFileOutOfScope.php b/tests/Features/ScopedDatasets/TestFileOutOfScope.php new file mode 100644 index 00000000..e2d23225 --- /dev/null +++ b/tests/Features/ScopedDatasets/TestFileOutOfScope.php @@ -0,0 +1,12 @@ +text = ''; +test('uses dataset', function ($value) use ($state) { + $state->text .= $value; + expect(true)->toBe(true); +})->with('numbers.array'); + +test('the right dataset is taken', function () use ($state) { + expect($state->text)->toBe('12'); +}); diff --git a/tests/Unit/Datasets.php b/tests/Unit/DatasetsTests.php similarity index 81% rename from tests/Unit/Datasets.php rename to tests/Unit/DatasetsTests.php index 83b33b04..a1914f82 100644 --- a/tests/Unit/Datasets.php +++ b/tests/Unit/DatasetsTests.php @@ -3,31 +3,31 @@ use Pest\Repositories\DatasetsRepository; it('show only the names of named datasets in their description', function () { - $descriptions = array_keys(DatasetsRepository::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve([ [ 'one' => [1], 'two' => [[2]], ], - ])); + ], __FILE__)); expect($descriptions[0])->toBe('data set "one"') ->and($descriptions[1])->toBe('data set "two"'); }); it('show the actual dataset of non-named datasets in their description', function () { - $descriptions = array_keys(DatasetsRepository::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve([ [ [1], [[2]], ], - ])); + ], __FILE__)); expect($descriptions[0])->toBe('(1)'); expect($descriptions[1])->toBe('(array(2))'); }); it('show only the names of multiple named datasets in their description', function () { - $descriptions = array_keys(DatasetsRepository::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve([ [ 'one' => [1], 'two' => [[2]], @@ -36,7 +36,7 @@ it('show only the names of multiple named datasets in their description', functi 'three' => [3], 'four' => [[4]], ], - ])); + ], __FILE__)); expect($descriptions[0])->toBe('data set "one" / data set "three"'); expect($descriptions[1])->toBe('data set "one" / data set "four"'); @@ -45,7 +45,7 @@ it('show only the names of multiple named datasets in their description', functi }); it('show the actual dataset of multiple non-named datasets in their description', function () { - $descriptions = array_keys(DatasetsRepository::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve([ [ [1], [[2]], @@ -54,7 +54,7 @@ it('show the actual dataset of multiple non-named datasets in their description' [3], [[4]], ], - ])); + ], __FILE__)); expect($descriptions[0])->toBe('(1) / (3)'); expect($descriptions[1])->toBe('(1) / (array(4))'); @@ -63,7 +63,7 @@ it('show the actual dataset of multiple non-named datasets in their description' }); it('show the correct description for mixed named and not-named datasets', function () { - $descriptions = array_keys(DatasetsRepository::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve([ [ 'one' => [1], [[2]], @@ -72,7 +72,7 @@ it('show the correct description for mixed named and not-named datasets', functi [3], 'four' => [[4]], ], - ])); + ], __FILE__)); expect($descriptions[0])->toBe('data set "one" / (3)'); expect($descriptions[1])->toBe('data set "one" / data set "four"'); From 80854d5f8700a958be28433cd60c2de74404c4a3 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 10:33:15 +0200 Subject: [PATCH 02/59] Add DatasetInfo support class --- src/Bootstrappers/BootFiles.php | 18 ++++---------- src/Functions.php | 13 ++-------- src/Support/DatasetInfo.php | 38 ++++++++++++++++++++++++++++++ src/Support/Str.php | 18 -------------- tests/.snapshots/success.txt | 22 ++++++++++++++++- tests/Unit/Support/DatasetInfo.php | 36 ++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 44 deletions(-) create mode 100644 src/Support/DatasetInfo.php create mode 100644 tests/Unit/Support/DatasetInfo.php diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 13ad0504..7629e7ed 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Bootstrappers; +use Pest\Support\DatasetInfo; use Pest\Support\Str; use function Pest\testDirectory; use Pest\TestSuite; @@ -79,20 +80,9 @@ final class BootFiles { $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); - foreach ($files as $fullPath) { - $filename = Str::afterLast($fullPath, DIRECTORY_SEPARATOR); - - if ($filename === 'Datasets.php') { - $this->load($fullPath); - - continue; - } - - $directoryFullPath = Str::beforeLast($fullPath, DIRECTORY_SEPARATOR); - $directory = Str::afterLast($directoryFullPath, DIRECTORY_SEPARATOR); - - if ($directory === 'Datasets') { - $this->load($fullPath); + foreach ($files as $file) { + if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) { + $this->load($file); } } } diff --git a/src/Functions.php b/src/Functions.php index a1dad4fe..ea53c22c 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -9,8 +9,8 @@ use Pest\PendingCalls\TestCall; use Pest\PendingCalls\UsesCall; use Pest\Repositories\DatasetsRepository; use Pest\Support\Backtrace; +use Pest\Support\DatasetInfo; use Pest\Support\HigherOrderTapProxy; -use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\TestCase; @@ -61,16 +61,7 @@ if (! function_exists('dataset')) { */ function dataset(string $name, Closure|iterable $dataset): void { - $file = Backtrace::datasetsFile(); - $filename = Str::afterLast($file, DIRECTORY_SEPARATOR); - $scope = Str::beforeLast($file, DIRECTORY_SEPARATOR); - - if (Str::afterLast($scope, DIRECTORY_SEPARATOR) === 'Datasets') { - $scope = Str::beforeLast($scope, DIRECTORY_SEPARATOR); - } elseif ($filename !== 'Datasets.php') { - $scope = $file; - } - + $scope = DatasetInfo::scope(Backtrace::datasetsFile()); DatasetsRepository::set($name, $dataset, $scope); } } diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php new file mode 100644 index 00000000..cfcf65b6 --- /dev/null +++ b/src/Support/DatasetInfo.php @@ -0,0 +1,38 @@ +toBe($inside); +})->with([ + ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => true], + ['file' => '/var/www/project/tests/Datasets.php', 'inside' => false], + ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => true], + ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false], + ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => false], +]); + +it('can check if dataset is defined inside a Datasets.php file', function (string $path, bool $inside) { + expect(DatasetInfo::isADatasetsFile($path))->toBe($inside); +})->with([ + ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => false], + ['file' => '/var/www/project/tests/Datasets.php', 'inside' => true], + ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => false], + ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false], + ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => true], +]); + +it('computes the dataset scope', function (string $file, string $scope) { + expect(DatasetInfo::scope($file))->toBe($scope); +})->with([ + ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'scope' => '/var/www/project/tests'], + ['file' => '/var/www/project/tests/Datasets.php', 'scope' => '/var/www/project/tests'], + ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features'], + ['file' => '/var/www/project/tests/Features/Numbers.php', 'scope' => '/var/www/project/tests/Features/Numbers.php'], + ['file' => '/var/www/project/tests/Features/Datasets.php', 'scope' => '/var/www/project/tests/Features'], + ['file' => '/var/www/project/tests/Features/Controllers/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers'], + ['file' => '/var/www/project/tests/Features/Controllers/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers/Numbers.php'], + ['file' => '/var/www/project/tests/Features/Controllers/Datasets.php', 'scope' => '/var/www/project/tests/Features/Controllers'], +]); From e34364d8b179b9c8e2e56cf62ea4e25b1bae09b0 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 10:35:57 +0200 Subject: [PATCH 03/59] remove dump --- src/Repositories/DatasetsRepository.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index d9f06874..d5dc1433 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -69,7 +69,6 @@ final class DatasetsRepository */ public static function get(string $filename, string $description) { - // dump("requesting file: " . $filename); $dataset = self::$withs[$filename.'>>>'.$description]; $dataset = self::resolve($dataset, $filename); From 84b8c389b263b6360c0013b4123cc0f359acb6a8 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 10:56:03 +0200 Subject: [PATCH 04/59] remove hardcoded string --- src/Repositories/DatasetsRepository.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index d5dc1433..21bfe9eb 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -17,6 +17,8 @@ use Traversable; */ final class DatasetsRepository { + private const SEPARATOR = '>>'; + /** * Holds the datasets. * @@ -38,7 +40,7 @@ final class DatasetsRepository */ public static function set(string $name, Closure|iterable $data, string $scope): void { - $datasetKey = "$scope>>>$name"; + $datasetKey = "$scope".self::SEPARATOR."$name"; if (array_key_exists("$datasetKey", self::$datasets)) { throw new DatasetAlreadyExist($name, $scope); @@ -54,12 +56,12 @@ final class DatasetsRepository */ public static function with(string $filename, string $description, array $with): void { - self::$withs["$filename>>>$description"] = $with; + self::$withs["$filename".self::SEPARATOR."$description"] = $with; } public static function has(string $filename, string $description): bool { - return array_key_exists($filename.'>>>'.$description, self::$withs); + return array_key_exists($filename.self::SEPARATOR.$description, self::$withs); } /** @@ -69,7 +71,7 @@ final class DatasetsRepository */ public static function get(string $filename, string $description) { - $dataset = self::$withs[$filename.'>>>'.$description]; + $dataset = self::$withs[$filename.self::SEPARATOR.$description]; $dataset = self::resolve($dataset, $filename); @@ -140,7 +142,7 @@ final class DatasetsRepository private static function getScopedDataset(string $name, string $currentTestFile) { $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { - [$datasetScope, $datasetName] = explode('>>>', $key); + [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); if ($name !== $datasetName) { return false; From cbd4cefc1a530c1e219895f60919d33c9a80e61e Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 10:56:47 +0200 Subject: [PATCH 05/59] Update src/Repositories/DatasetsRepository.php Co-authored-by: Luke Downing --- src/Repositories/DatasetsRepository.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 21bfe9eb..7e48d160 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -155,7 +155,10 @@ final class DatasetsRepository return true; }, ARRAY_FILTER_USE_KEY); - $closestScopeDatasetKey = array_reduce(array_keys($matchingDatasets), function ($keyA, $keyB) { + $closestScopeDatasetKey = array_reduce( + array_keys($matchingDatasets), + fn ($keyA, $keyB) => $keyA !== null && strlen($keyA) > strlen($keyB) ? $keyA : $keyB + ); if ($keyA === null) { return $keyB; } From 98b04632ce5107c22827404c2957d5b1724f6508 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 10:58:12 +0200 Subject: [PATCH 06/59] remove hardcoded string --- src/Repositories/DatasetsRepository.php | 68 +++++++++++-------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 7e48d160..06bfd8db 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -136,43 +136,6 @@ final class DatasetsRepository return $namedData; } - /** - * @return Closure|iterable - */ - private static function getScopedDataset(string $name, string $currentTestFile) - { - $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { - [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); - - if ($name !== $datasetName) { - return false; - } - - if (! str_starts_with($currentTestFile, $datasetScope)) { - return false; - } - - return true; - }, ARRAY_FILTER_USE_KEY); - - $closestScopeDatasetKey = array_reduce( - array_keys($matchingDatasets), - fn ($keyA, $keyB) => $keyA !== null && strlen($keyA) > strlen($keyB) ? $keyA : $keyB - ); - if ($keyA === null) { - return $keyB; - } - - return strlen($keyA) > strlen($keyB) ? $keyA : $keyB; - }); - - if ($closestScopeDatasetKey === null) { - throw new DatasetDoesNotExist($name); - } - - return $matchingDatasets[$closestScopeDatasetKey]; - } - /** * @param array|string> $datasets * @return array> @@ -211,6 +174,37 @@ final class DatasetsRepository return $processedDatasets; } + /** + * @return Closure|iterable + */ + private static function getScopedDataset(string $name, string $currentTestFile) + { + $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { + [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); + + if ($name !== $datasetName) { + return false; + } + + if (! str_starts_with($currentTestFile, $datasetScope)) { + return false; + } + + return true; + }, ARRAY_FILTER_USE_KEY); + + $closestScopeDatasetKey = array_reduce( + array_keys($matchingDatasets), + fn ($keyA, $keyB) => $keyA !== null && strlen($keyA) > strlen($keyB) ? $keyA : $keyB + ); + + if ($closestScopeDatasetKey === null) { + throw new DatasetDoesNotExist($name); + } + + return $matchingDatasets[$closestScopeDatasetKey]; + } + /** * @param array> $combinations * @return array>> From db00fc8c09d60188a395df43f7ae95cbac2f3827 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 22 Sep 2022 11:01:31 +0200 Subject: [PATCH 07/59] Renamed DatasetAlreadyExists exception --- .../{DatasetAlreadyExist.php => DatasetAlreadyExists.php} | 2 +- src/Repositories/DatasetsRepository.php | 4 ++-- tests/Features/DatasetsTests.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Exceptions/{DatasetAlreadyExist.php => DatasetAlreadyExists.php} (80%) diff --git a/src/Exceptions/DatasetAlreadyExist.php b/src/Exceptions/DatasetAlreadyExists.php similarity index 80% rename from src/Exceptions/DatasetAlreadyExist.php rename to src/Exceptions/DatasetAlreadyExists.php index 91146997..4e42397a 100644 --- a/src/Exceptions/DatasetAlreadyExist.php +++ b/src/Exceptions/DatasetAlreadyExists.php @@ -12,7 +12,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface; /** * @internal */ -final class DatasetAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace +final class DatasetAlreadyExists extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace { /** * Creates a new Exception instance. diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 06bfd8db..26de6dd2 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; -use Pest\Exceptions\DatasetAlreadyExist; +use Pest\Exceptions\DatasetAlreadyExists; use Pest\Exceptions\DatasetDoesNotExist; use Pest\Exceptions\ShouldNotHappen; use SebastianBergmann\Exporter\Exporter; @@ -43,7 +43,7 @@ final class DatasetsRepository $datasetKey = "$scope".self::SEPARATOR."$name"; if (array_key_exists("$datasetKey", self::$datasets)) { - throw new DatasetAlreadyExist($name, $scope); + throw new DatasetAlreadyExists($name, $scope); } self::$datasets[$datasetKey] = $data; diff --git a/tests/Features/DatasetsTests.php b/tests/Features/DatasetsTests.php index 9dc72b2b..65a12651 100644 --- a/tests/Features/DatasetsTests.php +++ b/tests/Features/DatasetsTests.php @@ -1,6 +1,6 @@ expectException(DatasetAlreadyExist::class); + $this->expectException(DatasetAlreadyExists::class); $this->expectExceptionMessage('A dataset with the name `second` already exist in scope ['.__DIR__.'].'); DatasetsRepository::set('second', [[]], __DIR__); }); From 8944fdd96f0c2ee48a8d11655d8b2ba9b26f78aa Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Wed, 30 Nov 2022 14:08:47 +0100 Subject: [PATCH 08/59] update snapshots --- tests/.snapshots/success.txt | 68 ++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index af0bfcef..be2b2d57 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -29,7 +29,7 @@ ✓ it does not append CoversNothing to other methods ✓ it throws exception if no class nor method has been found - PASS Tests\Features\Datasets + PASS Tests\Features\DatasetsTests ✓ it throws exception if dataset does not exist ✓ it throws exception if dataset already exist ✓ it sets closures @@ -121,6 +121,7 @@ ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2 ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...)) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...)) + ↓ forbids to define tests in Datasets dirs and Datasets.php files PASS Tests\Features\Depends ✓ first @@ -668,6 +669,47 @@ ✓ get 'foo' → get 'bar' → expect true → toBeTrue ✓ get 'foo' → expect true → toBeTrue + PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/NestedDirector...ts.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory2\TestFileInNestedDirectory + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\TestFileWithLocallyDefinedDataset + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/ScopedDatasets.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\Directory\TestFileWithScopedDataset + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ uses dataset with (3) + ✓ uses dataset with (4) + ✓ uses dataset with (5) + ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php') + ✓ the right dataset is taken + + PASS Tests\Features\ScopedDatasets\TestFileOutOfScope + ✓ uses dataset with (1) + ✓ uses dataset with (2) + ✓ the right dataset is taken + WARN Tests\Features\Skip ✓ it do not skips - it skips with truthy → 1 @@ -766,7 +808,7 @@ PASS Tests\Unit\Console\Help ✓ it outputs the help information when --help is used - PASS Tests\Unit\Datasets + PASS Tests\Unit\DatasetsTests ✓ it show only the names of named datasets in their description ✓ it show the actual dataset of non-named datasets in their description ✓ it show only the names of multiple named datasets in their description @@ -792,6 +834,26 @@ ✓ it can resolve builtin value types ✓ it cannot resolve a parameter without type + PASS Tests\Unit\Support\DatasetInfo + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase...rs.php', true) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datasets.php', false) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...rs.php', true) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...rs.php', false) + ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...ts.php', false) + ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datase...rs.php', false) + ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datasets.php', true) + ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...rs.php', false) #1 + ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...rs.php', false) #2 + ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...ts.php', true) + ✓ it computes the dataset scope with ('/var/www/project/tests/Datase...rs.php', '/var/www/project/tests') + ✓ it computes the dataset scope with ('/var/www/project/tests/Datasets.php', '/var/www/project/tests') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Features') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...rs.php') #1 + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...ts.php', '/var/www/project/tests/Features') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...ollers') + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...rs.php') #2 + ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...ts.php', '/var/www/project/tests/Featur...ollers') + PASS Tests\Unit\Support\Reflection ✓ it gets file name from closure ✓ it gets property values @@ -823,4 +885,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 1 todo, 18 skipped, 567 passed (1465 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 18 skipped, 616 passed (1526 assertions) \ No newline at end of file From 3324455e0a6badc6ac0c4e59b057c4fb578299a8 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 16:32:45 +0000 Subject: [PATCH 09/59] feat: only registers globals if necessary --- bin/pest | 2 + src/Functions.php | 284 +++++++++++++++++++++++----------------------- 2 files changed, 145 insertions(+), 141 deletions(-) diff --git a/bin/pest b/bin/pest index ac1e98b6..8a6b8596 100755 --- a/bin/pest +++ b/bin/pest @@ -11,6 +11,8 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; (static function () { + $_SERVER['__PEST__'] = true; + // Ensures Collision's Printer is registered. $_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter'; diff --git a/src/Functions.php b/src/Functions.php index ea53c22c..7adfccc3 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -14,149 +14,151 @@ use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; -if (! function_exists('expect')) { - /** - * Creates a new expectation. - * - * @template TValue - * - * @param TValue|null $value - * @return Expectation - */ - function expect(mixed $value = null): Expectation - { - return new Expectation($value); - } -} - -if (! function_exists('beforeAll')) { - /** - * Runs the given closure before all tests in the current file. - */ - function beforeAll(Closure $closure): void - { - TestSuite::getInstance()->beforeAll->set($closure); - } -} - -if (! function_exists('beforeEach')) { - /** - * Runs the given closure before each test in the current file. - * - * @return BeforeEachCall|TestCase|mixed - */ - function beforeEach(Closure $closure = null): BeforeEachCall - { - $filename = Backtrace::file(); - - return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure); - } -} - -if (! function_exists('dataset')) { - /** - * Registers the given dataset. - * - * @param Closure|iterable $dataset - */ - function dataset(string $name, Closure|iterable $dataset): void - { - $scope = DatasetInfo::scope(Backtrace::datasetsFile()); - DatasetsRepository::set($name, $dataset, $scope); - } -} - -if (! function_exists('uses')) { - /** - * The uses function binds the given - * arguments to test closures. - * - * @param class-string ...$classAndTraits - */ - function uses(string ...$classAndTraits): UsesCall - { - $filename = Backtrace::file(); - - return new UsesCall($filename, array_values($classAndTraits)); - } -} - -if (! function_exists('test')) { - /** - * Adds the given closure as a test. The first argument - * is the test description; the second argument is - * a closure that contains the test expectations. - * - * @return TestCall|TestCase|mixed - */ - function test(string $description = null, Closure $closure = null) - { - if ($description === null && TestSuite::getInstance()->test !== null) { - return new HigherOrderTapProxy(TestSuite::getInstance()->test); +if (array_key_exists('__PEST__', $_SERVER)) { + if (! function_exists('expect')) { + /** + * Creates a new expectation. + * + * @template TValue + * + * @param TValue|null $value + * @return Expectation + */ + function expect(mixed $value = null): Expectation + { + return new Expectation($value); } + } - $filename = Backtrace::testFile(); + if (! function_exists('beforeAll')) { + /** + * Runs the given closure before all tests in the current file. + */ + function beforeAll(Closure $closure): void + { + TestSuite::getInstance()->beforeAll->set($closure); + } + } - return new TestCall(TestSuite::getInstance(), $filename, $description, $closure); - } -} - -if (! function_exists('it')) { - /** - * Adds the given closure as a test. The first argument - * is the test description; the second argument is - * a closure that contains the test expectations. - * - * @return TestCall|TestCase|mixed - */ - function it(string $description, Closure $closure = null): TestCall - { - $description = sprintf('it %s', $description); - - /** @var TestCall $test */ - $test = test($description, $closure); - - return $test; - } -} - -if (! function_exists('todo')) { - /** - * Adds the given todo test. Internally, this test - * is marked as incomplete. Yet, Collision, Pest's - * printer, will display it as a "todo" test. - * - * @return TestCall|TestCase|mixed - */ - function todo(string $description): TestCall - { - /* @phpstan-ignore-next-line */ - return test($description, fn () => self::markTestSkipped( - '__TODO__', - )); - } -} - -if (! function_exists('afterEach')) { - /** - * Runs the given closure after each test in the current file. - * - * @return AfterEachCall|TestCase|mixed - */ - function afterEach(Closure $closure = null): AfterEachCall - { - $filename = Backtrace::file(); - - return new AfterEachCall(TestSuite::getInstance(), $filename, $closure); - } -} - -if (! function_exists('afterAll')) { - /** - * Runs the given closure after all tests in the current file. - */ - function afterAll(Closure $closure): void - { - TestSuite::getInstance()->afterAll->set($closure); + if (! function_exists('beforeEach')) { + /** + * Runs the given closure before each test in the current file. + * + * @return BeforeEachCall|TestCase|mixed + */ + function beforeEach(Closure $closure = null): BeforeEachCall + { + $filename = Backtrace::file(); + + return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure); + } + } + + if (! function_exists('dataset')) { + /** + * Registers the given dataset. + * + * @param Closure|iterable $dataset + */ + function dataset(string $name, Closure|iterable $dataset): void + { + $scope = DatasetInfo::scope(Backtrace::datasetsFile()); + DatasetsRepository::set($name, $dataset, $scope); + } + } + + if (! function_exists('uses')) { + /** + * The uses function binds the given + * arguments to test closures. + * + * @param class-string ...$classAndTraits + */ + function uses(string ...$classAndTraits): UsesCall + { + $filename = Backtrace::file(); + + return new UsesCall($filename, array_values($classAndTraits)); + } + } + + if (! function_exists('test')) { + /** + * Adds the given closure as a test. The first argument + * is the test description; the second argument is + * a closure that contains the test expectations. + * + * @return TestCall|TestCase|mixed + */ + function test(string $description = null, Closure $closure = null) + { + if ($description === null && TestSuite::getInstance()->test !== null) { + return new HigherOrderTapProxy(TestSuite::getInstance()->test); + } + + $filename = Backtrace::testFile(); + + return new TestCall(TestSuite::getInstance(), $filename, $description, $closure); + } + } + + if (! function_exists('it')) { + /** + * Adds the given closure as a test. The first argument + * is the test description; the second argument is + * a closure that contains the test expectations. + * + * @return TestCall|TestCase|mixed + */ + function it(string $description, Closure $closure = null): TestCall + { + $description = sprintf('it %s', $description); + + /** @var TestCall $test */ + $test = test($description, $closure); + + return $test; + } + } + + if (! function_exists('todo')) { + /** + * Adds the given todo test. Internally, this test + * is marked as incomplete. Yet, Collision, Pest's + * printer, will display it as a "todo" test. + * + * @return TestCall|TestCase|mixed + */ + function todo(string $description): TestCall + { + /* @phpstan-ignore-next-line */ + return test($description, fn () => self::markTestSkipped( + '__TODO__', + )); + } + } + + if (! function_exists('afterEach')) { + /** + * Runs the given closure after each test in the current file. + * + * @return AfterEachCall|TestCase|mixed + */ + function afterEach(Closure $closure = null): AfterEachCall + { + $filename = Backtrace::file(); + + return new AfterEachCall(TestSuite::getInstance(), $filename, $closure); + } + } + + if (! function_exists('afterAll')) { + /** + * Runs the given closure after all tests in the current file. + */ + function afterAll(Closure $closure): void + { + TestSuite::getInstance()->afterAll->set($closure); + } } } From 7ad045d6b771dddd929f772d0d1b7adfb363d8ac Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 18:38:20 +0000 Subject: [PATCH 10/59] feat: inline testing --- .gitattributes | 4 ++++ composer.json | 8 +++++++ phpunit.inline.xml | 34 +++++++++++++++++++++++++++++ src/NotExported/MyTestCase.php | 18 +++++++++++++++ src/NotExported/MyTestableClass.php | 22 +++++++++++++++++++ src/Plugin.php | 2 +- tests/Pest.php | 3 +++ 7 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 phpunit.inline.xml create mode 100644 src/NotExported/MyTestCase.php create mode 100644 src/NotExported/MyTestableClass.php diff --git a/.gitattributes b/.gitattributes index 3654b689..9f929526 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,7 @@ phpstan.neon export-ignore CHANGELOG.md export-ignore CONTRIBUTING.md export-ignore README.md export-ignore + +# Inline +/src/NotExported export-ignore +/phpunit.inline.xml export-ignore diff --git a/composer.json b/composer.json index 31b8bdb7..35dfe005 100644 --- a/composer.json +++ b/composer.json @@ -51,8 +51,15 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.0.0", + "pestphp/pest-plugin-inline": "2.x-dev", "symfony/process": "^6.2.0" }, + "repositories": { + "inline": { + "type": "path", + "url": "../pest-plugin-inline" + } + }, "minimum-stability": "dev", "prefer-stable": true, "config": { @@ -70,6 +77,7 @@ "test:lint": "pint --test", "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", + "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml", "test:parallel": "exit 1", "test:integration": "php bin/pest --colors=always --group=integration -v", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", diff --git a/phpunit.inline.xml b/phpunit.inline.xml new file mode 100644 index 00000000..0e18d164 --- /dev/null +++ b/phpunit.inline.xml @@ -0,0 +1,34 @@ + + + + + ./src + + + + + ./src + + + diff --git a/src/NotExported/MyTestCase.php b/src/NotExported/MyTestCase.php new file mode 100644 index 00000000..1e403fba --- /dev/null +++ b/src/NotExported/MyTestCase.php @@ -0,0 +1,18 @@ +assertIsTestable(get_class($testable)); // @phpstan-ignore-line +}); diff --git a/src/Plugin.php b/src/Plugin.php index 55c91134..54780337 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -23,7 +23,7 @@ final class Plugin public static function uses(string ...$traits): void { self::$callables[] = function () use ($traits): void { - uses(...$traits)->in(TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.testDirectory()); + uses(...$traits)->in(TestSuite::getInstance()->rootPath); }; } } diff --git a/tests/Pest.php b/tests/Pest.php index d0f656bd..83c8512b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,9 +1,12 @@ in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder'); +uses(MyTestCase::class)->in('../src/NotExported'); + uses()->group('integration')->in('Visual'); // NOTE: global test value container to be mutated and checked across files, as needed From 3bc356ceec3ac43549eefc26573e4d3027fa0aef Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 19:02:04 +0000 Subject: [PATCH 11/59] fix: default tests path --- bin/pest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pest b/bin/pest index 8a6b8596..abf697e6 100755 --- a/bin/pest +++ b/bin/pest @@ -48,7 +48,7 @@ use Symfony\Component\Console\Output\OutputInterface; $testSuite = TestSuite::getInstance( $rootPath, - $argv->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()) + $argv->getParameterOption('--test-directory', ConfigLoader::DEFAULT_TESTS_PATH) ); $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; From dfe8a3deeb22eae72aed1b02c4c692ffd2df4d71 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 20:06:07 +0000 Subject: [PATCH 12/59] revert: inline testing --- .gitattributes | 3 --- composer.json | 7 ------ phpunit.inline.xml | 34 ----------------------------- src/NotExported/MyTestCase.php | 18 --------------- src/NotExported/MyTestableClass.php | 22 ------------------- tests/Pest.php | 3 --- 6 files changed, 87 deletions(-) delete mode 100644 phpunit.inline.xml delete mode 100644 src/NotExported/MyTestCase.php delete mode 100644 src/NotExported/MyTestableClass.php diff --git a/.gitattributes b/.gitattributes index 9f929526..c6e004ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,3 @@ CHANGELOG.md export-ignore CONTRIBUTING.md export-ignore README.md export-ignore -# Inline -/src/NotExported export-ignore -/phpunit.inline.xml export-ignore diff --git a/composer.json b/composer.json index 35dfe005..0e962b5c 100644 --- a/composer.json +++ b/composer.json @@ -51,15 +51,8 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.0.0", - "pestphp/pest-plugin-inline": "2.x-dev", "symfony/process": "^6.2.0" }, - "repositories": { - "inline": { - "type": "path", - "url": "../pest-plugin-inline" - } - }, "minimum-stability": "dev", "prefer-stable": true, "config": { diff --git a/phpunit.inline.xml b/phpunit.inline.xml deleted file mode 100644 index 0e18d164..00000000 --- a/phpunit.inline.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - ./src - - - - - ./src - - - diff --git a/src/NotExported/MyTestCase.php b/src/NotExported/MyTestCase.php deleted file mode 100644 index 1e403fba..00000000 --- a/src/NotExported/MyTestCase.php +++ /dev/null @@ -1,18 +0,0 @@ -assertIsTestable(get_class($testable)); // @phpstan-ignore-line -}); diff --git a/tests/Pest.php b/tests/Pest.php index 83c8512b..d0f656bd 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,12 +1,9 @@ in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder'); -uses(MyTestCase::class)->in('../src/NotExported'); - uses()->group('integration')->in('Visual'); // NOTE: global test value container to be mutated and checked across files, as needed From 606d627f1db5cde2b1a8beda4895fb0bfb74068f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 20:06:42 +0000 Subject: [PATCH 13/59] Revert "fix: default tests path" This reverts commit 3bc356ceec3ac43549eefc26573e4d3027fa0aef. --- bin/pest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/pest b/bin/pest index abf697e6..8a6b8596 100755 --- a/bin/pest +++ b/bin/pest @@ -48,7 +48,7 @@ use Symfony\Component\Console\Output\OutputInterface; $testSuite = TestSuite::getInstance( $rootPath, - $argv->getParameterOption('--test-directory', ConfigLoader::DEFAULT_TESTS_PATH) + $argv->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()) ); $isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never'; From 850955d7dd510800d08c079610d96ad2dfe2716a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 23:07:39 +0000 Subject: [PATCH 14/59] fix: reverts autoloading functions --- bin/pest | 2 - src/Functions.php | 290 +++++++++++++++++++++++----------------------- 2 files changed, 144 insertions(+), 148 deletions(-) diff --git a/bin/pest b/bin/pest index 8a6b8596..ac1e98b6 100755 --- a/bin/pest +++ b/bin/pest @@ -11,8 +11,6 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; (static function () { - $_SERVER['__PEST__'] = true; - // Ensures Collision's Printer is registered. $_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter'; diff --git a/src/Functions.php b/src/Functions.php index 7adfccc3..ea53c22c 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -14,151 +14,149 @@ use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; -if (array_key_exists('__PEST__', $_SERVER)) { - if (! function_exists('expect')) { - /** - * Creates a new expectation. - * - * @template TValue - * - * @param TValue|null $value - * @return Expectation - */ - function expect(mixed $value = null): Expectation - { - return new Expectation($value); - } - } - - if (! function_exists('beforeAll')) { - /** - * Runs the given closure before all tests in the current file. - */ - function beforeAll(Closure $closure): void - { - TestSuite::getInstance()->beforeAll->set($closure); - } - } - - if (! function_exists('beforeEach')) { - /** - * Runs the given closure before each test in the current file. - * - * @return BeforeEachCall|TestCase|mixed - */ - function beforeEach(Closure $closure = null): BeforeEachCall - { - $filename = Backtrace::file(); - - return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure); - } - } - - if (! function_exists('dataset')) { - /** - * Registers the given dataset. - * - * @param Closure|iterable $dataset - */ - function dataset(string $name, Closure|iterable $dataset): void - { - $scope = DatasetInfo::scope(Backtrace::datasetsFile()); - DatasetsRepository::set($name, $dataset, $scope); - } - } - - if (! function_exists('uses')) { - /** - * The uses function binds the given - * arguments to test closures. - * - * @param class-string ...$classAndTraits - */ - function uses(string ...$classAndTraits): UsesCall - { - $filename = Backtrace::file(); - - return new UsesCall($filename, array_values($classAndTraits)); - } - } - - if (! function_exists('test')) { - /** - * Adds the given closure as a test. The first argument - * is the test description; the second argument is - * a closure that contains the test expectations. - * - * @return TestCall|TestCase|mixed - */ - function test(string $description = null, Closure $closure = null) - { - if ($description === null && TestSuite::getInstance()->test !== null) { - return new HigherOrderTapProxy(TestSuite::getInstance()->test); - } - - $filename = Backtrace::testFile(); - - return new TestCall(TestSuite::getInstance(), $filename, $description, $closure); - } - } - - if (! function_exists('it')) { - /** - * Adds the given closure as a test. The first argument - * is the test description; the second argument is - * a closure that contains the test expectations. - * - * @return TestCall|TestCase|mixed - */ - function it(string $description, Closure $closure = null): TestCall - { - $description = sprintf('it %s', $description); - - /** @var TestCall $test */ - $test = test($description, $closure); - - return $test; - } - } - - if (! function_exists('todo')) { - /** - * Adds the given todo test. Internally, this test - * is marked as incomplete. Yet, Collision, Pest's - * printer, will display it as a "todo" test. - * - * @return TestCall|TestCase|mixed - */ - function todo(string $description): TestCall - { - /* @phpstan-ignore-next-line */ - return test($description, fn () => self::markTestSkipped( - '__TODO__', - )); - } - } - - if (! function_exists('afterEach')) { - /** - * Runs the given closure after each test in the current file. - * - * @return AfterEachCall|TestCase|mixed - */ - function afterEach(Closure $closure = null): AfterEachCall - { - $filename = Backtrace::file(); - - return new AfterEachCall(TestSuite::getInstance(), $filename, $closure); - } - } - - if (! function_exists('afterAll')) { - /** - * Runs the given closure after all tests in the current file. - */ - function afterAll(Closure $closure): void - { - TestSuite::getInstance()->afterAll->set($closure); - } +if (! function_exists('expect')) { + /** + * Creates a new expectation. + * + * @template TValue + * + * @param TValue|null $value + * @return Expectation + */ + function expect(mixed $value = null): Expectation + { + return new Expectation($value); + } +} + +if (! function_exists('beforeAll')) { + /** + * Runs the given closure before all tests in the current file. + */ + function beforeAll(Closure $closure): void + { + TestSuite::getInstance()->beforeAll->set($closure); + } +} + +if (! function_exists('beforeEach')) { + /** + * Runs the given closure before each test in the current file. + * + * @return BeforeEachCall|TestCase|mixed + */ + function beforeEach(Closure $closure = null): BeforeEachCall + { + $filename = Backtrace::file(); + + return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure); + } +} + +if (! function_exists('dataset')) { + /** + * Registers the given dataset. + * + * @param Closure|iterable $dataset + */ + function dataset(string $name, Closure|iterable $dataset): void + { + $scope = DatasetInfo::scope(Backtrace::datasetsFile()); + DatasetsRepository::set($name, $dataset, $scope); + } +} + +if (! function_exists('uses')) { + /** + * The uses function binds the given + * arguments to test closures. + * + * @param class-string ...$classAndTraits + */ + function uses(string ...$classAndTraits): UsesCall + { + $filename = Backtrace::file(); + + return new UsesCall($filename, array_values($classAndTraits)); + } +} + +if (! function_exists('test')) { + /** + * Adds the given closure as a test. The first argument + * is the test description; the second argument is + * a closure that contains the test expectations. + * + * @return TestCall|TestCase|mixed + */ + function test(string $description = null, Closure $closure = null) + { + if ($description === null && TestSuite::getInstance()->test !== null) { + return new HigherOrderTapProxy(TestSuite::getInstance()->test); + } + + $filename = Backtrace::testFile(); + + return new TestCall(TestSuite::getInstance(), $filename, $description, $closure); + } +} + +if (! function_exists('it')) { + /** + * Adds the given closure as a test. The first argument + * is the test description; the second argument is + * a closure that contains the test expectations. + * + * @return TestCall|TestCase|mixed + */ + function it(string $description, Closure $closure = null): TestCall + { + $description = sprintf('it %s', $description); + + /** @var TestCall $test */ + $test = test($description, $closure); + + return $test; + } +} + +if (! function_exists('todo')) { + /** + * Adds the given todo test. Internally, this test + * is marked as incomplete. Yet, Collision, Pest's + * printer, will display it as a "todo" test. + * + * @return TestCall|TestCase|mixed + */ + function todo(string $description): TestCall + { + /* @phpstan-ignore-next-line */ + return test($description, fn () => self::markTestSkipped( + '__TODO__', + )); + } +} + +if (! function_exists('afterEach')) { + /** + * Runs the given closure after each test in the current file. + * + * @return AfterEachCall|TestCase|mixed + */ + function afterEach(Closure $closure = null): AfterEachCall + { + $filename = Backtrace::file(); + + return new AfterEachCall(TestSuite::getInstance(), $filename, $closure); + } +} + +if (! function_exists('afterAll')) { + /** + * Runs the given closure after all tests in the current file. + */ + function afterAll(Closure $closure): void + { + TestSuite::getInstance()->afterAll->set($closure); } } From fab08f0e20f3ddfcf96700dc7f4a9fc9a6f76d8b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 23:38:46 +0000 Subject: [PATCH 15/59] Loads `Arch` files --- src/Bootstrappers/BootFiles.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 7629e7ed..6852dae6 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -23,6 +23,8 @@ final class BootFiles * @var array */ private const STRUCTURE = [ + 'Arch', + 'Arch.php', 'Expectations', 'Expectations.php', 'Helpers', From 34878bf43250292a39230c5c9b6bc6345787e926 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 23:43:20 +0000 Subject: [PATCH 16/59] Reverts loading `Arch` --- src/Bootstrappers/BootFiles.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 6852dae6..7629e7ed 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -23,8 +23,6 @@ final class BootFiles * @var array */ private const STRUCTURE = [ - 'Arch', - 'Arch.php', 'Expectations', 'Expectations.php', 'Helpers', From c5f6923e5a85bc5be9aaa49030ec452fc8154f94 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 Dec 2022 23:49:58 +0000 Subject: [PATCH 17/59] fix: methods name with `\` --- src/Support/Str.php | 2 +- tests/.snapshots/success.txt | 7 ++++++- tests/Unit/Support/Str.php | 13 +++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Support/Str.php diff --git a/src/Support/Str.php b/src/Support/Str.php index 69522b6a..6ceaed1d 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -56,7 +56,7 @@ final class Str { $code = str_replace(' ', '_', $code); - return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code); + return (string) preg_replace('/[^A-Z_a-z0-9]/', '_', $code); } /** diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index be2b2d57..e47760bb 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -858,6 +858,11 @@ ✓ it gets file name from closure ✓ it gets property values + PASS Tests\Unit\Support\Str + ✓ it evaluates the code with ('version()', 'version__') + ✓ it evaluates the code with ('version__ ', 'version___') + ✓ it evaluates the code with ('version\', 'version_') + PASS Tests\Unit\TestSuite ✓ it does not allow to add the same test description twice ✓ it alerts users about tests with arguments but no input @@ -885,4 +890,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 616 passed (1526 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 18 skipped, 619 passed (1529 assertions) \ No newline at end of file diff --git a/tests/Unit/Support/Str.php b/tests/Unit/Support/Str.php new file mode 100644 index 00000000..870adf6d --- /dev/null +++ b/tests/Unit/Support/Str.php @@ -0,0 +1,13 @@ +toBe($expected); +})->with([ + ['version()', 'version__'], + ['version__ ', 'version___'], + ['version\\', 'version_'], +]); From 70f447a8bce09e1e2aeee29bb0ad0bc1c1c6f52b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 09:17:35 +0000 Subject: [PATCH 18/59] chore: improves type coverage --- composer.json | 4 ++++ rector.php | 27 +++++++++++++++++++++ src/Bootstrappers/BootExceptionHandler.php | 5 ++-- src/Bootstrappers/BootFiles.php | 5 ++-- src/Bootstrappers/BootSubscribers.php | 5 ++-- src/Bootstrappers/BootView.php | 5 ++-- src/Concerns/Pipeable.php | 2 +- src/Concerns/Testable.php | 2 +- src/Contracts/Bootstrapper.php | 16 +++++++++++++ src/Exceptions/InvalidExpectationValue.php | 4 +--- src/Expectation.php | 8 ++++--- src/Expectations/OppositeExpectation.php | 3 +-- src/Factories/TestCaseFactory.php | 12 +++++----- src/Factories/TestCaseMethodFactory.php | 6 ++--- src/Functions.php | 2 +- src/Kernel.php | 6 ++++- src/PendingCalls/TestCall.php | 2 +- src/Repositories/DatasetsRepository.php | 15 ++++-------- src/Repositories/TestRepository.php | 2 +- src/Support/ChainableClosure.php | 4 ++-- src/Support/Container.php | 28 ++++++++-------------- src/Support/Coverage.php | 14 +++++------ 22 files changed, 109 insertions(+), 68 deletions(-) create mode 100644 rector.php create mode 100644 src/Contracts/Bootstrapper.php diff --git a/composer.json b/composer.json index 0e962b5c..3347466c 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,7 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.0.0", + "rector/rector": "^0.15.0", "symfony/process": "^6.2.0" }, "minimum-stability": "dev", @@ -66,7 +67,9 @@ "bin/pest" ], "scripts": { + "refacto": "rector", "lint": "pint", + "test:refactor": "rector --dry-run", "test:lint": "pint --test", "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", @@ -75,6 +78,7 @@ "test:integration": "php bin/pest --colors=always --group=integration -v", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "test": [ + "@rest:refacto", "@test:lint", "@test:types", "@test:unit", diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..589b8cd0 --- /dev/null +++ b/rector.php @@ -0,0 +1,27 @@ +paths([ + __DIR__.'/src', + ]); + + $rectorConfig->rules([ + InlineConstructorDefaultToPropertyRector::class, + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_81, + SetList::CODE_QUALITY, + SetList::DEAD_CODE, + SetList::EARLY_RETURN, + SetList::TYPE_DECLARATION, + SetList::PRIVATIZATION, + ]); +}; diff --git a/src/Bootstrappers/BootExceptionHandler.php b/src/Bootstrappers/BootExceptionHandler.php index a504ee7e..99119f41 100644 --- a/src/Bootstrappers/BootExceptionHandler.php +++ b/src/Bootstrappers/BootExceptionHandler.php @@ -5,16 +5,17 @@ declare(strict_types=1); namespace Pest\Bootstrappers; use NunoMaduro\Collision; +use Pest\Contracts\Bootstrapper; /** * @internal */ -final class BootExceptionHandler +final class BootExceptionHandler implements Bootstrapper { /** * Boots the Exception Handler. */ - public function __invoke(): void + public function boot(): void { $handler = new Collision\Provider(); diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 7629e7ed..44235b80 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Bootstrappers; +use Pest\Contracts\Bootstrapper; use Pest\Support\DatasetInfo; use Pest\Support\Str; use function Pest\testDirectory; @@ -15,7 +16,7 @@ use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator; /** * @internal */ -final class BootFiles +final class BootFiles implements Bootstrapper { /** * The Pest convention. @@ -33,7 +34,7 @@ final class BootFiles /** * Boots the Subscribers. */ - public function __invoke(): void + public function boot(): void { $rootPath = TestSuite::getInstance()->rootPath; $testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory(); diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 815c7821..e9819fa4 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Bootstrappers; +use Pest\Contracts\Bootstrapper; use Pest\Subscribers; use PHPUnit\Event; use PHPUnit\Event\Subscriber; @@ -11,7 +12,7 @@ use PHPUnit\Event\Subscriber; /** * @internal */ -final class BootSubscribers +final class BootSubscribers implements Bootstrapper { /** * The Kernel subscribers. @@ -28,7 +29,7 @@ final class BootSubscribers /** * Boots the Subscribers. */ - public function __invoke(): void + public function boot(): void { foreach (self::SUBSCRIBERS as $subscriber) { Event\Facade::registerSubscriber( diff --git a/src/Bootstrappers/BootView.php b/src/Bootstrappers/BootView.php index 226b7982..2ae4c509 100644 --- a/src/Bootstrappers/BootView.php +++ b/src/Bootstrappers/BootView.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace Pest\Bootstrappers; +use Pest\Contracts\Bootstrapper; use Pest\Support\View; use Symfony\Component\Console\Output\OutputInterface; /** * @internal */ -final class BootView +final class BootView implements Bootstrapper { public function __construct( private readonly OutputInterface $output @@ -21,7 +22,7 @@ final class BootView /** * Boots the view renderer. */ - public function __invoke(): void + public function boot(): void { View::renderUsing($this->output); } diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php index 0a29d6a1..c23427cd 100644 --- a/src/Concerns/Pipeable.php +++ b/src/Concerns/Pipeable.php @@ -58,6 +58,6 @@ trait Pipeable */ private function pipes(string $name, object $context, string $scope): array { - return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); + return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } } diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 36d47e38..f7075c28 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -259,7 +259,7 @@ trait Testable */ private function __callClosure(Closure $closure, array $arguments): mixed { - return ExceptionTrace::ensure(fn () => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); + return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); } /** diff --git a/src/Contracts/Bootstrapper.php b/src/Contracts/Bootstrapper.php new file mode 100644 index 00000000..520aede1 --- /dev/null +++ b/src/Contracts/Bootstrapper.php @@ -0,0 +1,16 @@ +|bool $value */ - $value = json_decode($this->value, true, 512); + $this->toBeJson(); - return $this->toBeJson()->and($value); + /** @var array|bool $value */ + $value = json_decode($this->value, true, 512, JSON_THROW_ON_ERROR); + + return $this->and($value); } /** diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index beed91a8..9a8fefbe 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -90,9 +90,8 @@ final class OppositeExpectation * Creates a new expectation failed exception with a nice readable message. * * @param array $arguments - * @return never */ - private function throwExpectationFailedException(string $name, array $arguments = []): void + private function throwExpectationFailedException(string $name, array $arguments = []): never { $exporter = new Exporter(); diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 4591d9f1..e1b4ea3a 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -85,7 +85,7 @@ final class TestCaseFactory $methods = array_values(array_filter( $this->methods, - fn ($method) => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true) + fn ($method): bool => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true) )); if ($methods !== []) { @@ -165,21 +165,21 @@ final class TestCaseFactory $classFQN .= $className; } - $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => $attribute::ABOVE_CLASS); - $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => ! $attribute::ABOVE_CLASS); + $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::ABOVE_CLASS); + $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::ABOVE_CLASS); $classAttributes = []; foreach ($classAvailableAttributes as $attribute) { $classAttributes = array_reduce( $methods, - fn (array $carry, TestCaseMethodFactory $methodFactory) => (new $attribute())->__invoke($methodFactory, $carry), + fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry), $classAttributes ); } $methodsCode = implode('', array_map( - fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation( + fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation( $classFQN, self::ANNOTATIONS, $methodAvailableAttributes @@ -188,7 +188,7 @@ final class TestCaseFactory )); $classAttributesCode = implode('', array_map( - static fn (string $attribute) => sprintf("\n%s", $attribute), + static fn (string $attribute): string => sprintf("\n%s", $attribute), array_unique($classAttributes), )); diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 09cf51e2..59b6c6c7 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -97,7 +97,7 @@ final class TestCaseMethodFactory $testCase->chains->chain($this); $method->chains->chain($this); - return \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); + return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args()); }; } @@ -147,11 +147,11 @@ final class TestCaseMethodFactory } $annotations = implode('', array_map( - static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations, + static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations, )); $attributes = implode('', array_map( - static fn ($attribute) => sprintf("\n %s", $attribute), $attributes, + static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes, )); return <<test !== null) { return new HigherOrderTapProxy(TestSuite::getInstance()->test); diff --git a/src/Kernel.php b/src/Kernel.php index 76ab91f1..bf7b200f 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest; +use Pest\Contracts\Bootstrapper; use Pest\Plugins\Actions\CallsAddsOutput; use Pest\Plugins\Actions\CallsBoot; use Pest\Plugins\Actions\CallsShutdown; @@ -49,7 +50,10 @@ final class Kernel public static function boot(): self { foreach (self::BOOTSTRAPPERS as $bootstrapper) { - Container::getInstance()->get($bootstrapper)->__invoke(); + $bootstrapper = Container::getInstance()->get($bootstrapper); + assert($bootstrapper instanceof Bootstrapper); + + $bootstrapper->boot(); } (new CallsBoot())->__invoke(); diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 215b4de8..d24c593b 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -154,7 +154,7 @@ final class TestCall $condition = is_callable($condition) ? $condition - : fn () => $condition; + : fn (): bool => $condition; $message = is_string($conditionOrMessage) ? $conditionOrMessage diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 3c3f5fc1..83eddac2 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -90,8 +90,7 @@ final class DatasetsRepository */ public static function resolve(array $dataset, string $currentTestFile): array|null { - /* @phpstan-ignore-next-line */ - if (empty($dataset)) { + if ($dataset === []) { return null; } @@ -177,25 +176,21 @@ final class DatasetsRepository /** * @return Closure|iterable */ - private static function getScopedDataset(string $name, string $currentTestFile) + private static function getScopedDataset(string $name, string $currentTestFile): Closure|iterable { - $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) { + $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile): bool { [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); if ($name !== $datasetName) { return false; } - if (! str_starts_with($currentTestFile, $datasetScope)) { - return false; - } - - return true; + return str_starts_with($currentTestFile, $datasetScope); }, ARRAY_FILTER_USE_KEY); $closestScopeDatasetKey = array_reduce( array_keys($matchingDatasets), - fn ($keyA, $keyB) => $keyA !== null && strlen($keyA) > strlen($keyB) ? $keyA : $keyB + fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB ); if ($closestScopeDatasetKey === null) { diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index ff302cf7..acf163b9 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -42,7 +42,7 @@ final class TestRepository */ public function getFilenames(): array { - $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => $testCase->methodsUsingOnly() !== []); + $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase): bool => $testCase->methodsUsingOnly() !== []); if ($testCases === []) { $testCases = $this->testCases; diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 3c228872..55dcca3a 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -22,8 +22,8 @@ final class ChainableClosure throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); } - \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); - \Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args()); + \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args()); + \Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args()); }; } diff --git a/src/Support/Container.php b/src/Support/Container.php index 8f865ce9..3afa9818 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -16,7 +16,7 @@ final class Container private static ?Container $instance = null; /** - * @var array + * @var array */ private array $instances = []; @@ -25,37 +25,30 @@ final class Container */ public static function getInstance(): self { - if (static::$instance === null) { - static::$instance = new self(); + if (self::$instance === null) { + self::$instance = new self(); } - return static::$instance; + return self::$instance; } /** * Gets a dependency from the container. - * - * @template TObject of object - * - * @param class-string $id - * @return TObject */ - public function get(string $id): mixed + public function get(string $id): object|string { if (! array_key_exists($id, $this->instances)) { + /** @var class-string $id */ $this->instances[$id] = $this->build($id); } - /** @var TObject $concrete */ - $concrete = $this->instances[$id]; - - return $concrete; + return $this->instances[$id]; } /** * Adds the given instance to the container. */ - public function add(string $id, mixed $instance): void + public function add(string $id, object|string $instance): void { $this->instances[$id] = $instance; } @@ -68,7 +61,7 @@ final class Container * @param class-string $id * @return TObject */ - private function build(string $id): mixed + private function build(string $id): object { $reflectionClass = new ReflectionClass($id); @@ -77,7 +70,7 @@ final class Container if ($constructor !== null) { $params = array_map( - function (ReflectionParameter $param) use ($id) { + function (ReflectionParameter $param) use ($id): object|string { $candidate = Reflection::getParameterClassName($param); if ($candidate === null) { @@ -90,7 +83,6 @@ final class Container } } - // @phpstan-ignore-next-line return $this->get($candidate); }, $constructor->getParameters() diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 743c73ab..c0e78e67 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -42,15 +42,15 @@ final class Coverage return false; } - if ($runtime->hasXdebug()) { - if (version_compare((string) phpversion('xdebug'), '3.1', '>=')) { - if (! in_array('coverage', xdebug_info('mode'), true)) { - return false; - } - } + if (! $runtime->hasXdebug()) { + return true; } - return true; + if (! version_compare((string) phpversion('xdebug'), '3.1', '>=')) { + return true; + } + + return in_array('coverage', xdebug_info('mode'), true); } /** From 0039dbde3847d1b898b6061de41f59d74691734e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 09:26:35 +0000 Subject: [PATCH 19/59] chore: adds type coverage at `96%` --- composer.json | 3 ++- phpstan.neon | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3347466c..8d94f76f 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ "require-dev": { "pestphp/pest-dev-tools": "^2.0.0", "rector/rector": "^0.15.0", - "symfony/process": "^6.2.0" + "symfony/process": "^6.2.0", + "symplify/phpstan-rules": "^11.1.17" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpstan.neon b/phpstan.neon index d8b81f41..30dd7294 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,9 @@ includes: - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/ergebnis/phpstan-rules/rules.neon - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon + - vendor/symplify/phpstan-rules/config/services/services.neon + - vendor/symplify/phpstan-rules/config/packages/cognitive-complexity/cognitive-complexity-services.neon + parameters: level: max @@ -21,3 +24,32 @@ parameters: - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" - "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#" +services: + - + class: Symplify\PHPStanRules\Rules\Explicit\PropertyTypeDeclarationSeaLevelRule + tags: [phpstan.rules.rule] + arguments: + minimalLevel: 0.99 + + - + class: Symplify\PHPStanRules\Rules\Explicit\ParamTypeDeclarationSeaLevelRule + tags: [phpstan.rules.rule] + arguments: + minimalLevel: 0.96 + + - + class: Symplify\PHPStanRules\Rules\Explicit\ReturnTypeDeclarationSeaLevelRule + tags: [phpstan.rules.rule] + arguments: + minimalLevel: 0.96 + + - + class: Symplify\PHPStanRules\Collector\FunctionLike\ParamTypeSeaLevelCollector + tags: [phpstan.collector] + - + class: Symplify\PHPStanRules\Collector\FunctionLike\ReturnTypeSeaLevelCollector + tags: [phpstan.collector] + - + class: Symplify\PHPStanRules\Collector\ClassLike\PropertyTypeSeaLevelCollector + tags: [phpstan.collector] + From ecb5d9c83ef741151b8c8ee63294d91f17a790b2 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 09:30:50 +0000 Subject: [PATCH 20/59] chore: bumps certain types of type coverage --- phpstan.neon | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 30dd7294..0a924aa9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -29,7 +29,7 @@ services: class: Symplify\PHPStanRules\Rules\Explicit\PropertyTypeDeclarationSeaLevelRule tags: [phpstan.rules.rule] arguments: - minimalLevel: 0.99 + minimalLevel: 1.00 - class: Symplify\PHPStanRules\Rules\Explicit\ParamTypeDeclarationSeaLevelRule @@ -41,14 +41,16 @@ services: class: Symplify\PHPStanRules\Rules\Explicit\ReturnTypeDeclarationSeaLevelRule tags: [phpstan.rules.rule] arguments: - minimalLevel: 0.96 + minimalLevel: 0.97 - class: Symplify\PHPStanRules\Collector\FunctionLike\ParamTypeSeaLevelCollector tags: [phpstan.collector] + - class: Symplify\PHPStanRules\Collector\FunctionLike\ReturnTypeSeaLevelCollector tags: [phpstan.collector] + - class: Symplify\PHPStanRules\Collector\ClassLike\PropertyTypeSeaLevelCollector tags: [phpstan.collector] From 1ad631c5287dfcd52c7d3290520fafbb8afd4c08 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 09:32:27 +0000 Subject: [PATCH 21/59] chore: ignores `eval` only on test factory --- phpstan.neon | 1 - src/Factories/TestCaseFactory.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 0a924aa9..b3be0341 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -19,7 +19,6 @@ parameters: - "#with a nullable type declaration#" - "#type mixed is not subtype of native#" - "#is not allowed to extend#" - - "#Language construct eval#" - "# with null as default value#" - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index e1b4ea3a..1f466e6e 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -209,7 +209,7 @@ final class TestCaseFactory } PHP; - eval($classCode); + eval($classCode); // @phpstan-ignore-line } catch (ParseError $caught) { throw new RuntimeException(sprintf( "Unable to create test case for test file at %s. \n %s", From c01654efcc6e76fce385d0017b33f348e474adfa Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 14:12:27 +0000 Subject: [PATCH 22/59] refactor: makes `const` final --- src/Factories/Attributes/Attribute.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Factories/Attributes/Attribute.php b/src/Factories/Attributes/Attribute.php index 70dae523..b875fe36 100644 --- a/src/Factories/Attributes/Attribute.php +++ b/src/Factories/Attributes/Attribute.php @@ -16,7 +16,7 @@ abstract class Attribute * * @var bool */ - public const ABOVE_CLASS = false; + final public const ABOVE_CLASS = false; /** * @param array $attributes From 21990ccd8bc152c288c8b71067035629fb9471c1 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 14:17:30 +0000 Subject: [PATCH 23/59] refacto: attribute `above` --- src/Factories/Attributes/Attribute.php | 4 +--- src/Factories/Attributes/Covers.php | 4 +--- src/Factories/TestCaseFactory.php | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Factories/Attributes/Attribute.php b/src/Factories/Attributes/Attribute.php index b875fe36..9bd43de9 100644 --- a/src/Factories/Attributes/Attribute.php +++ b/src/Factories/Attributes/Attribute.php @@ -13,10 +13,8 @@ abstract class Attribute { /** * Determine if the attribute should be placed above the class instead of above the method. - * - * @var bool */ - final public const ABOVE_CLASS = false; + public static bool $above = false; /** * @param array $attributes diff --git a/src/Factories/Attributes/Covers.php b/src/Factories/Attributes/Covers.php index da07b2a1..21d1f857 100644 --- a/src/Factories/Attributes/Covers.php +++ b/src/Factories/Attributes/Covers.php @@ -15,10 +15,8 @@ final class Covers extends Attribute { /** * Determine if the attribute should be placed above the classe instead of above the method. - * - * @var bool */ - public const ABOVE_CLASS = true; + public static bool $above = true; /** * Adds attributes regarding the "covers" feature. diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 1f466e6e..9a48cd5c 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -165,8 +165,8 @@ final class TestCaseFactory $classFQN .= $className; } - $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::ABOVE_CLASS); - $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::ABOVE_CLASS); + $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above); + $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above); $classAttributes = []; From f940b89284db8a0d01ba2a976e78fc1006a7b1ba Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 7 Dec 2022 14:17:44 +0000 Subject: [PATCH 24/59] chore: upgrades dev tools --- .github/workflows/static.yml | 3 +++ composer.json | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index eb2596ec..0a8089f6 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -28,5 +28,8 @@ jobs: - name: Types run: composer test:types + - name: Refacto + run: composer test:refacto + - name: Style run: composer test:lint diff --git a/composer.json b/composer.json index 8d94f76f..ace8c975 100644 --- a/composer.json +++ b/composer.json @@ -50,10 +50,8 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^2.0.0", - "rector/rector": "^0.15.0", - "symfony/process": "^6.2.0", - "symplify/phpstan-rules": "^11.1.17" + "pestphp/pest-dev-tools": "^2.1.0", + "symfony/process": "^6.2.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -70,7 +68,7 @@ "scripts": { "refacto": "rector", "lint": "pint", - "test:refactor": "rector --dry-run", + "test:refacto": "rector --dry-run", "test:lint": "pint --test", "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:unit": "php bin/pest --colors=always --exclude-group=integration --compact", From d33cc1977875a6c507805064d16daec9605d0321 Mon Sep 17 00:00:00 2001 From: Alex Manase Date: Wed, 7 Dec 2022 20:10:23 +0200 Subject: [PATCH 25/59] fix: ignored dataset description for string description --- src/Repositories/DatasetsRepository.php | 6 +++++- tests/.snapshots/success.txt | 4 +++- tests/Features/DatasetsTests.php | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 83eddac2..9cb3a62c 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; +use Generator; use Pest\Exceptions\DatasetAlreadyExists; use Pest\Exceptions\DatasetDoesNotExist; use Pest\Exceptions\ShouldNotHappen; @@ -155,7 +156,10 @@ final class DatasetsRepository } if ($datasets[$index] instanceof Traversable) { - $datasets[$index] = iterator_to_array($datasets[$index], false); + $preserveKeysForArrayIterator = $datasets[$index] instanceof Generator + && is_string($datasets[$index]->key()); + + $datasets[$index] = iterator_to_array($datasets[$index], $preserveKeysForArrayIterator); } // @phpstan-ignore-next-line diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index e47760bb..231c5ced 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -107,6 +107,8 @@ ✓ eager registered wrapped datasets with Generator functions with (3) ✓ eager registered wrapped datasets with Generator functions with (4) ✓ eager registered wrapped datasets with Generator functions did the job right + ✓ eager registered wrapped datasets with Generator functions display description with data set "taylor" + ✓ eager registered wrapped datasets with Generator functions display description with data set "james" ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #1 ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #2 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1 @@ -890,4 +892,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 619 passed (1529 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 18 skipped, 621 passed (1531 assertions) \ No newline at end of file diff --git a/tests/Features/DatasetsTests.php b/tests/Features/DatasetsTests.php index 1698824b..aeb0ba82 100644 --- a/tests/Features/DatasetsTests.php +++ b/tests/Features/DatasetsTests.php @@ -249,6 +249,13 @@ test('eager registered wrapped datasets with Generator functions did the job rig expect($wrapped_generator_state->text)->toBe('1234'); }); +test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) { + expect($wrapped_generator_state_with_description)->not->toBeEmpty(); +})->with(function () { + yield 'taylor' => 'taylor@laravel.com'; + yield 'james' => 'james@laravel.com'; +}); + it('can resolve a dataset after the test case is available', function ($result) { expect($result)->toBe('bar'); })->with([ From c0d9f739b30a8b4af5a8bf0725df4da9969e0af1 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 12 Dec 2022 13:44:39 +0000 Subject: [PATCH 26/59] chore: fixes script name --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ace8c975..425ed877 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,7 @@ "test:integration": "php bin/pest --colors=always --group=integration -v", "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", "test": [ - "@rest:refacto", + "@test:refacto", "@test:lint", "@test:types", "@test:unit", From 68bf8a2d26a71d9f3e0a5a9080082a73d02cc88f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 13 Dec 2022 15:27:22 +0000 Subject: [PATCH 27/59] feat: adds arch related expectations --- composer.json | 1 + src/Expectation.php | 45 ++++++++++++++++++++++-- src/Expectations/OppositeExpectation.php | 44 ++++++++++++++++++++++- src/Mixins/Expectation.php | 4 +++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 425ed877..320b58f1 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,7 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.1.0", + "pestphp/pest-plugin-arch": "^2.0.0", "symfony/process": "^6.2.0" }, "minimum-stability": "dev", diff --git a/src/Expectation.php b/src/Expectation.php index 2da24e75..21e2ee0e 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,6 +6,10 @@ namespace Pest; use BadMethodCallException; use Closure; +use Pest\Arch\ArchExpectation; +use Pest\Arch\Expectations\ToDependOn; +use Pest\Arch\Expectations\ToDependOnNothing; +use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -24,7 +28,7 @@ use PHPUnit\Framework\ExpectationFailedException; * * @template TValue * - * @property Expectation $not Creates the opposite expectation. + * @property OppositeExpectation $not Creates the opposite expectation. * @property EachExpectation $each Creates an expectation on each element on the traversable value. * * @mixin Mixins\Expectation @@ -286,11 +290,15 @@ final class Expectation return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); } - ExpectationPipeline::for($this->getExpectationClosure($method)) + $result = ExpectationPipeline::for($this->getExpectationClosure($method)) ->send(...$parameters) ->through($this->pipes($method, $this, Expectation::class)) ->run(); + if ($result !== null) { + return $result; + } + return $this; } @@ -350,4 +358,37 @@ final class Expectation { return new Any(); } + + /** + * Asserts that the layer depends (not exclusively) on the given layers. + * + * @param array|string $targets + * @return ArchExpectation + */ + public function toDependOn(array|string $targets): ArchExpectation + { + return ToDependOn::make($this, $targets); + } + + /** + * Asserts that the layer only depends on the given layers. + * + * @param array|string $targets + * @return ArchExpectation + */ + public function toOnlyDependOn(array|string $targets): ArchExpectation + { + return ToOnlyDependOn::make($this, $targets); + } + + /** + * Asserts that the layer is not allowed to depend on any other layer. + * + * @param array|string $targets + * @return ArchExpectation + */ + public function toDependOnNothing(): ArchExpectation + { + return ToDependOnNothing::make($this); + } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 9a8fefbe..4780a8a2 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -4,6 +4,10 @@ declare(strict_types=1); namespace Pest\Expectations; +use Pest\Arch\ArchExpectation; +use Pest\Arch\Expectations\ToDependOn; +use Pest\Arch\Expectations\ToDependOnNothing; +use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Expectation; use Pest\Support\Arr; use PHPUnit\Framework\ExpectationFailedException; @@ -52,6 +56,44 @@ final class OppositeExpectation return $this->original; } + /** + * Asserts that the layer does not depend on the given layers. + * + * @param array|string $targets + * @return ArchExpectation + */ + public function toDependOn(array|string $targets): ArchExpectation + { + return ToDependOn::make($this->original, $targets)->opposite( + fn () => $this->throwExpectationFailedException('toDependOn', $targets), + ); + } + + /** + * Asserts that the layer does not only depends on the given layers. + * + * @param array|string $targets + * @return ArchExpectation + */ + public function toOnlyDependOn(array|string $targets): ArchExpectation + { + return ToOnlyDependOn::make($this->original, $targets)->opposite( + fn () => $this->throwExpectationFailedException('toOnlyDependOn', $targets), + ); + } + + /** + * Asserts that the layer is depends on at least one layer. + * + * @return ArchExpectation + */ + public function toDependOnNothing(): ArchExpectation + { + return ToDependOnNothing::make($this->original)->opposite( + fn () => $this->throwExpectationFailedException('toDependOnNothing'), + ); + } + /** * Handle dynamic method calls into the original expectation. * @@ -91,7 +133,7 @@ final class OppositeExpectation * * @param array $arguments */ - private function throwExpectationFailedException(string $name, array $arguments = []): never + public function throwExpectationFailedException(string $name, array $arguments = []): never { $exporter = new Exporter(); diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 886ffab2..3c444591 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -8,6 +8,10 @@ use BadMethodCallException; use Closure; use Error; use InvalidArgumentException; +use Pest\Arch\ArchExpectation; +use Pest\Arch\Expectations\ToDependOn; +use Pest\Arch\Expectations\ToDependOnNothing; +use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Exceptions\InvalidExpectationValue; use Pest\Matchers\Any; use Pest\Support\Arr; From e1e926076af9e2792efdfb53dcbe708ed0527755 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 14 Dec 2022 14:56:11 +0000 Subject: [PATCH 28/59] fix: types on arch expectations --- src/Expectation.php | 13 ++++--------- src/Expectations/OppositeExpectation.php | 12 +++++++----- src/Mixins/Expectation.php | 4 ---- src/Repositories/DatasetsRepository.php | 3 +-- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 21e2ee0e..2277d95a 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -290,15 +290,11 @@ final class Expectation return new HigherOrderExpectation($this, call_user_func_array($this->value->$method(...), $parameters)); } - $result = ExpectationPipeline::for($this->getExpectationClosure($method)) + ExpectationPipeline::for($this->getExpectationClosure($method)) ->send(...$parameters) ->through($this->pipes($method, $this, Expectation::class)) ->run(); - if ($result !== null) { - return $result; - } - return $this; } @@ -363,7 +359,7 @@ final class Expectation * Asserts that the layer depends (not exclusively) on the given layers. * * @param array|string $targets - * @return ArchExpectation + * @return ArchExpectation */ public function toDependOn(array|string $targets): ArchExpectation { @@ -374,7 +370,7 @@ final class Expectation * Asserts that the layer only depends on the given layers. * * @param array|string $targets - * @return ArchExpectation + * @return ArchExpectation */ public function toOnlyDependOn(array|string $targets): ArchExpectation { @@ -384,8 +380,7 @@ final class Expectation /** * Asserts that the layer is not allowed to depend on any other layer. * - * @param array|string $targets - * @return ArchExpectation + * @return ArchExpectation */ public function toDependOnNothing(): ArchExpectation { diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 4780a8a2..a59b13b1 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -60,7 +60,7 @@ final class OppositeExpectation * Asserts that the layer does not depend on the given layers. * * @param array|string $targets - * @return ArchExpectation + * @return ArchExpectation */ public function toDependOn(array|string $targets): ArchExpectation { @@ -73,7 +73,7 @@ final class OppositeExpectation * Asserts that the layer does not only depends on the given layers. * * @param array|string $targets - * @return ArchExpectation + * @return ArchExpectation */ public function toOnlyDependOn(array|string $targets): ArchExpectation { @@ -85,7 +85,7 @@ final class OppositeExpectation /** * Asserts that the layer is depends on at least one layer. * - * @return ArchExpectation + * @return ArchExpectation */ public function toDependOnNothing(): ArchExpectation { @@ -131,10 +131,12 @@ final class OppositeExpectation /** * Creates a new expectation failed exception with a nice readable message. * - * @param array $arguments + * @param array|string $arguments */ - public function throwExpectationFailedException(string $name, array $arguments = []): never + public function throwExpectationFailedException(string $name, array|string $arguments = []): never { + $arguments = is_array($arguments) ? $arguments : [$arguments]; + $exporter = new Exporter(); $toString = fn ($argument): string => $exporter->shortenedExport($argument); diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index 3c444591..886ffab2 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -8,10 +8,6 @@ use BadMethodCallException; use Closure; use Error; use InvalidArgumentException; -use Pest\Arch\ArchExpectation; -use Pest\Arch\Expectations\ToDependOn; -use Pest\Arch\Expectations\ToDependOnNothing; -use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Exceptions\InvalidExpectationValue; use Pest\Matchers\Any; use Pest\Support\Arr; diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index 83eddac2..47f710ef 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -158,11 +158,10 @@ final class DatasetsRepository $datasets[$index] = iterator_to_array($datasets[$index], false); } - // @phpstan-ignore-next-line foreach ($datasets[$index] as $key => $values) { $values = is_array($values) ? $values : [$values]; $processedDataset[] = [ - 'label' => self::getDatasetDescription($key, $values), // @phpstan-ignore-line + 'label' => self::getDatasetDescription($key, $values), 'values' => $values, ]; } From 6d7fd66f829e18c88856ece5ae4eff311608302d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 14 Dec 2022 14:56:26 +0000 Subject: [PATCH 29/59] tests: on opposite `throwExpectationFailedException` --- tests/Unit/Expectations/OppositeExpectation.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/Unit/Expectations/OppositeExpectation.php diff --git a/tests/Unit/Expectations/OppositeExpectation.php b/tests/Unit/Expectations/OppositeExpectation.php new file mode 100644 index 00000000..448ebf6d --- /dev/null +++ b/tests/Unit/Expectations/OppositeExpectation.php @@ -0,0 +1,16 @@ +throwExpectationFailedException('toBe', 'bar'); +})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'."); + +it('throw expectation failed exception with array argument', function (): void { + $expectation = new OppositeExpectation(expect('foo')); + + $expectation->throwExpectationFailedException('toBe', ['bar']); +})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'."); From 3911cfec6df111b07c92b15ab825d41d8125cb81 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 14 Dec 2022 14:56:31 +0000 Subject: [PATCH 30/59] chore: updates snapshots --- tests/.snapshots/success.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index e47760bb..3c3a11d6 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -815,6 +815,10 @@ ✓ it show the actual dataset of multiple non-named datasets in their description ✓ it show the correct description for mixed named and not-named datasets + PASS Tests\Unit\Expectations\OppositeExpectation + ✓ it throw expectation failed exception with string argument + ✓ it throw expectation failed exception with array argument + PASS Tests\Unit\Plugins\Environment ✓ environment is set to CI when --ci option is used ✓ environment is set to Local when --ci option is not used @@ -890,4 +894,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 619 passed (1529 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 18 skipped, 621 passed (1533 assertions) \ No newline at end of file From 1c9c408cf386f18933a55b07c484d115486f7aae Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 15 Dec 2022 02:10:47 +0000 Subject: [PATCH 31/59] fix: style of memory plugin --- src/Plugins/Memory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/Memory.php b/src/Plugins/Memory.php index 5d29960e..0755f743 100644 --- a/src/Plugins/Memory.php +++ b/src/Plugins/Memory.php @@ -46,7 +46,7 @@ final class Memory implements AddsOutput, HandlesArguments { if ($this->enabled) { $this->output->writeln(sprintf( - ' Memory: %s MB', + ' Memory: %s MB', round(memory_get_usage(true) / 1000 ** 2, 3) )); } From 8aece3981dc4666857b588b20b5a7788a5d66932 Mon Sep 17 00:00:00 2001 From: Samuel Mwangi Date: Sat, 17 Dec 2022 22:06:21 +0300 Subject: [PATCH 32/59] Tests Badge Apply changes to the badge as per https://github.com/badges/shields/issues/8671 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4febffd5..151869b7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

PEST

- GitHub Workflow Status (master) + GitHub Workflow Status (master) Total Downloads Latest Version License From 8d018ea3f16eff9aaacd5765654ae25a3ee50b70 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 20 Dec 2022 22:42:09 +0000 Subject: [PATCH 33/59] chore: bumps termwind --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 320b58f1..9646056f 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "^8.1.0", "nunomaduro/collision": "^7.0.0", - "nunomaduro/termwind": "^1.14.2", + "nunomaduro/termwind": "^1.15", "pestphp/pest-plugin": "^2.0.0", "phpunit/phpunit": "10.0.x-dev" }, From 9596274b14ae914e1100a67c5e5b3b914512fe27 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 20 Dec 2022 22:42:29 +0000 Subject: [PATCH 34/59] fix: test suite loader duplicating tests --- overrides/Runner/TestSuiteLoader.php | 14 ++++---------- src/Concerns/Retrievable.php | 1 - tests/.snapshots/success.txt | 5 ++++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/overrides/Runner/TestSuiteLoader.php b/overrides/Runner/TestSuiteLoader.php index 0719288b..6e9df439 100644 --- a/overrides/Runner/TestSuiteLoader.php +++ b/overrides/Runner/TestSuiteLoader.php @@ -98,14 +98,14 @@ final class TestSuiteLoader self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses); - if (empty(self::$loadedClasses)) { + if (empty($loadedClasses)) { return $this->exceptionFor($suiteClassName, $suiteClassFile); } $testCaseFound = false; - foreach (self::$loadedClasses as $loadedClass) { - if (is_subclass_of($loadedClass, HasPrintableTestCaseName::class)) { + foreach (array_reverse($loadedClasses) as $loadedClass) { + if (is_subclass_of($loadedClass, HasPrintableTestCaseName::class) || is_subclass_of($loadedClass, TestCase::class)) { $suiteClassName = $loadedClass; $testCaseFound = true; @@ -115,13 +115,7 @@ final class TestSuiteLoader } if (! $testCaseFound) { - foreach (self::$loadedClasses as $loadedClass) { - if (is_subclass_of($loadedClass, TestCase::class)) { - $suiteClassName = $loadedClass; - - break; - } - } + return $this->exceptionFor($suiteClassName, $suiteClassFile); } if (! class_exists($suiteClassName, false)) { diff --git a/src/Concerns/Retrievable.php b/src/Concerns/Retrievable.php index a2716887..a95031b1 100644 --- a/src/Concerns/Retrievable.php +++ b/src/Concerns/Retrievable.php @@ -13,7 +13,6 @@ trait Retrievable * @template TRetrievableValue * * Safely retrieve the value at the given key from an object or array. - * * @template TRetrievableValue * * @param array|object $value diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 3c3a11d6..4df70c77 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -779,6 +779,9 @@ PASS Tests\PHPUnit\CustomAffixes\snakecasespec ✓ it runs file names like `snake_case_spec.php` + PASS Tests\CustomTestCase\ExecutedTest + ✓ that gets executed + PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory ✓ closure was bound to CustomTestCase @@ -894,4 +897,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 621 passed (1533 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 18 skipped, 622 passed (1509 assertions) \ No newline at end of file From b04207d9eaa1cfc5b92460cacc596f81487a5355 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Dec 2022 00:09:38 +0000 Subject: [PATCH 35/59] feat: improves `not->toDependOn` --- src/Exceptions/InvalidExpectation.php | 25 ++++++++++++++++++++ src/Expectation.php | 6 +---- src/Expectations/OppositeExpectation.php | 29 +++++++++++------------- 3 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 src/Exceptions/InvalidExpectation.php diff --git a/src/Exceptions/InvalidExpectation.php b/src/Exceptions/InvalidExpectation.php new file mode 100644 index 00000000..c13cf1ff --- /dev/null +++ b/src/Exceptions/InvalidExpectation.php @@ -0,0 +1,25 @@ + $methods + * @throws self + */ + public static function fromMethods(array $methods): never + { + throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); + } +} diff --git a/src/Expectation.php b/src/Expectation.php index 2277d95a..95b9b53f 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,7 +6,7 @@ namespace Pest; use BadMethodCallException; use Closure; -use Pest\Arch\ArchExpectation; +use Pest\Arch\Conctracts\ArchExpectation; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; use Pest\Arch\Expectations\ToOnlyDependOn; @@ -359,7 +359,6 @@ final class Expectation * Asserts that the layer depends (not exclusively) on the given layers. * * @param array|string $targets - * @return ArchExpectation */ public function toDependOn(array|string $targets): ArchExpectation { @@ -370,7 +369,6 @@ final class Expectation * Asserts that the layer only depends on the given layers. * * @param array|string $targets - * @return ArchExpectation */ public function toOnlyDependOn(array|string $targets): ArchExpectation { @@ -379,8 +377,6 @@ final class Expectation /** * Asserts that the layer is not allowed to depend on any other layer. - * - * @return ArchExpectation */ public function toDependOnNothing(): ArchExpectation { diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index a59b13b1..c45ced4e 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -4,10 +4,13 @@ declare(strict_types=1); namespace Pest\Expectations; -use Pest\Arch\ArchExpectation; +use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; use Pest\Arch\Expectations\ToOnlyDependOn; +use Pest\Arch\GroupArchExpectation; +use Pest\Arch\SingleArchExpectation; +use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; use PHPUnit\Framework\ExpectationFailedException; @@ -60,38 +63,32 @@ final class OppositeExpectation * Asserts that the layer does not depend on the given layers. * * @param array|string $targets - * @return ArchExpectation */ public function toDependOn(array|string $targets): ArchExpectation { - return ToDependOn::make($this->original, $targets)->opposite( - fn () => $this->throwExpectationFailedException('toDependOn', $targets), - ); + return GroupArchExpectation::fromExpectations(array_map(function (string $target) : SingleArchExpectation { + return ToDependOn::make($this->original, $target)->opposite( + fn () => $this->throwExpectationFailedException('toDependOn', $target), + ); + }, is_string($targets) ? [$targets] : $targets)); } /** * Asserts that the layer does not only depends on the given layers. * * @param array|string $targets - * @return ArchExpectation */ - public function toOnlyDependOn(array|string $targets): ArchExpectation + public function toOnlyDependOn(array|string $targets): never { - return ToOnlyDependOn::make($this->original, $targets)->opposite( - fn () => $this->throwExpectationFailedException('toOnlyDependOn', $targets), - ); + throw InvalidExpectation::fromMethods(['not', 'toOnlyDependOn']); } /** * Asserts that the layer is depends on at least one layer. - * - * @return ArchExpectation */ - public function toDependOnNothing(): ArchExpectation + public function toDependOnNothing(): never { - return ToDependOnNothing::make($this->original)->opposite( - fn () => $this->throwExpectationFailedException('toDependOnNothing'), - ); + throw InvalidExpectation::fromMethods(['not', 'toDependOnNothing']); } /** From 522504916b63db21376ce19df135aa708f4cccee Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Dec 2022 00:11:55 +0000 Subject: [PATCH 36/59] fix: namespace import --- src/Expectation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expectation.php b/src/Expectation.php index 95b9b53f..8283a870 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,7 +6,7 @@ namespace Pest; use BadMethodCallException; use Closure; -use Pest\Arch\Conctracts\ArchExpectation; +use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; use Pest\Arch\Expectations\ToOnlyDependOn; From 138bdf599b6cba1f6b2143179dead6e066ff9f58 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 21 Dec 2022 04:00:45 +0000 Subject: [PATCH 37/59] feat: adds `toOnlyBeUsedOn` --- src/Exceptions/InvalidExpectation.php | 3 +- src/Expectation.php | 37 +++++++++++++++--------- src/Expectations/OppositeExpectation.php | 32 +++++++++++--------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/Exceptions/InvalidExpectation.php b/src/Exceptions/InvalidExpectation.php index c13cf1ff..544baac3 100644 --- a/src/Exceptions/InvalidExpectation.php +++ b/src/Exceptions/InvalidExpectation.php @@ -12,10 +12,11 @@ use Symfony\Component\Console\Exception\ExceptionInterface; /** * @internal */ -final class InvalidExpectation extends LogicException implements ExceptionInterface, RenderlessEditor, RenderlessTrace +final class InvalidExpectation extends LogicException implements ExceptionInterface, RenderlessEditor, RenderlessTrace { /** * @param array $methods + * * @throws self */ public static function fromMethods(array $methods): never diff --git a/src/Expectation.php b/src/Expectation.php index 8283a870..507e6915 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -9,6 +9,7 @@ use Closure; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; +use Pest\Arch\Expectations\ToOnlyBeUsedOn; use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; @@ -356,17 +357,35 @@ final class Expectation } /** - * Asserts that the layer depends (not exclusively) on the given layers. + * Asserts that the given expectation target depends on the given dependencies. * - * @param array|string $targets + * @param array|string $dependencies */ - public function toDependOn(array|string $targets): ArchExpectation + public function toDependOn(array|string $dependencies): ArchExpectation { - return ToDependOn::make($this, $targets); + return ToDependOn::make($this, $dependencies); } /** - * Asserts that the layer only depends on the given layers. + * Asserts that the given expectation target does not have any dependencies. + */ + public function toDependOnNothing(): ArchExpectation + { + return ToDependOnNothing::make($this); + } + + /** + * Asserts that the given expectation dependency is only depended on by the given targets. + * + * @param array|string $targets + */ + public function toOnlyBeUsedOn(array|string $targets): ArchExpectation + { + return ToOnlyBeUsedOn::make($this, $targets); + } + + /** + * Asserts that the given expectation target does "only" depend on the given dependencies. * * @param array|string $targets */ @@ -374,12 +393,4 @@ final class Expectation { return ToOnlyDependOn::make($this, $targets); } - - /** - * Asserts that the layer is not allowed to depend on any other layer. - */ - public function toDependOnNothing(): ArchExpectation - { - return ToDependOnNothing::make($this); - } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index c45ced4e..f73b3345 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -6,8 +6,6 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToDependOn; -use Pest\Arch\Expectations\ToDependOnNothing; -use Pest\Arch\Expectations\ToOnlyDependOn; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Exceptions\InvalidExpectation; @@ -60,31 +58,39 @@ final class OppositeExpectation } /** - * Asserts that the layer does not depend on the given layers. + * Asserts that the given expectation target depends on the given dependencies. * - * @param array|string $targets + * @param array|string $dependencies */ - public function toDependOn(array|string $targets): ArchExpectation + public function toDependOn(array|string $dependencies): ArchExpectation { - return GroupArchExpectation::fromExpectations(array_map(function (string $target) : SingleArchExpectation { - return ToDependOn::make($this->original, $target)->opposite( - fn () => $this->throwExpectationFailedException('toDependOn', $target), - ); - }, is_string($targets) ? [$targets] : $targets)); + return GroupArchExpectation::fromExpectations(array_map(fn (string $target): SingleArchExpectation => ToDependOn::make($this->original, $target)->opposite( + fn () => $this->throwExpectationFailedException('toDependOn', $target), + ), is_string($dependencies) ? [$dependencies] : $dependencies)); } /** - * Asserts that the layer does not only depends on the given layers. + * Asserts that the given expectation dependency is only depended on by the given targets. * * @param array|string $targets */ - public function toOnlyDependOn(array|string $targets): never + public function toOnlyBeUsedOn(array|string $targets): never + { + throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedOn']); + } + + /** + * Asserts that the given expectation target does "only" depend on the given dependencies. + * + * @param array|string $dependencies + */ + public function toOnlyDependOn(array|string $dependencies): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyDependOn']); } /** - * Asserts that the layer is depends on at least one layer. + * Asserts that the given expectation target does not have any dependencies. */ public function toDependOnNothing(): never { From a55953a8e02992bc46009aa078b2362cfb0c7442 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 23 Dec 2022 23:03:46 +0000 Subject: [PATCH 38/59] chore: remove coverage type --- phpstan.neon | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index b3be0341..e74b4290 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,9 +2,6 @@ includes: - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/ergebnis/phpstan-rules/rules.neon - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon - - vendor/symplify/phpstan-rules/config/services/services.neon - - vendor/symplify/phpstan-rules/config/packages/cognitive-complexity/cognitive-complexity-services.neon - parameters: level: max @@ -23,34 +20,3 @@ parameters: - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" - "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#" -services: - - - class: Symplify\PHPStanRules\Rules\Explicit\PropertyTypeDeclarationSeaLevelRule - tags: [phpstan.rules.rule] - arguments: - minimalLevel: 1.00 - - - - class: Symplify\PHPStanRules\Rules\Explicit\ParamTypeDeclarationSeaLevelRule - tags: [phpstan.rules.rule] - arguments: - minimalLevel: 0.96 - - - - class: Symplify\PHPStanRules\Rules\Explicit\ReturnTypeDeclarationSeaLevelRule - tags: [phpstan.rules.rule] - arguments: - minimalLevel: 0.97 - - - - class: Symplify\PHPStanRules\Collector\FunctionLike\ParamTypeSeaLevelCollector - tags: [phpstan.collector] - - - - class: Symplify\PHPStanRules\Collector\FunctionLike\ReturnTypeSeaLevelCollector - tags: [phpstan.collector] - - - - class: Symplify\PHPStanRules\Collector\ClassLike\PropertyTypeSeaLevelCollector - tags: [phpstan.collector] - From 37b1367d25049e945113085af9ef8f04f9c99b75 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 23 Dec 2022 23:03:53 +0000 Subject: [PATCH 39/59] chore: adjusts snapshots --- tests/.snapshots/success.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 9ee929b7..47e9b6d4 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -899,4 +899,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - + Tests: 4 incomplete, 2 todos, 18 skipped, 624 passed (1511 assertions) \ No newline at end of file From 406fcf72ae7fb58626d0db44873e766c5d8299e6 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 28 Dec 2022 14:21:07 +0000 Subject: [PATCH 40/59] fix: overrides being used on regular phpunit --- composer.json | 8 ------ src/Bootstrappers/BootOverrides.php | 40 +++++++++++++++++++++++++++++ src/Kernel.php | 1 + src/Support/Reflection.php | 5 ++-- src/TestSuite.php | 2 +- 5 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 src/Bootstrappers/BootOverrides.php diff --git a/composer.json b/composer.json index 9646056f..557eebcf 100644 --- a/composer.json +++ b/composer.json @@ -27,15 +27,7 @@ "psr-4": { "Pest\\": "src/" }, - "exclude-from-classmap": [ - "../phpunit/phpunit/src/Runner/Filter/NameFilterIterator.php", - "vendor/phpunit/phpunit/src/Runner/Filter/NameFilterIterator.php", - "../phpunit/src/Runner/TestSuiteLoader.php", - "vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php" - ], "files": [ - "overrides/Runner/Filter/NameFilterIterator.php", - "overrides/Runner/TestSuiteLoader.php", "src/Functions.php", "src/Pest.php" ] diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php new file mode 100644 index 00000000..9c637d0f --- /dev/null +++ b/src/Bootstrappers/BootOverrides.php @@ -0,0 +1,40 @@ + + */ + private const FILES = [ + 'Runner/Filter/NameFilterIterator.php', + 'Runner/TestSuiteLoader.php', + ]; + + /** + * Boots the Subscribers. + */ + public function boot(): void + { + foreach (self::FILES as $file) { + $file = __DIR__."/../../overrides/$file"; + + if (! file_exists($file)) { + throw ShouldNotHappen::fromMessage(sprintf('File [%s] does not exist.', $file)); + } + + require_once $file; + } + } +} diff --git a/src/Kernel.php b/src/Kernel.php index bf7b200f..aa8afa34 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -23,6 +23,7 @@ final class Kernel * @var array */ private const BOOTSTRAPPERS = [ + Bootstrappers\BootOverrides::class, Bootstrappers\BootExceptionHandler::class, Bootstrappers\BootSubscribers::class, Bootstrappers\BootFiles::class, diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index c16b926e..14edd7ad 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -12,6 +12,7 @@ use ReflectionException; use ReflectionFunction; use ReflectionNamedType; use ReflectionParameter; +use ReflectionProperty; use ReflectionUnionType; /** @@ -95,7 +96,7 @@ final class Reflection $reflectionProperty = null; - while ($reflectionProperty === null) { + while (! $reflectionProperty instanceof ReflectionProperty) { try { /* @var ReflectionProperty $reflectionProperty */ $reflectionProperty = $reflectionClass->getProperty($property); @@ -127,7 +128,7 @@ final class Reflection $reflectionProperty = null; - while ($reflectionProperty === null) { + while (! $reflectionProperty instanceof ReflectionProperty) { try { /* @var ReflectionProperty $reflectionProperty */ $reflectionProperty = $reflectionClass->getProperty($property); diff --git a/src/TestSuite.php b/src/TestSuite.php index c460f532..ab428e1a 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -95,7 +95,7 @@ final class TestSuite return self::$instance; } - if (self::$instance === null) { + if (! self::$instance instanceof self) { throw new InvalidPestCommand(); } From 174a9ca60b6caa6f3f4f862e17ef666de68c3de7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 28 Dec 2022 16:09:47 +0000 Subject: [PATCH 41/59] feat: adds `toBeUsedOn` --- src/Expectation.php | 39 +++++++++++++------- src/Expectations/OppositeExpectation.php | 47 +++++++++++++----------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 507e6915..f63a4778 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\ToBeUsedOn; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; use Pest\Arch\Expectations\ToOnlyBeUsedOn; @@ -359,11 +360,21 @@ final class Expectation /** * Asserts that the given expectation target depends on the given dependencies. * - * @param array|string $dependencies + * @param array|string $targets */ - public function toDependOn(array|string $dependencies): ArchExpectation + public function toDependOn(array|string $targets): ArchExpectation { - return ToDependOn::make($this, $dependencies); + return ToDependOn::make($this, $targets); + } + + /** + * Asserts that the given expectation target "only" depends on the given dependencies. + * + * @param array|string $targets + */ + public function toOnlyDependOn(array|string $targets): ArchExpectation + { + return ToOnlyDependOn::make($this, $targets); } /** @@ -375,7 +386,17 @@ final class Expectation } /** - * Asserts that the given expectation dependency is only depended on by the given targets. + * Asserts that the given expectation dependency is used by the given targets. + * + * @param array|string $targets + */ + public function toBeUsedOn(array|string $targets): ArchExpectation + { + return ToBeUsedOn::make($this, $targets); + } + + /** + * Asserts that the given expectation dependency is "only" used by the given targets. * * @param array|string $targets */ @@ -383,14 +404,4 @@ final class Expectation { return ToOnlyBeUsedOn::make($this, $targets); } - - /** - * Asserts that the given expectation target does "only" depend on the given dependencies. - * - * @param array|string $targets - */ - public function toOnlyDependOn(array|string $targets): ArchExpectation - { - return ToOnlyDependOn::make($this, $targets); - } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index f73b3345..808b5874 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\ToBeUsedOn; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; @@ -58,45 +59,47 @@ final class OppositeExpectation } /** - * Asserts that the given expectation target depends on the given dependencies. - * - * @param array|string $dependencies - */ - public function toDependOn(array|string $dependencies): ArchExpectation - { - return GroupArchExpectation::fromExpectations(array_map(fn (string $target): SingleArchExpectation => ToDependOn::make($this->original, $target)->opposite( - fn () => $this->throwExpectationFailedException('toDependOn', $target), - ), is_string($dependencies) ? [$dependencies] : $dependencies)); - } - - /** - * Asserts that the given expectation dependency is only depended on by the given targets. + * Asserts that the given expectation target does not depend on any of the given dependencies. * * @param array|string $targets */ - public function toOnlyBeUsedOn(array|string $targets): never + public function toDependOn(array|string $targets): ArchExpectation { - throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedOn']); + return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToDependOn::make($this->original, $target)->opposite( + fn () => $this->throwExpectationFailedException('toDependOn', $target), + ), is_string($targets) ? [$targets] : $targets)); } /** - * Asserts that the given expectation target does "only" depend on the given dependencies. - * - * @param array|string $dependencies + * @param array|string $targets */ - public function toOnlyDependOn(array|string $dependencies): never + public function toOnlyDependOn(array|string $targets): never { throw InvalidExpectation::fromMethods(['not', 'toOnlyDependOn']); } - /** - * Asserts that the given expectation target does not have any dependencies. - */ public function toDependOnNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toDependOnNothing']); } + /** + * Asserts that the given expectation dependency is not used by any of the given targets. + * + * @param array|string $targets + */ + public function toBeUsedOn(array|string $targets): ArchExpectation + { + return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedOn::make($this->original, $target)->opposite( + fn () => $this->throwExpectationFailedException('toBeUsedOn', $target), + ), is_string($targets) ? [$targets] : $targets)); + } + + public function toOnlyBeUsedOn(): never + { + throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedOn']); + } + /** * Handle dynamic method calls into the original expectation. * From 476f56b6170235aa4380f97624996537ce0b3968 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 28 Dec 2022 16:27:08 +0000 Subject: [PATCH 42/59] feat: adds `toBeUsedOnNothing` and `toBeUsed` --- src/Expectation.php | 15 +++++++++++++++ src/Expectations/OppositeExpectation.php | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Expectation.php b/src/Expectation.php index f63a4778..ddca5bda 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -8,6 +8,7 @@ use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToBeUsedOn; +use Pest\Arch\Expectations\ToBeUsedOnNothing; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\Expectations\ToDependOnNothing; use Pest\Arch\Expectations\ToOnlyBeUsedOn; @@ -16,6 +17,7 @@ use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; use Pest\Exceptions\ExpectationNotFound; +use Pest\Exceptions\InvalidExpectation; use Pest\Exceptions\InvalidExpectationValue; use Pest\Expectations\EachExpectation; use Pest\Expectations\HigherOrderExpectation; @@ -385,6 +387,11 @@ final class Expectation return ToDependOnNothing::make($this); } + public function toBeUsed(): never + { + throw InvalidExpectation::fromMethods(['toBeUsed']); + } + /** * Asserts that the given expectation dependency is used by the given targets. * @@ -404,4 +411,12 @@ final class Expectation { return ToOnlyBeUsedOn::make($this, $targets); } + + /** + * Asserts that the given expectation dependency is not used. + */ + public function toBeUsedOnNothing(): ArchExpectation + { + return ToBeUsedOnNothing::make($this); + } } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 808b5874..67d47994 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -6,6 +6,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToBeUsedOn; +use Pest\Arch\Expectations\ToBeUsedOnNothing; use Pest\Arch\Expectations\ToDependOn; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; @@ -83,6 +84,14 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toDependOnNothing']); } + /** + * Asserts that the given expectation dependency is not used. + */ + public function toBeUsed(): ArchExpectation + { + return ToBeUsedOnNothing::make($this->original); + } + /** * Asserts that the given expectation dependency is not used by any of the given targets. * @@ -100,6 +109,14 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedOn']); } + /** + * Asserts that the given expectation dependency is not used. + */ + public function toBeUsedOnNothing(): ArchExpectation + { + throw InvalidExpectation::fromMethods(['not', 'toBeUsedOnNothing']); + } + /** * Handle dynamic method calls into the original expectation. * From 0fd5b2efe1843adfe5a9d526a5e5055746c511c4 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 09:31:17 +0000 Subject: [PATCH 43/59] chore: adjusts return type --- src/Expectations/OppositeExpectation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 67d47994..6fa22de7 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -112,7 +112,7 @@ final class OppositeExpectation /** * Asserts that the given expectation dependency is not used. */ - public function toBeUsedOnNothing(): ArchExpectation + public function toBeUsedOnNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toBeUsedOnNothing']); } From c9a02b964de2bb52b75a1e015d7e1847b43bc398 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 09:34:53 +0000 Subject: [PATCH 44/59] fix: `--retry` not running all tests after passing --- src/Factories/TestCaseMethodFactory.php | 4 +++- src/Repositories/TempRepository.php | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 59b6c6c7..3620b710 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -123,7 +123,9 @@ final class TestCaseMethodFactory $methodName = Str::evaluable($this->description); - if (Retry::$retrying && ! TestSuite::getInstance()->retryTempRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) { + $retryRepository = TestSuite::getInstance()->retryTempRepository; + + if (Retry::$retrying && ! $retryRepository->isEmpty() && ! $retryRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) { return ''; } diff --git a/src/Repositories/TempRepository.php b/src/Repositories/TempRepository.php index 9ad1b601..24118551 100644 --- a/src/Repositories/TempRepository.php +++ b/src/Repositories/TempRepository.php @@ -37,6 +37,14 @@ final class TempRepository $this->save([]); } + /** + * Checks if there is any element. + */ + public function isEmpty(): bool + { + return $this->all() === []; + } + /** * Checks if the given element exists. */ From b7e2cd758f8a4d3840b6d45417b8e793c23c7ac3 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 09:37:45 +0000 Subject: [PATCH 45/59] fix: when `retry.json` does not exists --- src/Repositories/TempRepository.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Repositories/TempRepository.php b/src/Repositories/TempRepository.php index 24118551..701aec99 100644 --- a/src/Repositories/TempRepository.php +++ b/src/Repositories/TempRepository.php @@ -60,7 +60,9 @@ final class TempRepository */ private function all(): array { - $contents = file_get_contents(self::FOLDER.'/'.$this->filename.'.json'); + $path = self::FOLDER.'/'.$this->filename.'.json'; + + $contents = file_exists($path) ? file_get_contents($path) : '{}'; assert(is_string($contents)); From 227d32a1fd6b7d7ce0b6c7e4cf378ccc14369ff1 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 11:35:10 +0000 Subject: [PATCH 46/59] feat: improves `--coverage` output --- src/Support/Coverage.php | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index c0e78e67..868546c9 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -10,9 +10,9 @@ use SebastianBergmann\CodeCoverage\Node\Directory; use SebastianBergmann\CodeCoverage\Node\File; use SebastianBergmann\Environment\Runtime; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Terminal; use function Termwind\render; use function Termwind\renderUsing; +use function Termwind\terminal; /** * @internal @@ -83,10 +83,6 @@ final class Coverage $codeCoverage = require $reportPath; unlink($reportPath); - $totalWidth = (new Terminal())->getWidth(); - - $dottedLineLength = $totalWidth - 6; - $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines(); /** @var Directory $report */ @@ -103,36 +99,24 @@ final class Coverage $dirname, $basename, ]); - $rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [ - $dirname, - $basename, - ]); - - $linesExecutedTakenSize = 0; - - if ($file->percentageOfExecutedLines()->asString() != '0.00%') { - $linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1; - $name .= sprintf(' %s', $uncoveredLines); - } $percentage = $file->numberOfExecutableLines() === 0 ? '100.0' : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', ''); - $takenSize = strlen($rawName.$percentage) + 2 + $linesExecutedTakenSize; // adding 3 space and percent sign + $color = $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'); - $percentage = sprintf( - '%s', - $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'), - $percentage - ); + $truncateAt = max(1, terminal()->width() - 12); + + renderUsing($output); + render(<< + {$name} + + {$percentage}% + + HTML); - $output->writeln(sprintf( - ' %s %s %s %%', - $name, - str_repeat('.', max($dottedLineLength - $takenSize, 1)), - $percentage - )); } $totalCoverageAsString = $totalCoverage->asFloat() === 0.0 From 39ee5b9b084f1d0a3ef992c6b17c95795f6a732e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 16:34:15 +0000 Subject: [PATCH 47/59] change: refactors `DependOn` to `Use` --- src/Expectation.php | 24 ++++++++++++------------ src/Expectations/OppositeExpectation.php | 18 +++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index ddca5bda..727fdc27 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -9,10 +9,10 @@ use Closure; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToBeUsedOn; use Pest\Arch\Expectations\ToBeUsedOnNothing; -use Pest\Arch\Expectations\ToDependOn; -use Pest\Arch\Expectations\ToDependOnNothing; +use Pest\Arch\Expectations\ToUse; +use Pest\Arch\Expectations\ToUseNothing; use Pest\Arch\Expectations\ToOnlyBeUsedOn; -use Pest\Arch\Expectations\ToOnlyDependOn; +use Pest\Arch\Expectations\ToOnlyUse; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -360,31 +360,31 @@ final class Expectation } /** - * Asserts that the given expectation target depends on the given dependencies. + * Asserts that the given expectation target use the given dependencies. * * @param array|string $targets */ - public function toDependOn(array|string $targets): ArchExpectation + public function toUse(array|string $targets): ArchExpectation { - return ToDependOn::make($this, $targets); + return ToUse::make($this, $targets); } /** - * Asserts that the given expectation target "only" depends on the given dependencies. + * Asserts that the given expectation target "only" use on the given dependencies. * * @param array|string $targets */ - public function toOnlyDependOn(array|string $targets): ArchExpectation + public function toOnlyUse(array|string $targets): ArchExpectation { - return ToOnlyDependOn::make($this, $targets); + return ToOnlyUse::make($this, $targets); } /** - * Asserts that the given expectation target does not have any dependencies. + * Asserts that the given expectation target does not use any dependencies. */ - public function toDependOnNothing(): ArchExpectation + public function toUseNothing(): ArchExpectation { - return ToDependOnNothing::make($this); + return ToUseNothing::make($this); } public function toBeUsed(): never diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index 6fa22de7..2c9e53a2 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -7,7 +7,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToBeUsedOn; use Pest\Arch\Expectations\ToBeUsedOnNothing; -use Pest\Arch\Expectations\ToDependOn; +use Pest\Arch\Expectations\ToUse; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; use Pest\Exceptions\InvalidExpectation; @@ -60,28 +60,28 @@ final class OppositeExpectation } /** - * Asserts that the given expectation target does not depend on any of the given dependencies. + * Asserts that the given expectation target does not use any of the given dependencies. * * @param array|string $targets */ - public function toDependOn(array|string $targets): ArchExpectation + public function toUse(array|string $targets): ArchExpectation { - return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToDependOn::make($this->original, $target)->opposite( - fn () => $this->throwExpectationFailedException('toDependOn', $target), + return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($this->original, $target)->opposite( + fn () => $this->throwExpectationFailedException('toUse', $target), ), is_string($targets) ? [$targets] : $targets)); } /** * @param array|string $targets */ - public function toOnlyDependOn(array|string $targets): never + public function toOnlyUse(array|string $targets): never { - throw InvalidExpectation::fromMethods(['not', 'toOnlyDependOn']); + throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } - public function toDependOnNothing(): never + public function toUseNothing(): never { - throw InvalidExpectation::fromMethods(['not', 'toDependOnNothing']); + throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); } /** From 0e98c5c5c49589bd3f983f9057a8fa1403817b5e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 29 Dec 2022 16:36:17 +0000 Subject: [PATCH 48/59] style: removes non-used imports --- src/Expectation.php | 4 ++-- src/Support/Coverage.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 727fdc27..4d5e001c 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -9,10 +9,10 @@ use Closure; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Expectations\ToBeUsedOn; use Pest\Arch\Expectations\ToBeUsedOnNothing; -use Pest\Arch\Expectations\ToUse; -use Pest\Arch\Expectations\ToUseNothing; use Pest\Arch\Expectations\ToOnlyBeUsedOn; use Pest\Arch\Expectations\ToOnlyUse; +use Pest\Arch\Expectations\ToUse; +use Pest\Arch\Expectations\ToUseNothing; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php index 868546c9..91ecfe1a 100644 --- a/src/Support/Coverage.php +++ b/src/Support/Coverage.php @@ -116,7 +116,6 @@ final class Coverage {$percentage}% HTML); - } $totalCoverageAsString = $totalCoverage->asFloat() === 0.0 From 43a7df2cc1fbf19dd1e6ee043c0c31311e5a8902 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 1 Jan 2023 20:29:47 +0000 Subject: [PATCH 49/59] tests: update snapshots --- tests/.snapshots/help-command.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/.snapshots/help-command.txt b/tests/.snapshots/help-command.txt index f6caacbc..b291ec65 100644 --- a/tests/.snapshots/help-command.txt +++ b/tests/.snapshots/help-command.txt @@ -76,9 +76,9 @@ LOGGING OPTIONS: --log-junit ................ Log test execution in JUnit XML format to file --log-teamcity .............. Log test execution in TeamCity format to file - --testdox-html ........... Write agile documentation in HTML format to file - --testdox-text ........... Write agile documentation in Text format to file - --testdox-xml ............. Write agile documentation in XML format to file + --testdox-html ................. Write documentation in HTML format to file + --testdox-text ................. Write documentation in Text format to file + --testdox-xml ................... Write documentation in XML format to file --log-events-text ..................... Stream events as plain text to file --log-events-verbose-text Stream events as plain text to file (with telemetry information) --no-logging .................................. Ignore logging configuration From 330b4f017122111e8d82a300ff94654e205574c7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 2 Jan 2023 20:59:59 +0000 Subject: [PATCH 50/59] fix: ensures `--retry` works with errored tests --- src/Bootstrappers/BootSubscribers.php | 3 ++- src/Factories/TestCaseMethodFactory.php | 2 +- ...TempRepository.php => RetryRepository.php} | 16 +++++++++---- .../EnsureErroredTestsAreRetryable.php | 23 +++++++++++++++++++ ....php => EnsureFailedTestsAreRetryable.php} | 4 ++-- .../EnsureRetryRepositoryExists.php | 2 +- src/TestSuite.php | 8 +++---- 7 files changed, 44 insertions(+), 14 deletions(-) rename src/Repositories/{TempRepository.php => RetryRepository.php} (76%) create mode 100644 src/Subscribers/EnsureErroredTestsAreRetryable.php rename src/Subscribers/{EnsureFailedTestsAreStoredForRetry.php => EnsureFailedTestsAreRetryable.php} (64%) diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index e9819fa4..ab31e337 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -23,7 +23,8 @@ final class BootSubscribers implements Bootstrapper Subscribers\EnsureConfigurationIsValid::class, Subscribers\EnsureConfigurationDefaults::class, Subscribers\EnsureRetryRepositoryExists::class, - Subscribers\EnsureFailedTestsAreStoredForRetry::class, + Subscribers\EnsureErroredTestsAreRetryable::class, + Subscribers\EnsureFailedTestsAreRetryable::class, ]; /** diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 3620b710..1bca4dd9 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -123,7 +123,7 @@ final class TestCaseMethodFactory $methodName = Str::evaluable($this->description); - $retryRepository = TestSuite::getInstance()->retryTempRepository; + $retryRepository = TestSuite::getInstance()->retryRepository; if (Retry::$retrying && ! $retryRepository->isEmpty() && ! $retryRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) { return ''; diff --git a/src/Repositories/TempRepository.php b/src/Repositories/RetryRepository.php similarity index 76% rename from src/Repositories/TempRepository.php rename to src/Repositories/RetryRepository.php index 701aec99..e2a158c2 100644 --- a/src/Repositories/TempRepository.php +++ b/src/Repositories/RetryRepository.php @@ -7,9 +7,15 @@ namespace Pest\Repositories; /** * @internal */ -final class TempRepository +final class RetryRepository { - private const FOLDER = __DIR__.'/../../.temp'; + private const TEMPORARY_FOLDER = __DIR__ + .DIRECTORY_SEPARATOR + .'..' + .DIRECTORY_SEPARATOR + .'..' + .DIRECTORY_SEPARATOR + .'.temp'; /** * Creates a new Temp Repository instance. @@ -32,7 +38,7 @@ final class TempRepository */ public function boot(): void { - @unlink(self::FOLDER.'/'.$this->filename.'.json'); // @phpstan-ignore-line + @unlink(self::TEMPORARY_FOLDER.'/'.$this->filename.'.json'); // @phpstan-ignore-line $this->save([]); } @@ -60,7 +66,7 @@ final class TempRepository */ private function all(): array { - $path = self::FOLDER.'/'.$this->filename.'.json'; + $path = self::TEMPORARY_FOLDER.'/'.$this->filename.'.json'; $contents = file_exists($path) ? file_get_contents($path) : '{}'; @@ -80,6 +86,6 @@ final class TempRepository { $contents = json_encode($elements, JSON_THROW_ON_ERROR); - file_put_contents(self::FOLDER.'/'.$this->filename.'.json', $contents); + file_put_contents(self::TEMPORARY_FOLDER.'/'.$this->filename.'.json', $contents); } } diff --git a/src/Subscribers/EnsureErroredTestsAreRetryable.php b/src/Subscribers/EnsureErroredTestsAreRetryable.php new file mode 100644 index 00000000..6eb2661d --- /dev/null +++ b/src/Subscribers/EnsureErroredTestsAreRetryable.php @@ -0,0 +1,23 @@ +retryRepository->add($event->test()->id()); + } +} diff --git a/src/Subscribers/EnsureFailedTestsAreStoredForRetry.php b/src/Subscribers/EnsureFailedTestsAreRetryable.php similarity index 64% rename from src/Subscribers/EnsureFailedTestsAreStoredForRetry.php rename to src/Subscribers/EnsureFailedTestsAreRetryable.php index 46fce87a..73468723 100644 --- a/src/Subscribers/EnsureFailedTestsAreStoredForRetry.php +++ b/src/Subscribers/EnsureFailedTestsAreRetryable.php @@ -11,13 +11,13 @@ use PHPUnit\Event\Test\FailedSubscriber; /** * @internal */ -final class EnsureFailedTestsAreStoredForRetry implements FailedSubscriber +final class EnsureFailedTestsAreRetryable implements FailedSubscriber { /** * Runs the subscriber. */ public function notify(Failed $event): void { - TestSuite::getInstance()->retryTempRepository->add($event->test()->id()); + TestSuite::getInstance()->retryRepository->add($event->test()->id()); } } diff --git a/src/Subscribers/EnsureRetryRepositoryExists.php b/src/Subscribers/EnsureRetryRepositoryExists.php index dc988d1d..91226b0c 100644 --- a/src/Subscribers/EnsureRetryRepositoryExists.php +++ b/src/Subscribers/EnsureRetryRepositoryExists.php @@ -18,6 +18,6 @@ final class EnsureRetryRepositoryExists implements StartedSubscriber */ public function notify(Started $event): void { - TestSuite::getInstance()->retryTempRepository->boot(); + TestSuite::getInstance()->retryRepository->boot(); } } diff --git a/src/TestSuite.php b/src/TestSuite.php index ab428e1a..c7911b87 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -9,7 +9,7 @@ use Pest\Repositories\AfterAllRepository; use Pest\Repositories\AfterEachRepository; use Pest\Repositories\BeforeAllRepository; use Pest\Repositories\BeforeEachRepository; -use Pest\Repositories\TempRepository; +use Pest\Repositories\RetryRepository; use Pest\Repositories\TestRepository; use PHPUnit\Framework\TestCase; @@ -49,9 +49,9 @@ final class TestSuite public AfterAllRepository $afterAll; /** - * Holds the retry temp repository. + * Holds the retry repository. */ - public TempRepository $retryTempRepository; + public RetryRepository $retryRepository; /** * Holds the root path. @@ -75,7 +75,7 @@ final class TestSuite $this->tests = new TestRepository(); $this->afterEach = new AfterEachRepository(); $this->afterAll = new AfterAllRepository(); - $this->retryTempRepository = new TempRepository('retry'); + $this->retryRepository = new RetryRepository('retry'); $this->rootPath = (string) realpath($rootPath); } From 6595e6960ba027fde4ed4a0f41c7573a195cd0b6 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 6 Jan 2023 20:09:25 +0000 Subject: [PATCH 51/59] tests: adjusts snapshots based on code highlight --- tests/.snapshots/success.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 47e9b6d4..fc45145f 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -757,29 +757,29 @@ ✓ global beforeEach execution order PASS Tests\PHPUnit\CustomAffixes\InvalidTestName - ✓ it runs file names like `@#$%^&()-_=+.php` + ✓ it runs file names like @#$%^&()-_=+.php PASS Tests\PHPUnit\CustomAffixes\ATestWithSpaces - ✓ it runs file names like `A Test With Spaces.php` + ✓ it runs file names like A Test With Spaces.php PASS Tests\PHPUnit\CustomAffixes\AdditionalFileExtension - ✓ it runs file names like `AdditionalFileExtension.spec.php` + ✓ it runs file names like AdditionalFileExtension.spec.php PASS Tests\PHPUnit\CustomAffixes\FolderWithAn\ExampleTest ✓ custom traits can be used ✓ trait applied in this file PASS Tests\PHPUnit\CustomAffixes\ManyExtensions - ✓ it runs file names like `ManyExtensions.class.test.php` + ✓ it runs file names like ManyExtensions.class.test.php PASS Tests\PHPUnit\CustomAffixes\TestCaseWithQuotes - ✓ it runs file names like `Test 'Case' With Quotes.php` + ✓ it runs file names like Test 'Case' With Quotes.php PASS Tests\PHPUnit\CustomAffixes\kebabcasespec - ✓ it runs file names like `kebab-case-spec.php` + ✓ it runs file names like kebab-case-spec.php PASS Tests\PHPUnit\CustomAffixes\snakecasespec - ✓ it runs file names like `snake_case_spec.php` + ✓ it runs file names like snake_case_spec.php PASS Tests\CustomTestCase\ExecutedTest ✓ that gets executed From 3ecf35143241d1a5e4bf428fa8f558cd14d6f79c Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 8 Jan 2023 22:56:09 +0000 Subject: [PATCH 52/59] docs: remove todo --- TODO.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 58d25d72..00000000 --- a/TODO.md +++ /dev/null @@ -1,5 +0,0 @@ -1. TeamCity. (oliver) -2. Junit. (nuno) -3. Check all plugins. (nuno - almost done) -4. Parallel testing. (luke) -5. Finish Collision's todo. (nuno) From 4083e6e26e8ab71a3882954790a0e09f17309ef7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 8 Jan 2023 23:02:01 +0000 Subject: [PATCH 53/59] change: removes junit support --- src/Logging/JUnit.php | 23 ----------------------- tests/.snapshots/success.txt | 5 +---- tests/Visual/JUnit.php | 29 ----------------------------- 3 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 src/Logging/JUnit.php delete mode 100644 tests/Visual/JUnit.php diff --git a/src/Logging/JUnit.php b/src/Logging/JUnit.php deleted file mode 100644 index 22c05afd..00000000 --- a/src/Logging/JUnit.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Pest\Logging; - -use Pest\Support\Printer; - -/** - * @internal This class is not covered by the backward compatibility promise for PHPUnit - */ -final class JUnit extends Printer -{ - // @todo -} diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index fc45145f..789c001e 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -882,9 +882,6 @@ PASS Tests\Visual\Help ✓ visual snapshot of help command output - WARN Tests\Visual\JUnit - - it is can successfully call all public methods → Not supported yet. - PASS Tests\Visual\SingleTestOrDirectory ✓ allows to run a single test ✓ allows to run a directory @@ -899,4 +896,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 624 passed (1511 assertions) \ No newline at end of file + Tests: 4 incomplete, 2 todos, 17 skipped, 624 passed (1511 assertions) \ No newline at end of file diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php deleted file mode 100644 index 41481b4d..00000000 --- a/tests/Visual/JUnit.php +++ /dev/null @@ -1,29 +0,0 @@ -startTestSuite(new TestSuite()); - $junit->startTest($this); - $junit->addError($this, new Exception(), 0); - $junit->addFailure($this, new AssertionFailedError(), 0); - $junit->addWarning($this, new Warning(), 0); - $junit->addIncompleteTest($this, new Exception(), 0); - $junit->addRiskyTest($this, new Exception(), 0); - $junit->addSkippedTest($this, new Exception(), 0); - $junit->endTest($this, 0); - $junit->endTestSuite(new TestSuite()); - $this->expectNotToPerformAssertions(); -})->skip('Not supported yet.'); - -afterEach(function () { - unlink(__DIR__.'/junit.html'); -}); From e8b10fcc913a30c24ea0730e761323ab6c259d53 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 8 Jan 2023 23:14:17 +0000 Subject: [PATCH 54/59] chore: run tests on macos --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a855c50..6f740f20 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] # (macos-latest, windows-latest) 2.x-dev is under development + os: [ubuntu-latest, macos-latest] # (windows-latest) 2.x-dev is under development php: ['8.1', '8.2'] dependency-version: [prefer-lowest, prefer-stable] parallel: ['', '--parallel'] From 5436ff8c4957bf5dff3e6f4a8f26df25c36d7c2a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 8 Jan 2023 23:16:42 +0000 Subject: [PATCH 55/59] chore: run tests on windows --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f740f20..e8733b0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] # (windows-latest) 2.x-dev is under development + os: [ubuntu-latest, macos-latest, windows-latest] php: ['8.1', '8.2'] dependency-version: [prefer-lowest, prefer-stable] parallel: ['', '--parallel'] @@ -38,7 +38,7 @@ jobs: - name: Unit Tests run: test:parallel - if: ${{ false }} # 2.x-dev is under development + if: ${{ false }} # Parallel's not yet supported. - name: Integration Tests run: composer test:integration From 76d1a8ffedf79e69b7769746b3f48d52667fa0f5 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 10 Jan 2023 00:20:42 +0000 Subject: [PATCH 56/59] fix: ensures view are boot --- src/Console/Thanks.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php index 10583b41..cd149a0d 100644 --- a/src/Console/Thanks.php +++ b/src/Console/Thanks.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Console; +use Pest\Bootstrappers\BootView; use Pest\Support\View; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Input\ArrayInput; @@ -39,6 +40,9 @@ final class Thanks */ public function __invoke(): void { + $bootstrapper = new BootView($this->output); + $bootstrapper->boot(); + $wantsToSupport = (new SymfonyQuestionHelper())->ask( new ArrayInput([]), $this->output, From feedeab7e33331da847a88334bb408ba6dfc003b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 10 Jan 2023 00:20:49 +0000 Subject: [PATCH 57/59] chore: ignores phpstan error --- src/Support/ExceptionTrace.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index faa5080b..0e175677 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -44,7 +44,7 @@ final class ExceptionTrace */ public static function removePestReferences(Throwable $t): void { - if (! property_exists($t, 'serializableTrace')) { + if (! property_exists($t, 'serializableTrace')) { // @phpstan-ignore-line return; } From 51bcf6a2be5149e4448209683ba64cd40fd2633a Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 10 Jan 2023 00:57:23 +0000 Subject: [PATCH 58/59] chore: fixes tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8733b0f..d45b90ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] # "windows-latest" is waiting for https://github.com/pestphp/pest/issues/638 php: ['8.1', '8.2'] dependency-version: [prefer-lowest, prefer-stable] parallel: ['', '--parallel'] From e228d565af29e270485f4c6040fda7fa0c1a740e Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 10 Jan 2023 20:21:33 +0000 Subject: [PATCH 59/59] feat: allows to chain `todo` --- src/Functions.php | 11 ++++++----- src/PendingCalls/TestCall.php | 10 ++++++++++ src/Support/ExceptionTrace.php | 2 +- src/Support/HigherOrderTapProxy.php | 4 ++++ tests/.snapshots/success.txt | 4 +++- tests/Features/Todo.php | 6 ++++++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/Functions.php b/src/Functions.php index 8d033694..ba9856c0 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -87,7 +87,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 HigherOrderTapProxy|TestCall */ function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall { @@ -130,10 +130,11 @@ if (! function_exists('todo')) { */ function todo(string $description): TestCall { - /* @phpstan-ignore-next-line */ - return test($description, fn () => self::markTestSkipped( - '__TODO__', - )); + $test = test($description); + + assert($test instanceof TestCall); + + return $test->skip('__TODO__'); } } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index d24c593b..3cd8cee6 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -170,6 +170,16 @@ final class TestCall return $this; } + /** + * Sets the test as "todo". + */ + public function todo(): self + { + $this->skip('__TODO__'); + + return $this; + } + /** * Sets the covered classes or methods. */ diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 0e175677..faa5080b 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -44,7 +44,7 @@ final class ExceptionTrace */ public static function removePestReferences(Throwable $t): void { - if (! property_exists($t, 'serializableTrace')) { // @phpstan-ignore-line + if (! property_exists($t, 'serializableTrace')) { return; } diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php index 1b5f12cf..6a4f8ecd 100644 --- a/src/Support/HigherOrderTapProxy.php +++ b/src/Support/HigherOrderTapProxy.php @@ -10,6 +10,10 @@ use Throwable; /** * @internal + * + * @template TProxy + * + * @mixin TProxy */ final class HigherOrderTapProxy { diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 789c001e..e91aae32 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -734,6 +734,8 @@ PASS Tests\Features\Todo ↓ something todo later + ↓ something todo later chained + ↓ something todo later chained and with function body ✓ it does something within a file with a todo PASS Tests\Fixtures\DirectoryWithTests\ExampleTest @@ -896,4 +898,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 17 skipped, 624 passed (1511 assertions) \ No newline at end of file + Tests: 4 incomplete, 4 todos, 17 skipped, 624 passed (1511 assertions) \ No newline at end of file diff --git a/tests/Features/Todo.php b/tests/Features/Todo.php index f9cb4cc8..6086b77c 100644 --- a/tests/Features/Todo.php +++ b/tests/Features/Todo.php @@ -2,6 +2,12 @@ todo('something todo later'); +test('something todo later chained')->todo(); + +test('something todo later chained and with function body', function () { + expect(true)->toBeFalse(); +})->todo(); + it('does something within a file with a todo', function () { expect(true)->toBeTrue(); });