Add Testo test framework support; improve PHPUnit runnable detection#127
Open
roxblnfk wants to merge 6 commits into
Open
Add Testo test framework support; improve PHPUnit runnable detection#127roxblnfk wants to merge 6 commits into
roxblnfk wants to merge 6 commits into
Conversation
- Require a base_clause on the name-convention (*Test) patterns: a PHPUnit test class always extends TestCase (directly or transitively), so a *Test class with no `extends` is not PHPUnit and is no longer tagged phpunit-test (it is most likely a test from another framework). - Disambiguate method-level #[Test] by the imported attribute (PHPUnit\Framework\Attributes\Test) via the file's `use`, and by a fully-qualified #[\PHPUnit\Framework\Attributes\Test], instead of the class name — so it no longer misfires on another framework's method-level #[Test]. This supersedes zed-extensions#121, which matched any attribute whose last name segment is "Test" (and would also tag e.g. #[\Testo\Test] or #[\App\Test] as PHPUnit).
Testo (https://php-testo.github.io) marks tests with the #[Test] attribute instead of naming/inheritance conventions. runnables.scm (tag `testo-test`): - class-level #[Test] -> the class plus every public void/never method (other return types are data providers and are skipped); - free functions annotated with #[Test]; - method-level #[Test], disambiguated from PHPUnit by `use Testo\Test` (PHP forbids two imports sharing an alias) or a fully-qualified #[\Testo\Test]; - `return new ApplicationConfig(...)` config files (tag `testo-config`). tasks.json: run a symbol/file/whole suite via `php vendor/bin/testo` (--filter/--path), and run a config via --config.
|
We require contributors to sign our Contributor License Agreement, and we don't have @roxblnfk on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'. |
Author
|
@cla-bot check |
|
The cla-bot has been summoned, and re-checked this pull request! |
This was referenced Jul 2, 2026
samdark
approved these changes
Jul 2, 2026
Per-attribute type runnables (gutter icon on the attribute, running only that kind via `--type=`), for methods and free functions alike: * #[Test] / #[\Testo\Test] -> --type=test * #[TestInline] -> --type=inline (repeatable) * #[Bench] -> --type=bench * #[TestRectorFixtures] (class-level) -> --type=rector-fixture Each test symbol also keeps a "run all kinds" icon on its name (no `--type`), and matching tasks were added to tasks.json. Match #[Test] locally instead of via program-rooted `use` correlation. Correlating a bare #[Test] with its `use Testo\Test` / `use PHPUnit\...\Test` import required a query rooted at `program`/`namespace` spanning the whole file; tree-sitter binds the `use` and the method independently, so it materializes one in-progress match state per (use-statement x method) pair (predicates only filter at completion, so the states pile up even when the pattern matches nothing). On real files this exhausts the query match limit and later runnables are dropped -- gutters vanish from some line downward, triggered by a second attribute_group or by many `use` lines. All patterns are now rooted at the declaration (method/function/class), so each match is O(1) states. Consequence: a bare #[Test] can no longer be attributed by syntax alone, so it is treated as Testo; PHPUnit keeps its naming-convention detection and the fully-qualified #[\PHPUnit\Framework\Attributes\Test].
For a bare method-level `#[Test]` (ambiguous between Testo and PHPUnit, with no local way to tell them apart), stop guessing and split the two frameworks by gutter position: the Testo run stays on the attribute row (the `--type=test` icon), and the method-name row now runs PHPUnit. The user picks a framework by where they click. Fully-qualified `#[\Testo\Test]` and free functions remain unambiguously Testo on the name. Also add a run-all gutter on the class name for a class whose `#[Test]` lives on the methods rather than on the class, so the whole class can be run at once. Rooted at the class (never at `program`), so it stays linear in the number of methods and does not reintroduce the match-state blow-up; it fires once per matching method but those collapse to one indicator by row.
Author
PHPUnit tests always extend `TestCase`, so a bare `#[Test]` in a class with no `extends` is not PHPUnit. Gate the method-name PHPUnit runnable on a `base_clause`: an inheriting class still gets the PHPUnit run on the method name, while a bare `#[Test]` in a non-inheriting class (a Testo test) keeps only its Testo attribute runnable and a clean method-name row. The rule is rooted at the class (like the naming-convention PHPUnit patterns), so it stays linear -- the base_clause is a single node, not a per-`use` multiplier, and does not reintroduce the match-state blow-up.
Author
|
Follow-up: only offer PHPUnit on a bare Refined the split from the previous comment. PHPUnit tests always extend
Same shape as the naming-convention PHPUnit patterns (rooted at the class), so it stays linear and doesn't bring back the match-limit blow-up. |
… class A public void/never method in a class-level #[Test] class is a test case, so tag it `testo-type-test` (the single "run this test" action, same as a method-level #[Test] attribute) instead of `testo-test`. The run/file/all menu now lives only on the class name, consistent with how the method-level attribute already behaves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Added: Testo support
Runnables and tasks for Testo, which marks tests with attributes rather than by naming convention:
#[Test]→ run button on the class and on each publicvoid/nevermethod (other return types are data providers, skipped);#[Test]is on the methods (not on the class) → run button on the class name too, so the whole class can be run at once;#[Test];#[Test]→--type=test,#[TestInline]→--type=inline,#[Bench]→--type=bench,#[TestRectorFixtures](class-level) →--type=rector-fixture;return new ApplicationConfig(...)).Tasks run
php vendor/bin/testofor a symbol / file / whole suite / config / kind.Changed: PHPUnit detection
*Testclass with noextendsis no longer treated as PHPUnit. PHPUnit tests always extendTestCase, so the name-based patterns now require abase_clause— this stops false buttons on non-PHPUnit*Testclasses (e.g. Testo tests).#[\PHPUnit\Framework\Attributes\Test]now gets a gutter button.#[Test]runs PHPUnit from the method-name row, but only when the class extends — PHPUnit tests always extendTestCase, so a bare#[Test]in a class with no parent is left to Testo (see the note below).Note: the ambiguous bare
#[Test]A bare
#[Test]is shared by Testo and PHPUnit, and the only exact way to tell them apart — the file'suseimport — requires aprogram-rooted query that exhausts tree-sitter's match limit and makes gutters vanish on real files (details in the comment below). So instead of correlating the import, the two frameworks are split by gutter position: the attribute row runs it as Testo, and the method-name row runs it as PHPUnit — the latter only when the classextendssomething (PHPUnit always inheritsTestCase). A bare#[Test]in a class with no parent stays Testo. Fully-qualified#[\Testo\Test]and free functions are unambiguously Testo.Tests
Runnable-query tests for this extension are proposed separately in #129. They cover the existing PHPUnit/Pest queries and document, as
#[ignore]d tests, the exact over-tagging cases this PR fixes (a*Testclass with no inheritance, and a bare#[Test]in such a class). If #129 is merged first, I'll add tests covering the Testo runnables introduced here and flip those documented-gap cases to passing.