Skip to content

Commit ac0b670

Browse files
committed
refactor(test): enhance test failure handling with async and related code collection #259
- Refactor `sendResult` to run asynchronously with a delay for test failures. - Add related code collection from console hyperlinks to provide more context in error notifications. - Simplify `getConsoleView` logic and improve error handling. - Remove redundant build issue handling in `ExternalTaskAgentObserver`.
1 parent 19cf4ba commit ac0b670

File tree

2 files changed

+107
-39
lines changed

2 files changed

+107
-39
lines changed

core/src/main/kotlin/cc/unitmesh/devti/observer/ExternalTaskAgentObserver.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,17 @@ class ExternalTaskAgentObserver : AgentObserver, Disposable {
4949
return
5050
}
5151

52-
if (exitCode != 0 && exitCode != IDEA_INTERRUPTED_CODE) {
52+
// if (exitCode != 0 && exitCode != IDEA_INTERRUPTED_CODE) {
53+
// val prompt = "Help Me fix follow build issue:\n```bash\n$globalBuffer\n```\n"
54+
// sendErrorNotification(project, prompt)
55+
// } else {
56+
val isSpringFailureToStart =
57+
globalBuffer.contains("***************************") && globalBuffer.contains("APPLICATION FAILED TO START")
58+
if (isSpringFailureToStart) {
5359
val prompt = "Help Me fix follow build issue:\n```bash\n$globalBuffer\n```\n"
5460
sendErrorNotification(project, prompt)
55-
} else {
56-
val isSpringFailureToStart =
57-
globalBuffer.contains("***************************") && globalBuffer.contains("APPLICATION FAILED TO START")
58-
if (isSpringFailureToStart) {
59-
val prompt = "Help Me fix follow build issue:\n```bash\n$globalBuffer\n```\n"
60-
sendErrorNotification(project, prompt)
61-
}
6261
}
62+
// }
6363
}
6464
})
6565
}

core/src/main/kotlin/cc/unitmesh/devti/observer/TestAgentObserver.kt

Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,66 @@
11
package cc.unitmesh.devti.observer
22

33
import cc.unitmesh.devti.provider.observer.AgentObserver
4+
import cc.unitmesh.devti.sketch.ui.patch.readText
5+
import cc.unitmesh.devti.util.isInProject
46
import cc.unitmesh.devti.util.relativePath
7+
import com.intellij.build.BuildView
8+
import com.intellij.execution.filters.FileHyperlinkInfo
59
import com.intellij.execution.impl.ConsoleViewImpl
10+
import com.intellij.execution.impl.EditorHyperlinkSupport
611
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsAdapter
712
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener
813
import com.intellij.execution.testframework.sm.runner.SMTestProxy
914
import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView
1015
import com.intellij.execution.ui.ExecutionConsole
1116
import com.intellij.execution.ui.RunContentManager
1217
import com.intellij.openapi.Disposable
13-
import com.intellij.openapi.application.runInEdt
1418
import com.intellij.openapi.application.runReadAction
15-
import com.intellij.openapi.editor.Editor
1619
import com.intellij.openapi.editor.markup.RangeHighlighter
20+
import com.intellij.openapi.progress.ProgressIndicator
21+
import com.intellij.openapi.progress.ProgressManager
22+
import com.intellij.openapi.progress.Task
23+
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator
1724
import com.intellij.openapi.project.Project
1825
import com.intellij.openapi.util.TextRange
26+
import com.intellij.openapi.vfs.VirtualFile
1927
import com.intellij.psi.search.GlobalSearchScope
2028
import com.intellij.psi.search.ProjectScope
2129
import com.intellij.refactoring.suggested.range
2230
import com.intellij.util.messages.MessageBusConnection
31+
import kotlinx.coroutines.DelicateCoroutinesApi
32+
import kotlinx.coroutines.GlobalScope
33+
import kotlinx.coroutines.delay
34+
import kotlinx.coroutines.launch
2335
import java.lang.reflect.Field
2436

