Skip to content

Commit 0fda1cd

Browse files
committed
fix(intellij): leave PyCharm's profilers out of the run marker menu
The menu of a run marker no longer offers PyCharm Professional's Profile and Concurrency Diagram, which Robot Framework runs don't support. Concurrency Diagram, which More Run/Debug and the run widget still offer, now ends with a message as Profile does, instead of running the test under PyCharm's debugger.
1 parent d0cfc19 commit 0fda1cd

8 files changed

Lines changed: 215 additions & 11 deletions

File tree

-13.3 KB
Loading
-13.1 KB
Loading

‎docs/src/content/docs/guides/pycharm/running-tests.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ PyCharm has no Test Explorer like VS Code. You run tests where they are: from th
1212

1313
## Running from the editor
1414

15-
A run marker in the gutter sits beside every test and task, and on the first line of a suite file for the whole suite. Its icon shows the result of the last run. A click on it offers **Run**, **Debug**, **Run with Profile...** and **Debug with Profile...**; in PyCharm Professional also PyCharm's **Profile**, which Robot Framework runs do not support:
15+
A run marker in the gutter sits beside every test and task, and on the first line of a suite file for the whole suite. Its icon shows the result of the last run. A click on it offers **Run**, **Debug**, **Run with Profile...** and **Debug with Profile...**:
1616

1717
<ThemeImage
1818
folder="pycharm"
1919
name="running-gutter-menu"
20-
alt="The menu of the run marker of the test Order Coffee with Run, Debug, Profile and Concurrency Diagram for 'Test Order Coffee', then Run with Profile..., Debug with Profile... and Modify Run Configuration...."
20+
alt="The menu of the run marker of the test Order Coffee with Run, Debug, Run with Profile..., Debug with Profile... and Modify Run Configuration...."
2121
/>
2222

23-
The context menu of the editor runs or debugs what is at the cursor: the test or task when the cursor is in its body, otherwise the suite of the file. <kbd>Ctrl+Shift+F10</kbd> (<kbd>Ctrl+Shift+R</kbd> on macOS) runs it directly. The context menu of the editor tab runs the suite of the file.
23+
The context menu of the editor runs or debugs what is at the cursor: the test or task when the cursor is in its body, otherwise the suite of the file. <kbd>Ctrl+Shift+F10</kbd> (<kbd>Ctrl+Shift+R</kbd> on macOS) runs it directly. The context menu of the editor tab runs the suite of the file. In PyCharm Professional, **More Run/Debug** in the context menu also lists PyCharm's **Profile** and **Concurrency Diagram**; Robot Framework runs support neither, and both end with a message.
2424

2525
A file that Robot Framework cannot read as a suite, for example one with both tests and tasks, gets an error marker on its first line instead; its tooltip names the problem.
2626

‎intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeProgramRunner.kt‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,34 @@ class RobotCodeProgramRunner : AsyncProgramRunner<RunnerSettings>() {
6767
}
6868
}
6969

70+
// the ids of the executors of PyCharm Professional's "Profile" and "Concurrency Diagram"
71+
private const val PROFILER_EXECUTOR_ID = "Profiler"
72+
private const val CONCURRENCY_DIAGRAM_EXECUTOR_ID = "ConcurrencyExecutor"
73+
74+
/** The end of a run of [environment] with the message that Robot Framework runs do not support its executor. */
75+
internal fun unsupportedRun(environment: ExecutionEnvironment): Promise<RunContentDescriptor?> {
76+
val key = when (environment.executor.id) {
77+
CONCURRENCY_DIAGRAM_EXECUTOR_ID -> "run.concurrencyDiagram.notSupported"
78+
else -> "run.profile.notSupported"
79+
}
80+
return rejectedPromise(ExecutionException(RobotCodeBundle.message(key)))
81+
}
82+
7083
/**
71-
* Takes PyCharm Professional's "Profile" for Robot Framework configurations, which its profiler runner would otherwise
72-
* accept as Python run configurations, and ends it with a message.
84+
* Takes PyCharm Professional's "Profile" and "Concurrency Diagram" for Robot Framework configurations, which its
85+
* runners would otherwise accept as Python run configurations, and ends them with a message.
7386
*/
7487
class RobotCodeProfileRunner : AsyncProgramRunner<RunnerSettings>() {
7588
override fun getRunnerId(): String {
7689
return "dev.robotcode.robotcode4ij.execution.RobotCodeProfileRunner"
7790
}
78-
79-
// the id of PyCharm Professional's profiler executor
91+
8092
override fun canRun(executorId: String, profile: RunProfile): Boolean {
81-
return executorId == "Profiler" && isRobotRunProfile(profile)
93+
return (executorId == PROFILER_EXECUTOR_ID || executorId == CONCURRENCY_DIAGRAM_EXECUTOR_ID) &&
94+
isRobotRunProfile(profile)
8295
}
83-
96+
8497
override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
85-
return rejectedPromise(ExecutionException(RobotCodeBundle.message("run.profile.notSupported")))
98+
return unsupportedRun(environment)
8699
}
87100
}

