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..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 @@ -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,7 @@ 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 + 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 +189,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. + 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.