diff --git a/.gitattributes b/.gitattributes index 3654b689..c6e004ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,4 @@ phpstan.neon export-ignore CHANGELOG.md export-ignore CONTRIBUTING.md export-ignore README.md export-ignore + 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/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a855c50..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) 2.x-dev is under development + 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'] @@ -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 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 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) diff --git a/composer.json b/composer.json index 31b8bdb7..557eebcf 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" }, @@ -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" ] @@ -50,7 +42,8 @@ ] }, "require-dev": { - "pestphp/pest-dev-tools": "^2.0.0", + "pestphp/pest-dev-tools": "^2.1.0", + "pestphp/pest-plugin-arch": "^2.0.0", "symfony/process": "^6.2.0" }, "minimum-stability": "dev", @@ -66,14 +59,18 @@ "bin/pest" ], "scripts": { + "refacto": "rector", "lint": "pint", + "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", + "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", "test": [ + "@test:refacto", "@test:lint", "@test:types", "@test:unit", 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/phpstan.neon b/phpstan.neon index d8b81f41..e74b4290 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,7 +16,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/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 765d0843..44235b80 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -4,16 +4,19 @@ declare(strict_types=1); namespace Pest\Bootstrappers; +use Pest\Contracts\Bootstrapper; +use Pest\Support\DatasetInfo; use Pest\Support\Str; use function Pest\testDirectory; use Pest\TestSuite; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator; /** * @internal */ -final class BootFiles +final class BootFiles implements Bootstrapper { /** * The Pest convention. @@ -21,8 +24,6 @@ final class BootFiles * @var array */ private const STRUCTURE = [ - 'Datasets', - 'Datasets.php', 'Expectations', 'Expectations.php', 'Helpers', @@ -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(); @@ -56,6 +57,8 @@ final class BootFiles $this->load($filename); } } + + $this->bootDatasets($testsPath); } /** @@ -73,4 +76,15 @@ final class BootFiles include_once $filename; } + + private function bootDatasets(string $testsPath): void + { + $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php'); + + foreach ($files as $file) { + if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) { + $this->load($file); + } + } + } } 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/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 815c7821..ab31e337 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. @@ -22,13 +23,14 @@ final class BootSubscribers Subscribers\EnsureConfigurationIsValid::class, Subscribers\EnsureConfigurationDefaults::class, Subscribers\EnsureRetryRepositoryExists::class, - Subscribers\EnsureFailedTestsAreStoredForRetry::class, + Subscribers\EnsureErroredTestsAreRetryable::class, + Subscribers\EnsureFailedTestsAreRetryable::class, ]; /** * 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/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/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/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, 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 @@ + $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/Exceptions/InvalidExpectationValue.php b/src/Exceptions/InvalidExpectationValue.php index 68a15c64..7a223f8c 100644 --- a/src/Exceptions/InvalidExpectationValue.php +++ b/src/Exceptions/InvalidExpectationValue.php @@ -12,11 +12,9 @@ use InvalidArgumentException; final class InvalidExpectationValue extends InvalidArgumentException { /** - * @return never - * * @throws self */ - public static function expected(string $type): void + public static function expected(string $type): never { throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type)); } diff --git a/src/Expectation.php b/src/Expectation.php index 59d5f573..4d5e001c 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,10 +6,18 @@ namespace Pest; use BadMethodCallException; use Closure; +use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\ToBeUsedOn; +use Pest\Arch\Expectations\ToBeUsedOnNothing; +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; use Pest\Exceptions\ExpectationNotFound; +use Pest\Exceptions\InvalidExpectation; use Pest\Exceptions\InvalidExpectationValue; use Pest\Expectations\EachExpectation; use Pest\Expectations\HigherOrderExpectation; @@ -24,7 +32,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 @@ -70,10 +78,12 @@ final class Expectation InvalidExpectationValue::expected('string'); } - /** @var array|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); } /** @@ -348,4 +358,65 @@ final class Expectation { return new Any(); } + + /** + * Asserts that the given expectation target use the given dependencies. + * + * @param array|string $targets + */ + public function toUse(array|string $targets): ArchExpectation + { + return ToUse::make($this, $targets); + } + + /** + * Asserts that the given expectation target "only" use on the given dependencies. + * + * @param array|string $targets + */ + public function toOnlyUse(array|string $targets): ArchExpectation + { + return ToOnlyUse::make($this, $targets); + } + + /** + * Asserts that the given expectation target does not use any dependencies. + */ + public function toUseNothing(): ArchExpectation + { + return ToUseNothing::make($this); + } + + public function toBeUsed(): never + { + throw InvalidExpectation::fromMethods(['toBeUsed']); + } + + /** + * 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 + */ + public function toOnlyBeUsedOn(array|string $targets): ArchExpectation + { + 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 beed91a8..2c9e53a2 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -4,6 +4,13 @@ declare(strict_types=1); namespace Pest\Expectations; +use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\ToBeUsedOn; +use Pest\Arch\Expectations\ToBeUsedOnNothing; +use Pest\Arch\Expectations\ToUse; +use Pest\Arch\GroupArchExpectation; +use Pest\Arch\SingleArchExpectation; +use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; use PHPUnit\Framework\ExpectationFailedException; @@ -52,6 +59,64 @@ final class OppositeExpectation return $this->original; } + /** + * Asserts that the given expectation target does not use any of the given dependencies. + * + * @param array|string $targets + */ + public function toUse(array|string $targets): ArchExpectation + { + 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 toOnlyUse(array|string $targets): never + { + throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); + } + + public function toUseNothing(): never + { + throw InvalidExpectation::fromMethods(['not', 'toUseNothing']); + } + + /** + * 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. + * + * @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']); + } + + /** + * Asserts that the given expectation dependency is not used. + */ + public function toBeUsedOnNothing(): never + { + throw InvalidExpectation::fromMethods(['not', 'toBeUsedOnNothing']); + } + /** * Handle dynamic method calls into the original expectation. * @@ -89,11 +154,12 @@ final class OppositeExpectation /** * Creates a new expectation failed exception with a nice readable message. * - * @param array $arguments - * @return never + * @param array|string $arguments */ - private function throwExpectationFailedException(string $name, array $arguments = []): void + 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/Factories/Attributes/Attribute.php b/src/Factories/Attributes/Attribute.php index 70dae523..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 */ - 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 4591d9f1..9a48cd5c 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); + $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above); $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), )); @@ -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", diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 09cf51e2..1bca4dd9 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()); }; } @@ -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()->retryRepository; + + if (Retry::$retrying && ! $retryRepository->isEmpty() && ! $retryRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) { return ''; } @@ -147,11 +149,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 <<|TestCall */ - function test(string $description = null, Closure $closure = null) + function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall { if ($description === null && TestSuite::getInstance()->test !== null) { return new HigherOrderTapProxy(TestSuite::getInstance()->test); @@ -128,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/Kernel.php b/src/Kernel.php index 76ab91f1..aa8afa34 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; @@ -22,6 +23,7 @@ final class Kernel * @var array */ private const BOOTSTRAPPERS = [ + Bootstrappers\BootOverrides::class, Bootstrappers\BootExceptionHandler::class, Bootstrappers\BootSubscribers::class, Bootstrappers\BootFiles::class, @@ -49,7 +51,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/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/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 215b4de8..3cd8cee6 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 @@ -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/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/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) )); } diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php index f4c7c11d..3b00b7b4 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -5,7 +5,8 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; -use Pest\Exceptions\DatasetAlreadyExist; +use Generator; +use Pest\Exceptions\DatasetAlreadyExists; use Pest\Exceptions\DatasetDoesNotExist; use Pest\Exceptions\ShouldNotHappen; use SebastianBergmann\Exporter\Exporter; @@ -17,6 +18,8 @@ use Traversable; */ final class DatasetsRepository { + private const SEPARATOR = '>>'; + /** * Holds the datasets. * @@ -36,13 +39,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".self::SEPARATOR."$name"; + + if (array_key_exists("$datasetKey", self::$datasets)) { + throw new DatasetAlreadyExists($name, $scope); } - self::$datasets[$name] = $data; + self::$datasets[$datasetKey] = $data; } /** @@ -52,12 +57,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); } /** @@ -67,9 +72,9 @@ 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($description, $dataset); + $dataset = self::resolve($dataset, $filename); if ($dataset === null) { throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); @@ -84,14 +89,13 @@ 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)) { + if ($dataset === []) { return null; } - $dataset = self::processDatasets($dataset); + $dataset = self::processDatasets($dataset, $currentTestFile); $datasetCombinations = self::getDatasetsCombinations($dataset); @@ -136,7 +140,7 @@ final class DatasetsRepository * @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 +148,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])) { @@ -156,14 +156,16 @@ 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 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, ]; } @@ -174,6 +176,33 @@ final class DatasetsRepository return $processedDatasets; } + /** + * @return Closure|iterable + */ + private static function getScopedDataset(string $name, string $currentTestFile): Closure|iterable + { + $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile): bool { + [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key); + + if ($name !== $datasetName) { + return false; + } + + return str_starts_with($currentTestFile, $datasetScope); + }, ARRAY_FILTER_USE_KEY); + + $closestScopeDatasetKey = array_reduce( + array_keys($matchingDatasets), + fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB + ); + + if ($closestScopeDatasetKey === null) { + throw new DatasetDoesNotExist($name); + } + + return $matchingDatasets[$closestScopeDatasetKey]; + } + /** * @param array> $combinations * @return array>> diff --git a/src/Repositories/TempRepository.php b/src/Repositories/RetryRepository.php similarity index 64% rename from src/Repositories/TempRepository.php rename to src/Repositories/RetryRepository.php index 9ad1b601..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,11 +38,19 @@ 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([]); } + /** + * Checks if there is any element. + */ + public function isEmpty(): bool + { + return $this->all() === []; + } + /** * Checks if the given element exists. */ @@ -52,7 +66,9 @@ final class TempRepository */ private function all(): array { - $contents = file_get_contents(self::FOLDER.'/'.$this->filename.'.json'); + $path = self::TEMPORARY_FOLDER.'/'.$this->filename.'.json'; + + $contents = file_exists($path) ? file_get_contents($path) : '{}'; assert(is_string($contents)); @@ -70,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/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index f0bffa6a..ad553487 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -50,7 +50,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/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/Support/Backtrace.php b/src/Support/Backtrace.php index 33a44a2e..5077fb5a 100644 --- a/src/Support/Backtrace.php +++ b/src/Support/Backtrace.php @@ -44,6 +44,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/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..91ecfe1a 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 @@ -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); } /** @@ -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,23 @@ 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); - $output->writeln(sprintf( - ' %s %s %s %%', - $name, - str_repeat('.', max($dottedLineLength - $takenSize, 1)), - $percentage - )); + renderUsing($output); + render(<< + {$name} + + {$percentage}% + + HTML); } $totalCoverageAsString = $totalCoverage->asFloat() === 0.0 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 @@ +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/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/src/TestSuite.php b/src/TestSuite.php index 79402046..1bf64ace 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; @@ -44,9 +44,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. @@ -71,7 +71,7 @@ final class TestSuite $this->beforeEach = new BeforeEachRepository(); $this->afterEach = new AfterEachRepository(); $this->afterAll = new AfterAllRepository(); - $this->retryTempRepository = new TempRepository('retry'); + $this->retryRepository = new RetryRepository('retry'); $this->rootPath = (string) realpath($rootPath); } @@ -95,7 +95,7 @@ final class TestSuite return self::$instance; } - if (self::$instance === null) { + if (! self::$instance instanceof self) { throw new InvalidPestCommand(); } 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 diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index af0bfcef..e91aae32 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 @@ -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 @@ -121,6 +123,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 +671,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 @@ -690,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 @@ -713,29 +759,32 @@ ✓ 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 PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory ✓ closure was bound to CustomTestCase @@ -766,13 +815,17 @@ 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 ✓ 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 @@ -792,10 +845,35 @@ ✓ 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 + 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 @@ -806,9 +884,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 @@ -823,4 +898,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, 4 todos, 17 skipped, 624 passed (1511 assertions) \ No newline at end of file diff --git a/tests/Features/Datasets.php b/tests/Features/DatasetsTests.php similarity index 91% rename from tests/Features/Datasets.php rename to tests/Features/DatasetsTests.php index 073b1e68..aeb0ba82 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/DatasetsTests.php @@ -1,6 +1,6 @@ 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', [[]]); - $this->expectException(DatasetAlreadyExist::class); - $this->expectExceptionMessage('A dataset with the name `second` already exist.'); - DatasetsRepository::set('second', [[]]); + DatasetsRepository::set('second', [[]], __DIR__); + $this->expectException(DatasetAlreadyExists::class); + $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) { @@ -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([ @@ -323,3 +330,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/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(); }); 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"'); 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'."); diff --git a/tests/Unit/Support/DatasetInfo.php b/tests/Unit/Support/DatasetInfo.php new file mode 100644 index 00000000..dfe2b417 --- /dev/null +++ b/tests/Unit/Support/DatasetInfo.php @@ -0,0 +1,36 @@ +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'], +]); 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_'], +]); 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'); -});