diff --git a/CHANGELOG.md b/CHANGELOG.md index fc77caa..e1f3bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.7.0 — 2026-07-25 + +- Ship an AI agent skill (`resources/skills/rasuvaeff-property-testing/SKILL.md` + + `extra.skills` in composer.json): projects using the `llm/skills` Composer + plugin get the skill synced into `.agents/skills/` automatically on install. + ## 2.6.0 — 2026-07-18 - Psalm generics across the public surface: `ArbitraryInterface`, diff --git a/README.md b/README.md index 7210a07..f4abcbc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ of random inputs per test, find the failing one, and shrink it to a minimal counterexample you can actually read. > Using an AI coding assistant? [llms.txt](llms.txt) contains a compact API reference you can share with the model. +> Projects using the [llm/skills](https://github.com/roxblnfk/skills) Composer plugin also get this package's agent skill synced into `.agents/skills/` automatically on install. Since 2.0 shrinking is **integrated**: `generate()` returns a [`Shrinkable`](src/Shrinkable.php) — the value plus a lazy tree of smaller diff --git a/README.ru.md b/README.ru.md index cfedd57..f45cea4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -16,6 +16,9 @@ Property-based тестирование для PHP 8.3+, реализованн > Используете AI-ассистента? [llms.txt](llms.txt) содержит компактный > API-справочник, которым можно поделиться с моделью. +> Проекты с Composer-плагином [llm/skills](https://github.com/roxblnfk/skills) +> дополнительно получают agent-скилл этого пакета в `.agents/skills/` +> автоматически при установке. Начиная с 2.0 shrinking **интегрированный**: `generate()` возвращает [`Shrinkable`](src/Shrinkable.php) — значение плюс ленивое дерево меньших diff --git a/composer.json b/composer.json index d29288d..04a362e 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,11 @@ }, "sort-packages": true }, + "extra": { + "skills": { + "source": "resources/skills" + } + }, "scripts": { "bc-check": "roave-backward-compatibility-check", "bench": "testo --suite=Benchmarks -vv", diff --git a/resources/skills/rasuvaeff-property-testing/SKILL.md b/resources/skills/rasuvaeff-property-testing/SKILL.md new file mode 100644 index 0000000..e0695a7 --- /dev/null +++ b/resources/skills/rasuvaeff-property-testing/SKILL.md @@ -0,0 +1,86 @@ +--- +name: rasuvaeff-property-testing +description: >- + Write property-based tests for the Testo framework with + rasuvaeff/property-testing — the #[Property] attribute, the Gen generator + facade, Assume::that(), Gen::draw(), Shrinkable, StateMachine::check(). + Use when writing, reviewing or debugging property tests (random inputs, + shrinking, counterexamples, GaveUpException/GenerationExhausted failures) + in a project that has this package installed. +--- + +# rasuvaeff/property-testing + +Property-based testing plugin for Testo (PHP 8.3+): generate random inputs, +find a falsifying case, shrink it to a minimal counterexample. +Namespace `Rasuvaeff\PropertyTesting\`. + +## Safety rules — verify these on every change + +1. **Generators live in a `public static` method, never inline.** Attribute + arguments are constant expressions, so arbitraries CANNOT be passed to + `#[Property]`. Declare `public static function Generators(): array` + returning `['paramName' => Gen::...]`. STRICTLY `public static` (`public` + only if the body needs `$this`): the only call site is reflection, so + Rector's `RemoveUnusedPrivateMethodRector` silently deletes private ones. + Public is safe and never becomes a test (non-void return). + +2. **The attribute is `#[Property(runs: N)]` — `#[Given]` does NOT exist.** + Do not invent PHPUnit-style or other frameworks' attributes. + +3. **Construct dependent values, do not discard.** Build them with + `Gen::flatMap()` / `Gen::draw()` (e.g. `$max = $n + $slack`), not by + rejecting via `Assume::that(...)` — broad rejection burns attempts into + discards and fails with `GaveUpException` at `maxDiscards` (default + `runs * 10`). `Gen::filter()` throws `GenerationExhausted` after 100 tries. + + ```php + Gen::flatMap(Gen::nonEmptyArrayOf(Gen::int()), static fn(array $items) => + Gen::tuple(Gen::constant($items), Gen::intBetween(0, count($items) - 1))); + ``` + +4. **`Gen::draw()` works only inside a property body** (throws a + `RuntimeException` elsewhere). Counterexamples report draws as `draw#N`; + after shrinking, assert what the body requires — a replayed draw is not + re-validated against a changed range. + +5. **Generated values are MT19937 pseudo-random — never use them for + cryptography.** Pin `seed:` (or `PROPERTY_SEED`) only to reproduce a + reported failure; leave it unset otherwise. + +## Canonical usage + +```php +use Rasuvaeff\PropertyTesting\Gen; +use Rasuvaeff\PropertyTesting\Property; +use Testo\Assert; + +#[Property(runs: 200)] +public function sortedThenSortedIsIdempotent(array $xs): void +{ + $once = $this->sort($xs); + + Assert::same($this->sort($once), $once); +} + +/** @return array */ +public static function sortedThenSortedIsIdempotentGenerators(): array +{ + return ['xs' => Gen::arrayOf(Gen::intBetween(-100, 100))]; +} +``` + +| Need | Use | +|---|---| +| One dependent value | `Gen::flatMap()` | +| Several dependent / mid-body values | `Gen::draw()` inside the body | +| Rare precondition, construction impossible | `Assume::that()` (last resort) | +| Sequences of ops vs a model | `Gen::commands()` + `StateMachine::check()` | +| Prove the property is not vacuous | `Classify::label()` / `Classify::cover()` | + +## Full API + +The complete reference — every `Gen` combinator, `#[Property]` parameter +(`seed`, `examples`, `timeoutMs`, `budgetMs`, ...), `Shrinkable`, stateful +testing — ships with the package: read +`vendor/rasuvaeff/property-testing/llms.txt` before guessing a method name.