diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f76f19..c43f9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # Changelog -## Unreleased - +## 1.1.0 — 2026-10-02 + +- **Added:** generators written on the closure's parameters with core's + `#[Generate]` (#61). Without `auto()`, the `forAll()` map and the + attributes together have to cover every parameter — the map may be empty; + under `auto()` the attributes sit between the map and the `@param` type. + Requires `rasuvaeff/property-testing-core` `^1.2`. +- **Changed:** without `auto()`, a parameter neither the map nor `#[Generate]` + covers is refused naming the function and the parameter, instead of the + engine's `No generator for parameter "x"`. - **Docs:** the READMEs and `llms.txt` now state what follows from driving the engine through a single PHPUnit test method: `setUp()`/`tearDown()` and `#[Before]`/`#[After]` run once per property rather than per run, test-double diff --git a/README.md b/README.md index f61578f..26ac399 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,38 @@ Rules worth knowing — verbatim the Testo adapter's (`#[Property(auto: true)]`) environment dials the suite, while `auto()` changes what one property's arguments mean. +### Generators on the parameter (`#[Generate]`) + +A parameter can carry its generator itself — core's `#[Generate]` +(`rasuvaeff/property-testing-core` 1.2): + +```php +use Rasuvaeff\PropertyTesting\Arbitrary\IntArbitrary; +use Rasuvaeff\PropertyTesting\Generate; + +$this->forAll()->check(static function ( + #[Generate(new IntArbitrary(0, 10_000))] + int $base, + #[Generate([Generators::class, 'attempts'])] + int $attempt, +): void { + // ... +}); +``` + +- It works **without `auto`**: the `forAll()` map can be empty, the attributes + alone are enough — as long as they cover every parameter. The map and the + attributes may also split the parameters between them. A parameter that + neither covers is refused by name; nothing is derived from the types + unless `auto()` says so. +- Under `auto()` it is read after the `forAll()` map and before the `@param` + type and the native type. +- The argument is an arbitrary built with `new`, or a reference to a static + factory returning one (`'method'`, `'Class::method'`, + `[Class::class, 'method']`, an invokable) — the way to use `Gen::map()`, + `Gen::email()`, `Gen::regex()` and other generators built with a closure. + What `new` can express is listed in core's README. + ### Naming a property (`id()`) `id()` names the property: diff --git a/README.ru.md b/README.ru.md index 8c0eccd..1eb3ba8 100644 --- a/README.ru.md +++ b/README.ru.md @@ -185,6 +185,38 @@ $this->forAll(['multiplier' => Gen::floatBetween(1.0, 4.0)]) - Переменной окружения `PROPERTY_AUTO` нет намеренно: окружение крутит сьют, а `auto()` меняет смысл аргументов конкретной property. +### Генераторы на параметре (`#[Generate]`) + +Параметр может нести генератор сам — атрибут `#[Generate]` из ядра +(`rasuvaeff/property-testing-core` 1.2): + +```php +use Rasuvaeff\PropertyTesting\Arbitrary\IntArbitrary; +use Rasuvaeff\PropertyTesting\Generate; + +$this->forAll()->check(static function ( + #[Generate(new IntArbitrary(0, 10_000))] + int $base, + #[Generate([Generators::class, 'attempts'])] + int $attempt, +): void { + // ... +}); +``` + +- Работает **без `auto`**: карта `forAll()` может быть пустой, достаточно + одних атрибутов — при условии, что они покрывают каждый параметр. Карта и + атрибуты могут и поделить параметры между собой. Параметр, не покрытый ни тем, ни другим, — + отказ с его именем; из типов ничего не выводится, пока не сказано + `auto()`. +- Под `auto()` атрибут читается после карты `forAll()` и до `@param`-типа и + нативного типа. +- Аргумент — arbitrary, построенный через `new`, или ссылка на статическую + фабрику, которая его возвращает (`'method'`, `'Class::method'`, + `[Class::class, 'method']`, invokable), — так используются `Gen::map()`, + `Gen::email()`, `Gen::regex()` и другие генераторы на замыканиях. Что + выражается через `new` — в README ядра. + ### Имя property (`id()`) `id()` именует property: diff --git a/composer.json b/composer.json index 075eb66..a62d8ed 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": "8.3 - 8.5", "phpunit/phpunit": "^11.5 || ^12.0 || ^13.0", - "rasuvaeff/property-testing-core": "^1.0" + "rasuvaeff/property-testing-core": "^1.2" }, "require-dev": { "ergebnis/composer-normalize": "^2.51", diff --git a/llms.txt b/llms.txt index a03d1c7..c50fa22 100644 --- a/llms.txt +++ b/llms.txt @@ -117,6 +117,11 @@ fully-typed closure needs no map. A map key that is not a parameter is an error, with auto as without. Strictly opt-in, never the default (bare int/float derive their full native domain). No PROPERTY_AUTO env variable, deliberately. +`#[Generate(...)]` on a closure parameter (core 1.2) holds its generator: an arbitrary built +with `new`, or a static factory reference ('Class::method', [Class::class, 'method'], +invokable). Works WITHOUT auto(): the forAll() map plus the attributes must cover every +parameter, an uncovered one throws by name. Under auto(): map > #[Generate] > @param > native. + ### Example ```php diff --git a/src/PhpUnit/PropertyCheck.php b/src/PhpUnit/PropertyCheck.php index 89a03ef..9ebe5b2 100644 --- a/src/PhpUnit/PropertyCheck.php +++ b/src/PhpUnit/PropertyCheck.php @@ -590,9 +590,9 @@ public function check(\Closure $property): void } /** - * The generators the property runs with: the {@see forAll()} map, or — - * under {@see auto()} — that map as overrides with every uncovered - * parameter derived from the closure's signature. The derivation errors are + * The generators the property runs with: the {@see forAll()} map plus any + * `#[Generate]` on the closure's parameters, and — under {@see auto()} — + * every parameter neither covers derived from the closure's signature. The derivation errors are * {@see Gen::forParameters()}'s own, naming the function and the parameter. * * The map is checked here, before the engine sees it, because the engine @@ -636,11 +636,10 @@ private function resolveGenerators(\ReflectionFunction $property, array $paramet $generators[$name] = $generator; } - if (!$this->auto) { - return $generators; - } - - return Gen::forParameters($property, $generators); + // Without auto nothing is derived from the declared types, but a + // parameter may still carry its generator as #[Generate]: the map and + // the attributes together have to cover every parameter. + return Gen::forParameters($property, $generators, derive: $this->auto); } /** diff --git a/tests/PropertyCheckTest.php b/tests/PropertyCheckTest.php index 3121f2c..5689123 100644 --- a/tests/PropertyCheckTest.php +++ b/tests/PropertyCheckTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\IncompleteTestError; use PHPUnit\Framework\SkippedWithMessageException; use PHPUnit\Framework\TestCase; +use Rasuvaeff\PropertyTesting\Arbitrary\IntArbitrary; use Rasuvaeff\PropertyTesting\Assume; use Rasuvaeff\PropertyTesting\Classify; use Rasuvaeff\PropertyTesting\CoverageViolationException; @@ -21,6 +22,7 @@ use Rasuvaeff\PropertyTesting\ExampleViolationException; use Rasuvaeff\PropertyTesting\GaveUpException; use Rasuvaeff\PropertyTesting\Gen; +use Rasuvaeff\PropertyTesting\Generate; use Rasuvaeff\PropertyTesting\PhpUnit\PropertyCheck; use Rasuvaeff\PropertyTesting\PhpUnit\PropertyTesting; use Rasuvaeff\PropertyTesting\PhpUnit\Tests\Support\Env; @@ -1144,7 +1146,7 @@ private function pathOfAFailure(): string public function testWithoutAutoTheForAllMapIsUsedVerbatim(): void { // auto stays opt-in: no derivation happens unless auto() was called, - // so a missing generator surfaces exactly as before. + // so a parameter the map does not cover is refused, not derived. try { $this->forAll(['x' => Gen::constant(7)]) ->runs(1) @@ -1153,12 +1155,43 @@ public function testWithoutAutoTheForAllMapIsUsedVerbatim(): void self::fail('Expected a failure about the missing generator'); } catch (\InvalidArgumentException $failure) { - // The engine's own refusal for a map that does not cover a - // parameter — not a derivation error, which auto would produce. - self::assertSame('No generator for parameter "missing"', $failure->getMessage()); + self::assertStringEndsWith( + 'parameter $missing has no generator; pass an override or #[Generate]', + $failure->getMessage(), + ); } } + public function testWithoutAutoAGenerateAttributeCoversItsParameter(): void + { + $seen = []; + + $this->forAll(['x' => Gen::constant(7)]) + ->runs(20) + ->seed(1) + ->check(static function (int $x, #[Generate(new IntArbitrary(3, 3))] int $y) use (&$seen): void { + $seen[] = [$x, $y]; + }); + + self::assertNotSame([], $seen); + self::assertSame([[7, 3]], array_values(array_unique($seen, SORT_REGULAR))); + } + + public function testAutoReadsAGenerateAttributeBeforeTheType(): void + { + $seen = []; + + $this->forAll() + ->auto() + ->runs(20) + ->seed(1) + ->check(static function (#[Generate(new IntArbitrary(5, 5))] int $n) use (&$seen): void { + $seen[] = $n; + }); + + self::assertSame([5], array_values(array_unique($seen))); + } + private function falsifiedOriginal(): mixed { try {