From 7583249ff88b8d8ed0625bfcb90ac8635da29625 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 14 Aug 2026 14:28:18 +0400 Subject: [PATCH 1/2] fix(console): hop the channel icon inlay to the EDT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 5 ++++ .../testo/tests/console/TestoChannelsUi.kt | 13 +++++--- .../testo/tests/console/TestoReportAction.kt | 30 ++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dbd010..a738dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## [Unreleased] +### Fixed + +- The channel console no longer throws an EDT-threading error while streaming live output into an aggregate tab. +- The report buttons no longer trigger a "slow operations on EDT" error: report paths now resolve off the UI thread. + ## [2026.5.262] - 2026-08-12 ### Added diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoChannelsUi.kt b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoChannelsUi.kt index 124f5a0..465fa4c 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoChannelsUi.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoChannelsUi.kt @@ -1073,13 +1073,18 @@ object TestoChannelsUi { // Prefix the aggregate's per-test header (a hyperlink to the test) with the Testo icon. Console output is // buffered, so the editor offset only resolves once it is flushed — hence performWhenNoDeferredOutput. + // Live chunks arrive off the EDT (test-reader thread), but performWhenNoDeferredOutput asserts EDT — hop first. private fun addTestoIconInlay(view: ConsoleViewImpl, offset: Int) { - view.performWhenNoDeferredOutput { - val editor = view.editor as? EditorEx ?: return@performWhenNoDeferredOutput - if (offset in 0..editor.document.textLength) { - editor.inlayModel.addInlineElement(offset, false, TestoIconInlayRenderer) + val app = ApplicationManager.getApplication() + val task = Runnable { + view.performWhenNoDeferredOutput { + val editor = view.editor as? EditorEx ?: return@performWhenNoDeferredOutput + if (offset in 0..editor.document.textLength) { + editor.inlayModel.addInlineElement(offset, false, TestoIconInlayRenderer) + } } } + if (app.isDispatchThread) task.run() else app.invokeLater(task) } private fun ensureInstalled(): JBEditorTabs? { diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt index a41f4a2..07f5774 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt @@ -15,6 +15,8 @@ import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.RightAlignedToolbarAction import com.intellij.openapi.actionSystem.ToggleAction import com.intellij.openapi.actionSystem.ex.CustomComponentAction +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project @@ -150,6 +152,8 @@ class TestoReportsAction( // A fresh cell has no tooltip yet, so the first refresh must go through however little has changed. private var refreshed = false private var hovered = false + // A background resolve is in flight; ticks skip while it is, so a slow resolve doesn't stack up tasks. + private var resolving = false // Asked for per paint: a font set once on a raw JComponent outlives a zoom (no UI delegate reinstalls it). override fun getFont(): Font = UIUtil.getLabelFont() @@ -186,7 +190,31 @@ class TestoReportsAction( // Not while the run is going: a report is announced as Testo starts writing it, over the path the // previous run wrote to — a check now would offer that run's file. val finished = reports.runFinished - val found = if (finished) resolveReport(ref, project, mapToLocal, reports.runStartedAt) else null + if (!finished) { + applyResolved(null, false) + return + } + // resolveReport goes through the PHP path mapper, whose getLocalPath hits the file index — a slow operation + // forbidden on the EDT, and this runs off a Swing timer on the EDT. Resolve on a pool thread, apply on the EDT. + if (resolving) return + resolving = true + val startedAt = reports.runStartedAt + val cellRef = ref + ApplicationManager.getApplication().executeOnPooledThread { + val found = resolveReport(cellRef, project, mapToLocal, startedAt) + ApplicationManager.getApplication().invokeLater( + { + resolving = false + // A rerun may have started while this resolved; applying then would auto-open the previous + // run's report and mark the new run as already opened. Drop it — the next tick sees the run. + if (reports.runStartedAt == startedAt && reports.runFinished) applyResolved(found, true) + }, + ModalityState.any(), + ) { project.isDisposed } + } + } + + private fun applyResolved(found: Path?, finished: Boolean) { maybeAutoOpen(found, finished) val willOpen = !finished && TestoReportAutoOpen.decide(project, reports, ref).isNotEmpty() // Tooltip and repaint only on a real change: this runs twice a second. From 7ecd8f85aa26cb5e87fa5303e2d9d3de125b603b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 14 Aug 2026 14:51:11 +0400 Subject: [PATCH 2/2] chore(report): drop redundant comments from the EDT resolve fix Assisted-By: Claude Opus 4.8 (1M context) --- .../com/github/xepozz/testo/tests/console/TestoReportAction.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt index 07f5774..578bbf4 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/console/TestoReportAction.kt @@ -152,7 +152,6 @@ class TestoReportsAction( // A fresh cell has no tooltip yet, so the first refresh must go through however little has changed. private var refreshed = false private var hovered = false - // A background resolve is in flight; ticks skip while it is, so a slow resolve doesn't stack up tasks. private var resolving = false // Asked for per paint: a font set once on a raw JComponent outlives a zoom (no UI delegate reinstalls it). @@ -195,7 +194,7 @@ class TestoReportsAction( return } // resolveReport goes through the PHP path mapper, whose getLocalPath hits the file index — a slow operation - // forbidden on the EDT, and this runs off a Swing timer on the EDT. Resolve on a pool thread, apply on the EDT. + // forbidden on the EDT, and this runs off a Swing timer on the EDT. if (resolving) return resolving = true val startedAt = reports.runStartedAt