diff --git a/adapter/build.gradle b/adapter/build.gradle index 9a2d554..ed83fce 100644 --- a/adapter/build.gradle +++ b/adapter/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation 'com.github.fwcd.kotlin-language-server:shared:229c762a4d75304d21eba6d8e1231ed949247629' // The Java Debug Interface classes (com.sun.jdi.*) implementation files("${System.properties['java.home']}/../lib/tools.jar") + // For CommandLineUtils.translateCommandLine + implementation 'org.codehaus.plexus:plexus-utils:3.3.0' testImplementation 'junit:junit:4.12' testImplementation 'org.hamcrest:hamcrest-all:1.3' } 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 cd1fe33..57c29a4 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -92,6 +92,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") @@ -100,13 +102,21 @@ class KotlinDebugAdapter( val vmArguments = (args["vmArguments"] as? String) ?: "" + var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) } + + // Cast from com.google.gson.internal.LinkedTreeMap + @Suppress("UNCHECKED_CAST") + var envs = args["envs"] as? Map ?: mapOf() + setupCommonInitializationParams(args) val config = LaunchConfiguration( debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty, mainClass, projectRoot, - vmArguments + vmArguments, + cwd, + envs ) 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..680ed80 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 envs: 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 882032b..88d43a1 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 @@ -57,6 +53,8 @@ class JDILauncher( args["suspend"]!!.setValue("true") args["options"]!!.setValue(formatOptions(config)) args["main"]!!.setValue(formatMainClass(config)) + args["cwd"]!!.setValue(config.cwd.toAbsolutePath().toString()) + args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs.map { "${it.key}=${it.value}" }) ?: "") } private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() @@ -70,9 +68,9 @@ class JDILauncher( .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } ?: 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() } + private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors().also { LOG.debug("connectors: $it") } + // Using our own connector to support cwd and envs + .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") private fun sourcesRootsOf(projectRoot: Path): Set = projectRoot.resolve("src") @@ -100,13 +98,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..c1db319 --- /dev/null +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -0,0 +1,208 @@ +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 org.codehaus.plexus.util.cli.CommandLineUtils +import org.javacs.kt.LOG +import java.io.File +import java.io.IOException +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.Executors +import java.util.concurrent.TimeUnit + +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_ENVS = "envs" + +/** + * A custom LaunchingConnector that supports cwd and env variables + */ +open class KDACommandLineLauncher : LaunchingConnector { + + protected val defaultArguments = mutableMapOf() + + /** + * We only support SocketTransportService + */ + protected var transportService : TransportService? = null + protected var 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[ARG_HOME] = StringArgument(ARG_HOME, description = "Java home", value = System.getProperty("java.home")) + + defaultArguments[ARG_OPTIONS] = StringArgument(ARG_OPTIONS, description = "Jvm arguments") + + defaultArguments[ARG_MAIN] = StringArgument(ARG_MAIN, description = "Main class name and parameters", mustSpecify = true) + + defaultArguments[ARG_SUSPEND] = StringArgument(ARG_SUSPEND, description = "Whether launch the debugee in suspend mode", value = "true") + + defaultArguments[ARG_QUOTE] = StringArgument(ARG_QUOTE, description = "Quote char", value = "\"") + + defaultArguments[ARG_VM_EXEC] = StringArgument(ARG_VM_EXEC, description = "The java exec", value = "java") + + defaultArguments[ARG_CWD] = StringArgument(ARG_CWD, description = "Current working directory") + + defaultArguments[ARG_ENVS] = StringArgument(ARG_ENVS, description = "Environment variables") + + // Load TransportService 's implementation + transportService = Class.forName("com.sun.tools.jdi.SocketTransportService").getDeclaredConstructor().newInstance() as TransportService + + if(transportService == null){ + throw IllegalStateException("Failed to load com.sun.tools.jdi.SocketTransportService") + } + + } + + override fun name(): String { + return this.javaClass.name + } + + override fun description(): String { + return "A custom launcher supporting cwd and env variables" + } + + override fun defaultArguments(): Map { + return this.defaultArguments + } + + override fun toString(): String { + return name() + } + + override fun transport(): Transport { + return transport + } + + protected fun getOrDefault(arguments: Map, argName: String): String { + return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: "" + } + + /** + * 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 vm: VirtualMachine + + val home = getOrDefault(arguments, ARG_HOME) + val options = getOrDefault(arguments, ARG_OPTIONS) + val main = getOrDefault(arguments, ARG_MAIN) + val suspend = getOrDefault(arguments, ARG_SUSPEND).toBoolean() + val quote = getOrDefault(arguments, ARG_QUOTE) + var exe = getOrDefault(arguments, ARG_VM_EXEC) + val cwd = getOrDefault(arguments, ARG_CWD) + val envs = urlDecode(getOrDefault(arguments, ARG_ENVS))?.toTypedArray() + + check(quote.length == 1) {"Invalid length for $ARG_QUOTE: $quote"} + check(!options.contains("-Djava.compiler=") || + options.toLowerCase().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 address = listenKey.address() + + try { + val command = StringBuilder() + + exe = if (home.isNotEmpty()) Paths.get(home, "bin", exe).toString() else exe + command.append(wrapWhitespace(exe)) + + command.append(" $options") + + //debug options + command.append(" -agentlib:jdwp=transport=${transport.name()},address=$address,server=n,suspend=${if (suspend) 'y' else 'n'}") + + command.append(" $main") + + LOG.debug("command before tokenize: $command") + + vm = launch(commandArray = CommandLineUtils.translateCommandline(command.toString()), listenKey = listenKey, + ts = transportService!!, cwd = cwd, envs = envs + ) + + } finally { + transportService?.stopListening(listenKey) + } + return vm + } + + internal fun wrapWhitespace(str: String): String { + return if(str.contains(' ')) "\"$str\" " else str + } + + @Throws(IOException::class, VMStartException::class) + fun launch(commandArray: Array, + listenKey: TransportService.ListenKey, + ts: TransportService, cwd: String, envs: Array? = null): VirtualMachine { + + val (connection, process) = launchAndConnect(commandArray, listenKey, ts, cwd = cwd, envs = envs) + + return Bootstrap.virtualMachineManager().createVirtualMachine(connection, + process) + } + + + /** + * 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 + + var threadCount = 0 + + val executors = Executors.newFixedThreadPool(2) { Thread(it, "${this.javaClass.simpleName}-${threadCount++}") } + val process = Runtime.getRuntime().exec(commandArray, envs, dir) + + val connectionTask: Callable = Callable { ts.accept(listenKey, 0,0).also { LOG.debug("ts.accept invoked") } } + val exitCodeTask: Callable = Callable { process.waitFor().also { LOG.debug("process.waitFor invoked") } } + + try { + when (val result = executors.invokeAny(listOf(connectionTask, exitCodeTask))) { + // successfully connected to transport service + is Connection -> return Pair(result, process) + + // cmd exited before connection. some thing wrong + is Int -> throw VMStartException( + "VM initialization failed. exit code: ${process?.exitValue()}, cmd: $commandArray", process) + + // should never occur + else -> throw IllegalStateException("Unknown result: $result") + } + } finally { + // release the executors. no longer needed. + executors.shutdown() + executors.awaitTermination(1, TimeUnit.SECONDS) + } + + } + +} \ No newline at end of file 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 1e512b0..8264597 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 @@ -31,7 +31,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 de342c6..e267250 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, + "envs" 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..430615a --- /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["envs"], 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