Skip to content
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

Add Ability to Run Tests on Android ART VM #2744

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.debugger.test

import com.intellij.util.io.Compressor
import com.intellij.util.io.delete
import java.nio.file.Files
import java.nio.file.Path
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.io.path.isDirectory
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit.MILLISECONDS

private const val RUN_ON_ART_ENV = "INTELLIJ_DEBUGGER_TESTS_ART"
private const val RUN_ON_ART_PROPERTY = "intellij.debugger.tests.art"
private const val STUDIO_ROOT_ENV = "INTELLIJ_DEBUGGER_TESTS_STUDIO_ROOT"
private const val STUDIO_ROOT_PROPERTY = "intellij.debugger.tests.studio.root"
private const val TIMEOUT_MILLIS_ENV = "INTELLIJ_DEBUGGER_TESTS_TIMEOUT_MILLIS"
private const val TIMEOUT_MILLIS_PROPERTY = "intellij.debugger.tests.timeout.millis"

private const val DEX_COMPILER = "prebuilts/r8/r8.jar"
private const val ART_ROOT = "prebuilts/tools/linux-x86_64/art"
private const val LIB_ART = "framework/core-libart-hostdex.jar"
private const val OJ = "framework/core-oj-hostdex.jar"
private const val ICU4J = "framework/core-icu4j-hostdex.jar"
private const val ART = "bin/art"
private const val JVMTI = "lib64/libopenjdkjvmti.so"
private const val JDWP = "lib64/libjdwp.so"

