Skip to content

Run Rector fixture cases and #[Group] from the gutter - #72

Merged
xepozz merged 8 commits into
j-plugins:mainfrom
roxblnfk:feat/rector-fixtures-and-group-gutter
Aug 2, 2026
Merged

Run Rector fixture cases and #[Group] from the gutter#72
xepozz merged 8 commits into
j-plugins:mainfrom
roxblnfk:feat/rector-fixtures-and-group-gutter

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Two attributes you couldn't run before.

#[TestRectorFixtures] — a Rector rule that declares fixtures is a test case now: gutter icon, test-file icon, no "unused" warnings. Running it from the attribute adds --type=rector-fixture. It got its own attribute group instead of joining TEST_ATTRIBUTES, otherwise the rule's own refactor() and getRuleDefinition() would have shown up as tests.

#[Group] — running it means --group=<name> and nothing else: no path, no name filter, no type. #[Group('db', 'slow')] emits one flag per name, and the Group / Exclude group fields in the run configuration now take a comma-separated list too.

Along the way the same rule got applied to #[Test] on a class: from the attribute you get --type=test, from the class itself nothing — running a class should keep its #[Bench] methods.

Inspection. A new TestoGroupName inspection warns on #[Group] names the toolchain cannot select cleanly: blank or whitespace-padded, !-prefixed (the CLI reads that as an exclusion), containing a comma (the separator of the run configuration's Group field), or no names at all. Names built from constants are left alone.

Notable plumbing:

  • A group-only run borrows the ConfigurationFile scope to keep path/filter flags off the command line, and the platform then demands a configuration file Testo doesn't need. TestoRunConfiguration.checkConfiguration swallows exactly that one error, matched by message — a platform rewording fails closed (the validation error merely comes back).
  • Typed and untyped class runs are different contexts: isConfigurationFromContext compares testoType, so a run from #[Test] on a class and a run of the class itself get separate configurations instead of silently reusing one another.
  • A class-level attribute goes through the same abstract-class inheritor chooser as the class itself.
  • #[Group] without resolvable names gets no gutter icon — there is nothing such a run could select.

Unit tests are green. The PSI tests fail, but they already do on main for an unrelated reason ('filetype.java.module.display.name' is not found in JavaPsiBundle kills every BasePlatformTestCase), so the 21 new ones here can only be verified once that's fixed.

🤖 Generated with Claude Code

Two attributes had no way into a run configuration.

`#[TestRectorFixtures]` marks a Rector rule as a test case: the bridge's harness discovers the
declared fixtures and runs each as a data set of one synthesized test. It gets its own group,
TEST_CASE_ATTRIBUTES, rather than joining TEST_ATTRIBUTES — that array drives
isPublicMethodOfTestoMarkedClass, so `refactor()` and `getRuleDefinition()` would have become
tests. A run started from the attribute narrows to `--type=rector-fixture`; running the class
itself stays untyped and keeps whatever the class holds. The same rule now applies to `#[Test]`
on a class (`--type=test` from the attribute, nothing from the class), which is why
findTestElement accepts an attribute whose owner is a Testo class instead of letting the context
fall back to the class and lose the type.

`#[Group]` is not a test but a selector, so it gets a marker branch of its own and a deliberately
unscoped configuration: ConfigurationFile scope, `--group=<name>` and nothing else — no path, no
name filter, no type. A variadic `#[Group('db', 'slow')]` keeps both names; the persisted
group/excludeGroup fields hold them comma-separated and the handler emits one flag per name,
which Testo ORs. The same fields in the editor now accept a list too.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Versions had drifted (Kotlin, platform SDK, Qodana, Kover, Gradle) and the file tree predated
coverage/, the whole tests/console/ package, the history code vision and the util/ move.

Beyond fixing that, it now records the things that are expensive to rediscover: the CLI contract
(including that `--teamcity` in the default runner options is what makes Testo emit the service
messages we parse), the `methodName` selector encoding, the php_qn:// location formats, and a
gotchas section for constraints that were paid for in blood — the id-based tree, the channel
storage keys, the two reflection sites, why imported history needs our own console properties.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The persisted group/excludeGroup fields join several names with commas,
so a group whose own name contains one (#[Group('a,b')]) would split
into two --group flags. joinNames now escapes such commas as \, and
splitNames folds them back, keeping the name a single flag; the escape
never reaches the command line — Testo's --group is VALUE_IS_ARRAY and
does no comma splitting of its own.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
- A group-only run (ConfigurationFile scope, no config file) tripped the
  platform's "Configuration file is not specified" validation and never
  launched; TestoRunConfiguration.checkConfiguration now swallows exactly
  that error, matched by message so a platform rewording fails closed.
- isConfigurationFromContext conflated typed and untyped class runs: an
  untyped class configuration was reused for a class-level attribute and
  vice versa, erasing the --type narrowing on the second run. The class
  comparison now includes testoType.
- Running a class-level attribute of an abstract class skipped the
  inheritor chooser because findTestElement no longer falls back to the
  class; onFirstRun now unwraps the attribute's owner.
- #[Group] without resolvable names no longer gets a gutter icon — the
  producer refuses such a context, so the icon offered a run that did
  nothing.
- suggestedName no longer renames ApplicationConfig/SuiteConfig runs that
  happen to have a group typed into them.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
@roxblnfk
roxblnfk force-pushed the feat/rector-fixtures-and-group-gutter branch from c4dcc13 to 6df9df9 Compare July 31, 2026 22:10
A group name is free-form PHP, but three shapes of it silently misbehave
downstream: a `!` prefix reads as an exclusion on the CLI, a comma is
the separator of the run configuration's Group field, and blank or
whitespace-padded names cannot be typed back into that field. #[Group]
with no names at all selects nothing. The new TestoGroupName inspection
surfaces each of these as a warning right on the attribute instead of
letting the run quietly do the wrong thing. Constant/expression names
are left alone — they cannot be judged statically.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Three comments narrated the edit ("the branch below", "mirrors the
setup branch", "without this the context would fall back") instead of
stating the constraint. Same facts, said for the next reader.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
The comma is simply the separator of the persisted group field, and a
group name containing one is now reported by TestoGroupNameInspection
at the source instead of being silently re-encoded. splitNames is a
plain split again; joinNames is gone.

Assisted-By: Claude Opus 5 <noreply@anthropic.com>
The comma was the plugin's own invention: a group name is an opaque PHP
string, and joining names into one persisted field made the separator
part of the data model. TestoRunnerSettings now keeps groups and
excludeGroups as @XCollection lists — the same mechanism PhpUnit's own
runner settings use for test_patterns — so a name travels from #[Group]
to the command line untouched.

The comma survives only where a single text field genuinely cannot hold
a list: the editor parses and renders it (parseNames/formatNames). Old
configurations keep working — migrateLegacyNames folds a saved
group="a,b" into the list on load and clears it, so the next save
writes the new shape.

TestoRunnerSettingsSerializationTest pins the persisted XML, since
run configurations live in users' workspace.xml.

Assisted-By: Claude Opus 5 <noreply@anthropic.com>
@xepozz
xepozz merged commit 6098c7b into j-plugins:main Aug 2, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants