Skip to content

feat(pycharm): add configuration screen with variable, environment variables and testsuite file support #431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package dev.robotcode.robotcode4ij.execution

import com.intellij.execution.Executor
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.LocatableConfigurationBase
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.testframework.sm.runner.SMRunnerConsolePropertiesProvider
Expand All @@ -17,32 +17,49 @@ class RobotCodeRunConfiguration(project: Project, factory: ConfigurationFactory)
LocatableConfigurationBase<ConfigurationFactory>
(project, factory, "Robot Framework"), SMRunnerConsolePropertiesProvider {

override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
return RobotCodeRunProfileState(this, environment)
}
// Environment variables
var environmentVariables: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT

override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> {
// TODO: Implement configuration editor
return RobotCodeRunConfigurationEditor()
}
// Variables
var variables: String? = null

// Test suite path
var testSuitePath: String? = null

var includedTestItems: List<RobotCodeTestItem> = emptyList()
// Additional arguments
var additionalArguments: String? = null

var paths: List<String> = emptyList()
var includedTestItems: String? = null

override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
return RobotCodeRunProfileState(this, environment)
}

override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties {
return RobotRunnerConsoleProperties(this, "Robot Framework", executor)
}

override fun getConfigurationEditor(): SettingsEditor<out RobotCodeRunConfiguration> {
return RobotCodeRunConfigurationEditor(project)
}

override fun writeExternal(element: Element) {
super.writeExternal(element)
// TODO: Implement serialization
// XmlSerializer.serializeInto(this, element)
// Save data to XML
environmentVariables.writeExternal(element)
element.setAttribute("testitems", includedTestItems ?: "")
element.setAttribute("variables", variables ?: "")
element.setAttribute("testSuitePath", testSuitePath ?: "")
element.setAttribute("additionalArguments", additionalArguments ?: "")
}

override fun readExternal(element: Element) {
super.readExternal(element)
// TODO: Implement deserialization
// XmlSerializer.deserializeInto(this, element)
// Read data from XML
environmentVariables = EnvironmentVariablesData.readExternal(element)
variables = element.getAttributeValue("variables")
testSuitePath = element.getAttributeValue("testSuitePath")
additionalArguments = element.getAttributeValue("additionalArguments")
includedTestItems = element.getAttributeValue("testitems")
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
package dev.robotcode.robotcode4ij.execution

import com.intellij.execution.configuration.EnvironmentVariablesComponent
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.ui.RawCommandLineEditor
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.ComponentWithEmptyText
import javax.swing.JComponent
import javax.swing.JTextField

class RobotCodeRunConfigurationEditor : SettingsEditor<RobotCodeRunConfiguration>() {
class RobotCodeRunConfigurationEditor(private val project: Project) : SettingsEditor<RobotCodeRunConfiguration>() {

private val environmentVariablesField = EnvironmentVariablesComponent()

private val variablesField =
RawCommandLineEditor().apply {
if (textField is ComponentWithEmptyText) {
(textField as ComponentWithEmptyText).emptyText.text = "Define variables, e.g. VAR1=value1, VAR2=value2"
}
}

private val includedTestItemsField = JTextField().apply {
toolTipText = "Enter testitems separated by commas, e.g., MyProject.tests.testsuite1.testcase1,MyProject.tests.testsuite1.testcase2"
}

private val testSuitePathField = TextFieldWithBrowseButton().apply {
addBrowseFolderListener(
project,
FileChooserDescriptorFactory.createSingleFileDescriptor()
.withTitle("Select Test Suite")
.withDescription("Select the path to the test suite")
)
}

private val argumentsField =
RawCommandLineEditor().apply {
if (textField is ComponentWithEmptyText) {
Expand All @@ -21,23 +45,56 @@ class RobotCodeRunConfigurationEditor : SettingsEditor<RobotCodeRunConfiguration
}

override fun resetEditorFrom(s: RobotCodeRunConfiguration) {
// TODO("Not yet implemented")
// Reset the environment variables field
environmentVariablesField.envData = s.environmentVariables

// Reset the variables field
variablesField.text = s.variables ?: ""

// Reset the test suite path field
testSuitePathField.text = s.testSuitePath ?: ""

// Reset the additional arguments field
argumentsField.text = s.additionalArguments ?: ""

// Reset the included test cases field
includedTestItemsField.text = s.includedTestItems
}

override fun applyEditorTo(s: RobotCodeRunConfiguration) {
// TODO("Not yet implemented")
// Apply the environment variables field
s.environmentVariables = environmentVariablesField.envData

// Apply the variables field
s.variables = variablesField.text.ifBlank { null }

// Apply the test suite path field
s.testSuitePath = testSuitePathField.text.ifBlank { null }

// Apply the additional arguments field
s.additionalArguments = argumentsField.text.ifBlank { null }

// Apply the included test items field
s.includedTestItems = includedTestItemsField.text
}

override fun createEditor(): JComponent {
return panel {
row("&Robot:") {
textField().label("Suite:")
row("Test Suite Path:") {
cell(testSuitePathField).align(AlignX.FILL)
}
row("Included Test Items:") {
cell(includedTestItemsField).align(AlignX.FILL)
}
row(environmentVariablesField.label) {
row("Environment Variables:") {
cell(environmentVariablesField.component).align(AlignX.FILL)
}
row("A&rguments:") { cell(argumentsField).align(AlignX.FILL) }
row("Variables:") {
cell(variablesField).align(AlignX.FILL)
}
row("Additional Arguments:") {
cell(argumentsField).align(AlignX.FILL)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class RobotCodeRunConfigurationProducer : LazyRunConfigurationProducer<RobotCode
} ${testItem.name}"

if (testItem.type != "workspace") {
configuration.includedTestItems = listOf(testItem)
configuration.includedTestItems = testItem.longname
}

return true
Expand All @@ -57,9 +57,9 @@ class RobotCodeRunConfigurationProducer : LazyRunConfigurationProducer<RobotCode
val testItem = configuration.project.testManger.findTestItem(psiElement) ?: return false

if (testItem.type == "workspace") {
return configuration.includedTestItems.isEmpty()
return configuration.includedTestItems.isNullOrEmpty()
}
return configuration.includedTestItems == listOf(testItem)
return configuration.includedTestItems == testItem.longname
}

override fun isPreferredConfiguration(self: ConfigurationFromContext?, other: ConfigurationFromContext?): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.intellij.execution.ExecutionException
import com.intellij.execution.ExecutionResult
import com.intellij.execution.Executor
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
Expand All @@ -25,6 +26,7 @@ import com.jetbrains.rd.util.reactive.adviseEternal
import dev.robotcode.robotcode4ij.buildRobotCodeCommandLine
import dev.robotcode.robotcode4ij.debugging.RobotCodeDebugProgramRunner
import dev.robotcode.robotcode4ij.debugging.RobotCodeDebugProtocolClient
import dev.robotcode.robotcode4ij.testing.testManger
import dev.robotcode.robotcode4ij.utils.NetUtils.findFreePort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
Expand All @@ -40,6 +42,7 @@ import org.eclipse.lsp4j.debug.services.IDebugProtocolServer
import org.eclipse.lsp4j.jsonrpc.Launcher
import java.net.Socket
import java.net.SocketTimeoutException
import kotlin.io.path.Path
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

Expand Down Expand Up @@ -74,46 +77,84 @@ class RobotCodeRunProfileState(private val config: RobotCodeRunConfiguration, en

override fun startProcess(): ProcessHandler {
val project = environment.project
val profile =
environment.runProfile as? RobotCodeRunConfiguration ?: throw CantRunException("Invalid run configuration")
val profile = environment.runProfile as? RobotCodeRunConfiguration
?: throw CantRunException("Invalid run configuration")

// Determine default paths
val testSuitePath = profile.testSuitePath

// TODO: Add support for configurable paths
val defaultPaths = arrayOf("--default-path", ".")

// Prepare variables as command line arguments
val variables = profile.variables?.split(",")?.mapNotNull {
val keyValue = it.split("=")
if (keyValue.size == 2) "-v ${keyValue[0]}:${keyValue[1]}" else null
}?.toTypedArray() ?: emptyArray()

// Collect additional arguments
val additionalArguments = profile.additionalArguments?.split(" ")?.toTypedArray() ?: emptyArray()

val debug = environment.runner is RobotCodeDebugProgramRunner

val included = mutableListOf<String>()
for (test in profile.includedTestItems) {
included.add("--by-longname")
included.add(test.longname)

if (testSuitePath != null){
val testitem = profile.project.testManger.findTestItem(Path(testSuitePath).toUri().toString())
if (testitem != null) {
included.add("--by-longname")
included.add(testitem.longname)
}
}
if (!profile.includedTestItems.isNullOrEmpty()) {
val testItemLongNames = profile.includedTestItems!!.split(",").map { it.trim() }.filter { it.isNotEmpty() }
for (testItemLongName in testItemLongNames){
included.add("--by-longname")
included.add(testItemLongName)
}
}

val connection = mutableListOf<String>()

val port = findFreePort(DEBUGGER_DEFAULT_PORT)
if (port != DEBUGGER_DEFAULT_PORT) {
included.add("--tcp")
included.add(port.toString())
}

// Combine all parts into the final command line
val commandLine = project.buildRobotCodeCommandLine(
arrayOf(
*defaultPaths,
"debug",
*connection.toTypedArray(),
*(if (!debug) arrayOf("--no-debug") else arrayOf()),
*(included.toTypedArray())
), noColor = false // ,extraArgs = arrayOf("-v", "--log", "--log-level", "TRACE")
*included.toTypedArray(),
*variables,
*additionalArguments
),
noColor = false
)

// Apply environment variables
val environmentVariables = profile.environmentVariables.envs
for ((key, value) in environmentVariables) {
commandLine.environment[key] = value
}

// Apply "passParentEnvironment" if set to false
commandLine.withParentEnvironmentType(
if (profile.environmentVariables.isPassParentEnvs) {
GeneralCommandLine.ParentEnvironmentType.CONSOLE
} else {
GeneralCommandLine.ParentEnvironmentType.NONE
}
)

val handler = KillableColoredProcessHandler(commandLine) // handler.setHasPty(true)
val handler = KillableColoredProcessHandler(commandLine)
handler.putUserData(DEBUG_PORT, port)
ProcessTerminatedListener.attach(handler)
handler.addProcessListener(this)

// RunContentManager.getInstance(project).showRunContent(environment.executor, handler)

return handler
}

Expand Down Expand Up @@ -142,8 +183,8 @@ class RobotCodeRunProfileState(private val config: RobotCodeRunConfiguration, en
consoleProperties.state = this
}

var splitterPropertyName = SMTestRunnerConnectionUtil.getSplitterPropertyName(TESTFRAMEWORK_NAME)
var consoleView = RobotCodeRunnerConsoleView(consoleProperties, splitterPropertyName)
val splitterPropertyName = SMTestRunnerConnectionUtil.getSplitterPropertyName(TESTFRAMEWORK_NAME)
val consoleView = RobotCodeRunnerConsoleView(consoleProperties, splitterPropertyName)
SMTestRunnerConnectionUtil.initConsoleView(consoleView, TESTFRAMEWORK_NAME)
consoleView.attachToProcess(processHandler)
consoleRef.set(consoleView)
Expand Down Expand Up @@ -192,7 +233,7 @@ class RobotCodeRunProfileState(private val config: RobotCodeRunConfiguration, en
@OptIn(ExperimentalUuidApi::class) override fun startNotified(event: ProcessEvent) {
runBlocking(Dispatchers.IO) {

var port = event.processHandler.getUserData(DEBUG_PORT) ?: throw CantRunException("No debug port found.")
val port = event.processHandler.getUserData(DEBUG_PORT) ?: throw CantRunException("No debug port found.")

socket = tryConnectToServerWithTimeout("127.0.0.1", port, 10000, retryIntervalMillis = 100)
?: throw CantRunException("Unable to establish connection to debug server.")
Expand Down Expand Up @@ -232,7 +273,7 @@ class RobotCodeRunProfileState(private val config: RobotCodeRunConfiguration, en
}

afterConfigurationDone.fire(Unit)
debugServer.attach(emptyMap<String, Object>())
debugServer.attach(emptyMap<String, Any>())
}
}

Expand Down