[evaluation-plugin] LME-463 Implement Gradle test runner

GitOrigin-RevId: 167190c4a967626abb5704932bd84a5c1b040930
This commit is contained in:
Roman Vasiliev
2025-07-08 20:43:03 +00:00
committed by intellij-monorepo-bot
parent 2f8b206433
commit 66f67ad877
8 changed files with 215 additions and 67 deletions
@@ -30,6 +30,7 @@ jvm_library(
"//platform/lang-core",
"//platform/projectModel-api:projectModel",
"//platform/refactoring",
"//platform/smRunner",
],
runtime_deps = [":java_resources"]
)
@@ -60,6 +61,8 @@ jvm_library(
"//platform/testFramework:testFramework_test_lib",
"@lib//:kotlinx-coroutines-test",
"//java/testFramework",
"//platform/smRunner",
"//platform/smRunner:smRunner_test_lib",
],
runtime_deps = [":java_resources"]
)
@@ -28,5 +28,6 @@
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-test" level="project" />
<orderEntry type="module" module-name="intellij.java.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
</component>
</module>
@@ -21,7 +21,7 @@
<completionEvaluationVisitor implementation="com.intellij.cce.java.visitor.JavaSelfIdentificationVisitor"/>
<apiCallExtractor implementation="com.intellij.cce.java.chat.JavaApiCallExtractorProvider"/>
<exposedApiExtractor implementation="com.intellij.cce.java.evaluable.JavaExposedApiExtractor"/>
<testRunner implementation="com.intellij.cce.java.test.JavaTestRunnerForMaven"/>
<testRunner implementation="com.intellij.cce.java.test.JavaTestRunner"/>
<completionEvaluationVisitor implementation="com.intellij.cce.java.visitor.JavaSandboxFeatureVisitor"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,55 @@
package com.intellij.cce.java.test
import com.intellij.cce.core.Language
import com.intellij.cce.test.TestRunRequest
import com.intellij.cce.test.TestRunResult
import com.intellij.cce.test.TestRunner
import com.intellij.cce.test.TestRunnerParams
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import org.jetbrains.idea.maven.project.MavenProjectsManager
import org.jetbrains.plugins.gradle.util.GradleConstants
internal val LOG = fileLogger()
class JavaTestRunner : TestRunner {
override fun isApplicable(params: TestRunnerParams): Boolean =
params.language == Language.JAVA ||
params.language == Language.KOTLIN
override suspend fun runTests(request: TestRunRequest): TestRunResult {
LOG.info("Running tests: ${request.tests.joinToString()}")
if (request.tests.isEmpty()) {
return TestRunResult(0, emptyList(), emptyList(), true, true, "")
}
val moduleTests = request.tests
.map {
val parts = it.split(":")
if (parts.size == 1) null to parts[0]
else if (parts.size == 2) parts[0] to parts[1]
else throw IllegalArgumentException("Test name has invalid format: $it")
}
.groupBy { it.first }
.map { ModuleTests(it.key, it.value.map { it.second }) }
if (isMaven(request.project)) {
return JavaTestRunnerForMaven.run(request.project, moduleTests)
}
if (isGradle(request.project)) {
return JavaTestRunnerForGradle.run(request.project, moduleTests)
}
throw IllegalStateException("Unknown build system. Project: ${request.project.basePath}")
}
}
internal data class ModuleTests(val module: String?, val tests: List<String>)
private fun isMaven(project: Project): Boolean =
MavenProjectsManager.getInstance(project).hasProjects()
private fun isGradle(project: Project): Boolean =
ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID).getLinkedProjectsSettings().isNotEmpty()
@@ -0,0 +1,96 @@
package com.intellij.cce.java.test
import com.intellij.cce.test.TestRunResult
import com.intellij.execution.RunManager
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.configurations.ConfigurationTypeUtil
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.impl.DefaultJavaProgramRunner
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.extensions.impl.ExtensionPointImpl
import com.intellij.openapi.externalSystem.execution.ExternalSystemExecutionConsoleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import org.jetbrains.plugins.gradle.service.execution.GradleExternalTaskConfigurationType
import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration
import org.jetbrains.plugins.gradle.service.task.GradleTaskManagerExtension
internal object JavaTestRunnerForGradle {
@Suppress("TestOnlyProblems")
suspend fun run(project: Project, moduleTests: List<ModuleTests>): TestRunResult {
return Disposer.newDisposable().use { disposable ->
// `KotlinWasmBrowserDebugGradleTaskManagerExtension` can break older gradle versions
val testExtensions = GradleTaskManagerExtension.EP_NAME.extensionList.filterNot { it.javaClass.name.contains("Wasm") }
(GradleTaskManagerExtension.EP_NAME.point as ExtensionPointImpl<GradleTaskManagerExtension>).maskAll(testExtensions, disposable, true)
// Default gradle test console doesn't provide output to ProcessListener
(ExternalSystemExecutionConsoleManager.EP_NAME.point as ExtensionPointImpl<ExternalSystemExecutionConsoleManager<*, *>>)
.maskAll(listOf(), disposable, true)
doRunTests(project, moduleTests)
}
}
private suspend fun doRunTests(project: Project, moduleTests: List<ModuleTests>): TestRunResult {
val runner = DefaultJavaProgramRunner.getInstance()
val executor = DefaultRunExecutor.getRunExecutorInstance()
val configSettings = configSettings(project, moduleTests)
val environment = ExecutionEnvironment(executor, runner, configSettings, project)
val results = RunConfigurationResults.compute { callback ->
environment.callback = callback
runner.execute(environment)
}
val allTests = mutableSetOf<String>()
val failedTests = mutableSetOf<String>()
val output = results.output.lines().joinToString("\n") { line ->
if (line.contains("<event type='afterTest'>")) {
val className = readAttribute(line, "className")
val resultType = readAttribute(line, "resultType")
val displayName = readAttribute(line, "displayName")
val success = resultType == "SUCCESS"
allTests.add(className)
if (!success) {
failedTests.add(className)
}
val highlightPrefix = if (success) " <<<++++<<< " else " <<<----<<< "
line + "${highlightPrefix}[$resultType] $className:$displayName"
}
else line
}
return TestRunResult(results.exitCode, (allTests - failedTests).toList(), failedTests.toList(), true, true, output)
}
private fun configSettings(project: Project, moduleTests: List<ModuleTests>): RunnerAndConfigurationSettings {
val settings = RunManager.getInstance(project).createConfiguration(
"gradle-tests",
ConfigurationTypeUtil.findConfigurationType(GradleExternalTaskConfigurationType::class.java).factory
)
val runConfiguration = settings.configuration as GradleRunConfiguration
runConfiguration.isRunAsTest = true
runConfiguration.settings.scriptParameters = moduleTests.joinToString(" ") { (moduleName, testNames) ->
if (moduleName != null) ":$moduleName:test ${testNames.joinToString(" ") { "--tests $it" }}"
else ":test ${testNames.joinToString(" ") { "--tests $it" }}"
}
return settings
}
private fun readAttribute(xml: String, name: String): String {
val startTag = "$name='"
val startIndex = xml.indexOf(startTag) + startTag.length
val endIndex = xml.indexOf("'", startIndex)
return if (startIndex >= startTag.length && endIndex > startIndex) {
xml.substring(startIndex, endIndex)
}
else {
throw IllegalArgumentException("Attribute '$name' not found in the given XML")
}
}
}
@@ -1,38 +1,16 @@
package com.intellij.cce.java.test
import com.intellij.cce.core.Language
import com.intellij.cce.test.TestRunRequest
import com.intellij.cce.test.TestRunResult
import com.intellij.cce.test.TestRunner
import com.intellij.cce.test.TestRunnerParams
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ProgramRunner
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.Key
import kotlinx.coroutines.CompletableDeferred
import org.jetbrains.idea.maven.execution.MavenRunConfigurationType
import org.jetbrains.idea.maven.execution.MavenRunnerParameters
import org.jetbrains.idea.maven.execution.MavenRunnerSettings
private val LOG = fileLogger()
internal object JavaTestRunnerForMaven {
internal class JavaTestRunnerForMaven : TestRunner {
override fun isApplicable(params: TestRunnerParams): Boolean {
return params.language == Language.JAVA
|| params.language == Language.KOTLIN // TODO temporary solution for docker testing
}
override fun runTests(request: TestRunRequest): TestRunResult {
LOG.info("Running tests: ${request.tests.joinToString()}")
if (request.tests.isEmpty()) {
return TestRunResult(0, emptyList(), emptyList(), true, true, "")
}
val project = request.project
suspend fun run(project: Project, moduleTests: List<ModuleTests>): TestRunResult {
val projectDir = project.guessProjectDir()!!
@@ -41,33 +19,10 @@ internal class JavaTestRunnerForMaven : TestRunner {
/* pomFileName = */ "",
/* goals = */ listOf("test"),
/* explicitEnabledProfiles = */ emptyList<String>())
val deferred = CompletableDeferred<Int>()
val sb = StringBuilder()
val callback = ProgramRunner.Callback { descriptor ->
LOG.info("processStarted $descriptor")
val processHandler = descriptor.processHandler ?: error("processHandler is null")
processHandler.addProcessListener(object : ProcessListener {
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
sb.append(event.text)
}
override fun processTerminated(event: ProcessEvent) {
LOG.info("processTerminated. exitCode=${event.exitCode}")
deferred.complete(event.exitCode)
}
override fun processNotStarted() {
LOG.error("processNotStarted")
deferred.complete(-1)
}
})
}
// in multi-module projects tests will be prefixed with module:test
request.tests.map { it.substringBeforeLast(":", "") }
.filter { it.isNotBlank() }
moduleTests
.mapNotNull { it.module }
.also {
if (it.isNotEmpty()) {
params.projectsCmdOptionValues = it
@@ -75,28 +30,19 @@ internal class JavaTestRunnerForMaven : TestRunner {
}
val runnerSettings = MavenRunnerSettings().also {
if (request.tests.any()) {
//todo check
it.setVmOptions("-Dtest=${request.tests.joinToString(separator = ",") { it.substringAfterLast(":") }}")
}
//todo check
it.setVmOptions("-Dtest=${moduleTests.flatMap { it.tests }.joinToString(separator = ",")}")
}
MavenRunConfigurationType.runConfiguration(project,
params,
null,
runnerSettings,
callback)
LOG.info("await for process termination")
val exitCode = runBlockingCancellable {
deferred.await()
val results = RunConfigurationResults.compute { callback ->
MavenRunConfigurationType.runConfiguration(project, params, null, runnerSettings, callback)
}
val output = sb.toString()
val output = results.output
val compilationSuccessful = MavenOutputParser.compilationSuccessful(output)
val projectIsResolvable = MavenOutputParser.checkIfProjectIsResolvable(output)
val (passed, failed) = MavenOutputParser.parse(output)
return TestRunResult(exitCode, passed, failed, compilationSuccessful, projectIsResolvable, output)
return TestRunResult(results.exitCode, passed, failed, compilationSuccessful, projectIsResolvable, output)
}
}
@@ -0,0 +1,47 @@
package com.intellij.cce.java.test
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ProgramRunner
import com.intellij.openapi.util.Key
import kotlinx.coroutines.CompletableDeferred
internal data class RunConfigurationResults(
val exitCode: Int,
val output: String
) {
companion object {
suspend fun compute(f: (ProgramRunner.Callback) -> Unit): RunConfigurationResults {
val deferred = CompletableDeferred<Int>()
val sb = StringBuilder()
val callback = ProgramRunner.Callback { descriptor ->
LOG.info("processStarted $descriptor")
val processHandler = descriptor.processHandler ?: error("processHandler is null")
processHandler.addProcessListener(object : ProcessListener {
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
sb.append(event.text)
}
override fun processTerminated(event: ProcessEvent) {
LOG.info("processTerminated. exitCode=${event.exitCode}")
deferred.complete(event.exitCode)
}
override fun processNotStarted() {
LOG.error("processNotStarted")
deferred.complete(-1)
}
})
}
f(callback)
LOG.info("await for process termination")
val exitCode = deferred.await()
return RunConfigurationResults(exitCode, sb.toString())
}
}
}