diff --git a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt index f70f836..892a92e 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -90,6 +90,8 @@ class KotlinDebugAdapter( override fun launch(args: Map) = launcherAsync.execute { performInitialization() + LOG.debug("launch args: $args") + val projectRoot = (args["projectRoot"] as? String)?.let { Paths.get(it) } ?: throw missingRequestArgument("launch", "projectRoot") @@ -98,13 +100,36 @@ class KotlinDebugAdapter( val vmArguments = (args["vmArguments"] as? String) ?: "" + var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) } + + val env = mutableMapOf().apply { + (args["envFile"] as? String)?.let { Paths.get(it) }?.let { envFile -> + envFile.toFile().readLines() + .map { it.trim() } + .filter { it.isNotBlank() && !it.startsWith("#") } + .forEach { + val (key, value) = it.split("=", limit = 2) + set(key, value) + } + } + + // apply 'env' from launch request overriding contents of 'envFile' + args.get("env")?.let { env -> + // Cast from com.google.gson.internal.LinkedTreeMap + @Suppress("UNCHECKED_CAST") + putAll(env as Map) + } + }.toMap() + setupCommonInitializationParams(args) val config = LaunchConfiguration( debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty.map { it.compiledJar }.toSet(), mainClass, projectRoot, - vmArguments + vmArguments, + cwd, + env ) debuggee = launcher.launch( config, diff --git a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt index 2ffa567..e61e1e6 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt @@ -6,5 +6,7 @@ class LaunchConfiguration( val classpath: Set, val mainClass: String, val projectRoot: Path, - val vmArguments: String = "" + val vmArguments: String = "", + val cwd: Path = projectRoot, + val env: Map = mapOf() ) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt index ecb8814..5b9c5e8 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt @@ -4,7 +4,6 @@ import org.javacs.kt.LOG import org.javacs.ktda.core.launch.DebugLauncher import org.javacs.ktda.core.launch.LaunchConfiguration import org.javacs.ktda.core.launch.AttachConfiguration -import org.javacs.ktda.core.Debuggee import org.javacs.ktda.core.DebugContext import org.javacs.ktda.util.KotlinDAException import org.javacs.ktda.jdi.JDIDebuggee @@ -16,14 +15,11 @@ import com.sun.jdi.connect.AttachingConnector import java.io.File import java.nio.file.Path import java.nio.file.Files -import java.net.URLEncoder -import java.net.URLDecoder -import java.nio.charset.StandardCharsets import java.util.stream.Collectors +import org.javacs.kt.LOG class JDILauncher( private val attachTimeout: Int = 50, - private val vmArguments: String? = null, private val modulePaths: String? = null ) : DebugLauncher { private val vmManager: VirtualMachineManager @@ -31,7 +27,7 @@ class JDILauncher( override fun launch(config: LaunchConfiguration, context: DebugContext): JDIDebuggee { val connector = createLaunchConnector() - LOG.info("Starting JVM debug session with main class {}", config.mainClass) + LOG.info("Starting JVM debug session with main class {} and connector {}", config.mainClass, connector) LOG.debug("Launching VM") val vm = connector.launch(createLaunchArgs(config, connector)) ?: throw KotlinDAException("Could not launch a new VM") @@ -54,9 +50,11 @@ class JDILauncher( private fun createLaunchArgs(config: LaunchConfiguration, connector: Connector): Map = connector.defaultArguments() .also { args -> - args["suspend"]!!.setValue("true") - args["options"]!!.setValue(formatOptions(config)) - args["main"]!!.setValue(formatMainClass(config)) + args.get("suspend")?.setValue("true") + args.get("options")?.setValue(formatOptions(config)) + args.get("main")?.setValue(formatMainClass(config)) + args.get("cwd")?.setValue(config.cwd.toAbsolutePath().toString()) + args.get("env")?.setValue(KDACommandLineLauncher.urlEncode(config.env.map { "${it.key}=${it.value}" }) ?: "") } private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() @@ -67,12 +65,13 @@ class JDILauncher( } private fun createAttachConnector(): AttachingConnector = vmManager.attachingConnectors() - .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } + .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } } ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") - + private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors() - // Workaround for JDK 11+ where the first launcher (RawCommandLineLauncher) does not properly support args - .let { it.find { it.javaClass.name == "com.sun.tools.jdi.SunCommandLineLauncher" } ?: it.firstOrNull() } + .also { LOG.debug("connectors: $it") } + // Using our own connector to support cwd and envs + .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") private fun sourcesRootsOf(projectRoot: Path): Set = @@ -104,13 +103,5 @@ class JDILauncher( private fun formatClasspath(config: LaunchConfiguration): String = config.classpath .map { it.toAbsolutePath().toString() } .reduce { prev, next -> "$prev${File.pathSeparatorChar}$next" } - - private fun urlEncode(arg: Collection?) = arg - ?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) } - ?.reduce { a, b -> "$a\n$b" } - - private fun urlDecode(arg: String?) = arg - ?.split("\n") - ?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } - ?.toList() + } diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt new file mode 100644 index 0000000..7b22ef2 --- /dev/null +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -0,0 +1,282 @@ +package org.javacs.ktda.jdi.launch + +import com.sun.jdi.Bootstrap +import com.sun.jdi.VirtualMachine +import com.sun.jdi.connect.* +import com.sun.jdi.connect.spi.Connection +import com.sun.jdi.connect.spi.TransportService +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import org.javacs.kt.LOG + +internal const val ARG_HOME = "home" +internal const val ARG_OPTIONS = "options" +internal const val ARG_MAIN = "main" +internal const val ARG_SUSPEND = "suspend" +internal const val ARG_QUOTE = "quote" +internal const val ARG_VM_EXEC = "vmexec" +internal const val ARG_CWD = "cwd" +internal const val ARG_ENV = "env" + +/** A custom LaunchingConnector that supports cwd and env variables */ +open class KDACommandLineLauncher : LaunchingConnector { + + protected val defaultArguments = mutableMapOf() + + /** We only support SocketTransportService */ + protected val transportService: TransportService + protected val transport = Transport { "dt_socket" } + + companion object { + + fun urlEncode(arg: Collection?) = arg + ?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) } + ?.fold("") { a, b -> "$a\n$b" } + + fun urlDecode(arg: String?) = arg + ?.trim('\n') + ?.split("\n") + ?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } + ?.toList() + } + + constructor() : super() { + defaultArguments.apply { + set(ARG_HOME, StringArgument(ARG_HOME, "Java home", value = System.getProperty("java.home"))) + set(ARG_OPTIONS, StringArgument(ARG_OPTIONS, "Jvm arguments")) + set(ARG_MAIN, StringArgument(ARG_MAIN, "Main class name and parameters", mustSpecify = true)) + set(ARG_SUSPEND, StringArgument(ARG_SUSPEND, "Whether launch the debugee in suspend mode", value = "true")) + set(ARG_QUOTE, StringArgument(ARG_QUOTE, "Quote char", value = "\"")) + set(ARG_VM_EXEC, StringArgument(ARG_VM_EXEC, "The java exec", value = "java")) + set(ARG_CWD, StringArgument(ARG_CWD, "Current working directory")) + set(ARG_ENV, StringArgument(ARG_ENV, "Environment variables")) + } + + // Load TransportService 's implementation + try { + transportService = + Class.forName("com.sun.tools.jdi.SocketTransportService") + .getDeclaredConstructor() + .newInstance() as + TransportService + } catch (e: Exception) { + throw IllegalStateException("Failed to load com.sun.tools.jdi.SocketTransportService") + } + } + + override fun name(): String = javaClass.name + + override fun description(): String = "A custom launcher supporting cwd and env variables" + + override fun defaultArguments(): Map = defaultArguments + + override fun toString(): String = name() + + override fun transport(): Transport = transport + + protected fun getOrDefault( + arguments: Map, + argName: String + ): String { + return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: "" + } + + private fun tokenizeCommandLine(args: String): Array { + val result = mutableListOf() + + val DEFAULT = 0 + val ARG = 1 + val IN_DOUBLE_QUOTE = 2 + val IN_SINGLE_QUOTE = 3 + + var state = DEFAULT + val buf = StringBuilder() + val len = args.length + var i = 0 + + while (i < len) { + var ch = args[i] + if (ch.isWhitespace()) { + if (state == DEFAULT) { + // skip + i++ + continue + } else if (state == ARG) { + state = DEFAULT + result.add(buf.toString()) + buf.setLength(0) + i++ + continue + } + } + when (state) { + DEFAULT, ARG -> { + when { + ch == '"' -> state = IN_DOUBLE_QUOTE + ch == '\'' -> state = IN_SINGLE_QUOTE + ch == '\\' && i + 1 < len -> { + state = ARG + ch = args[++i] + buf.append(ch) + } + else -> { + state = ARG + buf.append(ch) + } + } + } + IN_DOUBLE_QUOTE -> { + when { + ch == '"' -> state = ARG + ch == '\\' && i + 1 < len && (args[i + 1] == '\\' || args[i + 1] == '"') -> { + ch = args[++i] + buf.append(ch) + } + else -> buf.append(ch) + } + } + IN_SINGLE_QUOTE -> { + if (ch == '\'') { + state = ARG + } else { + buf.append(ch) + } + } + else -> throw IllegalStateException() + } + i++ + } + if (buf.isNotEmpty() || state != DEFAULT) { + result.add(buf.toString()) + } + + return result.toTypedArray() + } + + private fun buildCommandLine(arguments: Map, address: String): String { + val home = getOrDefault(arguments, ARG_HOME) + val javaExe = getOrDefault(arguments, ARG_VM_EXEC) + val options = getOrDefault(arguments, ARG_OPTIONS) + val suspend = getOrDefault(arguments, ARG_SUSPEND).toBoolean() + val main = getOrDefault(arguments, ARG_MAIN) + + val exe = if (home.isNotEmpty()) Paths.get(home, "bin", javaExe).toString() else javaExe + + return StringBuilder().apply { + append("$exe") + val jdwpLine = arrayOf( + "transport=${transport.name()}", + "address=$address", + "server=n", + "suspend=${if (suspend) 'y' else 'n'}" + ) + append(" -agentlib:jdwp=${jdwpLine.joinToString(",")}") + append(" $options") + append(" $main") + }.toString() + } + + /** A customized method to launch the vm and connect to it, supporting cwd and env variables */ + @Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class) + override fun launch(arguments: Map): VirtualMachine { + val quote = getOrDefault(arguments, ARG_QUOTE) + val options = getOrDefault(arguments, ARG_OPTIONS) + val cwd = getOrDefault(arguments, ARG_CWD) + val env = urlDecode(getOrDefault(arguments, ARG_ENV))?.toTypedArray() + + check(quote.length == 1) { "Invalid length for $ARG_QUOTE: $quote" } + check(!options.contains("-Djava.compiler=") || options.lowercase().contains("-djava.compiler=none")) { + "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options" + } + + val listenKey = transportService.startListening() + ?: throw IllegalStateException("Failed to do transportService.startListening()") + + val command = buildCommandLine(arguments, listenKey.address()) + val tokenizedCommand = tokenizeCommandLine(command) + + val (connection, process) = launchAndConnect(tokenizedCommand, listenKey, transportService, cwd, env) + return Bootstrap.virtualMachineManager().createVirtualMachine(connection, process) + } + + private fun streamToString(stream: InputStream): String { + val lineSep = System.getProperty("line.separator") + return StringBuilder().apply { + var line: String? + val reader = stream.bufferedReader() + + while (reader.readLine().also { line = it } != null) { + append(line) + append(lineSep) + } + }.toString() + } + + /** + * launch the command, connect to transportService, and returns the connection / process pair + */ + protected fun launchAndConnect( + commandArray: Array, + listenKey: TransportService.ListenKey, + ts: TransportService, + cwd: String = "", + envs: Array? = null + ): Pair { + + val dir = if (cwd.isNotBlank() && Files.isDirectory(Paths.get(cwd))) File(cwd) else null + + val process = Runtime.getRuntime().exec(commandArray, envs, dir) + + val result = CompletableFuture>() + + process.onExit().thenAcceptAsync() { pr -> + if (result.isDone) return@thenAcceptAsync + + val exitCode = pr.exitValue() + val stdOut = streamToString(process.inputStream) + val stdErr = streamToString(process.errorStream) + + LOG.error("Process exited with status: ${exitCode} \n $stdOut \n $stdErr") + result.completeExceptionally(VMStartException("Process exited with status: ${exitCode}", process)) + + try { + ts.stopListening(listenKey) + } catch (e: Exception) {} + } + + CompletableFuture.runAsync() { + try { + val vm = ts.accept(listenKey, 5_000, 5_000) + result.complete(Pair(vm, process)) + } catch (e: IllegalConnectorArgumentsException) { + result.completeExceptionally(e) + } catch (e: IOException) { + if (result.isDone) return@runAsync + + val stdOut = streamToString(process.inputStream) + val stdErr = streamToString(process.errorStream) + + LOG.error("Failed to connect to the launched process: \n $stdOut \n $stdErr") + + process.destroy() + + result.completeExceptionally(e) + } catch (e: RuntimeException) { + result.completeExceptionally(e) + } + } + + return result.get() + } +} diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt new file mode 100644 index 0000000..1868376 --- /dev/null +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt @@ -0,0 +1,43 @@ +package org.javacs.ktda.jdi.launch + +import com.sun.jdi.connect.Connector + +/** + * An implementation to Connector.Argument, used for arguments to launch a LaunchingConnector + */ +class StringArgument constructor(private val name: String, private val description: String = "", private val label: String = name, + private var value:String = "", private val mustSpecify: Boolean = false) : Connector.Argument { + + override fun name(): String { + return name + } + + override fun description(): String { + return description + } + + override fun label(): String { + return label + } + + override fun mustSpecify(): Boolean { + return mustSpecify + } + + override fun value(): String { + return value + } + + override fun setValue(value: String){ + this.value = value + } + + override fun isValid(value: String): Boolean{ + return true + } + override fun toString(): String { + return value + } + + +} \ No newline at end of file diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt index 1a3774d..4c230bd 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt @@ -32,7 +32,7 @@ class JDIVariable( } private fun arrayElementsOf(jdiValue: ArrayReference): List = jdiValue.values - .mapIndexed { i, it -> JDIVariable(i.toString(), it) } + .mapIndexed { i, it -> JDIVariable(i.toString(), it) } private fun fieldsOf(jdiValue: ObjectReference, jdiType: ReferenceType) = jdiType.allFields() .map { JDIVariable(it.name(), jdiValue.getValue(it), jdiType) } diff --git a/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector b/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector new file mode 100644 index 0000000..2a4c0ad --- /dev/null +++ b/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector @@ -0,0 +1 @@ +org.javacs.ktda.jdi.launch.KDACommandLineLauncher \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt index 7769ef2..0172664 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt @@ -17,7 +17,9 @@ import org.hamcrest.Matchers.equalTo abstract class DebugAdapterTestFixture( relativeWorkspaceRoot: String, private val mainClass: String, - private val vmArguments: String = "" + private val vmArguments: String = "", + private val cwd: String = "", + private val envs: Map = mapOf() ) : IDebugProtocolClient { val absoluteWorkspaceRoot: Path = Paths.get(DebugAdapterTestFixture::class.java.getResource("/Anchor.txt").toURI()).parent.resolve(relativeWorkspaceRoot) lateinit var debugAdapter: KotlinDebugAdapter @@ -60,7 +62,9 @@ abstract class DebugAdapterTestFixture( debugAdapter.launch(mapOf( "projectRoot" to absoluteWorkspaceRoot.toString(), "mainClass" to mainClass, - "vmArguments" to vmArguments + "vmArguments" to vmArguments, + "cwd" to cwd, + "env" to envs )).join() println("Launched") } diff --git a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt index 260ec5d..3376f88 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt @@ -10,18 +10,18 @@ import org.eclipse.lsp4j.debug.StoppedEventArguments import org.eclipse.lsp4j.debug.VariablesArguments import org.junit.Assert.assertThat import org.junit.Test -import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.nullValue import org.hamcrest.Matchers.not +import org.javacs.kt.LOG import java.util.concurrent.CountDownLatch /** * Tests a very basic debugging scenario * using a sample application. */ -class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample.workspace.AppKt", "-Dtest=testVmArgs") { +class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample.workspace.AppKt") { private val latch = CountDownLatch(1) private var asyncException: Throwable? = null @@ -39,7 +39,7 @@ class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample. .toString() } breakpoints = arrayOf(SourceBreakpoint().apply { - line = 8 + line = 11 }) }).join() @@ -74,7 +74,16 @@ class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample. variablesReference = receiver!!.variablesReference }).join().variables - assertThat(members.map { Pair(it.name, it.value) }, containsInAnyOrder(Pair("member", "\"testVmArgs\""))) + val memberMap = members.fold(mutableMapOf()) { + map, v -> + map[v.name] = v.value + map + } + + assertThat(memberMap["member"], equalTo(""""test"""")) + assertThat(memberMap["foo"], equalTo("null")) + assertThat(memberMap["cwd"]?.trim('"'), equalTo(absoluteWorkspaceRoot.toString())) + } catch (e: Throwable) { asyncException = e } finally { diff --git a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt new file mode 100644 index 0000000..655429d --- /dev/null +++ b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt @@ -0,0 +1,93 @@ +package org.javacs.ktda + +import org.eclipse.lsp4j.debug.ScopesArguments +import org.eclipse.lsp4j.debug.SetBreakpointsArguments +import org.eclipse.lsp4j.debug.Source +import org.eclipse.lsp4j.debug.SourceBreakpoint +import org.eclipse.lsp4j.debug.StackFrame +import org.eclipse.lsp4j.debug.StackTraceArguments +import org.eclipse.lsp4j.debug.StoppedEventArguments +import org.eclipse.lsp4j.debug.VariablesArguments +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Test +import org.javacs.kt.LOG +import java.util.concurrent.CountDownLatch + +/** + * Tests a very basic debugging scenario + * using a sample application. + */ +class SampleWorkspaceWithCustomConfigTest : DebugAdapterTestFixture( + "sample-workspace", "sample.workspace.AppKt", + vmArguments = "-Dfoo=bar", cwd = "/tmp", envs = mapOf("MSG" to "hello")) { + private val latch = CountDownLatch(1) + private var asyncException: Throwable? = null + + @Test fun testBreakpointsAndVariables() { + debugAdapter.setBreakpoints(SetBreakpointsArguments().apply { + source = Source().apply { + name = "App.kt" + path = absoluteWorkspaceRoot + .resolve("src") + .resolve("main") + .resolve("kotlin") + .resolve("sample") + .resolve("workspace") + .resolve("App.kt") + .toString() + } + breakpoints = arrayOf(SourceBreakpoint().apply { + line = 11 + }) + }).join() + + launch() + latch.await() // Wait for the breakpoint event to finish + asyncException?.let { throw it } + } + + override fun stopped(args: StoppedEventArguments) { + try { + assertThat(args.reason, equalTo("breakpoint")) + + // Query information about the debuggee's current state + val stackTrace = debugAdapter.stackTrace(StackTraceArguments().apply { + threadId = args.threadId + }).join() + val locals = stackTrace.stackFrames.asSequence().flatMap { + debugAdapter.scopes(ScopesArguments().apply { + frameId = it.id + }).join().scopes.asSequence().flatMap { + debugAdapter.variables(VariablesArguments().apply { + variablesReference = it.variablesReference + }).join().variables.asSequence() + } + }.toList() + val receiver = locals.find { it.name == "this" } + + assertThat(locals.map { Pair(it.name, it.value) }, hasItem(Pair("local", "123"))) + assertThat(receiver, not(nullValue())) + + val members = debugAdapter.variables(VariablesArguments().apply { + variablesReference = receiver!!.variablesReference + }).join().variables + + val memberMap = members.fold(mutableMapOf()) { + map, v -> + map[v.name] = v.value + map + } + + assertThat(memberMap["member"], equalTo(""""test"""")) + assertThat(memberMap["foo"], equalTo(""""bar"""")) + assertThat(memberMap["cwd"]?.trim('"'), containsString("/tmp")) + assertThat(memberMap["msg"], equalTo(""""hello"""")) + + } catch (e: Throwable) { + asyncException = e + } finally { + latch.countDown() + } + } +} diff --git a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt new file mode 100644 index 0000000..17c608b --- /dev/null +++ b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt @@ -0,0 +1,25 @@ +package org.javacs.ktda.jdi.launch + +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Test + +class KDACommandLineLauncherTest { + + @Test + fun `defaultArguments should include cwd, envs, suspend`() { + + val connector = KDACommandLineLauncher() + + val args = connector.defaultArguments() + + assertThat(args.size, greaterThanOrEqualTo(2)) + + assertThat(args["cwd"], notNullValue()) + assertThat(args["env"], notNullValue()) + //suspend should default to true + assertThat(args["suspend"]?.value(), equalTo("true")) + + } + +} \ No newline at end of file diff --git a/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt b/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt index cee06ee..b5a73f5 100644 --- a/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt +++ b/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt @@ -1,7 +1,10 @@ package sample.workspace class App { - private val member: String = System.getProperty("test") + private val member: String = "test" + private val foo: String? = System.getProperty("foo") + private val cwd: String = System.getProperty("user.dir") + private val msg: String? = System.getenv("MSG") val greeting: String get() { val local: Int = 123