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/composer.json b/composer.json index ace8c975..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" }, @@ -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", @@ -77,7 +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: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/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/Exceptions/InvalidExpectation.php b/src/Exceptions/InvalidExpectation.php new file mode 100644 index 00000000..544baac3 --- /dev/null +++ b/src/Exceptions/InvalidExpectation.php @@ -0,0 +1,26 @@ + $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 2da24e75..507e6915 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,6 +6,11 @@ namespace Pest; use BadMethodCallException; 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; use Pest\Concerns\Retrievable; @@ -24,7 +29,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 @@ -350,4 +355,42 @@ final class Expectation { return new Any(); } + + /** + * Asserts that the given expectation target depends on the given dependencies. + * + * @param array|string $dependencies + */ + public function toDependOn(array|string $dependencies): ArchExpectation + { + return ToDependOn::make($this, $dependencies); + } + + /** + * 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 + */ + 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 9a8fefbe..f73b3345 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -4,6 +4,11 @@ declare(strict_types=1); namespace Pest\Expectations; +use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\ToDependOn; +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 +57,46 @@ final class OppositeExpectation return $this->original; } + /** + * 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. + * + * @param array|string $targets + */ + 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 given expectation target does not have any dependencies. + */ + public function toDependOnNothing(): never + { + throw InvalidExpectation::fromMethods(['not', 'toDependOnNothing']); + } + /** * Handle dynamic method calls into the original expectation. * @@ -89,10 +134,12 @@ final class OppositeExpectation /** * Creates a new expectation failed exception with a nice readable message. * - * @param array $arguments + * @param array|string $arguments */ - private 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/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 9cb3a62c..3b00b7b4 100644 --- a/src/Repositories/DatasetsRepository.php +++ b/src/Repositories/DatasetsRepository.php @@ -162,11 +162,10 @@ final class DatasetsRepository $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, ]; } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 231c5ced..9ee929b7 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -781,6 +781,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 @@ -817,6 +820,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 @@ -892,4 +899,4 @@ PASS Tests\Visual\Version ✓ visual snapshot of help command output - Tests: 4 incomplete, 2 todos, 18 skipped, 621 passed (1531 assertions) \ No newline at end of file + 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'.");