2537
class TestAgentObserver : AgentObserver, Disposable {
2638
private var connection: MessageBusConnection? = null
39+
40+
@OptIn(DelicateCoroutinesApi::class)
2741
override fun onRegister(project: Project) {
2842
connection = project.messageBus.connect()
2943
connection?.subscribe(SMTRunnerEventsListener.TEST_STATUS, object : SMTRunnerEventsAdapter() {
3044
override fun onTestFailed(test: SMTestProxy) {
31-
sendResult(test, project, ProjectScope.getProjectScope(project))
45+
GlobalScope.launch {
46+
delay(3000)
47+
sendResult(test, project, ProjectScope.getProjectScope(project))
48+
}
3249
}
3350
})
3451
}
3552

3653
private fun sendResult(test: SMTestProxy, project: Project, searchScope: GlobalSearchScope) {
37-
runInEdt {
38-
getConsoleEditor(project)
39-
val sourceCode = test.getLocation(project, searchScope)
40-
val psiElement = sourceCode?.psiElement
41-
val language = psiElement?.language?.displayName ?: ""
42-
val filepath = psiElement?.containingFile?.virtualFile?.relativePath(project) ?: ""
43-
val code = runReadAction<String> { psiElement?.text ?: "" }
44-
val prompt = """Help me fix follow test issue:
54+
val task = object : Task.Backgroundable(project, "Processing context", false) {
55+
override fun run(indicator: ProgressIndicator) {
56+
val relatedCode = collectConsoleRelatedCode(project) ?: emptyList()
57+
val sourceCode = runReadAction { test.getLocation(project, searchScope) }
58+
val psiElement = sourceCode?.psiElement
59+
val language = psiElement?.language?.displayName ?: ""
60+
val filepath = psiElement?.containingFile?.virtualFile?.relativePath(project) ?: ""
61+
val code = runReadAction<String> { psiElement?.text ?: "" }
62+
val formatedRelatedCode = "\n## related code:\n```$language\n${relatedCode.joinToString("\n")}\n```"
63+
val prompt = """Help me fix follow test issue:
4564
|## ErrorMessage:
4665
|```
4766
|${test.errorMessage}
@@ -54,57 +73,106 @@ class TestAgentObserver : AgentObserver, Disposable {
5473
|```$language
5574
|$code
5675
|```
76+
|${formatedRelatedCode}
5777
|""".trimMargin()
5878

59-
sendErrorNotification(project, prompt)
79+
sendErrorNotification(project, prompt)
80+
}
6081
}
82+
83+
ProgressManager.getInstance()
84+
.runProcessWithProgressAsynchronously(task, BackgroundableProcessIndicator(task))
6185
}
6286

6387
/**
6488
*```kotlin
6589
* (content.component.components.firstOrNull() as? NonOpaquePanel)?.components?.firstOrNull { it is JBRunnerTabs }
6690
*```
6791
*/
68-
private fun getConsoleEditor(project: Project): Editor? {
92+
private fun collectConsoleRelatedCode(project: Project): List<String>? {
6993
val content = RunContentManager.getInstance(project).selectedContent ?: return null
7094
val executionConsole = content.executionConsole ?: return null
7195
val consoleViewImpl: ConsoleViewImpl = getConsoleView(executionConsole) ?: return null
7296
val editor = consoleViewImpl.editor ?: return null
7397

7498
val startOffset = 0
75-
val allText = editor.document.text
7699
val endOffset = editor.document.textLength
77100
val textRange = TextRange(startOffset, endOffset)
78101
val highlighters: Array<RangeHighlighter> = editor.markupModel.allHighlighters
79102

80-
for (highlighter in highlighters) {
103+
/// todo: first collect file path and line number, then build by range
104+
val relatedCode = highlighters.mapNotNull { highlighter ->
81105
if (textRange.contains(highlighter.range!!)) {
82-
// val hyperlinkInfo: FileHyperlinkInfo? = EditorHyperlinkSupport.getHyperlinkInfo(highlighter) as FileHyperlinkInfo
83-
// val descriptor = hyperlinkInfo?.descriptor ?: return null
84-
val range = highlighter.range!!
85-
val hyperlinkText = allText.substring(range.startOffset, range.endOffset)
86-
println("hyperlinkText: $hyperlinkText")
106+
// call be .DiffHyperlink
107+
val hyperlinkInfo: FileHyperlinkInfo? = EditorHyperlinkSupport.getHyperlinkInfo(highlighter) as? FileHyperlinkInfo
108+
val descriptor = hyperlinkInfo?.descriptor ?: return@mapNotNull null
109+
val virtualFile: VirtualFile = descriptor.file
110+
111+
val isProjectFile = runReadAction { project.isInProject(virtualFile) }
112+
if (isProjectFile) {
113+
val lineNumber = descriptor.line
114+
val allText = virtualFile.readText()
115+
val startLine = if (lineNumber - 10 < 0) {
116+
0
117+
} else {
118+
lineNumber - 10
119+
}
120+
// endLine should be less than allText.lines().size
121+
val endLine = if (lineNumber + 10 > allText.lines().size) {
122+
allText.lines().size
123+
} else {
124+
lineNumber + 10
125+
}
126+
127+
return@mapNotNull allText.lines().subList(startLine, endLine).joinToString("\n")
128+
} else {
129+
return@mapNotNull null
130+
}
131+
} else {
132+
return@mapNotNull null
87133
}
88134
}
89135

90-
return consoleViewImpl.editor
136+
return relatedCode.distinct()
91137
}
92138

139+
93140
private fun getConsoleView(executionConsole: ExecutionConsole): ConsoleViewImpl? {
94-
if (executionConsole is SMTRunnerConsoleView) {
95-
try {
96-
val resultsViewerClass = executionConsole.resultsViewer::class.java
97-
val myConsoleViewField: Field = resultsViewerClass.getDeclaredField("myConsoleView")
98-
myConsoleViewField.isAccessible = true
99-
val myConsoleView = myConsoleViewField.get(executionConsole.resultsViewer) as? ConsoleViewImpl
100-
return myConsoleView
101-
} catch (e: Exception) {
102-
e.printStackTrace()
141+
when (executionConsole) {
142+
is ConsoleViewImpl -> {
143+
return executionConsole
144+
}
145+
146+
is BuildView -> {
147+
when (executionConsole.consoleView) {
148+
is SMTRunnerConsoleView -> {
149+
return getTestView(executionConsole.consoleView as SMTRunnerConsoleView)
150+
}
151+
}
152+
}
153+
154+
is SMTRunnerConsoleView -> {
155+
return getTestView(executionConsole)
103156
}
104157
}
105-
return executionConsole as? ConsoleViewImpl
158+
159+
return null
106160
}
107161

162+
private fun getTestView(executionConsole: SMTRunnerConsoleView): ConsoleViewImpl? {
163+
try {
164+
val resultsViewerClass = executionConsole.resultsViewer::class.java
165+
val myConsoleViewField: Field = resultsViewerClass.getDeclaredField("myConsoleView")
166+
myConsoleViewField.isAccessible = true
167+
val myConsoleView = myConsoleViewField.get(executionConsole.resultsViewer) as? ConsoleViewImpl
168+
return myConsoleView
169+
} catch (e: Exception) {
170+
return null
171+
}
172+
}
173+
174+
// ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.DEBUG)
175+
108176
override fun dispose() {
109177
connection?.disconnect()
110178
}

0 commit comments

Comments
 (0)