It is my fervent wish that this file guide every AI coding agent working with code in this repository.
php artisan boost:add-skill nette/tester
Save this content to: AGENTS.md
# To My Agents!
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
## Documentation
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in `docs/`.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
This repo is the *implementation* of Tester (not a guide to using it). It has two
seams - the in-process framework (assertions, `TestCase`, `Environment`) and the
out-of-process runner (orchestration, isolation, annotation dispatch) - bridged by
process exit codes and env vars. Read `docs/internals.md` before editing either.
`docs/upgrading.md` records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
## Project Overview
Nette Tester is a lightweight, zero-dependency PHP testing framework. Each test
runs in a **fully isolated PHP process**; tests run in parallel by default (8
threads), are annotation-driven, and the framework is **self-hosting** (it tests
itself).
- **PHP Version**: 8.1–8.6 (composer upper bound)
- **Package**: `nette/tester`
## Essential Commands
```bash
# Run the suite with the in-repo runner (not vendor/bin)
src/tester tests -s
src/tester tests/Framework/Assert.same.phpt -s
# A single test is a plain runnable script
php tests/Framework/Assert.same.phpt
# Static analysis
composer phpstan
```
Useful runner flags: `-j <n>` (parallel jobs; `-j 1` = serial for debugging),
`-o <format>` (console/console-lines/github/tap/junit/log/none), `-C`/`-c`/`-d` (php.ini),
`--watch`, `--stop-on-fail`, `--timeout <s>`, `--slowest`, `--cider` (hidden, not in `--help`: live per-thread
panel; needs `-j` >= 2, otherwise falls back to console-lines).
## Conventions
- Every file starts with `declare(strict_types=1);`; **tabs**; Nette Coding Standard.
- Tests are `.phpt` (auto-discovered) or `*Test.php`; **`.phptx` is deliberately
NOT auto-discovered** (used in Tester's own suite for fixtures). There is **no
autoloading** - `tester.php` loads classes manually to avoid autoloader conflicts.
- Code must run on all supported PHP versions; run changed tests on the lowest one
too, as the platform branches (Windows < PHP 8.5, DOM < PHP 8.4) behave differently;
on Windows < PHP 8.5 the test time limit (`--timeout`, `@timeout`) is not enforced.
## Working in this repo
- **`AssertException` is deliberately catchable, and this is the single most
important invariant.** `Assert::notEqual` catches it to invert an `Expect`
comparison, `TestCase::runTest` catches it to decorate the failure message,
`TestCase::run` and the `test()` helper catch (and rethrow) so a `TestCase`
continues after one method fails and `test()` can print its × marker. **Never
turn `Assert::fail()` into a hard `exit`.**
- **`Assert::$counter` increments at the top of every assertion, before the check**
(it counts attempts). It backs the "No assertions were executed in this test"
guard, which `setup()` disables for `@outputMatch`/`@outputMatchFile` tests.
`fail()` throws during the test body but delegates to `$onFailure` once
`Environment` installs it in a shutdown handler.
- **`match()` uses PCRE only when the pattern is delimited with `~` or `#`**
(`isPcre`); otherwise it's a wildcard mask (`%a%`, `%A%`, `%d%`, `%h%`, ...). Keep
the two branches distinct - the delimiter is the only signal.
- **The runner reads annotations statically (regex over the source) before spawning
the process,** then dispatches by reflection to `initiate*`/`assess*` methods.
Consequences: file-level annotations **cannot** be replaced by PHP attributes, and
adding a new annotation usually means adding a method (`@timeout` is the exception,
read by `resolveTimeout`). Method-level `@dataProvider`/`@throws` are resolved in the
child by `TestCase`, which is why `#[DataProvider]`/`#[Throws]` attributes work there.
`@testCase` fans out in two phases: a listing job runs in parallel with other
tests and its completion callback adds one job per method; the method list is
cached in temp and invalidated by mtime of the class + parents + traits files, so
it must not depend on the environment. The test count thus grows during the run.
- **No test process outlives `Runner::run()`**: after `--stop-on-fail` running jobs
are finished, on an exception they are terminated; a job over its time limit is
terminated and reported as failed.
- **`FileMutator` (`bypassFinals`) registers itself as the `file://` stream
wrapper;** its `native()` must restore the original wrapper and re-register itself
in a `finally`, or every internal fs call recurses. Preserve that dance.
- **`Environment::print()` bypasses the ANSI-stripping `ob_start` buffer** (writes
straight to STDOUT), which is why status/fatal lines always appear.
- **The CLI help text IS the option grammar.** `CommandLine` parses option names,
arguments, enums and defaults out of the usage string (`(default: 8)` is where
`-j`'s default lives), so rewording `--help` can change parsing; hidden options
(`--debug`, `--cider`, ...) exist only in the `$defaults` array.
- Jobs are ordered by the previous run's result (cache in temp): new tests first,
then failed, passed, and skipped last. Jobs discovered during the run are appended
unsorted.
- User-facing how-to (writing tests, the assertion/annotation catalog, HttpAssert,
DomQuery, FileMock, coverage usage) is manual material and lives in the web docs.
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in docs/.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
This repo is the implementation of Tester (not a guide to using it). It has two
seams - the in-process framework (assertions, TestCase, Environment) and the
out-of-process runner (orchestration, isolation, annotation dispatch) - bridged by
process exit codes and env vars. Read docs/internals.md before editing either.
docs/upgrading.md records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
Nette Tester is a lightweight, zero-dependency PHP testing framework. Each test runs in a fully isolated PHP process; tests run in parallel by default (8 threads), are annotation-driven, and the framework is self-hosting (it tests itself).
nette/tester# Run the suite with the in-repo runner (not vendor/bin)
src/tester tests -s
src/tester tests/Framework/Assert.same.phpt -s
# A single test is a plain runnable script
php tests/Framework/Assert.same.phpt
# Static analysis
composer phpstan
Useful runner flags: -j <n> (parallel jobs; -j 1 = serial for debugging),
-o <format> (console/console-lines/github/tap/junit/log/none), -C/-c/-d (php.ini),
--watch, --stop-on-fail, --timeout <s>, --slowest, --cider (hidden, not in --help: live per-thread
panel; needs -j >= 2, otherwise falls back to console-lines).
declare(strict_types=1);; tabs; Nette Coding Standard..phpt (auto-discovered) or *Test.php; .phptx is deliberately
NOT auto-discovered (used in Tester's own suite for fixtures). There is no
autoloading - tester.php loads classes manually to avoid autoloader conflicts.--timeout, @timeout) is not enforced.AssertException is deliberately catchable, and this is the single most
important invariant. Assert::notEqual catches it to invert an Expect
comparison, TestCase::runTest catches it to decorate the failure message,
TestCase::run and the test() helper catch (and rethrow) so a TestCase
continues after one method fails and test() can print its × marker. Never
turn Assert::fail() into a hard exit.Assert::$counter increments at the top of every assertion, before the check
(it counts attempts). It backs the "No assertions were executed in this test"
guard, which setup() disables for @outputMatch/@outputMatchFile tests.
fail() throws during the test body but delegates to $onFailure once
Environment installs it in a shutdown handler.match() uses PCRE only when the pattern is delimited with ~ or #
(isPcre); otherwise it's a wildcard mask (%a%, %A%, %d%, %h%, ...). Keep
the two branches distinct - the delimiter is the only signal.initiate*/assess* methods.
Consequences: file-level annotations cannot be replaced by PHP attributes, and
adding a new annotation usually means adding a method (@timeout is the exception,
read by resolveTimeout). Method-level @dataProvider/@throws are resolved in the
child by TestCase, which is why #[DataProvider]/#[Throws] attributes work there.
@testCase fans out in two phases: a listing job runs in parallel with other
tests and its completion callback adds one job per method; the method list is
cached in temp and invalidated by mtime of the class + parents + traits files, so
it must not depend on the environment. The test count thus grows during the run.Runner::run(): after --stop-on-fail running jobs
are finished, on an exception they are terminated; a job over its time limit is
terminated and reported as failed.FileMutator (bypassFinals) registers itself as the file:// stream
wrapper; its native() must restore the original wrapper and re-register itself
in a finally, or every internal fs call recurses. Preserve that dance.Environment::print() bypasses the ANSI-stripping ob_start buffer (writes
straight to STDOUT), which is why status/fatal lines always appear.CommandLine parses option names,
arguments, enums and defaults out of the usage string ((default: 8) is where
-j's default lives), so rewording --help can change parsing; hidden options
(--debug, --cider, ...) exist only in the $defaults array.How can I help you explore Laravel packages today?