Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>`,
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) — значение плюс ленивое дерево меньших
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
},
"sort-packages": true
},
"extra": {
"skills": {
"source": "resources/skills"
}
},
"scripts": {
"bc-check": "roave-backward-compatibility-check",
"bench": "testo --suite=Benchmarks -vv",
Expand Down
86 changes: 86 additions & 0 deletions resources/skills/rasuvaeff-property-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 <testMethod>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<string, \Rasuvaeff\PropertyTesting\ArbitraryInterface> */
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.
Loading