/**
* A collection of methods that support running tests on an Android ART VM.
*
* Notes:
* * Only supported on Linux
* * Requires an internal Google 'studio-main` repo.
*/
internal object ArtUtils {
private val root by lazy(NONE) { getStudioRoot() }

/**
* Returns true if tests should be run on ART
*
* Can be set by providing a JVM property or via the environment. JVM property overrides environment.
*/
fun runTestOnArt(): Boolean {
val property = System.getProperty(RUN_ON_ART_PROPERTY)
if (property != null) {
// Property overrides environment
return property.toBoolean()
}
return System.getenv(RUN_ON_ART_ENV)?.toBoolean() ?: false
}

fun getTestTimeoutMillis(): Int {
val property = System.getProperty(TIMEOUT_MILLIS_PROPERTY)
if (property != null) {
// Property overrides environment
return property.toInt()
}
return System.getenv(TIMEOUT_MILLIS_ENV)?.toInt() ?: 30.seconds.toInt(MILLISECONDS)
}

/**
* Builds the command line to run the ART JVM
*/
fun buildCommandLine(dexFile: String, mainClass: String): List<String> {
val artDir = root.resolve(ART_ROOT)
val bootClasspath = listOf(
artDir.resolve(LIB_ART),
artDir.resolve(OJ),
artDir.resolve(ICU4J),
).joinToString(":") { it.pathString }

val art = artDir.resolve(ART).pathString
val jvmti = artDir.resolve(JVMTI).pathString
val jdwp = artDir.resolve(JDWP).pathString
return listOf(
art,
"--64",
"-Xbootclasspath:$bootClasspath",
"-Xplugin:$jvmti",
"-agentpath:$jdwp=transport=dt_socket,server=y,suspend=y",
"-classpath",
dexFile,
mainClass,
)
}

/**
* Builds a DEX file from a list of dependencies
*/
fun buildDexFile(deps: List<String>): Path {
val dexCompiler = root.resolve(DEX_COMPILER)
val tempFiles = mutableListOf<Path>()
val jarFiles = deps.map { Path.of(it) }.map { path ->
when {
path.isDirectory() -> {
val jarFile = Files.createTempFile("", ".jar")
Compressor.Jar(jarFile).use { jar ->
jar.addDirectory("", path)
}
tempFiles.add(jarFile)
jarFile
}

else -> path
}.pathString
}
try {
val dexFile = Files.createTempFile("", "-dex.jar")
Runtime.getRuntime().exec(
"java -cp $dexCompiler com.android.tools.r8.D8 --output ${dexFile.pathString} --min-api 30 ${jarFiles.joinToString(" ") { it }}"
).waitFor()
return dexFile
} finally {
tempFiles.forEach { it.delete() }
}
}

private fun getStudioRoot(): Path {
val property = System.getProperty(STUDIO_ROOT_PROPERTY)
val env = System.getenv(STUDIO_ROOT_ENV)
val path = property ?: env ?: throw IllegalStateException("Studio Root was not provided")
val root = Path.of(path)
if (root.isDirectory()) {
return root
}
throw IllegalStateException("'$path' is not a directory")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.idea.debugger.test
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.DefaultDebugEnvironment
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.RemoteStateState
import com.intellij.debugger.impl.*
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.execution.ExecutionTestCase
Expand All @@ -14,13 +15,15 @@ import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.execution.target.TargetEnvironmentRequest
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.observable.util.whenDisposed
import com.intellij.openapi.roots.LibraryOrderEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ModuleRootManager
Expand All @@ -34,6 +37,7 @@ import com.intellij.testFramework.IndexingTestUtil
import com.intellij.testFramework.runInEdtAndGet
import com.intellij.util.ThrowableRunnable
import com.intellij.util.containers.addIfNotNull
import com.intellij.util.io.delete
import com.intellij.xdebugger.XDebugSession
import org.jetbrains.kotlin.config.*
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
Expand All @@ -46,6 +50,10 @@ import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCommonCompilerArgu
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCompilerSettings
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinPluginLayout
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinEvaluator
import org.jetbrains.kotlin.idea.debugger.test.ArtUtils.buildCommandLine
import org.jetbrains.kotlin.idea.debugger.test.ArtUtils.buildDexFile
import org.jetbrains.kotlin.idea.debugger.test.ArtUtils.getTestTimeoutMillis
import org.jetbrains.kotlin.idea.debugger.test.ArtUtils.runTestOnArt
import org.jetbrains.kotlin.idea.debugger.test.preference.*
import org.jetbrains.kotlin.idea.debugger.test.util.BreakpointCreator
import org.jetbrains.kotlin.idea.debugger.test.util.KotlinOutputChecker
Expand All @@ -63,6 +71,8 @@ import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.test.TargetBackend
import org.junit.ComparisonFailure
import java.io.File
import java.lang.ProcessBuilder.Redirect.PIPE
import kotlin.io.path.pathString

internal const val KOTLIN_LIBRARY_NAME = "KotlinJavaRuntime"
internal const val TEST_LIBRARY_NAME = "TestLibrary"
Expand Down Expand Up @@ -352,28 +362,11 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), IgnorableTestCas
.runProfile(MockConfiguration(myProject))
.build()

val javaCommandLineState: JavaCommandLineState = object : JavaCommandLineState(environment) {
override fun createJavaParameters() = javaParameters

override fun createTargetedCommandLine(request: TargetEnvironmentRequest): TargetedCommandLineBuilder {
return getJavaParameters().toCommandLine(request)
}
val debuggerSession = when (runTestOnArt()) {
true -> createArtLocalProcess(javaParameters, environment)
false -> createJvmLocalProcess(javaParameters, environment)
}

val debugParameters =
RemoteConnectionBuilder(
debuggerRunnerSettings.LOCAL,
debuggerRunnerSettings.transport,
debuggerRunnerSettings.debugPort
)
.checkValidity(true)
.asyncAgent(true)
.create(javaCommandLineState.javaParameters)

val env = javaCommandLineState.environment
env.putUserData(DefaultDebugEnvironment.DEBUGGER_TRACE_MODE, traceMode)
val debuggerSession = attachVirtualMachine(javaCommandLineState, env, debugParameters, false)

val processHandler = debuggerSession.process.processHandler
debuggerSession.process.addProcessListener(object : ProcessAdapter() {
private val errorOutput = StringBuilder()
Expand Down Expand Up @@ -405,6 +398,69 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), IgnorableTestCas
return debuggerSession
}

private fun createJvmLocalProcess(javaParameters: JavaParameters, environment: ExecutionEnvironment): DebuggerSession {
val debuggerRunnerSettings = (environment.runnerSettings as GenericDebuggerRunnerSettings)
val javaCommandLineState: JavaCommandLineState = object : JavaCommandLineState(environment) {
override fun createJavaParameters() = javaParameters

override fun createTargetedCommandLine(request: TargetEnvironmentRequest): TargetedCommandLineBuilder {
return getJavaParameters().toCommandLine(request)
}
}

val debugParameters =
RemoteConnectionBuilder(
debuggerRunnerSettings.LOCAL,
debuggerRunnerSettings.transport,
debuggerRunnerSettings.debugPort
)
.checkValidity(true)
.asyncAgent(true)
.create(javaCommandLineState.javaParameters)

val env = javaCommandLineState.environment
env.putUserData(DefaultDebugEnvironment.DEBUGGER_TRACE_MODE, traceMode)

return attachVirtualMachine(javaCommandLineState, env, debugParameters, false)
}

private fun createArtLocalProcess(javaParameters: JavaParameters, environment: ExecutionEnvironment): DebuggerSession {
println("Running on ART VM")
zuevmaxim marked this conversation as resolved.
Show resolved Hide resolved
setTimeout(getTestTimeoutMillis())
val mainClass = javaParameters.mainClass
val dexFile = buildDexFile(javaParameters.classPath.pathList)
val command = buildCommandLine(dexFile.pathString, mainClass)
testRootDisposable.whenDisposed {
dexFile.delete()
}
val art = ProcessBuilder()
.command(command)
.redirectOutput(PIPE)
.start()


val port: String = art.inputStream.bufferedReader().use {
while (true) {
val line = it.readLine() ?: break
if (line.startsWith("Listening for transport")) {
val port = line.substringAfterLast(" ")
return@use port
}
}
throw IllegalStateException("Failed to read listening port from ART")
}

val debugParameters =
RemoteConnectionBuilder(false, DebuggerSettings.SOCKET_TRANSPORT, port)
.checkValidity(true)
.asyncAgent(true)
.create(javaParameters)

val remoteState = RemoteStateState(project, debugParameters)

return attachVirtualMachine(remoteState, environment, debugParameters, false)
}

open fun addMavenDependency(compilerFacility: DebuggerTestCompilerFacility, library: String) {
}

Expand Down