‎intellij-client/src/main/kotlin/dev/robotcode/robotcode4ij/execution/RobotCodeRunLineMarkerContributor.kt‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package dev.robotcode.robotcode4ij.execution
22

3+
import com.intellij.execution.executors.DefaultDebugExecutor
4+
import com.intellij.execution.executors.DefaultRunExecutor
5+
import com.intellij.execution.lineMarker.ExecutorAction
36
import com.intellij.execution.lineMarker.RunLineMarkerContributor
7+
import com.intellij.openapi.actionSystem.AnAction
48
import com.intellij.openapi.project.DumbAware
59
import com.intellij.psi.PsiElement
610
import com.intellij.util.Urls.newLocalFileUrl
@@ -24,6 +28,15 @@ internal fun runMarkerKind(item: RobotCodeTestItem): RunMarkerKind? {
2428
}
2529
}
2630

31+
// the executors that RobotCode's runners take; others, such as PyCharm Professional's Profile and Concurrency Diagram
32+
// in its profiler group, don't support Robot Framework runs
33+
private val RUN_MARKER_EXECUTORS = setOf(DefaultRunExecutor.EXECUTOR_ID, DefaultDebugExecutor.EXECUTOR_ID)
34+
35+
/** The [gutterActions] of a run marker without the executor actions of executors other than Run and Debug. */
36+
internal fun withRobotExecutors(gutterActions: Array<AnAction>): Array<AnAction> {
37+
return gutterActions.filter { it !is ExecutorAction || it.executor.id in RUN_MARKER_EXECUTORS }.toTypedArray()
38+
}
39+
2740
// the markers read the discovery result, not the indexes, so they stay while the IDE indexes
2841
class RobotCodeRunLineMarkerContributor : RunLineMarkerContributor(), DumbAware {
2942
override fun getInfo(element: PsiElement): Info? {
@@ -37,7 +50,7 @@ class RobotCodeRunLineMarkerContributor : RunLineMarkerContributor(), DumbAware
3750
val icon = getTestStateIcon(uri.toString(), element.project, kind == RunMarkerKind.SUITE)
3851
val info = withExecutorActions(icon)
3952
// running once with a profile comes after the executor actions, before "Modify Run Configuration..."
40-
val actions = withProfileActions(info.actions)
53+
val actions = withProfileActions(withRobotExecutors(info.actions))
4154

4255
// the marker on line 1 also shows the problems that discovery reported for the file
4356
val problems =

‎intellij-client/src/main/resources/messages/RobotCode.properties‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ run.target.paths.browse = Add Files or Folders...
214214
run.target.paths.comment = Files and folders separated by spaces, in quotes when they contain spaces; the expanded field shows one per line. Without any, the paths of <code>robot.toml</code> run, or the project folder when it sets none. Given paths replace the paths of <code>robot.toml</code>, and the top-level suite is then named after the given path.
215215
run.target.selection.comment = Full names as Robot Framework names the test, task or suite, separated by spaces, in quotes when they contain spaces, for example <code>"Project.Tests.Sample.First Test"</code>; the expanded field shows one per line.
216216
run.profile.notSupported = Robot Framework runs cannot be profiled.
217+
run.concurrencyDiagram.notSupported = Robot Framework runs have no concurrency diagram.
217218
run.target.selection.empty = No tests or suites are given, so the run covers the configured paths.
218219
run.target.paths.missing = {1,choice,1#The file or folder|1<The files or folders} {0} of the target {1,choice,1#does|1<do} not exist.
219220
run.target.selection.stale = The last test discovery did not find {1,choice,1#the item|1<the items} {0}, for example after a rename. The run passes {1,choice,1#it|1<them} as stored.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package dev.robotcode.robotcode4ij.execution
2+
3+
import com.intellij.execution.Executor
4+
import com.intellij.execution.configurations.RunConfiguration
5+
import com.intellij.execution.configurations.RunProfile
6+
import com.intellij.execution.configurations.RunProfileState
7+
import com.intellij.execution.configurations.runConfigurationType
8+
import com.intellij.execution.executors.DefaultDebugExecutor
9+
import com.intellij.execution.executors.DefaultRunExecutor
10+
import com.intellij.execution.runners.ExecutionEnvironment
11+
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
12+
import com.intellij.icons.AllIcons
13+
import com.intellij.openapi.wm.ToolWindowId
14+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
15+
import javax.swing.Icon
16+
17+
// an executor with the id of one of PyCharm Professional's, which the IDE of the tests need not have
18+
private class ExecutorWithId(private val id: String) : Executor() {
19+
override fun getToolWindowId(): String = ToolWindowId.RUN
20+
override fun getToolWindowIcon(): Icon = AllIcons.Actions.Execute
21+
override fun getIcon(): Icon = AllIcons.Actions.Execute
22+
override fun getDisabledIcon(): Icon? = null
23+
override fun getDescription(): String = id
24+
override fun getActionName(): String = id
25+
override fun getId(): String = id
26+
override fun getStartActionText(): String = id
27+
override fun getContextActionId(): String = "$id.context"
28+
override fun getHelpId(): String? = null
29+
}
30+
31+
class RobotCodeProfileRunnerTest : BasePlatformTestCase() {
32+
33+
private val runner = RobotCodeProfileRunner()
34+
35+
private fun robotConfiguration(): RunConfiguration =
36+
runConfigurationType<RobotCodeConfigurationType>().configurationFactories.first()
37+
.createTemplateConfiguration(project)
38+
39+
fun testTakesProfileAndConcurrencyDiagramOfARobotFrameworkConfiguration() {
40+
val configuration = robotConfiguration()
41+
42+
assertTrue(runner.canRun("Profiler", configuration))
43+
assertTrue(runner.canRun("ConcurrencyExecutor", configuration))
44+
}
45+
46+
fun testTakesNoOtherExecutorAndNoOtherConfiguration() {
47+
val configuration = robotConfiguration()
48+
val other = object : RunProfile {
49+
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? = null
50+
override fun getName(): String = "Python"
51+
override fun getIcon(): Icon? = null
52+
}
53+
54+
assertFalse(runner.canRun(DefaultRunExecutor.EXECUTOR_ID, configuration))
55+
assertFalse(runner.canRun(DefaultDebugExecutor.EXECUTOR_ID, configuration))
56+
assertFalse(runner.canRun("Profiler", other))
57+
assertFalse(runner.canRun("ConcurrencyExecutor", other))
58+
}
59+
60+
fun testEndsEachRunWithTheMessageOfItsExecutor() {
61+
val messages = mapOf(
62+
"Profiler" to "Robot Framework runs cannot be profiled.",
63+
"ConcurrencyExecutor" to "Robot Framework runs have no concurrency diagram.",
64+
)
65+
for ((executorId, message) in messages) {
66+
val environment =
67+
ExecutionEnvironmentBuilder.create(project, ExecutorWithId(executorId), robotConfiguration()).build()
68+
var error: Throwable? = null
69+
70+
unsupportedRun(environment).onError { error = it }
71+
72+
assertEquals(message, error?.message)
73+
}
74+
}
75+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dev.robotcode.robotcode4ij.execution
2+
3+
import com.intellij.execution.Executor
4+
import com.intellij.execution.ExecutorRegistry
5+
import com.intellij.execution.executors.DefaultDebugExecutor
6+
import com.intellij.execution.executors.DefaultRunExecutor
7+
import com.intellij.execution.lineMarker.ExecutorAction
8+
import com.intellij.execution.lineMarker.RunLineMarkerContributor
9+
import com.intellij.icons.AllIcons
10+
import com.intellij.openapi.actionSystem.ActionManager
11+
import com.intellij.openapi.actionSystem.AnAction
12+
import com.intellij.openapi.wm.ToolWindowId
13+
import com.intellij.psi.PsiDocumentManager
14+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
15+
import dev.robotcode.robotcode4ij.actions.DEBUG_WITH_PROFILE_ACTION_ID
16+
import dev.robotcode.robotcode4ij.actions.RUN_WITH_PROFILE_ACTION_ID
17+
import dev.robotcode.robotcode4ij.testing.Position
18+
import dev.robotcode.robotcode4ij.testing.Range
19+
import dev.robotcode.robotcode4ij.testing.RobotCodeTestItem
20+
import dev.robotcode.robotcode4ij.testing.testManger
21+
import dev.robotcode.robotcode4ij.testing.uri
22+
import javax.swing.Icon
23+
24+
private const val OTHER_EXECUTOR_ID = "dev.robotcode.robotcode4ij.test.OtherExecutor"
25+
26+
// an executor besides Run and Debug, as PyCharm Professional's profiler group is one; the platform registers its
27+
// context action, so that the gutter offers it
28+
private class OtherExecutor : Executor() {
29+
override fun getToolWindowId(): String = ToolWindowId.RUN
30+
override fun getToolWindowIcon(): Icon = AllIcons.Actions.Execute
31+
override fun getIcon(): Icon = AllIcons.Actions.Execute
32+
override fun getDisabledIcon(): Icon? = null
33+
override fun getDescription(): String = "Runs with another tool"
34+
override fun getActionName(): String = "Other"
35+
override fun getId(): String = OTHER_EXECUTOR_ID
36+
override fun getStartActionText(): String = "Run with Other"
37+
override fun getContextActionId(): String = "$OTHER_EXECUTOR_ID.context"
38+
override fun getHelpId(): String? = null
39+
}
40+
41+
class RobotCodeRunMarkerExecutorsTest : BasePlatformTestCase() {
42+
43+
override fun setUp() {
44+
super.setUp()
45+
// the registry adds the context action of an executor that is registered later
46+
ExecutorRegistry.getInstance()
47+
Executor.EXECUTOR_EXTENSION_NAME.point.registerExtension(OtherExecutor(), testRootDisposable)
48+
}
49+
50+
override fun tearDown() {
51+
try {
52+
project.testManger.setTestItemsForTests(arrayOf())
53+
} finally {
54+
super.tearDown()
55+
}
56+
}
57+
58+
// a test "First" on the second line of a suite file
59+
private fun suiteWithOneTest() {
60+
val file = myFixture.configureByText("suite.robot", "*** Test Cases ***\nFirst\n Log first\n").virtualFile
61+
val test = RobotCodeTestItem(
62+
type = "test", id = "suite.First", name = "First", longname = "Suite.First", lineno = 2,
63+
uri = file.uri, source = file.path, range = Range(Position(1u, 0u), Position(1u, 5u))
64+
)
65+
val suite = RobotCodeTestItem(
66+
type = "suite", id = "suite", name = "Suite", longname = "Suite", uri = file.uri, source = file.path,
67+
children = arrayOf(test)
68+
)
69+
project.testManger.setTestItemsForTests(arrayOf(suite))
70+
}
71+
72+
private fun executorIds(actions: Array<AnAction>) = actions.filterIsInstance<ExecutorAction>().map { it.executor.id }
73+
74+
private fun assertOnlyRunAndDebugAmongTheExecutors(info: RunLineMarkerContributor.Info?) {
75+
val gutter = ExecutorAction.getActions(1)
76+
assertTrue(OTHER_EXECUTOR_ID in executorIds(gutter))
77+
78+
val actions = info!!.actions
79+
val manager = ActionManager.getInstance()
80+
val extras = gutter.filter { it !is ExecutorAction }
81+
82+
assertEquals(listOf(DefaultRunExecutor.EXECUTOR_ID, DefaultDebugExecutor.EXECUTOR_ID), executorIds(actions))
83+
assertEquals(listOf(RUN_WITH_PROFILE_ACTION_ID, DEBUG_WITH_PROFILE_ACTION_ID), actions.drop(2).take(2).map {
84+
manager.getId(it)
85+
})
86+
assertEquals(extras, actions.drop(4))
87+
}
88+
89+
fun testTheRunMarkerOfATestOffersOnlyRunAndDebugAmongTheExecutors() {
90+
suiteWithOneTest()
91+
val document = PsiDocumentManager.getInstance(project).getDocument(myFixture.file)!!
92+
val testName = myFixture.file.findElementAt(document.getLineStartOffset(1))!!
93+
94+
assertOnlyRunAndDebugAmongTheExecutors(RobotCodeRunLineMarkerContributor().getInfo(testName))
95+
}
96+
97+
fun testTheRunMarkerOfASuiteOffersOnlyRunAndDebugAmongTheExecutors() {
98+
suiteWithOneTest()
99+
100+
assertOnlyRunAndDebugAmongTheExecutors(RobotCodeRunLineMarkerContributor().getInfo(myFixture.file))
101+
}
102+
}

0 commit comments

Comments
 (0)