feat: custom presets

This commit is contained in:
Nuno Maduro
2024-09-04 20:53:33 +01:00
parent a7ca7afe4e
commit dd20323ca7
12 changed files with 158 additions and 28 deletions

View File

@ -24,7 +24,7 @@
"pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.0.0", "pestphp/pest-plugin-arch": "^3.0.0",
"pestphp/pest-plugin-mutate": "^3.0.0", "pestphp/pest-plugin-mutate": "^3.0.0",
"phpunit/phpunit": "^11.3.2" "phpunit/phpunit": "^11.3.3"
}, },
"conflict": { "conflict": {
"sebastian/exporter": "<6.0.0", "sebastian/exporter": "<6.0.0",

View File

@ -15,7 +15,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line
/** /**
* The expectations. * The expectations.
* *
* @var array<int, ArchExpectation> * @var array<int, Expectation<mixed>|ArchExpectation>
*/ */
protected array $expectations = []; protected array $expectations = [];
@ -24,7 +24,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line
* *
* @param array<int, string> $userNamespaces * @param array<int, string> $userNamespaces
*/ */
final public function __construct( public function __construct(
private readonly array $userNamespaces, private readonly array $userNamespaces,
) { ) {
// //
@ -45,7 +45,7 @@ abstract class AbstractPreset // @pest-arch-ignore-line
final public function ignoring(array|string $targetsOrDependencies): void final public function ignoring(array|string $targetsOrDependencies): void
{ {
$this->expectations = array_map( $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, $this->expectations,
); );
} }

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Pest\ArchPresets;
use Closure;
use Pest\Arch\Contracts\ArchExpectation;
use Pest\Expectation;
/**
* @internal
*/
final class Custom extends AbstractPreset
{
/**
* Creates a new preset instance.
*
* @param array<int, string> $userNamespaces
* @param Closure(array<int, string>): array<Expectation<mixed>|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);
}
}

View File

@ -6,6 +6,7 @@ namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Exceptions\DatasetArgumentsMismatch; use Pest\Exceptions\DatasetArgumentsMismatch;
use Pest\Preset;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\Support\Reflection; 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)); 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] #[PostCondition]
protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void
{ {

View File

@ -86,6 +86,14 @@ final readonly class Configuration
return new Configuration\Theme; return new Configuration\Theme;
} }
/**
* Gets the presets configuration.
*/
public function presets(): Configuration\Presets
{
return new Configuration\Presets;
}
/** /**
* Gets the project configuration. * Gets the project configuration.
*/ */

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Configuration;
use Closure;
use Pest\Preset;
final class Presets
{
/**
* Creates a custom preset instance, and adds it to the list of presets.
*/
public function custom(string $name, Closure $execute): void
{
Preset::custom($name, $execute);
}
}

View File

@ -12,7 +12,6 @@ use Pest\Factories\TestCaseMethodFactory;
use Pest\Mutate\Decorators\TestCallDecorator as MutationTestCallDecorator; use Pest\Mutate\Decorators\TestCallDecorator as MutationTestCallDecorator;
use Pest\PendingCalls\Concerns\Describable; use Pest\PendingCalls\Concerns\Describable;
use Pest\Plugins\Only; use Pest\Plugins\Only;
use Pest\Preset;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Exporter; use Pest\Support\Exporter;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
@ -663,12 +662,4 @@ final class TestCall
$testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes); $testCase->attributes = array_merge($testCase->attributes, $this->testCaseFactoryAttributes);
} }
} }
/**
* Uses the given preset on the test.
*/
public function preset(): Preset
{
return new Preset($this);
}
} }

View File

@ -4,13 +4,16 @@ declare(strict_types=1);
namespace Pest; namespace Pest;
use Closure;
use Pest\Arch\Support\Composer; use Pest\Arch\Support\Composer;
use Pest\ArchPresets\AbstractPreset; use Pest\ArchPresets\AbstractPreset;
use Pest\ArchPresets\Custom;
use Pest\ArchPresets\Laravel; use Pest\ArchPresets\Laravel;
use Pest\ArchPresets\Php; use Pest\ArchPresets\Php;
use Pest\ArchPresets\Relaxed; use Pest\ArchPresets\Relaxed;
use Pest\ArchPresets\Security; use Pest\ArchPresets\Security;
use Pest\ArchPresets\Strict; use Pest\ArchPresets\Strict;
use Pest\Exceptions\InvalidArgumentException;
use Pest\PendingCalls\TestCall; use Pest\PendingCalls\TestCall;
use stdClass; use stdClass;
@ -26,10 +29,17 @@ final class Preset
*/ */
private static ?array $baseNamespaces = null; private static ?array $baseNamespaces = null;
/**
* The custom presets.
*
* @var array<string, Closure>
*/
private static array $customPresets = [];
/** /**
* Creates a new preset instance. * 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())); 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<int, mixed> $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. * Executes the given preset.
* *
@ -84,19 +129,13 @@ final class Preset
*/ */
private function executePreset(AbstractPreset $preset): AbstractPreset 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(); $this->baseNamespaces();
$preset->execute(); $preset->execute();
$this->testCall->testCaseMethod->closure = (function () use ($preset): void { // $this->testCall->testCaseMethod->closure = (function () use ($preset): void {
$preset->flush(); // $preset->flush();
})->bindTo(new stdClass); // })->bindTo(new stdClass);
return $preset; return $preset;
} }

View File

@ -66,6 +66,7 @@
--fail-on-warning Signal failure using shell exit code when a warning was triggered --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-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-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-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-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 --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-incomplete .................. Display details for incomplete tests
--display-skipped ........................ Display details for skipped tests --display-skipped ........................ Display details for skipped tests
--display-deprecations . Display details for deprecations triggered by 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-errors ............. Display details for errors triggered by tests
--display-notices ........... Display details for notices triggered by tests --display-notices ........... Display details for notices triggered by tests
--display-warnings ......... Display details for warnings triggered by tests --display-warnings ......... Display details for warnings triggered by tests

View File

@ -1,8 +1,8 @@
PASS Tests\Arch PASS Tests\Arch
arch "php" preset preset → php → ignoring ['Pest\Expectation', 'debug_backtrace', 'var_export', …]
arch "strict" preset preset → strict → ignoring ['usleep']
arch "security" preset preset → security → ignoring ['eval', 'str_shuffle', 'exec', …]
✓ globals ✓ globals
✓ dependencies ✓ dependencies
✓ contracts ✓ contracts
@ -1423,6 +1423,10 @@
PASS Tests\Unit\Plugins\Retry PASS Tests\Unit\Plugins\Retry
✓ it orders by defects and stop on defects if when --retry is used ✓ 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 PASS Tests\Unit\Support\Backtrace
✓ it gets file name from called file ✓ it gets file name from called file
@ -1574,4 +1578,4 @@
WARN Tests\Visual\Version WARN Tests\Visual\Version
- visual snapshot of help command output - visual snapshot of help command output
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1089 passed (2621 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1091 passed (2624 assertions)

13
tests/Unit/Preset.php Normal file
View File

@ -0,0 +1,13 @@
<?php
pest()->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();

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) { test('parallel', function () use ($run) {
expect($run('--exclude-group=integration')) 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'); ->toContain('Parallel: 3 processes');
})->skipOnWindows(); })->skipOnWindows();