diff --git a/java/testFramework/src/com/intellij/debugger/DebuggerTestCase.java b/java/testFramework/src/com/intellij/debugger/DebuggerTestCase.java index c04c2cd5ee6aa..e291ca7db05aa 100644 --- a/java/testFramework/src/com/intellij/debugger/DebuggerTestCase.java +++ b/java/testFramework/src/com/intellij/debugger/DebuggerTestCase.java @@ -504,7 +504,7 @@ public RunProfileState getRunnableState() { return myRunnableState; } - protected DebuggerSession attachVirtualMachine(RunProfileState state, + public DebuggerSession attachVirtualMachine(RunProfileState state, ExecutionEnvironment environment, RemoteConnection remoteConnection, boolean pollConnection) throws ExecutionException { diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtAttacher.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtAttacher.kt new file mode 100644 index 0000000000000..1929d948789c6 --- /dev/null +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtAttacher.kt @@ -0,0 +1,184 @@ +// 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.debugger.engine.RemoteStateState +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.impl.RemoteConnectionBuilder +import com.intellij.debugger.settings.DebuggerSettings +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RemoteConnection +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.openapi.observable.util.whenDisposed +import com.intellij.ui.classFilter.ClassFilter +import com.intellij.util.io.Compressor +import com.intellij.util.io.delete +import java.lang.ProcessBuilder.Redirect.PIPE +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 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" + +/** Attaches to an ART VM */ +internal class ArtAttacher : VmAttacher { + private val root by lazy(NONE) { getStudioRoot() } + private lateinit var steppingFilters: Array + + override fun setUp() { + steppingFilters = DebuggerSettings.getInstance().steppingFilters + DebuggerSettings.getInstance().steppingFilters += arrayOf( + ClassFilter("android.*"), + ClassFilter("com.android.*"), + ClassFilter("androidx.*"), + ClassFilter("libcore.*"), + ClassFilter("dalvik.*"), + ) + } + + override fun tearDown() { + DebuggerSettings.getInstance().steppingFilters = steppingFilters + } + + override fun attachVirtualMachine( + testCase: KotlinDescriptorTestCase, + javaParameters: JavaParameters, + environment: ExecutionEnvironment + ): DebuggerSession { + val remoteConnection = getRemoteConnection(testCase, javaParameters) + val remoteState = RemoteStateState(testCase.project, remoteConnection) + return testCase.attachVirtualMachine(remoteState, environment, remoteConnection, false) + } + + private fun getRemoteConnection(testCase: KotlinDescriptorTestCase, javaParameters: JavaParameters): RemoteConnection { + println("Running on ART VM") + testCase.setTimeout(getTestTimeoutMillis()) + val mainClass = javaParameters.mainClass + val dexFile = buildDexFile(javaParameters.classPath.pathList) + val command = buildCommandLine(dexFile.pathString, mainClass) + testCase.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") + } + + return RemoteConnectionBuilder(false, DebuggerSettings.SOCKET_TRANSPORT, port) + .checkValidity(true) + .asyncAgent(true) + .create(javaParameters) + } + + /** + * Builds a DEX file from a list of dependencies + */ + private fun buildDexFile(deps: List): Path { + val dexCompiler = root.resolve(DEX_COMPILER) + val tempFiles = mutableListOf() + 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") + val command = arrayOf( + "java", + "-cp", + dexCompiler.pathString, + "com.android.tools.r8.D8", + "--output", + dexFile.pathString, + "--min-api", + "30" + ) + jarFiles + Runtime.getRuntime().exec(command).waitFor() + return dexFile + } finally { + tempFiles.forEach { it.delete() } + } + } + + /** + * Builds the command line to run the ART JVM + */ + private fun buildCommandLine(dexFile: String, mainClass: String): List { + 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, + ) + } + +} + +private 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) +} + +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") +} diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtUtils.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtUtils.kt deleted file mode 100644 index cb380554f69a1..0000000000000 --- a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/ArtUtils.kt +++ /dev/null @@ -1,130 +0,0 @@ -// 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 { - 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): Path { - val dexCompiler = root.resolve(DEX_COMPILER) - val tempFiles = mutableListOf() - 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") - } -} diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/JvmAttacher.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/JvmAttacher.kt new file mode 100644 index 0000000000000..ce2918b0334b1 --- /dev/null +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/JvmAttacher.kt @@ -0,0 +1,44 @@ +// 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.debugger.impl.DebuggerSession +import com.intellij.debugger.impl.GenericDebuggerRunnerSettings +import com.intellij.debugger.impl.RemoteConnectionBuilder +import com.intellij.execution.configurations.JavaCommandLineState +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.target.TargetEnvironmentRequest +import com.intellij.execution.target.TargetedCommandLineBuilder + +/** Attaches to a Java VM */ +internal class JvmAttacher : VmAttacher { + + override fun attachVirtualMachine( + testCase: KotlinDescriptorTestCase, + 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 + + return testCase.attachVirtualMachine(javaCommandLineState, env, debugParameters, false) + } +} \ No newline at end of file diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCase.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCase.kt index 2457c45cb567c..42373ce11400c 100644 --- a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCase.kt +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDescriptorTestCase.kt @@ -5,25 +5,22 @@ 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.impl.DebuggerSession +import com.intellij.debugger.impl.DescriptorTestCase +import com.intellij.debugger.impl.GenericDebuggerRunnerSettings +import com.intellij.debugger.impl.OutputChecker import com.intellij.debugger.settings.DebuggerSettings import com.intellij.execution.ExecutionTestCase -import com.intellij.execution.configurations.JavaCommandLineState import com.intellij.execution.configurations.JavaParameters 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 @@ -37,7 +34,6 @@ 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 @@ -51,10 +47,6 @@ 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 @@ -71,8 +63,6 @@ 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" @@ -105,6 +95,7 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), private var logPropagator: LogPropagator? = null private var oldValues: OldValuesStorage? = null + private val myVmAttacher = VmAttacher.getInstance() override fun runBare(testRunnable: ThrowableRunnable) { testAppDirectory = tmpDir("debuggerTestSources") @@ -181,6 +172,9 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), atDebuggerTearDown { invokeAndWaitIfNeeded { oldValues?.revertValues() } } atDebuggerTearDown { KotlinEvaluator.LOG_COMPILATIONS = false } atDebuggerTearDown { restoreIdeCompilerSettings() } + + myVmAttacher.setUp() + atDebuggerTearDown { myVmAttacher.tearDown() } } protected fun dataFile(fileName: String): File = File(getTestDataPath(), fileName) @@ -372,10 +366,8 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), .runProfile(MockConfiguration(myProject)) .build() - val debuggerSession = when (runTestOnArt()) { - true -> createArtLocalProcess(javaParameters, environment) - false -> createJvmLocalProcess(javaParameters, environment) - } + environment.putUserData(DefaultDebugEnvironment.DEBUGGER_TRACE_MODE, traceMode) + val debuggerSession = myVmAttacher.attachVirtualMachine(this, javaParameters, environment) val processHandler = debuggerSession.process.processHandler debuggerSession.process.addProcessListener(object : ProcessAdapter() { @@ -408,68 +400,6 @@ abstract class KotlinDescriptorTestCase : DescriptorTestCase(), 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") - 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) { } diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/VmAttacher.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/VmAttacher.kt new file mode 100644 index 0000000000000..e350cad217555 --- /dev/null +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/VmAttacher.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.debugger.test + +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.runners.ExecutionEnvironment + +private const val PROVIDER_ENV = "INTELLIJ_DEBUGGER_TESTS_VM_ATTACHER" +private const val PROVIDER_PROPERTY = "intellij.debugger.tests.vm.attacher" + +/** Attaches to a VM */ +interface VmAttacher { + /** Perform any setup operations */ + fun setUp() {} + + /** Perform any tear down operations */ + fun tearDown() {} + + fun attachVirtualMachine( + testCase: KotlinDescriptorTestCase, + javaParameters: JavaParameters, + environment: ExecutionEnvironment + ): DebuggerSession + + companion object { + /** + * Returns a [VmAttacher] if configured + * + * The return value is determined from the environment or a JVM property. The property can be a preset value (for example `art` or + * a class name that is expected to be in the classpath). + * + * The default is a [JvmAttacher]. + */ + fun getInstance(): VmAttacher { + return when (val provider = System.getProperty(PROVIDER_PROPERTY) ?: System.getenv(PROVIDER_ENV) ?: "jvm") { + "jvm" -> JvmAttacher() + "art" -> ArtAttacher() + else -> loadProvider(provider) + } + } + } +} + +private fun loadProvider(className: String): VmAttacher { + // Exceptions will cause a test failure + val providerClass = ClassLoader.getSystemClassLoader().loadClass(className) + val constructor = providerClass.getDeclaredConstructor() + return constructor.newInstance() as VmAttacher +}