mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
test(jetbrains): add markdown stress leak coverage
This commit is contained in:
@@ -268,6 +268,22 @@ Tests for retained Swing components should assert:
|
||||
- `update(model)` changes existing labels/body text without duplicating components.
|
||||
- Updates while collapsed do not eagerly create lazy bodies.
|
||||
- No-op updates, empty deltas, repeated hover values, and toggling non-expandable cards do not repaint/revalidate the whole view.
|
||||
- Streaming/rebuilding surfaces additionally require stress + leak tests (see below).
|
||||
|
||||
### Stress and Leak Tests for Streaming UI
|
||||
|
||||
Session/transcript UI that streams updates or rebuilds its component tree (markdown
|
||||
views, code blocks, transcript parts, collapsible cards) must ship stress + leak tests in
|
||||
addition to behavior tests. These tests must:
|
||||
|
||||
- Drive many updates (hundreds of streamed deltas or `set` cycles) through the public API.
|
||||
- Assert that retained component instances stay identical across updates (`assertSame`).
|
||||
- Assert the component count stays bounded — no growth per update.
|
||||
- Assert disposable-backed resources return to baseline after churn + clear/dispose.
|
||||
For code editors, compare `EditorFactory.getInstance().allEditors.size` against a
|
||||
baseline captured before the loop.
|
||||
|
||||
See `MdViewHybridStressTest` for the reference pattern.
|
||||
|
||||
### Platform Components and Utilities
|
||||
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package ai.kilocode.client.ui.md
|
||||
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.ui.components.JBHtmlPane
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import javax.swing.Box
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* Stress + leak coverage for the hybrid markdown renderer.
|
||||
*
|
||||
* These tests drive many updates through the public [MdView] API and inspect the real
|
||||
* Swing component tree to prove that:
|
||||
* - retained component instances survive heavy streaming,
|
||||
* - the component tree stays bounded (no per-update growth),
|
||||
* - editors created for code blocks are released (no leak) after churn + clear.
|
||||
*/
|
||||
@Suppress("UnstableApiUsage")
|
||||
class MdViewHybridStressTest : BasePlatformTestCase() {
|
||||
private lateinit var view: MdView
|
||||
private var disposed = false
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
view = MdViewFactory.hybrid()
|
||||
disposed = false
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
if (this::view.isInitialized && !disposed) Disposer.dispose(view)
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun `test streaming a large mixed document token by token stays consistent`() {
|
||||
val doc = buildString {
|
||||
append("# Heading\n\n")
|
||||
append("Intro paragraph with **bold** text.\n\n")
|
||||
append("- one\n- two\n- three\n\n")
|
||||
append("```kotlin\nval x = 1\n```\n\n")
|
||||
append("middle prose paragraph\n\n")
|
||||
append("```java\nclass A {}\n```\n\n")
|
||||
append("closing prose")
|
||||
}
|
||||
|
||||
for (token in doc.chunked(3)) view.append(token)
|
||||
|
||||
assertEquals(doc, view.markdown())
|
||||
assertEquals(3, htmls().size)
|
||||
assertEquals(2, scrolls().size)
|
||||
assertEquals(4, struts().size) // blocks - 1
|
||||
assertEquals(9, panel().componentCount) // 5 blocks + 4 struts = 2*5 - 1
|
||||
|
||||
val html = view.html()
|
||||
assertTrue(html.contains("<h1>"))
|
||||
assertTrue(html.contains("<ul>"))
|
||||
assertTrue(html.contains("class A"))
|
||||
assertFalse(html.contains("<hr"))
|
||||
}
|
||||
|
||||
fun `test trailing appends retain prefix html and code instances`() {
|
||||
view.set("intro\n\n```kotlin\nval x = 1\n```\n\ntail")
|
||||
val intro = htmls().first()
|
||||
val tail = htmls().last()
|
||||
val editor = editors().single()
|
||||
editor.getEditor(true)
|
||||
|
||||
repeat(100) { i -> view.append(" more$i") }
|
||||
|
||||
assertSame(intro, htmls().first())
|
||||
assertSame(tail, htmls().last())
|
||||
assertSame(editor, editors().single())
|
||||
assertEquals(2, htmls().size)
|
||||
assertEquals(1, scrolls().size)
|
||||
assertFalse(editor.getEditor(true)!!.isDisposed)
|
||||
assertTrue(view.markdown().contains("more99"))
|
||||
}
|
||||
|
||||
fun `test repeated same structure set reuses single editor and stays bounded`() {
|
||||
repeat(150) { i ->
|
||||
view.set("```kotlin\nval x = $i\n```")
|
||||
editors().single().getEditor(true)
|
||||
}
|
||||
val editor = editors().single()
|
||||
|
||||
repeat(50) { i -> view.set("```kotlin\nval y = $i\n```") }
|
||||
|
||||
assertSame(editor, editors().single())
|
||||
assertEquals(1, scrolls().size)
|
||||
assertEquals(1, panel().componentCount)
|
||||
assertEquals("val y = 49", editor.text)
|
||||
}
|
||||
|
||||
fun `test structural churn releases every editor after clear`() {
|
||||
val base = EditorFactory.getInstance().allEditors.size
|
||||
|
||||
repeat(60) { i ->
|
||||
view.set("```kotlin\nval x = $i\n```")
|
||||
editors().single().getEditor(true)
|
||||
view.set("```java\nclass A$i {}\n```")
|
||||
editors().single().getEditor(true)
|
||||
view.set("plain prose $i")
|
||||
}
|
||||
|
||||
view.clear()
|
||||
drainEdt()
|
||||
|
||||
assertTrue(scrolls().isEmpty())
|
||||
assertTrue(htmls().isEmpty())
|
||||
assertEquals(0, panel().componentCount)
|
||||
assertEquals(base, EditorFactory.getInstance().allEditors.size)
|
||||
}
|
||||
|
||||
fun `test streaming code body reuses one editor and keeps html in sync`() {
|
||||
view.append("```java\n")
|
||||
val pane = scrolls().single()
|
||||
val editor = editors().single()
|
||||
|
||||
val body = StringBuilder()
|
||||
repeat(100) { i ->
|
||||
val line = "void m$i() {}\n"
|
||||
body.append(line)
|
||||
view.append(line)
|
||||
}
|
||||
|
||||
assertSame(pane, scrolls().single())
|
||||
assertSame(editor, editors().single())
|
||||
assertEquals(body.toString().trimEnd('\n'), editor.text)
|
||||
assertTrue(view.html().contains("void m0()"))
|
||||
assertTrue(view.html().contains("void m99()"))
|
||||
|
||||
view.append("```")
|
||||
|
||||
assertSame(pane, scrolls().single())
|
||||
assertSame(editor, editors().single())
|
||||
}
|
||||
|
||||
fun `test style changes during streaming do not rebuild components`() {
|
||||
view.append("intro\n\n```kotlin\nval x = 1\n```\n\n")
|
||||
val intro = htmls().first()
|
||||
val editor = editors().single()
|
||||
editor.getEditor(true)
|
||||
val styled = SessionEditorStyle.create(family = "Courier New", size = 18)
|
||||
val current = SessionEditorStyle.current()
|
||||
|
||||
repeat(50) { i ->
|
||||
view.append("line $i ")
|
||||
view.applyStyle(if (i % 2 == 0) styled else current)
|
||||
if (i % 5 == 0) view.resetStyles()
|
||||
}
|
||||
|
||||
assertSame(intro, htmls().first())
|
||||
assertSame(editor, editors().single())
|
||||
assertFalse(editor.getEditor(true)!!.isDisposed)
|
||||
assertEquals(2, htmls().size)
|
||||
assertEquals(1, scrolls().size)
|
||||
assertTrue(view.markdown().contains("line 49"))
|
||||
}
|
||||
|
||||
private fun panel(): JPanel = view.component as JPanel
|
||||
|
||||
private fun scrolls(): List<JBScrollPane> = panel().components.filterIsInstance<JBScrollPane>()
|
||||
|
||||
private fun htmls(): List<JBHtmlPane> = panel().components.filterIsInstance<JBHtmlPane>()
|
||||
|
||||
private fun struts(): List<Box.Filler> = panel().components.filterIsInstance<Box.Filler>()
|
||||
|
||||
private fun editors(): List<EditorTextField> = scrolls().mapNotNull { it.viewport.view as? EditorTextField }
|
||||
|
||||
private fun drainEdt() {
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user