Coverage engine, run history, and HTML/JUnit reports - #82
Merged
Conversation
fix(report): resolve report paths off the EDT Both console additions ran platform calls on the wrong thread. The channel aggregate tab appends live output from the process-reader thread, where performWhenNoDeferredOutput asserts EDT. The report toolbar polls from a Swing timer on the EDT, where getLocalPath hits the file index — a slow operation the platform forbids there. The report resolve now runs on a pool thread and drops its result if a rerun started meanwhile, so a stale apply cannot auto-open the previous run's report. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captured from a Testo v0.10.40 run over plugin/data at Path level (a fiber-free scope run with --group=!async, so XDebug branch collection does not segfault on fibers). One run yields all three formats over the same code, so line/branch/per-test data stay consistent across them: clover (line coverage, incl. uncovered count="0"), cobertura (branch condition-coverage with both m==2 and m==4), and phpunit-xml coverage-xml (per-test <covered by>). These back the upcoming coverage parser unit tests. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(coverage): unit-test the parsers against real Testo fixtures Foundation for Testo's own coverage engine (replacing the internal com.intellij.php.coverage inheritance that fails plugin verification on 262). The parsers turn the three writer formats — clover, cobertura, phpunit-xml — into one format-neutral ParsedReport, deferring the platform ProjectData/JumpData/SwitchData construction and path normalization to the runner, so the parsing logic tests without an IDE fixture. DTD/entity loading is disabled since Cobertura's DOCTYPE points at a remote coverage-04.dtd. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(coverage): resolve 262 binary incompatibility from internal php.coverage Replaces the coverage engine's inheritance from com.intellij.php.coverage — a content module declared visibility="internal" and closed to third-party plugins — with Testo's own five classes on 100% public API: TestoCoverageRunner (CoverageRunner), TestoCoverageEngine (CoverageEngine), TestoCoverageSuite (BaseCoverageSuite), TestoCoverageAnnotator (RemappingCoverageAnnotator) and TestoCoverageProgramRunner (GenericProgramRunner), plus a ParsedReport -> ProjectData builder. This clears the six verifier problems (unresolved PhpCoverageRunner/PhpCoverageSuite/PhpUnitCoverageRunner and PhpUnitCoverageEngine.createCoverageSuite) that made the plugin binary-incompatible with 2026.2. The program runner reproduces the internal PhpCoverageRunner Xdebug flow (resolve the IDE-managed report path, build the command, attach the platform so it loads coverage on process termination) on public PHP execution API alone. loadCoverageData overrides the File overload — the common denominator across 252 and 262, since 262's Path overload only delegates to it. Branch data maps condition-coverage to JumpData/SwitchData and calls fillArrays, since getStatus/getBranchData read the array fields the builder lists must be swapped into. The internal com.intellij.rt.coverage.data.* comes from the intellij.platform.coverage.agent bundled module, now requested for both variants. verifyPlugin passes clean on 252 and 262. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two build variants existed in source only for the coverage typealiases, which the public-API rewrite made dead: nothing imports PhpCoverageRunner/PhpCoverageSuite/PhpUnitCoverageEngine/PhpUnitCoverageRunner any longer. Removes both PhpCoverageApi.kt files and the srcDir("src/php$phpApi/kotlin") wiring. phpApi stays — it still selects the platform version, since/until range and per-platform modules — but the two artifacts are now identical in source.
Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lens Adds the per-test coverage layer from coverage-xml: TestoCoverageByTestData (which tests touched which lines, both directions, keyed by a normalized file path) backed by a project-service TestoCoverageByTestIndex that TestoCoverageRunner fills whenever a phpunit-xml report carries per-test data. TestoTestIdentityMapper maps a coverage TestId to a --filter selector (pure, for the future TIA rerun), a php_qn:// hint and its PSI, all in one place. Surfaces it as TestoCoverageByTestCodeVisionProvider — a code-vision lens showing "N covering tests" on any covered PHP method/function, click to list and navigate. The native CoverageEngine.getTestsForLine gutter is not an option: every per-test CoverageEngine hook is @ApiStatus.Internal and fails the verifier for a third-party plugin, so the same data drives our own lens instead. Index is in-memory for now (cross-restart persistence is deferred). Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Testo announces coverage reports (clover / cobertura / phpunit-xml) the same way it announces the HTML report, on any ordinary run with a writer configured. TestoReportRef gains coverageFormat / isCoverage, and the report toolbar draws a lean "Show coverage" cell beside the HTML ones — greyed until the file is on disk, then applying it on click with no process launch: addCoverageSuite through our runner yields a TestoCoverageSuite, its format is set, and coverageGathered hands it to the platform, which reads it via TestoCoverageRunner and paints the annotation. resolveCoverageDataFile points the platform's file provider at <dir>/index.xml for phpunit-xml (a directory it cannot consume) and at the report file otherwise. DefaultCoverageFileProvider is built from a File, not a Path — the Path ctor exists only on 262. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Show coverage" built the suite through addCoverageSuite, which registers it with CoverageDataManager and persists it to workspace.xml. On reload the platform's readDataFileProviderAttribute falls back to Path.of(systemPath, absolutePath) when the saved report path no longer exists, throwing InvalidPathException on Windows at the second drive letter — which breaks the whole CoverageDataManager and every subsequent coverage run. Testo report paths are arbitrary project files that get overwritten or removed between runs, so this was a live landmine. Build the suite through the engine and hand it straight to coverageGathered instead: it shows the same annotation and updates the per-test index, but is never added to the persisted suite set. The report is a transient view anyway. The run path is unaffected — its data file lives under the IDE-managed system dir and round-trips safely. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Show coverage" cell resolved its data file synchronously on the Swing-timer EDT — resolveCoverageDataFile goes through the PHP path mapper and touches the filesystem, forbidden on the EDT. Mirror the ReportCell fix on this branch's base: resolve on a pooled thread and apply the result back on the EDT, guarded so overlapping ticks and a disposed project are dropped. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… coverage The gradle.properties header still blamed the PHP coverage package move and the src/php<phpApi> typealias split for the two build variants. Both are gone: coverage runs on public API and the source split was removed. The variants now exist only because 2026.2 split smRunner/testRunner/ui.jcef out of the monolith, and those module names do not resolve on 252. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e init A coverage suite persisted in workspace.xml whose report file is gone makes the platform's CoverageDataSuitesManager throw InvalidPathException while loading — Path.of(systemPath, absolutePath) on Windows — and the whole coverage subsystem fails to init. It fires before any plugin code, on the first coverage use, and the offending suite is usually another plugin's (PhpUnit's PhpCoverage), so the load itself is out of reach. Catch the failure where it surfaces on a Testo coverage run — the tests have already launched — and, instead of failing the run with an internal error, show a notification offering to drop the persisted CoverageDataManager component from workspace.xml (it only remembers past result files). The cleanup takes an IDE restart to reload, since the failed component is not written back on exit. isStaleCoverageFailure matches the InvalidPathException or the wrapping component error; it is unit-tested. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orm cache feat(coverage): add a Branches, % column for reports that carry branch data refactor(coverage): drop the stale-coverage guard the platform never lets fire The inherited annotator caches are filled by a walk that descends into a file only when the engine claims it through `coverageProjectViewStatisticsApplicableTo`, which is `@ApiStatus.Internal` and defaults to false. Staying on public API therefore left every lookup null and the panel reading "No coverage results" over a complete `ProjectData`. The annotator now indexes that data once per suite and answers both overload families the view uses: the `PsiFile`/`PsiDirectory` pair decides which rows exist, the `VirtualFile` pair fills the statistics column. The guard could never run. A stale suite's `InvalidPathException` is raised inside the `invokeAndWait` of `CoverageHelper.attachToProcess`, where the platform logs it and does not propagate it, so the runner catch that was its only caller never saw it. Recovering from a poisoned `workspace.xml` stays manual until the platform bug is fixed. Assisted-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(coverage): take per-file line totals from a coverage-xml report refactor(coverage): rename the phpunit-xml format id to coverage-xml coverage-xml lists a file it recorded no covered line for, and a `ClassData` was created for it before the emptiness check. The platform reads `ClassData.getLines()` without a null check, so expanding the coverage tree threw an NPE out of `fileInfoForCoveredFile`. That format records only executed lines, so counting them called every file fully covered. Its per-file `<totals>` states `total`/`executed` even for a file with no covered line, which makes the percentages exact and puts fully uncovered files back in the tree. The report's own `percent` is ignored: a directory has to sum its files, and one rounding across formats keeps the column consistent. The format id follows the rename on the Testo side; nothing accepts `phpunit-xml` any more, so the "Show coverage" button appears only for a report announced as `coverage-xml`. Only the announced id changes — the report is still written in PHPUnit's coverage schema, and detection off a `<phpunit>` root or a directory path is untouched. Assisted-By: Claude Opus 4.8 <noreply@anthropic.com>
The platform's editor annotation pipeline (createSrcFileAnnotator, CoverageEditorAnnotatorImpl, CoverageLineMarkerRenderer) is @ApiStatus.Internal end to end, so the stripes come from a Testo-owned highlighter over the public MarkupModel/LineMarkerRenderer API, on the standard coverage colour keys so the user's Colors & Fonts settings apply unchanged. Applying rides CoverageSuiteListener.coverageDataCalculated — fired on the EDT only after the report is parsed — and installation hangs off the annotator's onSuiteChosen, the one hook that also fires on closeSuitesBundle, which emits no listener event. Reading bundle.getCoverageData() hops through a pooled thread: the getter re-parses the report when its soft cache was collected. Highlighters are range markers, so edits shift the stripes with the code; only a marker torn across two lines by an Enter in its middle is dropped — the run measured a line that no longer exists, and the platform's restore path (LineHistoryMapper) is internal. Clicking a stripe pops the line's numbers platform-style — Hits, Branches (branch lines only), Tests — with the covering tests listed below, navigating through TestoTestIdentityMapper off the per-test index. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(coverage): ask Testo for several report formats per run and apply them without a click feat(console): expand/collapse on the visible toolbar row, Test History on our own button fix(console): stop the run clock when the process ends before the toolbar is wired The platform import forces its own console properties and converter, so a reopened run had none of the Testo UI. Since every store here is filled by the converter, recording the raw teamcity stream and feeding it back through the live console rebuilds the run whole — channels, statuses, node tree, report buttons — and the reports and run parameters kept beside it make the reopened tab answer for that run rather than for whatever ran last. Retention is ours because the platform rotates its history with a bare FileUtil.delete, leaving no hook to drop the files we keep alongside. A replayed clock is frozen on the archived marks: replaying re-reports every event, so each mark would be restamped with today's time, and a small archive finishes replaying before the toolbar is wired — which left the elapsed time counting forever. That race also existed for very short live runs, and is closed in the same place. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ike the run they hold feat(runs): archive only the deduped coverage reports and replay from them alone feat(console): rerun a history tab with the executor its run used The platform builds the test toolbar with no seam to reorder it — the sort popup and the overflow group are created inline, without ids or an extension point, and a snapshot of the visible group goes to RunTab. So the only handle on the group the user sees is from inside it: an invisible action rides the same toolbar and moves the sort popup into the overflow group and the platform's expand/collapse out of it, matching structurally so a reshuffled platform leaves it a no-op. A replay's reports now come from the archive rather than from the announcements in the log it replays: those name paths a later run has overwritten. What is archived is the same one-per-format pick the live apply makes — the coverage runner hands its flag paths to the console properties, so the two cannot disagree — and a report that lost the pick is recorded but not offered. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… may do with it feat(runs): archive every report a run announces, not just its coverage The archive was only ever going to be as useful as it is complete: an HTML report is as much part of a run as its coverage, so everything announced is captured now. Testo announces a report by its entry file and lays it out either as that file alone or as a directory beside its assets (docs/spec/html-report.md), so an `index.*` entry means the directory travels whole and the manifest names the entry inside the copy — which also folds coverage-xml into the same rule instead of a special case. The one report deliberately left out is a coverage one that lost the per-format dedup: same data under a second path, already ignored by the bundle that was applied. Clearing the history spares the run the open tab is built on — it is marked discarded rather than deleted, so the tab keeps its files and choosing Keep there brings it back — and asks whether locked runs are included rather than assuming. "Locked" rather than "pinned" because the list draws it with a lock; archives written under the old spelling still read. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(console): lift Run with Coverage out of the test tree's submenu fix(console): move the sort button into the overflow group for real refactor(console): drop the metainfo bridge to the platform's test import Retention was a Tools menu away from the list it governs; it is now a submenu right above the button that clears that list, and each entry wears the tool window icon of what the run was — run, debug or coverage — so the lock overlay has a plain shape to sit on. An imported run arrives locked: it was carried here by hand, and rotation deleting it after ten local runs would throw away the only copy. The Replay group keeps its two file gestures together at the bottom, since a run leaves as a zip and comes back as one. The toolbar surgery was looking for any toolbar with a MoreActionGroup and settling for the first one it found, which on a run tab is the tab's own — so the test tree's sort button never moved. It now matches the toolbar by its place and takes the separator in front of the sort button along, leaving nothing between Show Ignored and our expand/collapse. Run with Coverage sits behind the platform's More Run/Debug submenu because ExecutorRegistryImpl sorts every executor but Run and Debug into RunContextGroupMore, switched by a global registry key. The test tree's popup now borrows those two actions by id one level up rather than moving them, which would empty the submenu everywhere else. With the platform's own import and export both gone from Testo tabs, nothing reads the channel output back out of SMTestProxy.metainfo any more — a replay rebuilds the tabs from the recorded stream. What is left of that file is the node selection a replayed lens click needs. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eared feat(runs): the Replay button wears its run's kind, and opens its folder revert(console): stop rearranging the platform's part of the test toolbar revert(console): drop the duplicated run-context actions from the test tree popup Clearing the history marks the run the open tab is showing as "do not keep" instead of deleting it out from under that tab, but the tab kept reporting "keep in history": the retention radio read the recording first, and the recording is a live object the store never touches. The manifest is the truth once it exists; the recording only answers for a run that has not been archived yet, and clearing sets it there directly for a run still in flight. The toolbar surgery could not work and is gone. ToolbarPanel copies both of its groups into arrays (actionsToMerge / additionalActionsToMerge) and RunTab rebuilds the tab's toolbar from those copies, so mutating the live groups afterwards changes nothing the user sees — the sort button never moved. The platform's sort popup, its separator and its expand/collapse stay where the platform put them. The same is true one level up: Run with Coverage is in the "More Run/Debug" submenu because ExecutorRegistryImpl sorts every executor but Run and Debug into RunContextGroupMore unless a global registry key says otherwise, and the actions there are shared instances — so offering them on the test tree popup's own level only duplicated the submenu. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e or a directory feat(coverage): select the file open in the editor in the Coverage view fix(coverage): highlight the hovered row in the covering-tests popup fix(coverage): keep the Tests column narrow and the format badges at the right Per-test coverage answers which tests touched which lines, and until now it only ever navigated. One launcher now turns any set of those tests into a run — explicit --filter selectors on a throwaway configuration, the shape rerun-failed already uses, under the coverage executor because seeing the coverage they produce is the point. It is offered from the line popup, from a gutter icon on every covered method, function and class, and from the Coverage panel for the selected file or directory, the last one folding a whole subtree into one set. The panel's own "Always select opened element" cannot work in a file-based coverage view: it hands the extension the PSI leaf under the caret and then looks for a tree node whose value equals it, while the nodes hold files and directories. PhpStorm's own coverage has the same dead button. The mapper it goes through is internal, and overriding it fails the verifier, so this adds a toggle of its own that follows the editor off the message bus and walks the tree with TreeUtil.promiseSelect. The Tests column was as wide as a percentage because the view sizes a column by whatever getPercentage answers for the root node, and ours fell through to the coverage string; it now answers with the count it displays. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(coverage): make the Tests column answer for its own width feat(coverage): open the covering tests as a list before running anything The highlighter was installed from the annotator's onSuiteChosen, which the platform calls only when a bundle is reloaded or closed — never on the first chooseSuitesBundle of a session, so the first run of an IDE session stayed unpainted until a toggle forced a refresh. It is now installed where the bundle is handed over, before the data-calculated event it listens for. The Tests column stayed as wide as a percentage because the view sizes a column by what getPercentage answers for the root node, and the index it was compared against was remembered from createColumnInfos — on a different instance. CoverageView, CoverageTableModel and CoverageViewTreeStructure each build their own extension, so the column position is now derived from the bundle instead. A width the user has already been shown outlives this: it is saved per project in an internal state bean that wins over the computed one. A gutter icon that starts a whole test run on the first click is a trap, so it opens the list instead: run them all from the top row, or pick one. The rows drop the namespace — it is the same for every one of them — and one declaration can no longer be marked twice, the marker now being pinned to the declaration's own name node. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… project declares feat(run): a Coverage group of its own, with the analysis level and coverage-only options fix(run): exclude a group as --group=!name — Testo's CLI has no --exclude-group refactor(run): drop the Repeat and Command fields, park Parallel beside the runner's own flags test(coverage): pin branch data surviving the merge of several reports An audit of Testo's own option definitions — `Run::configure` plus every `#[InputOption]` — says the CLI takes exactly config, teamcity, json, log-json, filter, path, suite, type, group, coverage, no-coverage, coverage-level, log-junit, log-html, log-report and the three coverage-* report flags. So `--exclude-group` and `--repeat` were being sent to a runner that would abort on them, and `--parallel` has no flag yet: exclusion now goes out as `--group=!name`, Repeat is gone, and Parallel is parked at 1 (the value that sends nothing) until the flag exists. A name is opaque to the plugin — whatever `#[Group]` spells reaches the CLI untouched — so the editor had no separator it could safely own. Groups and suites are tags now: groups are picked from `TestoGroupsIndex`, a file-based index of every name a `#[\Testo\Filter\Group]` in the project spells, and suites are typed and added with Enter. Suites became a list in the model at the same time (one `--suite` per name); a configuration saved with a single suite migrates without being split. Command went with the rest: a test run is always `testo run`, and the whole console — `--teamcity`, the tree, the channels — only makes sense for it. Other subcommands are what the Run Anything provider is for. Parallel sits in the PHP editor's own *Test Runner options* row rather than below its panel, which is where the flag belongs. `PhpTestRunConfigurationEditor` hands out the whole form and nothing smaller, so the row is found by its label, its one-row `GridLayoutManager` is rebuilt with a second row, and the platform's children are re-added with the constraints it gives back — that shared label column is what lines the two rows up. A form change on PhpStorm's side only drops the field back below the panel. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dropdown leaves the test toolbar for the console's own vertical strip (print, clear, scroll to end), after a separator, wearing a filter icon. That strip is the only free seam: TestResultsPanel builds it once from a DefaultActionGroup copy of its `protected final` actions array, and the group stays mutable through the public ActionToolbar.getActionGroup(). The tab-row entry point (TabInfo.setTabPaneActions) was tried and dropped — it costs a row of its own. The action now takes its LogLevelFilter in the constructor: installed next to the tabs it filters, it no longer needs to fish the filter out of the action context. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ensed fix(run): stop sending --parallel — Testo's CLI does not take it, and a legacy value broke every run fix(runs): Clear History spares a run another tab is still writing, with the same day of grace prune gives fix(runs): `::testPay` no longer answers for `::testPayment` — the history lens, the index and the replay selection require a delimiter after the prefix fix(console): run-archive write failures log once and disable recording for the run instead of silently retrying on every line fix(console): selecting a node in a replay no longer throws when the tab was closed while the log streamed refactor(coverage): drop the write-only TestoCoverageSuite.perTest, the dead popup editor parameter and a duplicate catch The `arch §N` / `report-formats §N` citations are gone from comments: they point at the untracked docs/, which a reader of the repository cannot open, and every cited constraint was already stated inline. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Every TestoRunManifest field has a default, so Gson turns a bare `{}` — or any foreign JSON object — into a valid-looking v=VERSION manifest, and importRun would file any zip carrying any JSON object as a locked run. Require `v` to be present; a manifest the plugin itself wrote always carries it, so legacy archives still load.
Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s it half-written The manifest was written with a plain Files.writeString, while listRuns/prune/retentionOf read it from other pooled threads. A reader hitting the write window got null from readManifest, and in prune a null manifest on a run older than a day means delete — so a retention toggle racing a prune could destroy a day-old run. Write to a temp file and atomically move it into place, in the one helper both writers now share. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he per-format dedup Deduping first let an unchecked flag-written report win its format and then be filtered out, dropping the format even when a checked testo.php-configured report of the same format existed — where a manual click on the grouped button would have applied it. The archiver keeps deduping all announced reports on purpose: it stores every report, it does not honour the checkboxes. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… two runs at once Two launches a millisecond apart start in the same millisecond, and TestoRunStore.beginRun keys the run directory by configuration name and start time — so a double-click interleaved two runs into one output.log and one console. A per-button cooldown swallows the second firing; a human cannot hit two different buttons that fast, so the same-button double-click is the only sub-millisecond case to guard. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rough TestoBundle The code-vision lens name, its "N covering tests" hints, the chooser titles and the tooltip, plus the program runner''s unsupported-profile error, were hardcoded English while every sibling in the feature goes through TestoBundle. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d report resolve The declaration-range → covering-tests walk was copied verbatim in the gutter line marker and the code-vision lens, the "class::method" sort key was spelled three times, and the resolve-off-EDT scaffolding (busy guard, started-at snapshot, rerun-drop) stood twice in TestoReportAction. Extract `testsCoveringElement` + `TEST_ID_ORDER` and a `resolveReportOffEdt` helper, so a fix to any of them lands once. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(run): drop the dead "Reports" group — one permanently disabled checkbox, nothing read or persisted. refactor(run): testoRerunExecutorId is never null (its fallback is the non-null executor.id), so declare it String and drop the dead null checks in rerunCurrent and rerunIcon. perf(run): compute the group-name suggestions under a modal progress instead of on the EDT — TestoGroupsIndex.allGroups queries the file index per key and would freeze a large project on the add-tag click. fix(coverage): count only a Cobertura class's own <lines>, not <line> nested under <methods> — a foreign (PHPUnit) report would otherwise double every line. fix(coverage): a blank coverage-xml <project source> no longer yields keys with a leading slash, matching the Cobertura parser's guard. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… run Testo grew --log-html and --log-junit. The run configuration now offers both as checkboxes (HTML on, JUnit off) and emits them, pointed at an IDE-managed folder, the same way a Coverage run points its --coverage-* flags: Testo writes there, announces the report, and the archiver copies it into history. HTML opens in a tab; JUnit is left unshown — it exists for external tooling (mutation testing). The flags go out from createCommand, the one chokepoint every executor (Run/Debug/Coverage) passes, rather than prepareArguments, which has neither the project nor the interpreter the IDE-managed path and the local-only gate need. A remote interpreter would write these to a host path that never maps back, so there they are left to whatever testo.php configures — no worse than before the flags existed. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Branch coverage needs the Xdebug engine (PCOV collects lines only) and a Cobertura report to carry it. When both are on and the level is left at auto, resolveCoverageLevel now sends --coverage-level=branch instead of leaving the level to testo.php — so the default Coverage run (Xdebug + Cobertura + auto) gathers branches. An explicit level still wins, and auto with PCOV or without Cobertura sends nothing. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(debug): wear the restart-debugger icon while the session runs The debug tab''s restart button is the platform Rerun, which TestoAwareRerunAction overrides; in SPLIT_BUTTON mode it hid itself on every Testo tab, deferring to the split button — but the split button lives on RunTab.TopToolbar, which the debug tab does not use, so its restart button vanished. Gate that hide on not being the Debug executor. A descriptor''s restart actions are constructor-only, so the debug descriptor cannot be handed its own. While the debug process is alive its restart button shows AllIcons.Actions.RestartDebugger, read off the run-content descriptor''s process handler. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XDebugSession.getRunContentDescriptor() is deprecated and logs a "split debugger" error under the 262 split-debugger runtime. Go through XDebuggerManager.newSessionBuilder(...).startSession() like PhpTestDebugRunner does and read the descriptor off its XSessionStartedResult, whose accessor is not deprecated. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
roxblnfk
force-pushed
the
feat/coverage-engine
branch
from
August 17, 2026 10:41
3cbc5d2 to
4432760
Compare
…nel tabs row The old per-level checkbox dropdown sat on the console's vertical strip and did not open. Replace it with a labeled ComboBoxAction reading `info +` (the chosen level, and everything above it) that lives in the channel tabs' entry-point action group, at the right edge of the tab row. The level set is the fixed eight PSR-3 levels; a message shows when its level is at or above the chosen minimum, default `info`. The "levels seen this run" registry is gone, so the converter no longer records them. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The covering-tests Code Vision lens listed its tests only to navigate. Its popup now leads with a *Run all covering tests* row that launches them with coverage through the same TestoCoveringTestsLauncher the gutter uses; the per-test rows still jump to source. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(inlay): name the Code Vision lenses in Inlay Hints settings feat(history): the Show history popup lists archived runs with inline Load replay / Repeat run The lens never matched on Windows: it built its location from the OS-native path (D:\…) while the archive stored Testo's D:/…, so nothing matched. Locations now compare through runLocationKey, which folds path separators. The archive index is also built synchronously on first lookup, so the first code-vision pass over a freshly opened file already answers instead of waiting on a repaint a daemon-bound provider does not reliably get. A click now pops up the runs holding the test (the one already open in a tab in bold) rather than replaying the newest at once; each row carries Load-replay and Repeat-run inline buttons, so a bare click launches nothing. Repeat run re-executes the archived configuration restored from its manifest, with its original executor. The two Code Vision lenses gained a CodeVisionGroupSettingProvider each, so their Inlay Hints settings show a name and description instead of blank. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpiles XDebuggerManager.newSessionBuilder exists only on 2026.2, so the direct call added with the split-debugger fix compiled on 262 but broke compileKotlin on the 252 variant in CI. The builder path is now reflective (methods resolved off the public interfaces), with the classic startSession fallback on 252, where the deprecated descriptor accessor is safe with no split debugger. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, lead with Output fix(console): host the log-level filter on public tabPaneActions, not the internal entry-point group The channel console rebuilds its tabs on a log-level toggle or a test switch, which dropped the selection back to Output. It now remembers the selected tab's title and reselects the same-named channel after the rebuild (Output when that channel is gone), and Output leads the tabs, apart from the channel-aggregating All. The filter itself used to override JBTabsImpl.getEntryPointActionGroup(), which is @ApiStatus.Internal and fails verifyPlugin's default failureLevel — a red neither buildPlugin nor test surfaces. It now rides on each tab's public TabInfo.setTabPaneActions; the platform feeds the selected tab's group into the same entry-point toolbar unchanged. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What was changed
Turns the plugin's coverage support into a first-class engine and builds a run archive around it.
Coverage engine on 100% public platform API. The Xdebug/PCOV Coverage run loads Clover / Cobertura / coverage-xml reports itself: editor gutter highlighting, per-file/dir percentages, the Coverage view's own columns and toolbar, and the covered-line popup are all reimplemented on public API — the old
php252/php262typealias split is gone, sophpApiis now purely a build selector.Per-test coverage. coverage-xml carries which test touched which line; a gutter icon and a Code Vision lens on every covered declaration list its covering tests (the lens popup led by Run all covering tests) and run them again with coverage, and the Coverage view gets a Tests column and a Run Covering Tests action.
Run history. Every run is archived (raw output, reports, parameters) and replays from Test History as the full Testo console — channels, statuses, report buttons, that run's own coverage. Show history above a test pops up the runs holding it (the one already open in a tab in bold), each row with inline Load replay / Repeat run buttons. Retention is the plugin's own, with export/import and keep/drop/lock per run.
Reports. A Coverage run applies every report it produced without a click; one Coverage button gathers them under a per-report checkbox. Separately, every run now writes an HTML report (
--log-html, on) and a JUnit report (--log-junit, off) into an IDE-managed folder, kept in the history.Run configuration. Suite / Group / Exclude group are tag lists (groups offered from the
#[Group]names the project declares); coverage settings sit in their own group. Repeat and Command are gone; Parallel is parked until Testo's CLI takes it.Console. The Log Levels filter is a minimum-level picker: an

info +combo box at the right of the channel tabs row shows that level and everything above it (defaultinfo), replacing the per-level checkbox dropdown.Debug toolbar. The rerun/restart button is back on the debug tab — during a session and after it ends — and wears the restart-debugger icon while the session runs. The run descriptor now comes off the session builder's result, so starting a debug session no longer logs a split debugger error on 262.
How coverage level resolves
--coverage-level(line/branch/path) always wins.branch— branches need Xdebug and Cobertura carries them — so the default Coverage run gathers them.Review notes
CoverageView/CoverageViewExtension, the coverage annotator entry point, the results-tree renderer and the report console are all@ApiStatus.Internal. The seams that were found (and the ones that don't exist) are recorded as gotchas inCLAUDE.md; that section is the map for anyone touching this area.--log-html/--log-junitflags go out fromcreateCommand, the one chokepoint every executor passes, and only for local interpreters — a remote one would write them to a host path that never maps back.RunTab.TopToolbar, where the SPLIT_BUTTON replacement lives, soTestoAwareRerunActiongates its hide branch on not the Debug executor to keep the restart button; andXDebugSession.getRunContentDescriptor()is deprecated under the split debugger, so the descriptor is read offXSessionStartedResultlikePhpTestDebugRunnerdoes.getLocationHintruns the file through the localPhpCommandLinePathProcessor(OS-nativeD:\…on Windows), while Testo emitsD:/…. The Show history lens compared the two raw, so it never lit up on Windows; lookups now fold separators throughrunLocationKey. A Code Vision lens also needs aCodeVisionGroupSettingProvider, or its Inlay Hints settings entry renders blank.Testing
BasePlatformTestCasesuite on 2026.2 (262): 444/446 green. The two failures (MyPluginTest.testRename, one producer PSI test) are pre-existing onmain— a sandboxVfsRootAccesslimit and a known PSI-context case, unrelated to this branch.buildPlugingreen for the 262 artifact.