Skip to content

Commit

Permalink
Support NodeJs SAM debugging (#995)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangzhx authored and Zhaoxi Zhang committed Jun 4, 2019
1 parent 2981e88 commit d660175
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Support NodeJs SAM debugging"
}
2 changes: 1 addition & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ task testJar (type: Jar) {

artifacts {
testArtifacts testJar
}
}
1 change: 1 addition & 0 deletions jetbrains-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ configurations {
task testJar (type: Jar) {
baseName = "${project.name}-test"
from sourceSets.test.output
from sourceSets.integrationTest.output
}

task pluginChangeLog(type: GenerateChangeLog) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ class PathMapper(private val mappings: List<PathMapping>) {
}

class PathMapping(localPath: String, remotePath: String) {
internal val localRoot = normalizeLocal(localPath) + "/"
internal val remoteRoot = FileUtil.normalize("$remotePath/")
val localRoot = normalizeLocal(localPath) + "/"
val remoteRoot = FileUtil.normalize("$remotePath/")

override fun toString() = "PathMapping(localRoot='$localRoot', remoteRoot='$remoteRoot')"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SamRunningState(
environment: ExecutionEnvironment,
val settings: LocalLambdaSettings
) : CommandLineState(environment) {
internal lateinit var builtLambda: BuiltLambda
lateinit var builtLambda: BuiltLambda

internal val runner = if (environment.executor.id == DefaultDebugExecutor.EXECUTOR_ID) {
SamDebugger()
Expand Down
1 change: 1 addition & 0 deletions jetbrains-ultimate/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ apply plugin: 'org.jetbrains.intellij'
dependencies {
compile project(":jetbrains-core")
testCompile project(path: ":jetbrains-core", configuration: 'testArtifacts')
integrationTestCompile project(path: ":jetbrains-core", configuration: 'testArtifacts')
}

intellij {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.nodejs

import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.xdebugger.XDebuggerUtil
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialsManager
import software.aws.toolkits.jetbrains.services.lambda.execution.local.createHandlerBasedRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.execution.local.createTemplateRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.sam.checkBreakPointHit
import software.aws.toolkits.jetbrains.services.lambda.sam.executeLambda
import software.aws.toolkits.jetbrains.settings.SamSettings
import softwere.aws.toolkits.jetbrains.utils.rules.NodeJsCodeInsightTestFixtureRule
import softwere.aws.toolkits.jetbrains.utils.rules.addPackageJsonFile

@RunWith(Parameterized::class)
class NodeJsLocalLambdaRunConfigurationIntegrationTest(private val runtime: Runtime) {
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun parameters(): Collection<Array<Runtime>> = listOf(
arrayOf(Runtime.NODEJS8_10),
arrayOf(Runtime.NODEJS10_X)
)
}

@Rule
@JvmField
val projectRule = NodeJsCodeInsightTestFixtureRule()

private val mockId = "MockCredsId"
private val mockCreds = AwsBasicCredentials.create("Access", "ItsASecret")

@Before
fun setUp() {
SamSettings.getInstance().savedExecutablePath = System.getenv()["SAM_CLI_EXEC"]

val fixture = projectRule.fixture

val psiFile = fixture.addFileToProject(
"hello_world/app.js",
"""
exports.lambdaHandler = async (event, context) => {
return 'Hello World'
};
""".trimIndent()
)

runInEdtAndWait {
fixture.openFileInEditor(psiFile.virtualFile)
}

MockCredentialsManager.getInstance().addCredentials(mockId, mockCreds)
}

@After
fun tearDown() {
MockCredentialsManager.getInstance().reset()
}

@Test
fun samIsExecuted() {
projectRule.fixture.addPackageJsonFile()

val runConfiguration = createHandlerBasedRunConfiguration(
project = projectRule.project,
runtime = runtime,
handler = "hello_world/app.lambdaHandler",
input = "\"Hello World\"",
credentialsProviderId = mockId
)

assertThat(runConfiguration).isNotNull

val executeLambda = executeLambda(runConfiguration)

assertThat(executeLambda.exitCode).isEqualTo(0)
assertThat(executeLambda.stdout).contains("Hello World")
}

@Test
fun samIsExecutedWithContainer() {
projectRule.fixture.addPackageJsonFile()

val samOptions = SamOptions().apply {
this.buildInContainer = true
}

val runConfiguration = createHandlerBasedRunConfiguration(
project = projectRule.project,
runtime = runtime,
handler = "hello_world/app.lambdaHandler",
input = "\"Hello World\"",
credentialsProviderId = mockId,
samOptions = samOptions
)

assertThat(runConfiguration).isNotNull

val executeLambda = executeLambda(runConfiguration)

assertThat(executeLambda.exitCode).isEqualTo(0)
assertThat(executeLambda.stdout).contains("Hello World")
}

@Test
fun samIsExecutedWhenRunWithATemplateServerless() {
projectRule.fixture.addPackageJsonFile(subPath = "hello_world")

val templateFile = projectRule.fixture.addFileToProject(
"template.yaml", """
Resources:
SomeFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambdaHandler
CodeUri: hello_world
Runtime: $runtime
Timeout: 900
""".trimIndent()
)

val runConfiguration = createTemplateRunConfiguration(
project = projectRule.project,
templateFile = templateFile.containingFile.virtualFile.path,
logicalId = "SomeFunction",
input = "\"Hello World\"",
credentialsProviderId = mockId
)

assertThat(runConfiguration).isNotNull

val executeLambda = executeLambda(runConfiguration)

assertThat(executeLambda.exitCode).isEqualTo(0)
assertThat(executeLambda.stdout).contains("Hello World")
}

@Test
fun samIsExecutedWithDebugger() {
projectRule.fixture.addPackageJsonFile()

val runConfiguration = createHandlerBasedRunConfiguration(
project = projectRule.project,
runtime = runtime,
handler = "hello_world/app.lambdaHandler",
input = "\"Hello World\"",
credentialsProviderId = mockId
)

assertThat(runConfiguration).isNotNull

addBreakpoint(2)

val debuggerIsHit = checkBreakPointHit(projectRule.project)
val executeLambda = executeLambda(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)

assertThat(executeLambda.exitCode).isEqualTo(137)
assertThat(executeLambda.stdout).contains("Hello World")

assertThat(debuggerIsHit.get()).isTrue()
}

@Test
fun samIsExecutedWithDebugger_sameFileNames() {
projectRule.fixture.addPackageJsonFile()

val psiFile = projectRule.fixture.addFileToProject(
"hello_world/subfolder/app.js",
"""
exports.lambdaHandler = async (event, context) => {
return 'Hello World'
};
""".trimIndent()
)

runInEdtAndWait {
projectRule.fixture.openFileInEditor(psiFile.virtualFile)
}

val runConfiguration = createHandlerBasedRunConfiguration(
project = projectRule.project,
runtime = runtime,
handler = "hello_world/subfolder/app.lambdaHandler",
input = "\"Hello World\"",
credentialsProviderId = mockId
)

assertThat(runConfiguration).isNotNull

addBreakpoint(2)

val debuggerIsHit = checkBreakPointHit(projectRule.project)
val executeLambda = executeLambda(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)

assertThat(executeLambda.exitCode).isEqualTo(137)
assertThat(executeLambda.stdout).contains("Hello World")

assertThat(debuggerIsHit.get()).isTrue()
}

private fun addBreakpoint(lineNumber: Int) {
runInEdtAndWait {
XDebuggerUtil.getInstance().toggleLineBreakpoint(
projectRule.project,
projectRule.fixture.file.virtualFile,
lineNumber
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,83 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.lambda.nodejs

import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.javascript.debugger.LocalFileSystemFileFinder
import com.intellij.javascript.debugger.RemoteDebuggingFileFinder
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Urls
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebugSession
import com.jetbrains.debugger.wip.WipLocalVmConnection
import com.jetbrains.nodeJs.NodeChromeDebugProcess
import org.jetbrains.io.LocalFileFinder
import software.aws.toolkits.jetbrains.services.lambda.execution.PathMapping
import software.aws.toolkits.jetbrains.services.lambda.execution.local.SamDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.local.SamRunningState
import java.net.InetSocketAddress

// TODO provide implementation of Node.js debugger support
class NodeJsSamDebugSupport : SamDebugSupport {
override fun createDebugProcess(
environment: ExecutionEnvironment,
state: SamRunningState,
debugPort: Int
): XDebugProcessStarter? = null
): XDebugProcessStarter? = object : XDebugProcessStarter() {
override fun start(session: XDebugSession): XDebugProcess {
val mappings = createBiMapMappings(state.builtLambda.mappings)
val fileFinder = RemoteDebuggingFileFinder(mappings, LocalFileSystemFileFinder(false))
val connection = WipLocalVmConnection()
val executionResult = state.execute(environment.executor, environment.runner)

val process = NodeChromeDebugProcess(session, fileFinder, connection, executionResult)

val processHandler = executionResult.processHandler
val socketAddress = InetSocketAddress("localhost", debugPort)

if (processHandler == null || processHandler.isStartNotified) {
connection.open(socketAddress)
} else {
processHandler.addProcessListener(object : ProcessAdapter() {
override fun startNotified(event: ProcessEvent) {
connection.open(socketAddress)
}
})
}
return process
}
}

/**
* Convert [PathMapping] to NodeJs debugger path mapping format.
*
* Docker uses the same project structure for dependencies in the folder node_modules. We map the source code and
* the dependencies in node_modules folder separately as the node_modules might not exist in the local project.
*/
private fun createBiMapMappings(pathMapping: List<PathMapping>): BiMap<String, VirtualFile> {
val mappings = HashBiMap.create<String, VirtualFile>(pathMapping.size)

listOf(".", NODE_MODULES).forEach { subPath ->
pathMapping.forEach {
val remotePath = FileUtil.toCanonicalPath("$TASK_PATH/${it.remoteRoot}/$subPath")
val remoteUrl = Urls.newUri("file", remotePath).toString()
LocalFileFinder.findFile("${it.localRoot}/$subPath")?.let { localFile ->
mappings.putIfAbsent(remoteUrl, localFile)
}
}
}

return mappings
}

private companion object {
const val TASK_PATH = "/var/task"
const val NODE_MODULES = "node_modules"
}
}

0 comments on commit d660175

Please sign in to comment.