diff --git a/composer.json b/composer.json index 06433b04..e7022bf8 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-mutate": "^3.0.0", - "phpunit/phpunit": "^11.3.2" + "phpunit/phpunit": "^11.3.3" }, "conflict": { "sebastian/exporter": "<6.0.0", diff --git a/src/ArchPresets/AbstractPreset.php b/src/ArchPresets/AbstractPreset.php index eee263f1..3b612812 100644 --- a/src/ArchPresets/AbstractPreset.php +++ b/src/ArchPresets/AbstractPreset.php @@ -15,7 +15,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line /** * The expectations. * - * @var array + * @var array|ArchExpectation> */ protected array $expectations = []; @@ -24,7 +24,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line * * @param array $userNamespaces */ - final public function __construct( + public function __construct( private readonly array $userNamespaces, ) { // @@ -45,7 +45,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line final public function ignoring(array|string $targetsOrDependencies): void { $this->expectations = array_map( - fn (ArchExpectation $expectation): \Pest\Arch\Contracts\ArchExpectation => $expectation->ignoring($targetsOrDependencies), + fn (ArchExpectation|Expectation $expectation): Expectation|ArchExpectation => $expectation instanceof ArchExpectation ? $expectation->ignoring($targetsOrDependencies) : $expectation, $this->expectations, ); } diff --git a/src/ArchPresets/Custom.php b/src/ArchPresets/Custom.php new file mode 100644 index 00000000..6c811afe --- /dev/null +++ b/src/ArchPresets/Custom.php @@ -0,0 +1,45 @@ + $userNamespaces + * @param Closure(array): array|ArchExpectation> $execute + */ + public function __construct( + private readonly array $userNamespaces, + private readonly string $name, + private readonly Closure $execute, + ) { + parent::__construct($userNamespaces); + } + + /** + * Returns the name of the preset. + */ + public function name(): string + { + return $this->name; + } + + /** + * Executes the arch preset. + */ + public function execute(): void + { + $this->expectations = ($this->execute)($this->userNamespaces); + } +} diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 27e3a268..9bd0e007 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -6,6 +6,7 @@ namespace Pest\Concerns; use Closure; use Pest\Exceptions\DatasetArgumentsMismatch; +use Pest\Preset; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\Support\Reflection; @@ -410,6 +411,14 @@ trait Testable return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); } + /** + * Uses the given preset on the test. + */ + public function preset(): Preset + { + return new Preset; + } + #[PostCondition] protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void { diff --git a/src/Configuration.php b/src/Configuration.php index 0872c833..555a130e 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -86,6 +86,14 @@ final readonly class Configuration return new Configuration\Theme; } + /** + * Gets the presets configuration. + */ + public function presets(): Configuration\Presets + { + return new Configuration\Presets; + } + /** * Gets the project configuration. */ diff --git a/src/Configuration/Presets.php b/src/Configuration/Presets.php new file mode 100644 index 00000000..ad8aec68 --- /dev/null +++ b/src/Configuration/Presets.php @@ -0,0 +1,19 @@ +attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes); } } - - /** - * Uses the given preset on the test. - */ - public function preset(): Preset - { - return new Preset($this); - } } diff --git a/src/Preset.php b/src/Preset.php index f17068e7..549b3845 100644 --- a/src/Preset.php +++ b/src/Preset.php @@ -4,13 +4,16 @@ declare(strict_types=1); namespace Pest; +use Closure; use Pest\Arch\Support\Composer; use Pest\ArchPresets\AbstractPreset; +use Pest\ArchPresets\Custom; use Pest\ArchPresets\Laravel; use Pest\ArchPresets\Php; use Pest\ArchPresets\Relaxed; use Pest\ArchPresets\Security; use Pest\ArchPresets\Strict; +use Pest\Exceptions\InvalidArgumentException; use Pest\PendingCalls\TestCall; use stdClass; @@ -26,10 +29,17 @@ final class Preset */ private static ?array $baseNamespaces = null; + /** + * The custom presets. + * + * @var array + */ + private static array $customPresets = []; + /** * Creates a new preset instance. */ - public function __construct(private readonly TestCall $testCall) + public function __construct() { // } @@ -74,6 +84,41 @@ final class Preset return $this->executePreset(new Relaxed($this->baseNamespaces())); } + /** + * Uses the Pest custom preset and returns the test call instance. + * + * @internal + */ + public static function custom(string $name, Closure $execute): void + { + if (preg_match('/^[a-zA-Z]+$/', $name) === false) { + throw new InvalidArgumentException('The preset name must only contain words from a-z or A-Z.'); + } + + self::$customPresets[$name] = $execute; + } + + /** + * Dynamically handle calls to the class. + * + * @param array $arguments + * + * @throws InvalidArgumentException + */ + public function __call(string $name, array $arguments): AbstractPreset + { + if (! array_key_exists($name, self::$customPresets)) { + $availablePresets = [ + ...['php', 'laravel', 'strict', 'security', 'relaxed'], + ...array_keys(self::$customPresets), + ]; + + throw new InvalidArgumentException(sprintf('The preset [%s] does not exist. The available presets are [%s].', $name, implode(', ', $availablePresets))); + } + + return $this->executePreset(new Custom($this->baseNamespaces(), $name, self::$customPresets[$name])); + } + /** * Executes the given preset. * @@ -84,19 +129,13 @@ final class Preset */ private function executePreset(AbstractPreset $preset): AbstractPreset { - if ((fn (): ?string => $this->description)->call($this->testCall) === null) { - $description = strtolower((new \ReflectionClass($preset))->getShortName()); - - (fn (): string => $this->description = sprintf('arch "%s" preset', $description))->call($this->testCall); - } - $this->baseNamespaces(); $preset->execute(); - $this->testCall->testCaseMethod->closure = (function () use ($preset): void { - $preset->flush(); - })->bindTo(new stdClass); + // $this->testCall->testCaseMethod->closure = (function () use ($preset): void { + // $preset->flush(); + // })->bindTo(new stdClass); return $preset; } diff --git a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap index b22c27f4..d26aa818 100644 --- a/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap +++ b/tests/.pest/snapshots/Visual/Help/visual_snapshot_of_help_command_output.snap @@ -66,6 +66,7 @@ --fail-on-warning Signal failure using shell exit code when a warning was triggered --fail-on-risky Signal failure using shell exit code when a test was considered risky --fail-on-deprecation Signal failure using shell exit code when a deprecation was triggered + --fail-on-phpunit-deprecation Signal failure using shell exit code when a PHPUnit deprecation was triggered --fail-on-notice Signal failure using shell exit code when a notice was triggered --fail-on-skipped Signal failure using shell exit code when a test was skipped --fail-on-incomplete Signal failure using shell exit code when a test was marked incomplete @@ -85,6 +86,7 @@ --display-incomplete .................. Display details for incomplete tests --display-skipped ........................ Display details for skipped tests --display-deprecations . Display details for deprecations triggered by tests + --display-phpunit-deprecations .... Display details for PHPUnit deprecations --display-errors ............. Display details for errors triggered by tests --display-notices ........... Display details for notices triggered by tests --display-warnings ......... Display details for warnings triggered by tests diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 34907b39..2020274b 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1,8 +1,8 @@ PASS Tests\Arch - ✓ arch "php" preset - ✓ arch "strict" preset - ✓ arch "security" preset + ✓ preset → php → ignoring ['Pest\Expectation', 'debug_backtrace', 'var_export', …] + ✓ preset → strict → ignoring ['usleep'] + ✓ preset → security → ignoring ['eval', 'str_shuffle', 'exec', …] ✓ globals ✓ dependencies ✓ contracts @@ -1423,6 +1423,10 @@ PASS Tests\Unit\Plugins\Retry ✓ it orders by defects and stop on defects if when --retry is used + PASS Tests\Unit\Preset + ✓ preset invalid name + ✓ preset → myFramework + PASS Tests\Unit\Support\Backtrace ✓ it gets file name from called file @@ -1574,4 +1578,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1089 passed (2621 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1091 passed (2624 assertions) \ No newline at end of file diff --git a/tests/Unit/Preset.php b/tests/Unit/Preset.php new file mode 100644 index 00000000..bdf461a7 --- /dev/null +++ b/tests/Unit/Preset.php @@ -0,0 +1,13 @@ +presets()->custom('myFramework', function (array $userNamespaces) { + return [ + expect($userNamespaces)->toBe(['Pest']), + ]; +}); + +test('preset invalid name', function () { + $this->preset()->myAnotherFramework(); +})->throws(InvalidArgumentException::class, 'The preset [myAnotherFramework] does not exist. The available presets are [php, laravel, strict, security, relaxed, myFramework].'); + +arch()->preset()->myFramework(); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index b30378b6..3ca89d4e 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -16,7 +16,7 @@ $run = function () { test('parallel', function () use ($run) { expect($run('--exclude-group=integration')) - ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1079 passed (2597 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1081 passed (2600 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows();