From 9e4826559eef7c734060421294645b6e90eeb4d7 Mon Sep 17 00:00:00 2001 From: sksamuel Date: Sun, 11 Aug 2024 12:41:56 -0500 Subject: [PATCH 1/2] Added gradle runner as default --- .../io/kotest/plugin/intellij/Constants.kt | 2 + .../intellij/KotestConfigurationFactory.kt | 1 + .../intellij/KotestTestConsoleProperties.kt | 2 + .../plugin/intellij/KotestTestLocator.kt | 3 +- .../plugin/intellij/RerunFailedTestsAction.kt | 1 + .../plugin/intellij/actions/RunAction.kt | 2 +- .../run/GradleSpecRunConfigurationProducer.kt | 1 + .../run/GradleTestRunConfigurationProducer.kt | 147 ++++++++++++++++++ .../intellij/run/KotestConsoleProperties.kt | 24 +++ .../intellij/run/KotestDebuggerRunner.kt | 2 +- .../{ => run}/KotestRunConfiguration.kt | 7 +- .../run/KotestRunConfigurationProducer.kt | 4 + .../intellij/run/KotestRunnableState.kt | 1 - .../run/KotestSMTRunnerConsoleView.kt | 32 ++++ .../run/KotestTestsExecutionConsoleManager.kt | 121 ++++++++++++++ .../run/PackageRunConfigurationProducer.kt | 1 - .../io/kotest/plugin/intellij/run/RunData.kt | 1 + .../run/SpecRunConfigurationProducer.kt | 2 +- .../plugin/intellij/run/TestEventHandler.kt | 72 +++++++++ .../run/TestPathRunConfigurationProducer.kt | 2 +- .../teamcity/TeamCityMessageParser.kt | 41 +++++ .../intellij/ui/KotestSettingsEditor.kt | 2 +- .../intellij/TeamCityMessageParserTest.kt | 36 +++++ 23 files changed, 495 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/GradleTestRunConfigurationProducer.kt create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/KotestConsoleProperties.kt rename src/main/kotlin/io/kotest/plugin/intellij/{ => run}/KotestRunConfiguration.kt (98%) create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfigurationProducer.kt create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/KotestSMTRunnerConsoleView.kt create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/KotestTestsExecutionConsoleManager.kt create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/run/TestEventHandler.kt create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt create mode 100644 src/test/kotlin/io/kotest/plugin/intellij/TeamCityMessageParserTest.kt diff --git a/src/main/kotlin/io/kotest/plugin/intellij/Constants.kt b/src/main/kotlin/io/kotest/plugin/intellij/Constants.kt index 59ddeb1e..9415f9fe 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/Constants.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/Constants.kt @@ -8,6 +8,8 @@ class Constants { // kotst 4.1.x -> 4.6.x protocol string val OldLocatorProtocol = "kotest" val FrameworkId = "ioKotest" + + val gradleTaskName = "kotest" } // flip this bit in tests diff --git a/src/main/kotlin/io/kotest/plugin/intellij/KotestConfigurationFactory.kt b/src/main/kotlin/io/kotest/plugin/intellij/KotestConfigurationFactory.kt index f44c8ac6..308de4e6 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/KotestConfigurationFactory.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/KotestConfigurationFactory.kt @@ -4,6 +4,7 @@ import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationType import com.intellij.execution.configurations.RunConfiguration import com.intellij.openapi.project.Project +import io.kotest.plugin.intellij.run.KotestRunConfiguration class KotestConfigurationFactory(configurationType: ConfigurationType) : ConfigurationFactory(configurationType) { diff --git a/src/main/kotlin/io/kotest/plugin/intellij/KotestTestConsoleProperties.kt b/src/main/kotlin/io/kotest/plugin/intellij/KotestTestConsoleProperties.kt index c50aa2c4..fa386fab 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/KotestTestConsoleProperties.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/KotestTestConsoleProperties.kt @@ -6,7 +6,9 @@ import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsActi import com.intellij.execution.testframework.sm.runner.SMTestLocator import com.intellij.execution.ui.ConsoleView import com.intellij.psi.search.GlobalSearchScope +import io.kotest.plugin.intellij.run.KotestRunConfiguration +@Deprecated("Migrating to Kotest external system runner") class KotestTestConsoleProperties( config: KotestRunConfiguration, executor: Executor diff --git a/src/main/kotlin/io/kotest/plugin/intellij/KotestTestLocator.kt b/src/main/kotlin/io/kotest/plugin/intellij/KotestTestLocator.kt index a5339f49..17e55d32 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/KotestTestLocator.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/KotestTestLocator.kt @@ -3,6 +3,7 @@ package io.kotest.plugin.intellij import com.intellij.execution.Location import com.intellij.execution.PsiLocation import com.intellij.execution.testframework.sm.runner.SMTestLocator +import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.util.ModificationTracker import com.intellij.psi.PsiElement @@ -23,7 +24,7 @@ import io.kotest.plugin.intellij.psi.toPsiLocation * * Kotest 4 reported it's located hints as kotest://class:linenumber */ -class KotestTestLocator : SMTestLocator { +class KotestTestLocator : SMTestLocator, DumbAware { override fun getLocation( protocol: String, diff --git a/src/main/kotlin/io/kotest/plugin/intellij/RerunFailedTestsAction.kt b/src/main/kotlin/io/kotest/plugin/intellij/RerunFailedTestsAction.kt index a61f78bd..da669871 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/RerunFailedTestsAction.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/RerunFailedTestsAction.kt @@ -7,6 +7,7 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.testframework.SourceScope import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.module.Module +import io.kotest.plugin.intellij.run.KotestRunConfiguration import io.kotest.plugin.intellij.run.KotestRunnableState class RerunFailedTestsAction(consoleView: ConsoleView, diff --git a/src/main/kotlin/io/kotest/plugin/intellij/actions/RunAction.kt b/src/main/kotlin/io/kotest/plugin/intellij/actions/RunAction.kt index 18d9b256..3459a161 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/actions/RunAction.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/actions/RunAction.kt @@ -10,7 +10,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import io.kotest.plugin.intellij.KotestConfigurationFactory import io.kotest.plugin.intellij.KotestConfigurationType -import io.kotest.plugin.intellij.KotestRunConfiguration +import io.kotest.plugin.intellij.run.KotestRunConfiguration import io.kotest.plugin.intellij.run.generateName import io.kotest.plugin.intellij.toolwindow.ModuleNodeDescriptor import io.kotest.plugin.intellij.toolwindow.SpecNodeDescriptor diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/GradleSpecRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/GradleSpecRunConfigurationProducer.kt index b95dbad3..a8a3e5d6 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/GradleSpecRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/GradleSpecRunConfigurationProducer.kt @@ -11,6 +11,7 @@ import org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigu /** * Shows a gradle run icon if the test runner is set to gradle or choose per test. */ +@Deprecated("Migrating to Kotest specific tasks") class GradleSpecRunConfigurationProducer : TestClassGradleConfigurationProducer() { override fun getPsiClassForLocation(contextLocation: Location<*>): PsiClass? { diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/GradleTestRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/GradleTestRunConfigurationProducer.kt new file mode 100644 index 00000000..53e47bd3 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/GradleTestRunConfigurationProducer.kt @@ -0,0 +1,147 @@ +package io.kotest.plugin.intellij.run + +import com.intellij.execution.JavaRunConfigurationExtensionManager +import com.intellij.execution.RunManager +import com.intellij.execution.actions.ConfigurationContext +import com.intellij.execution.actions.ConfigurationFromContext +import com.intellij.execution.actions.RunConfigurationProducer +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.openapi.module.Module +import com.intellij.openapi.util.Ref +import com.intellij.psi.PsiElement +import io.kotest.plugin.intellij.Constants +import io.kotest.plugin.intellij.Test +import io.kotest.plugin.intellij.psi.enclosingKtClass +import io.kotest.plugin.intellij.styles.SpecStyle +import org.jetbrains.plugins.gradle.execution.GradleRunnerUtil +import org.jetbrains.plugins.gradle.execution.build.CachedModuleDataFinder +import org.jetbrains.plugins.gradle.service.execution.GradleExternalTaskConfigurationType +import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration +import org.jetbrains.plugins.gradle.service.project.GradleTasksIndices +import org.jetbrains.plugins.gradle.util.GradleModuleData + +/** + * Runs a Kotest individual test using the Kotest custom gradle task. + */ +class GradleTestRunConfigurationProducer : RunConfigurationProducer(true) { + + override fun getConfigurationFactory(): ConfigurationFactory { + return GradleExternalTaskConfigurationType.getInstance().factory + } + + /** + * When two configurations are created from the same context by two different producers, checks if the configuration created by + * this producer should be discarded in favor of the other one. + * + * We always return true because no one else should be creating Kotest configurations. + */ + override fun isPreferredConfiguration(self: ConfigurationFromContext?, other: ConfigurationFromContext?): Boolean { + return true + } + + override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean { + return false + } + + /** + * This function is called to check if the given context is applicable to this run producer as + * well as configure the configuration based on the template. + * + * In other words, it's used to determine if this run producer should act on the run context that + * was initialized by the user. In our case, we want to act when the user clicks run on a test case. + */ + override fun setupConfigurationFromContext( + configuration: GradleRunConfiguration, + context: ConfigurationContext, + sourceElement: Ref + ): Boolean { + + if (!hasKotestTask(context.module)) return false + + val element = sourceElement.get() + if (element != null) { + val test = findTest(element) + if (test != null) { + + val spec = element.enclosingKtClass() ?: return false + val fqn = spec.fqName ?: return false + + val project = context.project ?: return false + val module = context.module ?: return false + + val externalProjectPath = resolveProjectPath(module) ?: return false + val location = context.location ?: return false + + val gradleModuleData = CachedModuleDataFinder.getGradleModuleData(module) ?: return false + val path = gradleModuleData.getTaskPath(Constants().gradleTaskName) + + configuration.name = generateName(spec, test) + configuration.setDebugServerProcess(false) + + val runManager = RunManager.getInstance(project) + runManager.setUniqueNameIfNeeded(configuration) + + configuration.settings.externalProjectPath = externalProjectPath + configuration.settings.taskNames = + listOf( + path, + "--tests '${fqn.asString()}'" + ) + configuration.settings.scriptParameters = "" + + JavaRunConfigurationExtensionManager.instance.extendCreatedConfiguration(configuration, location) + return true + } + } + return false + } + + private fun hasKotestTask(module: Module): Boolean { + val externalProjectPath = resolveProjectPath(module) ?: return false + return GradleTasksIndices.getInstance(module.project) + .findTasks(externalProjectPath) + .any { it.name.endsWith(Constants().gradleTaskName) } + } + + /** + * This function is called to check if the given configuration should be re-used. + * This stops a new context being created every time the user runs the same test. + */ + override fun isConfigurationFromContext( + configuration: GradleRunConfiguration, + context: ConfigurationContext + ): Boolean { + + if (!hasKotestTask(context.module)) return false + + val element = context.psiLocation + if (element != null) { + val test = findTest(element) + if (test != null) { + val spec = element.enclosingKtClass() + return false + +// return configuration.getTestPath() == test.testPath() +// && configuration.getPackageName().isNullOrBlank() +// && configuration.getSpecName() == spec?.fqName?.asString() + } + } + return false + } + + private fun findTest(element: PsiElement): Test? { + return SpecStyle.styles.asSequence() + .filter { it.isContainedInSpec(element) } + .mapNotNull { it.findAssociatedTest(element) } + .firstOrNull() + } + + private fun resolveProjectPath(module: Module): String? { + val gradleModuleData: GradleModuleData = CachedModuleDataFinder.getGradleModuleData(module) ?: return null + val isGradleProjectDirUsedToRunTasks = gradleModuleData.directoryToRunTask == gradleModuleData.gradleProjectDir + if (!isGradleProjectDirUsedToRunTasks) { + return gradleModuleData.directoryToRunTask + } + return GradleRunnerUtil.resolveProjectPath(module) + } +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestConsoleProperties.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestConsoleProperties.kt new file mode 100644 index 00000000..ef9f83a6 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestConsoleProperties.kt @@ -0,0 +1,24 @@ +package io.kotest.plugin.intellij.run + +import com.intellij.execution.Executor +import com.intellij.execution.Location +import com.intellij.execution.testframework.JavaAwareTestConsoleProperties +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties +import com.intellij.execution.testframework.sm.runner.SMTestLocator +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration +import com.intellij.pom.Navigatable +import io.kotest.plugin.intellij.KotestTestLocator +import javax.swing.tree.TreeSelectionModel + +class KotestConsoleProperties(conf: ExternalSystemRunConfiguration, executor: Executor) : + SMTRunnerConsoleProperties(conf, "kotest", executor) { + override fun getTestLocator(): SMTestLocator = KotestTestLocator() + override fun isIdBasedTestTree(): Boolean = true + override fun isEditable(): Boolean = true + override fun isPrintTestingStartedTime(): Boolean = true + override fun getSelectionMode(): Int = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION + override fun getErrorNavigatable(location: Location<*>, stacktrace: String): Navigatable? { + return JavaAwareTestConsoleProperties.getStackTraceErrorNavigatable(location, stacktrace) + } + +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestDebuggerRunner.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestDebuggerRunner.kt index 4f7ec59e..d06a0e46 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestDebuggerRunner.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestDebuggerRunner.kt @@ -3,8 +3,8 @@ package io.kotest.plugin.intellij.run import com.intellij.execution.JavaTestFrameworkDebuggerRunner import com.intellij.execution.configurations.RunProfile import io.kotest.plugin.intellij.Constants -import io.kotest.plugin.intellij.KotestRunConfiguration +@Deprecated("Migrating to Kotest external system runner") class KotestDebuggerRunner : JavaTestFrameworkDebuggerRunner() { override fun validForProfile(profile: RunProfile): Boolean { diff --git a/src/main/kotlin/io/kotest/plugin/intellij/KotestRunConfiguration.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfiguration.kt similarity index 98% rename from src/main/kotlin/io/kotest/plugin/intellij/KotestRunConfiguration.kt rename to src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfiguration.kt index aa9cc2a8..f11a3375 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/KotestRunConfiguration.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfiguration.kt @@ -1,6 +1,6 @@ @file:Suppress("RedundantOverride") -package io.kotest.plugin.intellij +package io.kotest.plugin.intellij.run import com.intellij.execution.AlternativeJrePathConverter import com.intellij.execution.Executor @@ -24,14 +24,13 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.refactoring.listeners.RefactoringElementListener -import io.kotest.plugin.intellij.run.KotestRunnableState -import io.kotest.plugin.intellij.run.RunData -import io.kotest.plugin.intellij.run.suggestedName +import io.kotest.plugin.intellij.KotestTestConsoleProperties import io.kotest.plugin.intellij.ui.KotestSettingsEditor import org.jdom.Element import org.jetbrains.jps.model.serialization.PathMacroUtil import org.jetbrains.kotlin.psi.KtClassOrObject +@Deprecated("Migrating to Kotest external system runner") class KotestRunConfiguration(name: String, factory: ConfigurationFactory, project: Project) : JavaTestConfigurationBase(name, JavaRunConfigurationModule(project, false), factory), TargetEnvironmentAwareRunProfile { diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfigurationProducer.kt new file mode 100644 index 00000000..786980f7 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunConfigurationProducer.kt @@ -0,0 +1,4 @@ +package io.kotest.plugin.intellij.run + +class KotestRunConfigurationProducer { +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunnableState.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunnableState.kt index 802efb19..3e103391 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunnableState.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestRunnableState.kt @@ -8,7 +8,6 @@ import com.intellij.execution.testframework.TestSearchScope import com.intellij.openapi.module.Module import com.intellij.util.PathUtil import io.kotest.plugin.intellij.Constants -import io.kotest.plugin.intellij.KotestRunConfiguration import java.io.File class KotestRunnableState( diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestSMTRunnerConsoleView.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestSMTRunnerConsoleView.kt new file mode 100644 index 00000000..41f09a76 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestSMTRunnerConsoleView.kt @@ -0,0 +1,32 @@ +package io.kotest.plugin.intellij.run + +import com.intellij.build.BuildViewSettingsProvider +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView +import com.intellij.execution.ui.ConsoleViewContentType + +class KotestSMTRunnerConsoleView( + consoleProperties: KotestConsoleProperties, + splitterPropertyName: String +) : SMTRunnerConsoleView(consoleProperties, splitterPropertyName), BuildViewSettingsProvider { + + private var lastMessageWasEmptyLine = false + + override fun isExecutionViewHidden() = true + + override fun print(s: String, contentType: ConsoleViewContentType) { + if (detectUnwantedEmptyLine(s)) return; + super.print(s, contentType); + } + + // IJ test runner events protocol produces many unwanted empty strings + // this is a workaround to avoid the trash in the console + private fun detectUnwantedEmptyLine(s: String): Boolean { + if (com.intellij.execution.Platform.current().lineSeparator == s) { + if (lastMessageWasEmptyLine) return true + lastMessageWasEmptyLine = true + } else { + lastMessageWasEmptyLine = false + } + return false + } +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/KotestTestsExecutionConsoleManager.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestTestsExecutionConsoleManager.kt new file mode 100644 index 00000000..f3e560a9 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/KotestTestsExecutionConsoleManager.kt @@ -0,0 +1,121 @@ +package io.kotest.plugin.intellij.run + +import com.intellij.execution.process.ProcessAdapter +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil +import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener +import com.intellij.execution.testframework.sm.runner.SMTestProxy +import com.intellij.execution.ui.ConsoleViewContentType +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.externalSystem.execution.ExternalSystemExecutionConsoleManager +import com.intellij.openapi.externalSystem.model.ProjectSystemId +import com.intellij.openapi.externalSystem.model.task.ExternalSystemTask +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration +import com.intellij.openapi.externalSystem.service.internal.ExternalSystemExecuteTaskTask +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import io.kotest.plugin.intellij.Constants +import io.kotest.plugin.intellij.teamcity.TeamCityMessageParser +import io.kotest.plugin.intellij.teamcity.TeamCityMessageType +import org.jetbrains.plugins.gradle.util.GradleConstants + +class KotestTestsExecutionConsoleManager : + ExternalSystemExecutionConsoleManager { + + private var handler: TestEventHandler? = null + + override fun getExternalSystemId(): ProjectSystemId { + return GradleConstants.SYSTEM_ID + } + + /** + * Provides actions to restart execution task process witch handled by given console. + * Params: + * consoleView – is console into which restart actions will be placed. + */ + override fun getRestartActions(consoleView: KotestSMTRunnerConsoleView): Array { + return emptyArray() + } + + override fun attachExecutionConsole( + project: Project, + task: ExternalSystemTask, + env: ExecutionEnvironment?, + processHandler: ProcessHandler? + ): KotestSMTRunnerConsoleView? { + + if (env == null) return null + val settings = env.runnerAndConfigurationSettings ?: return null + + val conf = settings.configuration as ExternalSystemRunConfiguration + val consoleProperties = KotestConsoleProperties(conf, env.executor) + val testFrameworkName = Constants().FrameworkName + val splitterPropertyName = SMTestRunnerConnectionUtil.getSplitterPropertyName(testFrameworkName) + val consoleView = KotestSMTRunnerConsoleView(consoleProperties, splitterPropertyName) + + // sets up the process listener on the console view, using the proeprties that were passed to the console + SMTestRunnerConnectionUtil.initConsoleView(consoleView, testFrameworkName) + val testsRootNode: SMTestProxy.SMRootTestProxy = consoleView.resultsViewer.testsRootNode + + testsRootNode.executionId = env.executionId + testsRootNode.setSuiteStarted() + + val publisher = project.messageBus.syncPublisher(SMTRunnerEventsListener.TEST_STATUS) + handler = TestEventHandler(publisher) + publisher.onTestingStarted(testsRootNode) + + processHandler?.addProcessListener(object : ProcessAdapter() { + override fun processTerminated(event: ProcessEvent) { + if (testsRootNode.isInProgress) { + ApplicationManager.getApplication().invokeLater { + if (event.exitCode == 1) { + testsRootNode.setTestFailed("", null, false) + } else { + testsRootNode.setFinished() + } + consoleView.resultsViewer.onBeforeTestingFinished(testsRootNode) + consoleView.resultsViewer.onTestingFinished(testsRootNode) + } + } + } + }) + + return consoleView + } + + override fun isApplicableFor(task: ExternalSystemTask): Boolean { + if (task is ExternalSystemExecuteTaskTask) { + if (task.externalSystemId.id == GradleConstants.SYSTEM_ID.id) { + return task.tasksToExecute.any { it.endsWith(Constants().gradleTaskName) } + } + } + return false + } + + override fun onOutput( + executionConsole: KotestSMTRunnerConsoleView, + processHandler: ProcessHandler, + text: String, + processOutputType: Key<*> + ) { + val message = TeamCityMessageParser().parse(text) + if (message == null) + executionConsole.print(text, ConsoleViewContentType.NORMAL_OUTPUT) + else { + when (message.type) { + TeamCityMessageType.TestSuiteStarted -> + handler?.handleTestSuiteStarted(message, executionConsole) + TeamCityMessageType.TestSuiteFinished -> + handler?.handleTestSuiteFinished(message, executionConsole) + TeamCityMessageType.TestStarted -> + handler?.handleTestStarted(message, executionConsole) + TeamCityMessageType.TestFinished -> + handler?.handleTestFinished(message, executionConsole) + } + } + } +} + diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/PackageRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/PackageRunConfigurationProducer.kt index 004d4d1d..ef60de13 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/PackageRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/PackageRunConfigurationProducer.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.util.Ref import com.intellij.psi.JavaDirectoryService import com.intellij.psi.PsiElement import com.intellij.psi.impl.file.PsiJavaDirectoryImpl -import io.kotest.plugin.intellij.KotestRunConfiguration import io.kotest.plugin.intellij.KotestConfigurationFactory import io.kotest.plugin.intellij.KotestConfigurationType diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/RunData.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/RunData.kt index fbf6f2ef..464211ba 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/RunData.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/RunData.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.psi.KtClassOrObject /** * Holds state for generating suggested names for our config. */ +@Deprecated("Migrating to Kotest external system runner") data class RunData( val specName: String?, val testPath: String?, diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/SpecRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/SpecRunConfigurationProducer.kt index 75419cb6..df608c6f 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/SpecRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/SpecRunConfigurationProducer.kt @@ -8,7 +8,6 @@ import com.intellij.psi.PsiElement import com.intellij.psi.impl.source.tree.LeafPsiElement import io.kotest.plugin.intellij.KotestConfigurationFactory import io.kotest.plugin.intellij.KotestConfigurationType -import io.kotest.plugin.intellij.KotestRunConfiguration import io.kotest.plugin.intellij.psi.asKtClassOrObjectOrNull import io.kotest.plugin.intellij.psi.isRunnableSpec import org.jetbrains.kotlin.lexer.KtKeywordToken @@ -20,6 +19,7 @@ import org.jetbrains.kotlin.psi.KtClassOrObject * * This producer creates run configurations for spec classes (run all). */ +@Deprecated("Migrating to Kotest specific tasks") class SpecRunConfigurationProducer : LazyRunConfigurationProducer() { override fun getConfigurationFactory(): ConfigurationFactory = KotestConfigurationFactory(KotestConfigurationType()) diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/TestEventHandler.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/TestEventHandler.kt new file mode 100644 index 00000000..06f28d6e --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/TestEventHandler.kt @@ -0,0 +1,72 @@ +package io.kotest.plugin.intellij.run + +import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener +import com.intellij.execution.testframework.sm.runner.SMTestProxy +import io.kotest.plugin.intellij.KotestTestLocator +import io.kotest.plugin.intellij.teamcity.TeamCityMessage + +class TestEventHandler(private val publisher: SMTRunnerEventsListener) { + + private val proxies = mutableMapOf() + + fun handleTestSuiteStarted(message: TeamCityMessage, console: KotestSMTRunnerConsoleView) { + val id = message["id"] ?: return + // parentId is null for top level tests in a spec + val parentId = message["parent_id"] + val name = message["name"] ?: return + val location = message["locationHint"] + println(message) + + val root = console.resultsViewer.testsRootNode + + val proxy = SMTestProxy(name, true, location) + proxy.setLocator(KotestTestLocator()) + + val parent = if (parentId == null) root else proxies[parentId] ?: root + + parent.addChild(proxy) + proxy.setSuiteStarted() + + proxies[id] = proxy + console.resultsViewer.onSuiteStarted(proxy) + publisher.onSuiteStarted(proxy) + } + + fun handleTestSuiteFinished(message: TeamCityMessage, console: KotestSMTRunnerConsoleView) { + val id = message["id"] ?: return + val proxy = proxies[id] ?: return + proxy.setFinished() + console.resultsViewer.onSuiteFinished(proxy) + publisher.onSuiteFinished(proxy) + } + + fun handleTestStarted(message: TeamCityMessage, console: KotestSMTRunnerConsoleView) { + val id = message["id"] ?: return + val parentId = message["parent_id"] ?: return + val name = message["name"] ?: return + val location = message["locationHint"] + println(message) + val proxy = SMTestProxy(name, false, location) + proxy.setLocator(KotestTestLocator()) + + val parent = proxies[parentId] ?: console.resultsViewer.testsRootNode + + parent.addChild(proxy) + proxy.setStarted() + + proxies[id] = proxy + console.resultsViewer.onTestStarted(proxy) + publisher.onTestStarted(proxy) + } + + fun handleTestFinished(message: TeamCityMessage, console: KotestSMTRunnerConsoleView) { + val id = message["id"] ?: return + val duration = message["duration"]?.toLongOrNull() ?: 0L + val proxy = proxies[id] ?: return + proxy.setDuration(duration) + proxy.setFinished() + console.resultsViewer.onTestFinished(proxy) + publisher.onTestFinished(proxy) + } + +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/run/TestPathRunConfigurationProducer.kt b/src/main/kotlin/io/kotest/plugin/intellij/run/TestPathRunConfigurationProducer.kt index c3fbed5b..b283d0db 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/run/TestPathRunConfigurationProducer.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/run/TestPathRunConfigurationProducer.kt @@ -6,7 +6,6 @@ import com.intellij.execution.actions.LazyRunConfigurationProducer import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.openapi.util.Ref import com.intellij.psi.PsiElement -import io.kotest.plugin.intellij.KotestRunConfiguration import io.kotest.plugin.intellij.KotestConfigurationFactory import io.kotest.plugin.intellij.KotestConfigurationType import io.kotest.plugin.intellij.psi.enclosingKtClass @@ -21,6 +20,7 @@ import io.kotest.plugin.intellij.Test * * This producer creates run configurations for individual tests. */ +@Deprecated("Migrating to Kotest specific tasks") class TestPathRunConfigurationProducer : LazyRunConfigurationProducer() { /** diff --git a/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt new file mode 100644 index 00000000..5daf2291 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt @@ -0,0 +1,41 @@ +package io.kotest.plugin.intellij.teamcity + +class TeamCityMessageParser { + + private val prefix: String = "##teamcity" + private val regex = "$prefix\\[(.+?)(\\s.*)?]".toRegex() + private val propRegex = "(.+?)='(.+?)'".toRegex() + + fun parse(input: String): TeamCityMessage? { + val match = regex.matchEntire(input.trim()) ?: return null + val properties = match.groupValues.getOrNull(2) ?: "" + return TeamCityMessage( + type = TeamCityMessageType.fromWireName(match.groupValues[1].trim()), + properties = propRegex.findAll(properties).associate { + val name = it.groupValues[1].trim() + val value = it.groupValues[2].trim() + Pair(name, value) + } + ) + } +} + +data class TeamCityMessage( + val type: TeamCityMessageType, + val properties: Map, +) { + operator fun get(name: String) = properties[name] +} + +enum class TeamCityMessageType(private val wireName: String) { + TestSuiteStarted("testSuiteStarted"), + TestSuiteFinished("testSuiteFinished"), + TestStarted("testStarted"), + TestFinished("testFinished"); + + companion object { + fun fromWireName(input: String): TeamCityMessageType { + return TeamCityMessageType.entries.first { it.wireName == input } + } + } +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/ui/KotestSettingsEditor.kt b/src/main/kotlin/io/kotest/plugin/intellij/ui/KotestSettingsEditor.kt index 4439d5c3..de7a7f99 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/ui/KotestSettingsEditor.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/ui/KotestSettingsEditor.kt @@ -12,7 +12,7 @@ import com.intellij.openapi.ui.LabeledComponent import com.intellij.ui.EditorTextField import com.intellij.ui.EditorTextFieldWithBrowseButton import com.intellij.ui.TextFieldWithHistory -import io.kotest.plugin.intellij.KotestRunConfiguration +import io.kotest.plugin.intellij.run.KotestRunConfiguration import java.awt.BorderLayout import java.util.function.BiConsumer diff --git a/src/test/kotlin/io/kotest/plugin/intellij/TeamCityMessageParserTest.kt b/src/test/kotlin/io/kotest/plugin/intellij/TeamCityMessageParserTest.kt new file mode 100644 index 00000000..025073bd --- /dev/null +++ b/src/test/kotlin/io/kotest/plugin/intellij/TeamCityMessageParserTest.kt @@ -0,0 +1,36 @@ +package io.kotest.plugin.intellij + +import io.kotest.matchers.shouldBe +import io.kotest.plugin.intellij.teamcity.TeamCityMessage +import io.kotest.plugin.intellij.teamcity.TeamCityMessageParser +import io.kotest.plugin.intellij.teamcity.TeamCityMessageType +import org.junit.Test + +class TeamCityMessageParserTest { + + @Test + fun happyPath() { + TeamCityMessageParser().parse("""##teamcity[testFinished name='enums' id='pck.Test/enums' duration='1' locationHint='kotest:class://pck.Test:59']""") shouldBe TeamCityMessage( + TeamCityMessageType.TestFinished, + mapOf( + "id" to "pck.Test/enums", + "duration" to "1", + "locationHint" to "kotest:class://pck.Test:59", + ) + ) + } + + @Test + fun spacesInIds() { + val input = + """##teamcity[testStarted name='support GenericEnumSymbol' id='com.sksamuel.centurion.avro.decoders.EnumDecoderTest/support GenericEnumSymbol' parent_id='com.sksamuel.centurion.avro.decoders.EnumDecoderTest' locationHint='kotest:class://com.sksamuel.centurion.avro.decoders.EnumDecoderTest:13']""" + TeamCityMessageParser().parse(input) shouldBe TeamCityMessage( + TeamCityMessageType.TestStarted, + mapOf( + "id" to "com.sksamuel.centurion.avro.decoders.EnumDecoderTest/support GenericEnumSymbol", + "parent_id" to "com.sksamuel.centurion.avro.decoders.EnumDecoderTest", + "locationHint" to "kotest:class://com.sksamuel.centurion.avro.decoders.EnumDecoderTest:13", + ) + ) + } +} From e5447e148fa5757779d49b3746aebf5b5392c8ef Mon Sep 17 00:00:00 2001 From: sksamuel Date: Sun, 19 Jan 2025 15:20:26 -0600 Subject: [PATCH 2/2] parser callback --- .../KotestServiceMessageParserCallback.kt | 183 ++++++++++++++++++ .../teamcity/TeamCityMessageParser.kt | 116 ++++++++--- 2 files changed, 267 insertions(+), 32 deletions(-) create mode 100644 src/main/kotlin/io/kotest/plugin/intellij/teamcity/KotestServiceMessageParserCallback.kt diff --git a/src/main/kotlin/io/kotest/plugin/intellij/teamcity/KotestServiceMessageParserCallback.kt b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/KotestServiceMessageParserCallback.kt new file mode 100644 index 00000000..b3a8b750 --- /dev/null +++ b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/KotestServiceMessageParserCallback.kt @@ -0,0 +1,183 @@ +package io.kotest.plugin.intellij.teamcity + +import jetbrains.buildServer.messages.serviceMessages.ServiceMessage +import jetbrains.buildServer.messages.serviceMessages.ServiceMessageParserCallback +import jetbrains.buildServer.messages.serviceMessages.ServiceMessageTypes +import org.gradle.api.internal.tasks.testing.DefaultTestSuiteDescriptor +import org.gradle.api.internal.tasks.testing.results.DefaultTestResult +import org.gradle.api.tasks.testing.TestDescriptor +import org.gradle.api.tasks.testing.TestFailure +import org.gradle.api.tasks.testing.TestListener +import org.gradle.api.tasks.testing.TestResult +import java.text.ParseException + +/** + * An implementation of [ServiceMessageParserCallback] that parses messages from the Kotest test runner + * in team city format. + */ +class KotestServiceMessageParserCallback( + private val root: DefaultTestSuiteDescriptor, + private val listeners: List, +// private val outputListeners: MutableList +) : ServiceMessageParserCallback { + + private val startTimes = mutableMapOf() + private val descriptors = mutableMapOf() + + override fun regularText(p0: String) { + println("Regular text $p0") + } + + override fun parseException(p0: ParseException, p1: String) { + println("Parse exception $p0 $p1") + } + + override fun serviceMessage(msg: ServiceMessage) { + println("Service message $msg") + when (msg.messageName) { + ServiceMessageTypes.TEST_STARTED -> notifyBeforeTest(msg) + ServiceMessageTypes.TEST_FAILED, + ServiceMessageTypes.TEST_IGNORED, + ServiceMessageTypes.TEST_FINISHED -> notifyAfterTest(msg) + + ServiceMessageTypes.TEST_SUITE_STARTED -> notifyBeforeTestSuite(msg) + ServiceMessageTypes.TEST_SUITE_FINISHED -> notifyAfterTestSuite(msg) + } + } + + private fun createDescriptor(msg: ServiceMessage): KotestTestDescriptor { + + val id = msg.attributes["id"] ?: error("id must be defined") + val name = msg.attributes["name"] ?: error("name must be defined") + val parent = descriptors[msg.attributes["parent_id"]] + + var temp = parent + while (temp != null) + temp = temp.parentTestDescription() + val fqn = temp?.testId ?: id + + val desc = when (msg.attributes["test_type"]?.lowercase()) { + "spec" -> SpecDescriptor(name, root) + "test" -> TestCaseDescriptor(id, name, fqn, parent ?: error("must have parent")) + "container" -> ContainerDescriptor(id, name, fqn, parent ?: error("must have parent")) + else -> error("Unknown test type $msg") + } + + descriptors[desc.testId] = desc + return desc + } + + private fun getDescriptor(msg: ServiceMessage): KotestTestDescriptor { + val id = msg.attributes["id"] ?: error("id must be defined") + return descriptors[id] ?: error("description for $id must exist") + } + + private fun notifyBeforeTest(msg: ServiceMessage) { + val desc = createDescriptor(msg) + startTimes[desc.testId] = System.currentTimeMillis() + listeners.forEach { it.beforeTest(desc) } + } + + private fun notifyBeforeTestSuite(msg: ServiceMessage) { + val desc = createDescriptor(msg) + startTimes[desc.testId] = System.currentTimeMillis() + listeners.forEach { it.beforeSuite(desc) } + } + + private fun notifyAfterTest(msg: ServiceMessage) { + + val desc = getDescriptor(msg) + val start = startTimes[desc.testId] ?: 0L + + val resultType = when (msg.messageName) { + ServiceMessageTypes.TEST_FAILED -> TestResult.ResultType.FAILURE + ServiceMessageTypes.TEST_FINISHED -> TestResult.ResultType.SUCCESS + ServiceMessageTypes.TEST_IGNORED -> TestResult.ResultType.SKIPPED + else -> error("Unsupported type for result ${msg.messageName}") + } + + // if we have a FAILURE, we treat the message if set as an error message + val errors = when (resultType) { + TestResult.ResultType.FAILURE -> + TestFailure.fromTestAssertionFailure(RuntimeException(msg.attributes["message"]), null, null) + + else -> null + } + + val result = DefaultTestResult(resultType, start, System.currentTimeMillis(), 0, 0, 0, listOf(errors)) + listeners.forEach { it.afterTest(desc, result) } + } + + private fun notifyAfterTestSuite(msg: ServiceMessage) { + + val desc = getDescriptor(msg) + val start = startTimes[desc.testId] ?: 0L + + // teamcity format doesn't have a status for test suite, so we use kotest's own + val resultType = when (msg.attributes["result_status"]?.lowercase()) { + "success" -> TestResult.ResultType.SUCCESS + else -> TestResult.ResultType.FAILURE + } + + // if we have a FAILURE, we treat the message if set as an error message + val errors = when (resultType) { + TestResult.ResultType.FAILURE -> + TestFailure.fromTestAssertionFailure(RuntimeException(msg.attributes["message"]), null, null) + + else -> null + } + + val result = DefaultTestResult(resultType, start, System.currentTimeMillis(), 0, 0, 0, listOf(errors)) + listeners.forEach { it.afterSuite(desc, result) } + } +} + +interface KotestTestDescriptor : TestDescriptor { + val testId: String + val testType: String + fun parentTestDescription(): KotestTestDescriptor? +} + +data class TestCaseDescriptor( + override val testId: String, + val testName: String, + val fqn: String, + val parentTestDescriptor: KotestTestDescriptor +) : KotestTestDescriptor { + override fun getName(): String = testName + override fun getDisplayName(): String = testName + override fun getClassName(): String = fqn + override fun isComposite(): Boolean = false + override fun getParent(): TestDescriptor = parentTestDescriptor + override fun parentTestDescription(): KotestTestDescriptor = parentTestDescriptor + override val testType: String = "Test" +} + +data class ContainerDescriptor( + override val testId: String, + val testName: String, + val fqn: String, + val parentTestDescriptor: KotestTestDescriptor +) : KotestTestDescriptor { + override fun getName(): String = testName + override fun getDisplayName(): String = testName + override fun getClassName(): String = fqn + override fun isComposite(): Boolean = true + override fun getParent(): TestDescriptor = parentTestDescriptor + override fun parentTestDescription(): KotestTestDescriptor = parentTestDescriptor + override val testType: String = "Container" +} + +data class SpecDescriptor( + val fqn: String, + val root: DefaultTestSuiteDescriptor +) : KotestTestDescriptor { + override fun getName(): String = fqn + override fun getDisplayName(): String = fqn + override fun getClassName(): String = fqn + override fun isComposite(): Boolean = true + override fun getParent(): TestDescriptor = root + override fun parentTestDescription(): KotestTestDescriptor? = null + override val testId: String = fqn + override val testType: String = "Spec" +} diff --git a/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt index 5daf2291..64a1a9ba 100644 --- a/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt +++ b/src/main/kotlin/io/kotest/plugin/intellij/teamcity/TeamCityMessageParser.kt @@ -1,41 +1,93 @@ package io.kotest.plugin.intellij.teamcity -class TeamCityMessageParser { - - private val prefix: String = "##teamcity" - private val regex = "$prefix\\[(.+?)(\\s.*)?]".toRegex() - private val propRegex = "(.+?)='(.+?)'".toRegex() - - fun parse(input: String): TeamCityMessage? { - val match = regex.matchEntire(input.trim()) ?: return null - val properties = match.groupValues.getOrNull(2) ?: "" - return TeamCityMessage( - type = TeamCityMessageType.fromWireName(match.groupValues[1].trim()), - properties = propRegex.findAll(properties).associate { - val name = it.groupValues[1].trim() - val value = it.groupValues[2].trim() - Pair(name, value) +import jetbrains.buildServer.messages.serviceMessages.ServiceMessagesParser +import org.gradle.api.internal.tasks.testing.DefaultTestSuiteDescriptor +import java.io.PipedInputStream +import java.io.PipedOutputStream +import kotlin.concurrent.thread +import kotlin.sequences.forEach + +//class TeamCityMessageParser { +// +// private val prefix: String = "##teamcity" +// private val regex = "$prefix\\[(.+?)(\\s.*)?]".toRegex() +// private val propRegex = "(.+?)='(.+?)'".toRegex() +// +// fun parse(input: String): TeamCityMessage? { +// val match = regex.matchEntire(input.trim()) ?: return null +// val properties = match.groupValues.getOrNull(2) ?: "" +// return TeamCityMessage( +// type = TeamCityMessageType.fromWireName(match.groupValues[1].trim()), +// properties = propRegex.findAll(properties).associate { +// val name = it.groupValues[1].trim() +// val value = it.groupValues[2].trim() +// Pair(name, value) +// } +// ) +// } +//} +// +//data class TeamCityMessage( +// val type: TeamCityMessageType, +// val properties: Map, +//) { +// operator fun get(name: String) = properties[name] +//} +// +//enum class TeamCityMessageType(private val wireName: String) { +// TestSuiteStarted("testSuiteStarted"), +// TestSuiteFinished("testSuiteFinished"), +// TestStarted("testStarted"), +// TestFinished("testFinished"); +// +// companion object { +// fun fromWireName(input: String): TeamCityMessageType { +// return TeamCityMessageType.entries.first { it.wireName == input } +// } +// } +//} + + +class TeamCityListener { + + // the intput stream will receive anything written to the output stream + private val input = PipedInputStream() + + // the output stream should be attached to the java exec process to receive whatever is written to stdout + val output = PipedOutputStream(input) + + // this parser is provided by the kotlin gradle plugin library and will parse teamcity messages + private val parser = ServiceMessagesParser() + + private val root = DefaultTestSuiteDescriptor("root", "root") + + // the service message parser emits events to a callback implementation which we provide + private val callback = KotestServiceMessageParserCallback(root, emptyList(), mutableListOf()) + + /** + * Starts a new thread which consumes the input stream and parses the teamcity messages. + */ + fun start() { + thread { +// listeners.forEach { +// it.beforeSuite(root) +// } + input.bufferedReader().useLines { lines -> + // the lines here is a lazy sequence which will be fed lines as they arrive from std out + lines.forEach { parser.parse(it, callback) } } - ) +// listeners.forEach { +// it.afterSuite(root, DefaultTestResult(TestResult.ResultType.SUCCESS, 0, 0, 0, 0, 0, emptyList())) +// } + } } } -data class TeamCityMessage( - val type: TeamCityMessageType, - val properties: Map, -) { - operator fun get(name: String) = properties[name] -} - -enum class TeamCityMessageType(private val wireName: String) { - TestSuiteStarted("testSuiteStarted"), - TestSuiteFinished("testSuiteFinished"), - TestStarted("testStarted"), - TestFinished("testFinished"); - companion object { - fun fromWireName(input: String): TeamCityMessageType { - return TeamCityMessageType.entries.first { it.wireName == input } - } +private fun hasRtJar(): Boolean { + return try { + this::class.java.classLoader.loadClass("com.intellij.rt.execution.application.AppMainV2") != null + } catch (_: ClassNotFoundException) { + false } }