mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-86256-PY-86511
[pycharm] PY-86511 Hide `exit` on tags hidden filter Space-RevId: c46d60295a8381d12ba98e8d159eeff8726bc076 [pycharm] PY-87115 Optimize exit info observation coroutines Space-RevId: d89e88ed5cb08ab4f0c1780f74d4825b8483ad1d [pycharm] PY-86256 Change stderr to default color Space-RevId: ffd3bb8f27900ded7c1197541651eb38d8f14dd6 [pycharm] PY-86256 Change error/output to stderr/stdout Space-RevId: 7e0ed184083d72431e5cb17ad8dcebbe65afe460 Merge-request: IJ-MR-189040 Merged-by: David Lysenko <david.lysenko@jetbrains.com> GitOrigin-RevId: 43e8ac85d133dccb37470bd257bd3d78451fad34
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ca8ed8572c
commit
409fe8c4e1
@@ -30,6 +30,10 @@ process.output.output.sections.info.cwd=cwd
|
||||
process.output.output.sections.info.target=target
|
||||
process.output.output.sections.info.env=env
|
||||
|
||||
process.output.output.tag.stdout=stdout
|
||||
process.output.output.tag.stderr=stderr
|
||||
process.output.output.tag.exit=exit
|
||||
|
||||
process.output.output.copySection.tooltip=Copy Section
|
||||
|
||||
process.output.output.sections.output=Process Output
|
||||
|
||||
+13
-3
@@ -434,6 +434,15 @@ class ProcessOutputControllerService(
|
||||
list
|
||||
.filter { it.traceContext == NON_INTERACTIVE_ROOT_TRACE_CONTEXT }
|
||||
.forEach { process ->
|
||||
val exitInfo = process.exitInfo.value
|
||||
|
||||
if (exitInfo != null) {
|
||||
if (exitInfo.exitValue != 0) {
|
||||
backgroundErrorProcesses.value += process.id
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
|
||||
backgroundObservingCoroutines +=
|
||||
launch(CoroutineName(CoroutineNames.EXIT_INFO_COLLECTOR)) {
|
||||
process.exitInfo.collect {
|
||||
@@ -445,6 +454,7 @@ class ProcessOutputControllerService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,9 +569,9 @@ class ProcessOutputControllerService(
|
||||
}
|
||||
|
||||
internal object Tag {
|
||||
const val ERROR = "error"
|
||||
const val OUTPUT = "output"
|
||||
const val EXIT = "exit"
|
||||
val ERROR = message("process.output.output.tag.stdout")
|
||||
val OUTPUT = message("process.output.output.tag.stderr")
|
||||
val EXIT = message("process.output.output.tag.exit")
|
||||
|
||||
val maxLength: Int =
|
||||
Tag::class.java.declaredFields
|
||||
|
||||
+45
-33
@@ -41,12 +41,16 @@ import com.intellij.python.processOutput.impl.Tag
|
||||
import com.intellij.python.processOutput.impl.formatFull
|
||||
import com.intellij.python.processOutput.impl.ui.Colors
|
||||
import com.intellij.python.processOutput.impl.ui.collectReplayAsState
|
||||
import com.intellij.python.processOutput.impl.ui.thenIfNotNull
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.jetbrains.jewel.foundation.theme.JewelTheme
|
||||
import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding
|
||||
|
||||
private object Styling {
|
||||
val COPY_SECTION_BUTTON_SPACE_SIZE = 18.dp
|
||||
val LINE_START_PADDING = 8.dp
|
||||
val LINE_HORIZONTAL_ALIGNMENT = 10.dp
|
||||
val LINE_SPACER_HEIGHT = 4.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -167,19 +171,17 @@ internal fun OutputSection(controller: ProcessOutputController) {
|
||||
items = lines,
|
||||
key = { index, _ -> index },
|
||||
) { index, line ->
|
||||
val outputColor = when (line.kind) {
|
||||
LoggedProcessLine.Kind.OUT -> Color.Unspecified
|
||||
LoggedProcessLine.Kind.ERR -> Colors.Output.ErrorText
|
||||
}
|
||||
// when the kind of the current line does not match the kind of the
|
||||
// previous line, it means that the current line is the start of a
|
||||
// new section
|
||||
val startOfNewSection =
|
||||
lines.getOrNull(index - 1)?.kind != line.kind
|
||||
|
||||
OutputLine(
|
||||
displayTags = isDisplayTags,
|
||||
sectionIndicator =
|
||||
if (lines.getOrNull(index - 1)?.kind != line.kind) {
|
||||
SectionIndicator(
|
||||
line.kind.tag,
|
||||
OutputSectionTestTags.COPY_OUTPUT_TAG_SECTION_BUTTON,
|
||||
) {
|
||||
if (startOfNewSection) {
|
||||
SectionIndicator(line.kind.tag) {
|
||||
controller.copyOutputTagAtIndexToClipboard(
|
||||
loggedProcess,
|
||||
index,
|
||||
@@ -189,16 +191,13 @@ internal fun OutputSection(controller: ProcessOutputController) {
|
||||
null
|
||||
},
|
||||
text = line.text,
|
||||
textStyle = SpanStyle(
|
||||
color = outputColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
exitInfo?.also { exitInfo ->
|
||||
item(key = "exit") {
|
||||
OutputLine(
|
||||
displayTags = true,
|
||||
displayTags = isDisplayTags,
|
||||
sectionIndicator =
|
||||
SectionIndicator(
|
||||
Tag.EXIT,
|
||||
@@ -253,22 +252,31 @@ private fun OutputLine(
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(end = scrollbarContentSafePadding()),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
.padding(
|
||||
end = scrollbarContentSafePadding(),
|
||||
start = Styling.LINE_START_PADDING,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(Styling.LINE_HORIZONTAL_ALIGNMENT),
|
||||
) {
|
||||
DisableSelection {
|
||||
val padding = Tag.maxLength + 3
|
||||
if (displayTags) {
|
||||
DisableSelection {
|
||||
val padding = Tag.maxLength + 1
|
||||
|
||||
Text(
|
||||
text =
|
||||
if (displayTags && sectionIndicator != null) {
|
||||
"${sectionIndicator.tag}:".padStart(padding, ' ')
|
||||
} else {
|
||||
" ".repeat(padding)
|
||||
},
|
||||
style = JewelTheme.consoleTextStyle,
|
||||
fontWeight = FontWeight.Thin,
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
if (sectionIndicator != null) {
|
||||
"${sectionIndicator.tag}:".padStart(padding, ' ')
|
||||
} else {
|
||||
" ".repeat(padding)
|
||||
},
|
||||
style = JewelTheme.consoleTextStyle,
|
||||
fontWeight = FontWeight.Thin,
|
||||
modifier =
|
||||
Modifier.thenIfNotNull(sectionIndicator) {
|
||||
testTag(OutputSectionTestTags.OUTPUT_SECTION_TAG)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
@@ -286,7 +294,7 @@ private fun OutputLine(
|
||||
ActionIconButton(
|
||||
modifier = Modifier
|
||||
.size(Styling.COPY_SECTION_BUTTON_SPACE_SIZE)
|
||||
.testTag(sectionIndicator.testTag),
|
||||
.testTag(sectionIndicator.copyButtonTestTag),
|
||||
iconKey = Icons.Keys.Copy,
|
||||
tooltipText = message("process.output.output.copySection.tooltip"),
|
||||
onClick = sectionIndicator.onCopy,
|
||||
@@ -302,7 +310,7 @@ private fun OutputLine(
|
||||
|
||||
private data class SectionIndicator(
|
||||
val tag: String,
|
||||
val testTag: String,
|
||||
val copyButtonTestTag: String = OutputSectionTestTags.COPY_OUTPUT_TAG_SECTION_BUTTON,
|
||||
val onCopy: () -> Unit,
|
||||
)
|
||||
|
||||
@@ -338,7 +346,7 @@ private fun LazyListScope.infoLineItems(
|
||||
else -> 0
|
||||
}
|
||||
} ?: 0
|
||||
val padding = maxLength + 2
|
||||
val padding = maxLength + 1
|
||||
|
||||
infoLines.forEach { infoLine ->
|
||||
when (infoLine) {
|
||||
@@ -375,8 +383,11 @@ private fun LazyListScope.infoLineItemSingle(
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(end = scrollbarContentSafePadding()),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
.padding(
|
||||
end = scrollbarContentSafePadding(),
|
||||
start = Styling.LINE_START_PADDING,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(Styling.LINE_HORIZONTAL_ALIGNMENT),
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
@@ -406,7 +417,7 @@ private fun LazyListScope.infoLineItemSingle(
|
||||
|
||||
@Composable
|
||||
private fun LineSpacer() {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(Styling.LINE_SPACER_HEIGHT))
|
||||
}
|
||||
|
||||
private sealed class InfoLine {
|
||||
@@ -427,6 +438,7 @@ internal object OutputSectionTestTags {
|
||||
const val NOT_SELECTED_TEXT = "ProcessOutput.Output.NotSelectedText"
|
||||
const val INFO_SECTION = "ProcessOutput.Output.InfoSection"
|
||||
const val OUTPUT_SECTION = "ProcessOutput.Output.OutputSection"
|
||||
const val OUTPUT_SECTION_TAG = "ProcessOutput.Output.OutputSection.Tag"
|
||||
const val FILTERS_TAGS = "ProcessOutput.Output.FiltersTags"
|
||||
const val FILTERS_BUTTON = "ProcessOutput.Output.FiltersButton"
|
||||
const val FILTERS_MENU = "ProcessOutput.Output.FiltersMenu"
|
||||
|
||||
+61
-18
@@ -196,7 +196,6 @@ class ProcessOutputControllerServiceTest {
|
||||
watcher.cancelAndJoin()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `exit info collector coroutines get properly cleaned up`(
|
||||
@TempDir cwd: Path,
|
||||
@@ -213,32 +212,45 @@ class ProcessOutputControllerServiceTest {
|
||||
import sys
|
||||
|
||||
print("test " + sys.argv[1])
|
||||
sys.stdin.read(1)
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
// no exit info collector coroutines should exist
|
||||
assertEquals(
|
||||
0,
|
||||
DebugProbes.dumpCoroutinesInfo()
|
||||
.filter { it.context[CoroutineName.Key]?.name == CoroutineNames.EXIT_INFO_COLLECTOR }
|
||||
.size,
|
||||
)
|
||||
assertEquals(0, exitInfoCollectorCoroutinesCount())
|
||||
|
||||
// spawn 1024 processes
|
||||
repeat(1024) {
|
||||
runBin(binOnEel, Args(MAIN_PY, it.toString()))
|
||||
// spawn 10 processes
|
||||
val processes = mutableListOf<Process>()
|
||||
repeat(10) {
|
||||
processes += runBinWithInput(binOnEel, Args(MAIN_PY, it.toString()))
|
||||
}
|
||||
|
||||
sleepCancellable(1000)
|
||||
// 10 collector coroutines should be active
|
||||
waitUntil {
|
||||
exitInfoCollectorCoroutinesCount() == 10
|
||||
}
|
||||
|
||||
// the count of active exit info collector coroutines should match MAX_PROCESSES
|
||||
assertEquals(
|
||||
ProcessOutputControllerServiceLimits.MAX_PROCESSES,
|
||||
DebugProbes.dumpCoroutinesInfo()
|
||||
.filter { it.context[CoroutineName.Key]?.name == CoroutineNames.EXIT_INFO_COLLECTOR }
|
||||
.size,
|
||||
)
|
||||
// spawn 1024 processes, instantly terminate them
|
||||
repeat(1024) {
|
||||
val process = runBinWithInput(binOnEel, Args(MAIN_PY, (it + 10).toString()))
|
||||
inputAndAwaitExit(process)
|
||||
}
|
||||
|
||||
// 10 collection coroutines should be active
|
||||
waitUntil {
|
||||
exitInfoCollectorCoroutinesCount() == 10
|
||||
}
|
||||
|
||||
// terminating all the processes
|
||||
for (process in processes) {
|
||||
inputAndAwaitExit(process)
|
||||
}
|
||||
|
||||
// no collection coroutines should be active
|
||||
waitUntil {
|
||||
exitInfoCollectorCoroutinesCount() == 0
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -403,6 +415,31 @@ class ProcessOutputControllerServiceTest {
|
||||
companion object {
|
||||
const val MAIN_PY = "main.py"
|
||||
|
||||
suspend fun runBinWithInput(binOnEel: BinOnEel, args: Args): Process =
|
||||
ExecService().executeGetProcess(
|
||||
binOnEel,
|
||||
args,
|
||||
CoroutineScope(NON_INTERACTIVE_ROOT_TRACE_CONTEXT),
|
||||
).getOrThrow()
|
||||
|
||||
suspend fun inputAndAwaitExit(process: Process) {
|
||||
process.outputStream.write(0)
|
||||
process.outputStream.flush()
|
||||
|
||||
coroutineScope {
|
||||
listOf(
|
||||
async(Dispatchers.IO) {
|
||||
process.errorStream.readAllBytes()
|
||||
},
|
||||
async(Dispatchers.IO) {
|
||||
process.inputStream.readAllBytes()
|
||||
},
|
||||
).awaitAll()
|
||||
|
||||
process.awaitExit()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runBin(binOnEel: BinOnEel, args: Args) {
|
||||
withContext(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) {
|
||||
val process = ExecService().executeGetProcess(
|
||||
@@ -425,5 +462,11 @@ class ProcessOutputControllerServiceTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun exitInfoCollectorCoroutinesCount(): Int =
|
||||
DebugProbes.dumpCoroutinesInfo()
|
||||
.filter { it.context[CoroutineName.Key]?.name == CoroutineNames.EXIT_INFO_COLLECTOR }
|
||||
.size
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -117,7 +117,7 @@ internal class OutputSectionTest : ProcessOutputTest() {
|
||||
// 3..6 - stderr
|
||||
// 7..9 - stdout
|
||||
val process = selectTestProcess(
|
||||
listOf(
|
||||
lines = listOf(
|
||||
outLine("out1"),
|
||||
outLine("out2"),
|
||||
outLine("out3"),
|
||||
@@ -207,6 +207,45 @@ internal class OutputSectionTest : ProcessOutputTest() {
|
||||
verify(exactly = 1) { controllerSpy.copyOutputExitInfoToClipboard(process) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tags are displayed or hidden depending on show tags filter`() = processOutputTest {
|
||||
// selecting a process with 3 tags:
|
||||
// 0..2 - stdout
|
||||
// 3..5 - stderr
|
||||
// 6 - exit
|
||||
selectTestProcess(
|
||||
lines = listOf(
|
||||
outLine("out1"),
|
||||
outLine("out2"),
|
||||
outLine("out3"),
|
||||
|
||||
errLine("err4"),
|
||||
errLine("err5"),
|
||||
errLine("err6"),
|
||||
),
|
||||
exitInfo =
|
||||
LoggedProcessExitInfo(
|
||||
exitedAt = Clock.System.now(),
|
||||
exitValue = 0,
|
||||
),
|
||||
)
|
||||
|
||||
// total displayed tags should be 3
|
||||
onAllNodesWithTag(
|
||||
OutputSectionTestTags.OUTPUT_SECTION_TAG,
|
||||
useUnmergedTree = true,
|
||||
).assertCountEquals(3)
|
||||
|
||||
// remove show tags filter
|
||||
processOutputFilters.remove(OutputFilter.ShowTags)
|
||||
|
||||
// total displayed tags should be 0
|
||||
onAllNodesWithTag(
|
||||
OutputSectionTestTags.OUTPUT_SECTION_TAG,
|
||||
useUnmergedTree = true,
|
||||
).assertCountEquals(0)
|
||||
}
|
||||
|
||||
private suspend fun selectTestProcess(
|
||||
lines: List<LoggedProcessLine> = listOf(),
|
||||
exitInfo: LoggedProcessExitInfo? = null,
|
||||
|
||||
+10
-8
@@ -35,17 +35,19 @@ import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
|
||||
import org.junit.Rule
|
||||
|
||||
internal abstract class ProcessOutputTest {
|
||||
private val processTree = MutableStateFlow(buildTree<TreeNode> {})
|
||||
private val processTreeFilters: SnapshotStateSet<TreeFilter> = mutableStateSetOf(
|
||||
protected val processTree = MutableStateFlow(buildTree<TreeNode> {})
|
||||
protected val processTreeFilters: SnapshotStateSet<TreeFilter> = mutableStateSetOf(
|
||||
TreeFilter.ShowTime,
|
||||
)
|
||||
|
||||
private val processOutputFilters: SnapshotStateSet<OutputFilter> = mutableStateSetOf()
|
||||
private val processOutputInfoExpanded = MutableStateFlow(false)
|
||||
private val processOutputOutputExpanded = MutableStateFlow(true)
|
||||
protected val processOutputFilters: SnapshotStateSet<OutputFilter> = mutableStateSetOf(
|
||||
OutputFilter.ShowTags,
|
||||
)
|
||||
protected val processOutputInfoExpanded = MutableStateFlow(false)
|
||||
protected val processOutputOutputExpanded = MutableStateFlow(true)
|
||||
|
||||
private val testSelectedProcess: MutableStateFlow<LoggedProcess?> = MutableStateFlow(null)
|
||||
private val testProcessTreeUiState: TreeUiState = run {
|
||||
protected val testSelectedProcess: MutableStateFlow<LoggedProcess?> = MutableStateFlow(null)
|
||||
protected val testProcessTreeUiState: TreeUiState = run {
|
||||
val selectableLazyListState = SelectableLazyListState(LazyListState())
|
||||
TreeUiState(
|
||||
filters = processTreeFilters,
|
||||
@@ -55,7 +57,7 @@ internal abstract class ProcessOutputTest {
|
||||
tree = processTree,
|
||||
)
|
||||
}
|
||||
private val testProcessOutputUiState: OutputUiState = OutputUiState(
|
||||
protected val testProcessOutputUiState: OutputUiState = OutputUiState(
|
||||
filters = processOutputFilters,
|
||||
isInfoExpanded = processOutputInfoExpanded,
|
||||
isOutputExpanded = processOutputOutputExpanded,
|
||||
|
||||
Reference in New Issue
Block a user