diff --git a/CHANGELOG.md b/CHANGELOG.md index ab55f1c1e1e..28f7d3e73a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,19 @@ All notable changes to the "aws-vscode-tools" extension will be documented in th ## NEXT (Developer Preview) -* Local Run/Debug of SAM Lambda Functions now outputs to the Output and Debug Console, and reduces timing issues for attaching the debugger -* Removed Lambda view that showed the Lambda Policy -* Removed Lambda view that showed the Lambda Configuration -* The AWS Explorer menu items no longer appear on other VS Code panel menus -* When creating a new SAM Application, the toolkit now checks for a valid SAM CLI version before prompting the user for inputs -* When deploying a SAM Application, the toolkit now checks for a valid SAM CLI version before prompting the user for inputs -* Telemetry now sends AWS account data +- Local Run/Debug is now available for .NET Core 2.1 functions within SAM Applications +- Local Run/Debug of SAM Lambda Functions now outputs to the Output and Debug Console, and reduces timing issues for attaching the debugger +- Removed Lambda view that showed the Lambda Policy +- Removed Lambda view that showed the Lambda Configuration +- The AWS Explorer menu items no longer appear on other VS Code panel menus +- When creating a new SAM Application, the toolkit now checks for a valid SAM CLI version before prompting the user for inputs +- When deploying a SAM Application, the toolkit now checks for a valid SAM CLI version before prompting the user for inputs +- Telemetry now sends AWS account data ## 0.1.1 (Developer Preview) -* Updated Marketplace page to display information on how to use the Toolkit once installed +- Updated Marketplace page to display information on how to use the Toolkit once installed ## 0.1.0 (Developer Preview) -* Initial release - +- Initial release diff --git a/build-scripts/copyNonCodeFiles.js b/build-scripts/copyNonCodeFiles.js index 8632b11b926..66fcd9dac06 100644 --- a/build-scripts/copyNonCodeFiles.js +++ b/build-scripts/copyNonCodeFiles.js @@ -14,7 +14,7 @@ const outRoot = path.join(repoRoot, 'out'); // May be individual files or entire directories. const relativePaths = [ path.join('src', 'schemas'), - path.join('src', 'test', 'shared', 'codelens', 'yaml') + path.join('src', 'test', 'shared', 'cloudformation', 'yaml') ]; (async () => { diff --git a/docs/debugging-nodejs-lambda-functions.md b/docs/debugging-nodejs-lambda-functions.md index a2631b14cfc..1d55f99bb74 100644 --- a/docs/debugging-nodejs-lambda-functions.md +++ b/docs/debugging-nodejs-lambda-functions.md @@ -13,16 +13,16 @@ You can debug your Serverless Application's Lambda Function locally using the Co Throughout these instructions, replace the following: -|Name|Replace With| -|-|-| -|``|The root of your SAM app (typically this is the directory containing `template.yaml`)| -|``|The root of your NodeJS source code (the directory containing `package.json`)| -|``|Either `inspector` (for NodeJS 6.3+) or `legacy` (for prior versions of NodeJS)| +| Name | Replace With | +| ----------------------- | ------------------------------------------------------------------------------------- | +| `` | The root of your SAM app (typically this is the directory containing `template.yaml`) | +| `` | The root of your NodeJS source code (the directory containing `package.json`) | +| `` | Either `inspector` (for NodeJS 6.3+) or `legacy` (for prior versions of NodeJS) | 1. Open `/.vscode/launch.json` (create a new file if it does not already exist), and add the following contents. - * Due to a bug in how VS Code handles path mappings, Windows users must provide an absolute path for `localRoot`. If you use a path relative to `${workspaceFolder}`, the path mappings will not work. - * If desired, replace `5678` with the port that you wish to use for debugging. + - Due to a bug in how VS Code handles path mappings, Windows users must provide an absolute path for `localRoot`. If you use a path relative to `${workspaceFolder}`, the path mappings will not work. + - If desired, replace `5678` with the port that you wish to use for debugging. ```jsonc { @@ -37,10 +37,7 @@ Throughout these instructions, replace the following: "localRoot": "", "remoteRoot": "/var/task", "protocol": "", - "skipFiles": [ - "/var/runtime/node_modules/**/*.js", - "/**/*.js" - ] + "skipFiles": ["/var/runtime/node_modules/**/*.js", "/**/*.js"] } ] } @@ -108,8 +105,8 @@ With the above steps, you need to manually invoke SAM CLI from the command line, "background": { // This is how the debugger knows when it can attach "activeOnStart": true, - "beginsPattern": "^Fetching lambci.* Docker container image......$", - "endsPattern": "^.* Mounting .* as .*:ro inside runtime container$" + "beginsPattern": "^Fetching lambci.* Docker container image......$", + "endsPattern": "^.* Mounting .* as .*:ro inside runtime container$" } } } diff --git a/docs/debugging-python-lambda-functions.md b/docs/debugging-python-lambda-functions.md index 70921d9e022..090ed7010e4 100644 --- a/docs/debugging-python-lambda-functions.md +++ b/docs/debugging-python-lambda-functions.md @@ -167,4 +167,4 @@ With the above steps, you need to manually invoke SAM CLI from the command line, "preLaunchTask": "Debug Python Lambda Function", ``` -Now you can just press `F5`, and Visual Studio Code will invoke SAM CLI and wait for the `waiting for debugger to attach...` message before attaching the debugger. +Now you can just press `F5`, and Visual Studio Code will invoke SAM CLI and wait for the `waiting for debugger to attach...` message before attaching the debugger. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dd149e1f064..e95d53025e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,9 +88,9 @@ "dev": true }, "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", "dev": true }, "@types/node": { @@ -365,6 +365,18 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, + "azure-devops-node-api": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz", + "integrity": "sha512-pMfGJ6gAQ7LRKTHgiRF+8iaUUeGAI0c8puLaqHLc7B8AR7W6GJLozK9RFeUHFjEGybC9/EB3r67WPd7e46zQ8w==", + "dev": true, + "requires": { + "os": "0.1.1", + "tunnel": "0.0.4", + "typed-rest-client": "1.2.0", + "underscore": "1.8.3" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -1460,9 +1472,9 @@ }, "dependencies": { "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -1586,6 +1598,16 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } } } }, @@ -2242,26 +2264,6 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "multimatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", - "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", - "dev": true, - "requires": { - "array-differ": "^2.0.3", - "array-union": "^1.0.2", - "arrify": "^1.0.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "array-differ": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz", - "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", - "dev": true - } - } - }, "multipipe": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", @@ -2278,9 +2280,9 @@ "dev": true }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, "nice-try": { "version": "1.0.5", @@ -2405,6 +2407,12 @@ } } }, + "os": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", + "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=", + "dev": true + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -2667,6 +2675,12 @@ "multimatch": "^3.0.0" }, "dependencies": { + "array-differ": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz", + "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", + "dev": true + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -2692,6 +2706,18 @@ "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "multimatch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", + "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", + "dev": true, + "requires": { + "array-differ": "^2.0.3", + "array-union": "^1.0.2", + "arrify": "^1.0.1", + "minimatch": "^3.0.4" + } } } }, @@ -2711,27 +2737,11 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -3295,7 +3305,7 @@ }, "tunnel": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=", "dev": true }, @@ -3329,21 +3339,13 @@ "dev": true }, "typed-rest-client": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-0.9.0.tgz", - "integrity": "sha1-92jMDcP06VDwbgSCXDaz54NKofI=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", + "integrity": "sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw==", "dev": true, "requires": { "tunnel": "0.0.4", "underscore": "1.8.3" - }, - "dependencies": { - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true - } } }, "typescript": { @@ -3358,9 +3360,9 @@ "dev": true }, "uglify-js": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.4.tgz", - "integrity": "sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA==", + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz", + "integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==", "optional": true, "requires": { "commander": "~2.20.0", @@ -3376,9 +3378,9 @@ } }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", "dev": true }, "universalify": { @@ -3409,12 +3411,12 @@ "dev": true }, "url-parse": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.6.tgz", - "integrity": "sha512-/B8AD9iQ01seoXmXf9z/MjLZQIdOoYl/+gvsQF6+mpnxaTfG9P7srYaiqaDMyKkR36XMXfhqSHss5MyFAO8lew==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", "dev": true, "requires": { - "querystringify": "^2.0.0", + "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, @@ -3449,11 +3451,12 @@ } }, "vsce": { - "version": "1.59.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.59.0.tgz", - "integrity": "sha512-tkB97885k5ce25Brbe9AZTCAXAkBh7oa5EOzY0BCJQ51W/mfRaQuCluCd9gZpWdgiU4AbPvwxtoVKKsenlSt8w==", + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.62.0.tgz", + "integrity": "sha512-KXtPBsdC0K27hmDksINyjoRl9BiuTB+ntmoJEDbbO3GIc+L3wfOclaSy8iYqddnpSA33YDkhKzKXhT0JGzrG/A==", "dev": true, "requires": { + "azure-devops-node-api": "^7.2.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.1", "commander": "^2.8.1", @@ -3468,8 +3471,8 @@ "read": "^1.0.7", "semver": "^5.1.0", "tmp": "0.0.29", + "typed-rest-client": "1.2.0", "url-join": "^1.1.0", - "vso-node-api": "6.1.2-preview", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, @@ -3506,9 +3509,9 @@ } }, "vscode": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.33.tgz", - "integrity": "sha512-sXedp2oF6y4ZvqrrFiZpeMzaCLSWV+PpYkIxjG/iYquNZ9KrLL2LujltGxPLvzn49xu2sZkyC+avVNFgcJD1Iw==", + "version": "1.1.34", + "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.34.tgz", + "integrity": "sha512-GuT3tCT2N5Qp26VG4C+iGmWMgg/MuqtY5G5TSOT3U/X6pgjM9LFulJEeqpyf6gdzpI4VyU3ZN/lWPo54UFPuQg==", "dev": true, "requires": { "glob": "^7.1.2", @@ -3517,7 +3520,7 @@ "semver": "^5.4.1", "source-map-support": "^0.5.0", "url-parse": "^1.4.4", - "vscode-test": "^0.1.4" + "vscode-test": "^0.4.1" }, "dependencies": { "browser-stdout": { @@ -3670,27 +3673,15 @@ } }, "vscode-test": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.1.5.tgz", - "integrity": "sha512-s+lbF1Dtasc0yXVB9iQTexBe2JK6HJAUJe3fWezHKIjq+xRw5ZwCMEMBaonFIPy7s95qg2HPTRDR5W4h4kbxGw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz", + "integrity": "sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w==", "dev": true, "requires": { "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1" } }, - "vso-node-api": { - "version": "6.1.2-preview", - "resolved": "https://registry.npmjs.org/vso-node-api/-/vso-node-api-6.1.2-preview.tgz", - "integrity": "sha1-qrNUbfJFHs2JTgcbuZtd8Zxfp48=", - "dev": true, - "requires": { - "q": "^1.0.1", - "tunnel": "0.0.4", - "typed-rest-client": "^0.9.0", - "underscore": "^1.8.3" - } - }, "vue": { "version": "2.5.16", "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.16.tgz", diff --git a/package.json b/package.json index e6cbc436265..210a4824e2b 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "engines": { "vscode": "^1.31.1" }, + "icon": "resources/aws-icon-256x256.png", "bugs": { "url": "https://github.com/aws/aws-toolkit-vscode/issues" }, - "icon": "resources/aws-icon-256x256.png", "galleryBanner": { "color": "#FF9900", "theme": "light" @@ -309,7 +309,7 @@ "copyNonCodeFiles": "node ./build-scripts/copyNonCodeFiles.js", "compile": "tsc -p ./ && npm run lint && npm run bundleDeps && npm run copyNonCodeFiles", "recompile": "npm run clean && npm run compile", - "watch": "tsc -watch -p ./", + "watch": "npm run copyNonCodeFiles && tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", "test": "npm run compile && node ./test-scripts/test.js", "lint": "tslint --project .", @@ -325,7 +325,7 @@ "@types/glob": "^7.1.1", "@types/js-yaml": "^3.12.0", "@types/lodash": "^4.14.110", - "@types/mocha": "^2.2.48", + "@types/mocha": "^5.2.0", "@types/node": "^10.12.12", "@types/opn": "^5.1.0", "@types/request": "^2.47.1", @@ -387,7 +387,7 @@ }, "husky": { "hooks": { - "pre-commit": "(git secrets --pre_commit_hook -- \"$@\" || echo 'Please install git-secrets https://github.com/awslabs/git-secrets to check for accidentally commited secrets!') && pretty-quick --staged" + "pre-commit": "(git secrets --pre_commit_hook -- \"$@\" || echo 'Please install git-secrets https://github.com/awslabs/git-secrets to check for accidentally commited secrets!')" } } } diff --git a/package.nls.json b/package.nls.json index d0d059473c0..5da937c2c9b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -140,6 +140,8 @@ "AWS.samcli.local.invoke.ended": "Local invoke of SAM Application has ended.", "AWS.samcli.local.invoke.error": "Error encountered running local SAM Application", "AWS.samcli.local.invoke.port.not.open": "The debug port doesn't appear to be open. The debugger might not succeed when attaching to your SAM Application.", + "AWS.samcli.local.invoke.debugger.install": "Installing .NET Core Debugger to {0}...", + "AWS.samcli.local.invoke.debugger.install.failed": "Error installing .NET Core Debugger: {0}", "AWS.samcli.local.invoke.debugger.timeout": "The SAM process did not make the debugger available within the time limit", "AWS.samcli.notification.not.found": "Unable to find SAM CLI. It is required in order to work with Serverless Applications locally.", "AWS.samcli.notification.unexpected.validation.issue": "An unexpected issue occured while validating SAM CLI: {0}", diff --git a/resources/light/help.svg b/resources/light/help.svg index 3092aed1a42..9f10a8815f9 100644 --- a/resources/light/help.svg +++ b/resources/light/help.svg @@ -7,4 +7,4 @@ - \ No newline at end of file + diff --git a/src/extension.ts b/src/extension.ts index 62513ccee6b..5da91d562f5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { DefaultAWSClientBuilder } from './shared/awsClientBuilder' import { AwsContextTreeCollection } from './shared/awsContextTreeCollection' import { DefaultToolkitClientBuilder } from './shared/clients/defaultToolkitClientBuilder' import { CodeLensProviderParams } from './shared/codelens/codeLensUtils' +import * as csLensProvider from './shared/codelens/csharpCodeLensProvider' import * as pyLensProvider from './shared/codelens/pythonCodeLensProvider' import * as tsLensProvider from './shared/codelens/typescriptCodeLensProvider' import { documentationUrl, extensionSettingsPrefix, githubUrl } from './shared/constants' @@ -230,6 +231,12 @@ async function activateCodeLensProviders( await pyLensProvider.makePythonCodeLensProvider() )) + await csLensProvider.initialize(providerParams) + disposables.push(vscode.languages.registerCodeLensProvider( + csLensProvider.CSHARP_ALLFILES, + await csLensProvider.makeCSharpCodeLensProvider() + )) + return disposables } diff --git a/src/lambda/commands/createNewSamApp.ts b/src/lambda/commands/createNewSamApp.ts index 22a740b73bb..6651bbb2aec 100644 --- a/src/lambda/commands/createNewSamApp.ts +++ b/src/lambda/commands/createNewSamApp.ts @@ -10,13 +10,13 @@ const localize = nls.loadMessageBundle() import * as path from 'path' import * as vscode from 'vscode' +import { fileExists } from '../../shared/filesystemUtilities' import { getSamCliContext, SamCliContext } from '../../shared/sam/cli/samCliContext' import { runSamCliInit, SamCliInitArgs } from '../../shared/sam/cli/samCliInit' import { throwAndNotifyIfInvalid } from '../../shared/sam/cli/samCliValidationUtils' import { SamCliValidator } from '../../shared/sam/cli/samCliValidator' import { METADATA_FIELD_NAME, MetadataResult } from '../../shared/telemetry/telemetryTypes' import { ChannelLogger } from '../../shared/utilities/vsCodeUtils' -import { getMainSourceFileUri } from '../utilities/getMainSourceFile' import { CreateNewSamAppWizard, CreateNewSamAppWizardResponse, @@ -25,7 +25,9 @@ import { export const URI_TO_OPEN_ON_INIT_KEY = 'URI_TO_OPEN_ON_INIT_KEY' -export async function resumeCreateNewSamApp(context: Pick) { +export async function resumeCreateNewSamApp( + context: Pick +) { const rawUri = context.globalState.get(URI_TO_OPEN_ON_INIT_KEY) if (!rawUri) { return @@ -137,15 +139,14 @@ async function validateSamCli(samCliValidator: SamCliValidator): Promise { async function getMainUri( config: Pick ): Promise { - try { - return await getMainSourceFileUri({ - root: vscode.Uri.file(path.join(config.location.fsPath, config.name)) - }) - } catch (err) { + const samTemplatePath = path.resolve(config.location.fsPath, config.name, 'template.yaml') + if (await fileExists(samTemplatePath)) { + return vscode.Uri.file(samTemplatePath) + } else { vscode.window.showWarningMessage(localize( 'AWS.samcli.initWizard.source.error.notFound', 'Project created successfully, but main source code file not found: {0}', - err + samTemplatePath )) } } diff --git a/src/lambda/commands/deploySamApplication.ts b/src/lambda/commands/deploySamApplication.ts index da6b47799ce..cc347e711c8 100644 --- a/src/lambda/commands/deploySamApplication.ts +++ b/src/lambda/commands/deploySamApplication.ts @@ -50,24 +50,21 @@ export async function deploySamApplication( channelLogger, regionProvider, extensionContext, - samDeployWizard = getDefaultSamDeployWizardResponseProvider( - regionProvider, - extensionContext - ), + samDeployWizard = getDefaultSamDeployWizardResponseProvider(regionProvider, extensionContext) }: { samCliContext?: SamCliContext - channelLogger: ChannelLogger, - regionProvider: RegionProvider, - samDeployWizard?: SamDeployWizardResponseProvider, + channelLogger: ChannelLogger + regionProvider: RegionProvider + samDeployWizard?: SamDeployWizardResponseProvider extensionContext: Pick }, { awsContext, - window = getDefaultWindowFunctions(), + window = getDefaultWindowFunctions() }: { - awsContext: Pick, - window?: WindowFunctions, - }, + awsContext: Pick + window?: WindowFunctions + } ): Promise { try { const profile: string | undefined = awsContext.getCredentialProfileName() @@ -90,27 +87,28 @@ export async function deploySamApplication( parameterOverrides: deployWizardResponse.parameterOverrides, profile, region: deployWizardResponse.region, - sourceTemplatePath: deployWizardResponse.template.fsPath, + sourceTemplatePath: deployWizardResponse.template.fsPath } const deployApplicationPromise = deploy({ deployParameters, channelLogger, invoker: samCliContext.invoker, - window, - }).then(async () => - // The parent method will exit shortly, and the status bar will run this promise - // Cleanup has to be chained into the promise as a result. - await del(deployParameters.deployRootFolder, { - force: true - }) + window + }).then( + async () => + // The parent method will exit shortly, and the status bar will run this promise + // Cleanup has to be chained into the promise as a result. + await del(deployParameters.deployRootFolder, { + force: true + }) ) window.setStatusBarMessage( localize( 'AWS.samcli.deploy.statusbar.message', '$(cloud-upload) Deploying SAM Application to {0}...', - deployWizardResponse.stackName, + deployWizardResponse.stackName ), deployApplicationPromise ) @@ -134,39 +132,34 @@ function getPackageTemplatePath(deployRootFolder: string): string { } async function buildOperation(params: { - deployParameters: DeploySamApplicationParameters, - invoker: SamCliProcessInvoker, - channelLogger: ChannelLogger, + deployParameters: DeploySamApplicationParameters + invoker: SamCliProcessInvoker + channelLogger: ChannelLogger }): Promise { - params.channelLogger.info( - 'AWS.samcli.deploy.workflow.init', - 'Building SAM Application...' - ) + params.channelLogger.info('AWS.samcli.deploy.workflow.init', 'Building SAM Application...') const buildDestination = getBuildRootFolder(params.deployParameters.deployRootFolder) - const build = new SamCliBuildInvocation( - { - buildDir: buildDestination, - baseDir: undefined, - templatePath: params.deployParameters.sourceTemplatePath, - invoker: params.invoker, - } - ) + const build = new SamCliBuildInvocation({ + buildDir: buildDestination, + baseDir: undefined, + templatePath: params.deployParameters.sourceTemplatePath, + invoker: params.invoker + }) await build.execute() } async function packageOperation(params: { - deployParameters: DeploySamApplicationParameters, - invoker: SamCliProcessInvoker, - channelLogger: ChannelLogger, + deployParameters: DeploySamApplicationParameters + invoker: SamCliProcessInvoker + channelLogger: ChannelLogger }): Promise { params.channelLogger.info( 'AWS.samcli.deploy.workflow.packaging', 'Packaging SAM Application to S3 Bucket: {0} with profile: {1}', params.deployParameters.packageBucketName, - params.deployParameters.profile, + params.deployParameters.profile ) const buildTemplatePath = getBuildTemplatePath(params.deployParameters.deployRootFolder) @@ -178,24 +171,24 @@ async function packageOperation(params: { destinationTemplateFile: packageTemplatePath, profile: params.deployParameters.profile, region: params.deployParameters.region, - s3Bucket: params.deployParameters.packageBucketName, + s3Bucket: params.deployParameters.packageBucketName }, params.invoker, - params.channelLogger.logger, + params.channelLogger.logger ) } async function deployOperation(params: { - deployParameters: DeploySamApplicationParameters, - invoker: SamCliProcessInvoker, - channelLogger: ChannelLogger, + deployParameters: DeploySamApplicationParameters + invoker: SamCliProcessInvoker + channelLogger: ChannelLogger }): Promise { try { params.channelLogger.info( 'AWS.samcli.deploy.workflow.stackName.initiated', 'Deploying SAM Application to CloudFormation Stack: {0} with profile: {1}', params.deployParameters.destinationStackName, - params.deployParameters.profile, + params.deployParameters.profile ) const packageTemplatePath = getPackageTemplatePath(params.deployParameters.deployRootFolder) @@ -206,10 +199,10 @@ async function deployOperation(params: { profile: params.deployParameters.profile, templateFile: packageTemplatePath, region: params.deployParameters.region, - stackName: params.deployParameters.destinationStackName, + stackName: params.deployParameters.destinationStackName }, params.invoker, - params.channelLogger.logger, + params.channelLogger.logger ) } catch (err) { // Handle sam deploy Errors to supplement the error message prior to writing it out @@ -225,17 +218,14 @@ async function deployOperation(params: { } async function deploy(params: { - deployParameters: DeploySamApplicationParameters, - invoker: SamCliProcessInvoker, - channelLogger: ChannelLogger, - window: WindowFunctions, + deployParameters: DeploySamApplicationParameters + invoker: SamCliProcessInvoker + channelLogger: ChannelLogger + window: WindowFunctions }): Promise { try { params.channelLogger.channel.show(true) - params.channelLogger.info( - 'AWS.samcli.deploy.workflow.start', - 'Starting SAM Application deployment...' - ) + params.channelLogger.info('AWS.samcli.deploy.workflow.start', 'Starting SAM Application deployment...') await buildOperation(params) await packageOperation(params) @@ -248,17 +238,15 @@ async function deploy(params: { params.deployParameters.profile ) - params.window.showInformationMessage(localize( - 'AWS.samcli.deploy.workflow.success.general', - 'SAM Application deployment succeeded.' - )) + params.window.showInformationMessage( + localize('AWS.samcli.deploy.workflow.success.general', 'SAM Application deployment succeeded.') + ) } catch (err) { outputDeployError(err as Error, params.channelLogger) - params.window.showErrorMessage(localize( - 'AWS.samcli.deploy.workflow.error', - 'Failed to deploy SAM application.' - )) + params.window.showErrorMessage( + localize('AWS.samcli.deploy.workflow.error', 'Failed to deploy SAM application.') + ) } } @@ -271,7 +259,11 @@ function enhanceAwsCloudFormationInstructions( // and append region to assist in troubleshooting the error // (command uses CLI configured value--users that don't know this and omit region won't see error) // tslint:disable-next-line:max-line-length - if (message.includes(`aws cloudformation describe-stack-events --stack-name ${deployParameters.destinationStackName}`)) { + if ( + message.includes( + `aws cloudformation describe-stack-events --stack-name ${deployParameters.destinationStackName}` + ) + ) { message += ` --region ${deployParameters.region}` if (deployParameters.profile) { message += ` --profile ${deployParameters.profile}` @@ -299,7 +291,7 @@ function getDefaultWindowFunctions(): WindowFunctions { return { setStatusBarMessage: vscode.window.setStatusBarMessage, showErrorMessage: vscode.window.showErrorMessage, - showInformationMessage: vscode.window.showInformationMessage, + showInformationMessage: vscode.window.showInformationMessage } } diff --git a/src/lambda/local/debugConfiguration.ts b/src/lambda/local/debugConfiguration.ts index c432c11b2d8..2f48c2b5e48 100644 --- a/src/lambda/local/debugConfiguration.ts +++ b/src/lambda/local/debugConfiguration.ts @@ -5,13 +5,15 @@ 'use strict' +import * as os from 'os' import * as vscode from 'vscode' +import { DRIVE_LETTER_REGEX } from '../../shared/codelens/codeLensUtils' + +const DOTNET_CORE_DEBUGGER_PATH = '/tmp/lambci_debug_files/vsdbg' export interface DebugConfiguration extends vscode.DebugConfiguration { - readonly type: 'node' | 'python' - readonly request: 'attach' | 'launch' - readonly name: string - readonly port: number + readonly type: 'node' | 'python' | 'coreclr' + readonly request: 'attach' } export interface NodejsDebugConfiguration extends DebugConfiguration { @@ -21,6 +23,12 @@ export interface NodejsDebugConfiguration extends DebugConfiguration { readonly localRoot: string readonly remoteRoot: '/var/task' readonly skipFiles?: string[] + readonly port: number +} + +export interface PythonPathMapping { + localRoot: string + remoteRoot: string } export interface PythonPathMapping { @@ -29,7 +37,68 @@ export interface PythonPathMapping { } export interface PythonDebugConfiguration extends DebugConfiguration { - type: 'python' - host: string - pathMappings: PythonPathMapping[] + readonly type: 'python' + readonly host: string + readonly port: number + readonly pathMappings: PythonPathMapping[] +} + +export interface DotNetCoreDebugConfiguration extends DebugConfiguration { + type: 'coreclr' + processId: string + pipeTransport: PipeTransport + windows: { + pipeTransport: PipeTransport + } + sourceFileMap: { + [key: string]: string + } +} + +export interface PipeTransport { + pipeProgram: 'sh' | 'powershell' + pipeArgs: string[] + debuggerPath: typeof DOTNET_CORE_DEBUGGER_PATH + pipeCwd: string +} + +export interface MakeCoreCLRDebugConfigurationArguments { + port: number + codeUri: string +} + +export function makeCoreCLRDebugConfiguration({ + codeUri, + port +}: MakeCoreCLRDebugConfigurationArguments): DotNetCoreDebugConfiguration { + const pipeArgs = ['-c', `docker exec -i $(docker ps -q -f publish=${port}) \${debuggerCommand}`] + + if (os.platform() === 'win32') { + // Coerce drive letter to uppercase. While Windows is case-insensitive, sourceFileMap is case-sensitive. + codeUri = codeUri.replace(DRIVE_LETTER_REGEX, match => match.toUpperCase()) + } + + return { + name: 'SamLocalDebug', + type: 'coreclr', + request: 'attach', + processId: '1', + pipeTransport: { + pipeProgram: 'sh', + pipeArgs, + debuggerPath: DOTNET_CORE_DEBUGGER_PATH, + pipeCwd: codeUri + }, + windows: { + pipeTransport: { + pipeProgram: 'powershell', + pipeArgs, + debuggerPath: DOTNET_CORE_DEBUGGER_PATH, + pipeCwd: codeUri + } + }, + sourceFileMap: { + ['/var/task']: codeUri + } + } } diff --git a/src/lambda/local/detectLocalLambdas.ts b/src/lambda/local/detectLocalLambdas.ts index 8e0440e1eed..eced3c32073 100644 --- a/src/lambda/local/detectLocalLambdas.ts +++ b/src/lambda/local/detectLocalLambdas.ts @@ -64,7 +64,7 @@ async function detectLambdasFromTemplate( } return Object.getOwnPropertyNames(resources) - .filter(key => resources[key]!.Type === 'AWS::Serverless::Function') + .filter(key => resources[key]!.Type === CloudFormation.SERVERLESS_FUNCTION_TYPE) .map(key => ({ lambda: key, workspaceFolder, diff --git a/src/lambda/models/samLambdaRuntime.ts b/src/lambda/models/samLambdaRuntime.ts index 4d8f444b4ba..a368a24c7c7 100644 --- a/src/lambda/models/samLambdaRuntime.ts +++ b/src/lambda/models/samLambdaRuntime.ts @@ -13,50 +13,23 @@ export type SamLambdaRuntime = 'python3.7' | 'python3.6' | 'python2.7' | - 'python' | 'nodejs6.10' | 'nodejs8.10' | - 'nodejs' | - 'dotnetcore2.1' | - 'dotnetcore2.0' | - 'dotnetcore1.0' | - 'dotnetcore' | - 'dotnet' | - 'go1.x' | - 'go' | - 'java8' | - 'java' | - 'ruby' | - 'ruby2.5' + 'dotnetcore2.1' export const samLambdaRuntimes: immutable.Set = immutable.Set([ 'python3.7', 'python3.6', 'python2.7', - 'python', 'nodejs6.10', 'nodejs8.10', - 'nodejs', 'dotnetcore2.1', - 'dotnetcore2.0', - 'dotnetcore1.0', - 'dotnetcore', - 'dotnet', - 'go1.x', - 'go', - 'java8', - 'java', - 'ruby', - 'ruby2.5' ] as SamLambdaRuntime[]) export enum SamLambdaRuntimeFamily { Python, NodeJS, - DotNet, - Go, - Java, - Ruby + DotNetCore, } export function getFamily(runtime: string | undefined): SamLambdaRuntimeFamily { @@ -71,22 +44,10 @@ export function getFamily(runtime: string | undefined): SamLambdaRuntimeFamily { case 'nodejs': return SamLambdaRuntimeFamily.NodeJS case 'dotnetcore2.1': - case 'dotnetcore2.0': - case 'dotnetcore1.0': case 'dotnetcore': case 'dotnet': - return SamLambdaRuntimeFamily.DotNet - case 'go1.x': - case 'go': - return SamLambdaRuntimeFamily.Go - case 'java8': - case 'java': - return SamLambdaRuntimeFamily.Java - case 'ruby2.5': - case 'ruby': - return SamLambdaRuntimeFamily.Ruby + return SamLambdaRuntimeFamily.DotNetCore default: throw new Error(`Unrecognized runtime: '${runtime}'`) - } } diff --git a/src/lambda/utilities/getMainSourceFile.ts b/src/lambda/utilities/getMainSourceFile.ts deleted file mode 100644 index 72f973d31cf..00000000000 --- a/src/lambda/utilities/getMainSourceFile.ts +++ /dev/null @@ -1,148 +0,0 @@ -/*! - * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -import * as os from 'os' -import * as path from 'path' -import * as vscode from 'vscode' -import { CloudFormation } from '../../shared/cloudformation/cloudformation' -import * as fs from '../../shared/filesystemUtilities' -import { filterAsync, first } from '../../shared/utilities/collectionUtils' -import { detectLocalTemplates } from '../local/detectLocalTemplates' -import { getFamily, SamLambdaRuntimeFamily } from '../models/samLambdaRuntime' - -export interface OpenMainSourceFileUriContext { - getLocalTemplates(...workspaceUris: vscode.Uri[]): AsyncIterable - loadSamTemplate(uri: vscode.Uri): Promise - fileExists(path: string): Promise -} - -export async function getMainSourceFileUri( - { - root, - getLocalTemplates = (...workspaceUris: vscode.Uri[]) => detectLocalTemplates({ workspaceUris }), - loadSamTemplate = async uri => await CloudFormation.load(uri.fsPath), - fileExists = fs.fileExists - }: Partial & { - root: vscode.Uri - } -): Promise { - const templateUri = await first(getLocalTemplates(root)) - if (!templateUri) { - throw new Error(`Invalid project format: '${root.fsPath}' does not contain a SAM template.`) - } - - const lambdaResource: CloudFormation.Resource = await getFirstLambdaResource({ - templateUri, - loadSamTemplate, - fileExists - }) - - return await getSourceFileUri({ - root: vscode.Uri.file(path.dirname(templateUri.fsPath)), - resource: lambdaResource, - fileExists - }) -} - -async function getFirstLambdaResource( - { - templateUri, - loadSamTemplate - }: Pick & { - templateUri: vscode.Uri - } -): Promise { - const template = await loadSamTemplate(templateUri) - if (!template.Resources) { - throw new Error(`SAM Template '${templateUri.fsPath}' does not contain any resources`) - } - - const lambdaResources = Object.getOwnPropertyNames(template.Resources) - .map(property => template.Resources![property]!) - .filter(resource => resource.Type === 'AWS::Serverless::Function') - - if (lambdaResources.length <= 0) { - throw new Error(`SAM Template '${templateUri.fsPath}' does not contain any lambda resources`) - } - - return lambdaResources[0] -} - -async function getSourceFileUri({ - root, - resource, - fileExists -}: Pick & { - root: vscode.Uri, - resource: CloudFormation.Resource -}) { - if (!resource.Properties) { - throw new Error( - `Lambda resource is missing the 'Properties' property:${os.EOL}` + - JSON.stringify(resource, undefined, 4) - ) - } - - const { Handler, Runtime } = resource.Properties - switch (getFamily(Runtime)) { - case SamLambdaRuntimeFamily.NodeJS: - return await getNodeSourceFileUri({ root, resource, fileExists }) - case SamLambdaRuntimeFamily.Python: - return await getPythonSourceFileUri({ root, resource, fileExists }) - default: - throw new Error(`Lambda resource '${Handler}' has unknown runtime '${Runtime}'`) - - } -} - -async function getNodeSourceFileUri({ - fileExists, - root, - resource, -}: Pick & { - root: vscode.Uri, - resource: CloudFormation.Resource -}): Promise { - const handler = resource.Properties!.Handler - const tokens = handler.split('.', 1) || [handler] - const basePath = path.join(root.fsPath, resource.Properties!.CodeUri, tokens[0]) - - const file = await first(filterAsync( - ['.ts', '.jsx', '.js'].map(extension => `${basePath}${extension}`), - async (p: string) => await fileExists(p) - )) - - if (file) { - return vscode.Uri.file(file) - } - - throw new Error(`Javascript file expected at ${basePath}.(ts|jsx|js), but no file was found`) -} - -async function getPythonSourceFileUri({ - fileExists, - root, - resource, -}: Pick & { - root: vscode.Uri, - resource: CloudFormation.Resource -}): Promise { - const handler = resource.Properties!.Handler - const tokens = handler.split('.', 1) || [handler] - const basePath = path.join(root.fsPath, resource.Properties!.CodeUri, tokens[0]) - - const file = await first(filterAsync( - [`${basePath}.py`], - async (p: string) => await fileExists(p) - )) - - if (file) { - return vscode.Uri.file(file) - } - - throw new Error(`Python file expected at ${basePath}.py, but no file was found`) -} diff --git a/src/lambda/wizards/samInitWizard.ts b/src/lambda/wizards/samInitWizard.ts index d2251bb456d..3c54d963c95 100644 --- a/src/lambda/wizards/samInitWizard.ts +++ b/src/lambda/wizards/samInitWizard.ts @@ -18,6 +18,7 @@ import * as input from '../../shared/ui/input' import * as picker from '../../shared/ui/picker' import * as lambdaRuntime from '../models/samLambdaRuntime' import { MultiStepWizard, WizardStep } from '../wizards/multiStepWizard' + export interface CreateNewSamAppWizardContext { readonly lambdaRuntimes: immutable.Set readonly workspaceFolders: vscode.WorkspaceFolder[] | undefined diff --git a/src/shared/clients/dockerClient.ts b/src/shared/clients/dockerClient.ts new file mode 100644 index 00000000000..64756052638 --- /dev/null +++ b/src/shared/clients/dockerClient.ts @@ -0,0 +1,122 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as vscode from 'vscode' +import { ChildProcess } from '../utilities/childProcess' +import { ChannelLogger, getChannelLogger } from '../utilities/vsCodeUtils' + +export interface DockerClient { + invoke(args: DockerInvokeArguments): Promise +} + +export interface DockerInvokeArguments { + command: 'run' + image: string + removeOnExit?: boolean + mount?: { + type: 'bind', + source: string, + destination: string + } + entryPoint?: { + command: string + args: string[] + } +} + +export interface DockerInvokeContext { + run(args: string[]): Promise +} + +// TODO: Replace with a library such as https://www.npmjs.com/package/node-docker-api. +class DefaultDockerInvokeContext implements DockerInvokeContext { + private readonly channelLogger: ChannelLogger + + public constructor( + outputChannel: vscode.OutputChannel, + ) { + this.channelLogger = getChannelLogger(outputChannel) + } + + public async run(args: string[]): Promise { + const process = new ChildProcess( + 'docker', + {}, + ...(args || []) + ) + + return new Promise(async (resolve, reject) => { + let stderr: string + + await process.start({ + onStdout: (text: string) => { + this.channelLogger.channel.append(text) + }, + onStderr: (text: string) => { + stderr += text + }, + onError: (error: Error) => { + reject(error) + }, + onClose: (code, signal) => { + if (code) { + const errorMessage: string = `Could not invoke docker with arguments: [${args.join(', ')}].` + + `${JSON.stringify( + { + exitCode: code, + stdErr: stderr, + }, + undefined, + 4)}` + + reject(new Error(errorMessage)) + } + + resolve() + } + }) + }) + } +} + +export class DefaultDockerClient implements DockerClient { + + public constructor( + outputChannel: vscode.OutputChannel, + private readonly context: DockerInvokeContext = new DefaultDockerInvokeContext(outputChannel) + ) { } + + public async invoke({ + command, + image, + removeOnExit, + mount, + entryPoint + }: DockerInvokeArguments): Promise { + const args: string[] = [command] + + if (removeOnExit) { + args.push('--rm') + } + + if (mount) { + args.push('--mount', `type=${mount.type},src=${mount.source},dst=${mount.destination}`) + } + + if (entryPoint) { + args.push('--entrypoint', entryPoint.command) + } + + args.push(image) + + if (entryPoint) { + args.push(...entryPoint.args) + } + + await this.context.run(args) + } +} diff --git a/src/shared/cloudformation/cloudformation.ts b/src/shared/cloudformation/cloudformation.ts index 83e5802fb39..6e052cba24b 100644 --- a/src/shared/cloudformation/cloudformation.ts +++ b/src/shared/cloudformation/cloudformation.ts @@ -12,6 +12,8 @@ import * as filesystemUtilities from '../filesystemUtilities' import { SystemUtilities } from '../systemUtilities' export namespace CloudFormation { + export const SERVERLESS_FUNCTION_TYPE = 'AWS::Serverless::Function' + export function validateProperties( { Handler, @@ -49,7 +51,7 @@ export namespace CloudFormation { } export interface Resource { - Type: 'AWS::Serverless::Function', + Type: typeof SERVERLESS_FUNCTION_TYPE, Properties?: ResourceProperties } @@ -175,7 +177,7 @@ export namespace CloudFormation { const lambdaResources = Object.getOwnPropertyNames(template.Resources) .map(key => template.Resources![key]!) - .filter(resource => resource.Type === 'AWS::Serverless::Function') + .filter(resource => resource.Type === SERVERLESS_FUNCTION_TYPE) .map(resource => resource as Resource) if (lambdaResources.length <= 0) { @@ -214,4 +216,59 @@ export namespace CloudFormation { } } + export function getRuntime(resource: Pick): string { + const properties = resource.Properties + if (!properties || !properties.Runtime) { + throw new Error('Resource does not specify a Runtime') + } + + return properties.Runtime + } + + export function getCodeUri(resource: Pick): string { + const properties = resource.Properties + if (!properties || !properties.CodeUri) { + throw new Error('Resource does not specify a CodeUri') + } + + return properties.CodeUri + } + + export async function getResourceFromTemplate( + { templatePath, handlerName }: { + templatePath: string, + handlerName: string + }, + context: { loadTemplate: typeof load } = { loadTemplate: load } + ): Promise { + const template = await context.loadTemplate(templatePath) + const resources = template.Resources || {} + + const matches = Object.keys(resources) + .filter(key => matchesHandler({ + resource: resources[key], + handlerName + })).map(key => resources[key]!) + + if (matches.length < 1) { + throw new Error(`Could not find a SAM resource for handler ${handlerName}`) + } + + if (matches.length > 1) { + // TODO: Is this a valid scenario? + throw new Error(`Found more than one SAM resource for handler ${handlerName}`) + } + + return matches[0] + } + + function matchesHandler({ resource, handlerName }: { + resource?: Resource + handlerName: string + }) { + return resource && + resource.Type === SERVERLESS_FUNCTION_TYPE && + resource.Properties && + resource.Properties.Handler === handlerName + } } diff --git a/src/shared/codelens/codeLensUtils.ts b/src/shared/codelens/codeLensUtils.ts index ce2210877fe..eaab224e742 100644 --- a/src/shared/codelens/codeLensUtils.ts +++ b/src/shared/codelens/codeLensUtils.ts @@ -5,9 +5,9 @@ 'use strict' +import * as path from 'path' import * as vscode from 'vscode' -import { dirname } from 'path' import { detectLocalTemplates } from '../../lambda/local/detectLocalTemplates' import { LambdaHandlerCandidate } from '../lambdaHandlerSearch' import { getLogger } from '../logger' @@ -20,7 +20,7 @@ import { defaultMetricDatum } from '../telemetry/telemetryUtils' import { toArrayAsync } from '../utilities/collectionUtils' import { localize } from '../utilities/vsCodeUtils' -export type Language = 'python' | 'javascript' +export type Language = 'python' | 'javascript' | 'csharp' export interface CodeLensProviderParams { configuration: SettingsConfiguration, @@ -39,6 +39,8 @@ interface MakeConfigureCodeLensParams { language: Language } +export const DRIVE_LETTER_REGEX = /^\w\:/ + export async function makeCodeLenses({ document, token, handlers, language }: { document: vscode.TextDocument, token: vscode.CancellationToken, @@ -113,11 +115,10 @@ function makeConfigureCodeLens({ workspaceFolder, samTemplate }: MakeConfigureCodeLensParams): vscode.CodeLens { - // Handler will be the fully-qualified name, so we also allow '.' despite it being forbidden in handler names. - if (/[^\w\-\.]/.test(handlerName)) { + // Handler will be the fully-qualified name, so we also allow '.' & ':' despite it being forbidden in handler names. + if (/[^\w\-\.\:]/.test(handlerName)) { throw new Error( - `Invalid handler name: '${handlerName}'. ` + - 'Handler names can contain only letters, numbers, hyphens, and underscores.' + `Invalid handler name: '${handlerName}'` ) } const command = { @@ -159,7 +160,7 @@ async function getAssociatedSamTemplate( const templates = await toArrayAsync(templatesAsync) const candidateTemplates = templates .filter(template => { - const folder = dirname(template.fsPath) + const folder = path.dirname(template.fsPath) return documentUri.fsPath.indexOf(folder) === 0 }) diff --git a/src/shared/codelens/csharpCodeLensProvider.ts b/src/shared/codelens/csharpCodeLensProvider.ts new file mode 100644 index 00000000000..69efbc5a3f1 --- /dev/null +++ b/src/shared/codelens/csharpCodeLensProvider.ts @@ -0,0 +1,509 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as path from 'path' +import * as vscode from 'vscode' + +import { makeCoreCLRDebugConfiguration } from '../../lambda/local/debugConfiguration' +import { DefaultDockerClient, DockerClient } from '../clients/dockerClient' +import { CloudFormation } from '../cloudformation/cloudformation' +import { access, mkdir } from '../filesystem' +import { LambdaHandlerCandidate } from '../lambdaHandlerSearch' +import { getLogger } from '../logger' +import { DefaultSamCliProcessInvoker } from '../sam/cli/samCliInvoker' +import { SamCliProcessInvoker } from '../sam/cli/samCliInvokerUtils' +import { + DefaultSamLocalInvokeCommand, + SamLocalInvokeCommand, + WAIT_FOR_DEBUGGER_MESSAGES, +} from '../sam/cli/samCliLocalInvoke' +import { SettingsConfiguration } from '../settingsConfiguration' +import { TelemetryService } from '../telemetry/telemetryService' +import { Datum } from '../telemetry/telemetryTypes' +import { registerCommand } from '../telemetry/telemetryUtils' +import { dirnameWithTrailingSlash } from '../utilities/pathUtils' +import { ChannelLogger, getChannelLogger, getDebugPort } from '../utilities/vsCodeUtils' +import { + CodeLensProviderParams, + getInvokeCmdKey, + getMetricDatum, + makeCodeLenses, +} from './codeLensUtils' +import { + executeSamBuild, + ExecuteSamBuildArguments, + invokeLambdaFunction, + InvokeLambdaFunctionArguments, + InvokeLambdaFunctionContext, + LambdaLocalInvokeParams, + makeBuildDir, + makeInputTemplate, +} from './localLambdaRunner' + +export const CSHARP_LANGUAGE = 'csharp' +export const CSHARP_ALLFILES: vscode.DocumentFilter[] = [ + { + scheme: 'file', + language: CSHARP_LANGUAGE + } +] + +const REGEXP_RESERVED_WORD_PUBLIC = /\bpublic\b/ + +export interface DotNetLambdaHandlerComponents { + assembly: string, + namespace: string, + class: string, + method: string, + // Range of the function representing the Lambda Handler + handlerRange: vscode.Range, +} + +export async function initialize({ + configuration, + outputChannel: toolkitOutputChannel, + processInvoker = new DefaultSamCliProcessInvoker(), + telemetryService, + localInvokeCommand = new DefaultSamLocalInvokeCommand( + getChannelLogger(toolkitOutputChannel), + [WAIT_FOR_DEBUGGER_MESSAGES.DOTNET] + ), +}: CodeLensProviderParams): Promise { + const command = getInvokeCmdKey(CSHARP_LANGUAGE) + registerCommand({ + command, + callback: async (params: LambdaLocalInvokeParams): Promise<{ datum: Datum }> => { + return await onLocalInvokeCommand({ + commandName: command, + lambdaLocalInvokeParams: params, + configuration, + toolkitOutputChannel, + processInvoker, + localInvokeCommand, + telemetryService + }) + }, + }) +} + +export interface OnLocalInvokeCommandContext { + installDebugger(args: InstallDebuggerArgs): Promise +} + +class DefaultOnLocalInvokeCommandContext implements OnLocalInvokeCommandContext { + private readonly dockerClient: DockerClient + + public constructor( + outputChannel: vscode.OutputChannel, + ) { + this.dockerClient = new DefaultDockerClient(outputChannel) + } + + public async installDebugger(args: InstallDebuggerArgs): Promise { + return await _installDebugger(args, { dockerClient: this.dockerClient }) + } +} + +function getCodeUri(resource: CloudFormation.Resource, samTemplateUri: vscode.Uri) { + const rawCodeUri = CloudFormation.getCodeUri(resource) + + return path.isAbsolute(rawCodeUri) ? + rawCodeUri : + path.join( + path.dirname(samTemplateUri.fsPath), + rawCodeUri + ) +} + +/** + * The command that is run when user clicks on Run Local or Debug Local CodeLens + * Accepts object containing the following params: + * @param configuration - SettingsConfiguration (for invokeLambdaFunction) + * @param toolkitOutputChannel - "AWS Toolkit" output channel + * @param commandName - Name of the VS Code Command currently running + * @param lambdaLocalInvokeParams - Information about the Lambda Handler to invoke locally + * @param processInvoker - SAM CLI Process invoker + * @param taskInvoker - SAM CLI Task invoker + * @param telemetryService - Telemetry service for metrics + */ +async function onLocalInvokeCommand( + { + configuration, + toolkitOutputChannel, + commandName, + lambdaLocalInvokeParams, + processInvoker, + localInvokeCommand, + telemetryService, + getResourceFromTemplate = async _args => await CloudFormation.getResourceFromTemplate(_args), + }: { + configuration: SettingsConfiguration + toolkitOutputChannel: vscode.OutputChannel, + commandName: string, + lambdaLocalInvokeParams: LambdaLocalInvokeParams, + processInvoker: SamCliProcessInvoker, + localInvokeCommand: SamLocalInvokeCommand + telemetryService: TelemetryService, + getResourceFromTemplate?(args: { + templatePath: string, + handlerName: string + }): Promise, + }, + context: OnLocalInvokeCommandContext = new DefaultOnLocalInvokeCommandContext(toolkitOutputChannel) +): Promise<{ datum: Datum }> { + + const channelLogger = getChannelLogger(toolkitOutputChannel) + const resource = await getResourceFromTemplate({ + templatePath: lambdaLocalInvokeParams.samTemplate.fsPath, + handlerName: lambdaLocalInvokeParams.handlerName, + }) + const runtime = CloudFormation.getRuntime(resource) + + try { + // Switch over to the output channel so the user has feedback that we're getting things ready + channelLogger.channel.show(true) + channelLogger.info( + 'AWS.output.sam.local.start', + 'Preparing to run {0} locally...', + lambdaLocalInvokeParams.handlerName + ) + + const baseBuildDir = await makeBuildDir() + const codeUri = getCodeUri(resource, lambdaLocalInvokeParams.samTemplate) + const documentUri = lambdaLocalInvokeParams.document.uri + const handlerName = lambdaLocalInvokeParams.handlerName + + const inputTemplatePath = await makeInputTemplate({ + baseBuildDir, + codeDir: codeUri, + relativeFunctionHandler: handlerName, + runtime, + properties: resource.Properties + }) + + const buildArgs: ExecuteSamBuildArguments = { + baseBuildDir, + channelLogger, + codeDir: codeUri, + inputTemplatePath, + samProcessInvoker: processInvoker, + } + if (lambdaLocalInvokeParams.isDebug) { + buildArgs.environmentVariables = { + SAM_BUILD_MODE: 'debug' + } + } + const samTemplatePath: string = await executeSamBuild(buildArgs) + + const invokeArgs: InvokeLambdaFunctionArguments = { + baseBuildDir, + documentUri, + originalHandlerName: handlerName, + handlerName, + originalSamTemplatePath: lambdaLocalInvokeParams.samTemplate.fsPath, + samTemplatePath, + runtime, + } + + const invokeContext: InvokeLambdaFunctionContext = { + channelLogger, + configuration, + samLocalInvokeCommand: localInvokeCommand, + telemetryService + } + + if (!lambdaLocalInvokeParams.isDebug) { + await invokeLambdaFunction( + invokeArgs, + invokeContext + ) + } else { + const { debuggerPath } = await context.installDebugger({ + runtime, + targetFolder: codeUri, + channelLogger + }) + const port = await getDebugPort() + const debugConfig = makeCoreCLRDebugConfiguration({ + port, + codeUri + }) + + await invokeLambdaFunction( + { + ...invokeArgs, + debugArgs: { + debugConfig, + debugPort: port, + debuggerPath + } + }, + { + channelLogger, + configuration, + samLocalInvokeCommand: localInvokeCommand, + telemetryService + } + ) + } + } catch (err) { + const error = err as Error + channelLogger.error( + 'AWS.error.during.sam.local', + 'An error occurred trying to run SAM Application locally: {0}', + error + ) + } + + return getMetricDatum({ + isDebug: lambdaLocalInvokeParams.isDebug, + command: commandName, + runtime, + }) +} + +export async function makeCSharpCodeLensProvider(): Promise { + const logger = getLogger() + + const codeLensProvider: vscode.CodeLensProvider = { + provideCodeLenses: async ( + document: vscode.TextDocument, + token: vscode.CancellationToken + ): Promise => { + const handlers: LambdaHandlerCandidate[] = await getLambdaHandlerCandidates(document) + logger.debug( + 'csharpCodeLensProvider.makeCSharpCodeLensProvider handlers:', + JSON.stringify(handlers, undefined, 2) + ) + + return makeCodeLenses({ + document, + handlers, + token, + language: 'csharp' + }) + } + } + + return codeLensProvider +} + +export async function getLambdaHandlerCandidates(document: vscode.TextDocument): Promise { + const assemblyName = await getAssemblyName(document.uri) + if (!assemblyName) { + return [] + } + + const symbols: vscode.DocumentSymbol[] = ( + (await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + document.uri + )) || [] + ) + + return getLambdaHandlerComponents(document, symbols, assemblyName) + .map(lambdaHandlerComponents => { + const handlerName = generateDotNetLambdaHandler(lambdaHandlerComponents) + + return { + filename: document.uri.fsPath, + handlerName, + range: lambdaHandlerComponents.handlerRange, + } + }) +} + +async function getAssemblyName(sourceCodeUri: vscode.Uri): Promise { + const projectFile: vscode.Uri | undefined = await findParentProjectFile(sourceCodeUri) + + if (!projectFile) { + return undefined + } + + // TODO : Perform an XPATH parse on the project file + // If Project/PropertyGroup/AssemblyName exists, use that. Otherwise use the file name. + + return path.parse(projectFile.fsPath).name +} + +export function getLambdaHandlerComponents( + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[], + assembly: string, +): DotNetLambdaHandlerComponents[] { + return symbols + .filter(symbol => symbol.kind === vscode.SymbolKind.Namespace) + // Find relevant classes within the namespace + .reduce<{ + namespace: vscode.DocumentSymbol, + class: vscode.DocumentSymbol, + }[]>( + (accumulator, namespaceSymbol: vscode.DocumentSymbol) => { + accumulator.push(...namespaceSymbol.children + .filter(namespaceChildSymbol => namespaceChildSymbol.kind === vscode.SymbolKind.Class) + .filter(classSymbol => isPublicClassSymbol(document, classSymbol)) + .map(classSymbol => { + return { + namespace: namespaceSymbol, + class: classSymbol, + } + }) + ) + + return accumulator + }, + [] + ) + // Find relevant methods within each class + .reduce( + (accumulator, lambdaHandlerComponents) => { + accumulator.push(...lambdaHandlerComponents.class.children + .filter(classChildSymbol => classChildSymbol.kind === vscode.SymbolKind.Method) + .filter(methodSymbol => isPublicMethodSymbol(document, methodSymbol)) + .map(methodSymbol => { + return { + assembly, + namespace: lambdaHandlerComponents.namespace.name, + class: document.getText(lambdaHandlerComponents.class.selectionRange), + method: document.getText(methodSymbol.selectionRange), + handlerRange: methodSymbol.range, + } + }) + ) + + return accumulator + }, + [] + ) +} + +export async function findParentProjectFile( + sourceCodeUri: vscode.Uri, + findWorkspaceFiles: typeof vscode.workspace.findFiles = vscode.workspace.findFiles, +): Promise { + const workspaceProjectFiles: vscode.Uri[] = await findWorkspaceFiles( + '**/*.csproj' + ) + + // Use the project file "closest" in the parent chain to sourceCodeUri + // Assumption: only one .csproj file will exist in a given folder + const parentProjectFiles = workspaceProjectFiles + .filter(uri => { + const dirname = dirnameWithTrailingSlash(uri.fsPath) + + return sourceCodeUri.fsPath.startsWith(dirname) + }) + .sort() + .reverse() + + return parentProjectFiles.length === 0 ? undefined : parentProjectFiles[0] +} + +export function isPublicClassSymbol( + document: Pick, + symbol: vscode.DocumentSymbol, +): boolean { + if (symbol.kind === vscode.SymbolKind.Class) { + // from "public class Processor" pull "public class " + const classDeclarationBeforeNameRange = new vscode.Range(symbol.range.start, symbol.selectionRange.start) + const classDeclarationBeforeName: string = document.getText(classDeclarationBeforeNameRange) + + return REGEXP_RESERVED_WORD_PUBLIC.test(classDeclarationBeforeName) + } + + return false +} + +export function isPublicMethodSymbol( + document: Pick, + symbol: vscode.DocumentSymbol, +): boolean { + if (symbol.kind === vscode.SymbolKind.Method) { + // from "public async Task foo()" pull "public async Task " + const signatureBeforeMethodNameRange = new vscode.Range(symbol.range.start, symbol.selectionRange.start) + const signatureBeforeMethodName: string = document.getText(signatureBeforeMethodNameRange) + + return REGEXP_RESERVED_WORD_PUBLIC.test(signatureBeforeMethodName) + } + + return false +} + +export function generateDotNetLambdaHandler(components: DotNetLambdaHandlerComponents): string { + return `${components.assembly}::${components.namespace}.${components.class}::${components.method}` +} + +interface InstallDebuggerArgs { + runtime: string, + targetFolder: string + channelLogger: ChannelLogger +} + +interface InstallDebuggerResult { + debuggerPath: string +} + +function getDebuggerPath(parentFolder: string): string { + return path.resolve(parentFolder, '.vsdbg') +} + +async function ensureDebuggerPathExists( + parentFolder: string +): Promise { + const vsdbgPath = getDebuggerPath(parentFolder) + + try { + await access(vsdbgPath) + } catch { + await mkdir(vsdbgPath) + } +} + +async function _installDebugger( + { runtime, targetFolder, channelLogger }: InstallDebuggerArgs, + { dockerClient }: { dockerClient: DockerClient } +): Promise { + await ensureDebuggerPathExists(targetFolder) + + try { + const vsdbgPath = getDebuggerPath(targetFolder) + + channelLogger.info( + 'AWS.samcli.local.invoke.debugger.install', + 'Installing .NET Core Debugger to {0}...', + vsdbgPath + ) + + await dockerClient.invoke({ + command: 'run', + image: `lambci/lambda:${runtime}`, + removeOnExit: true, + mount: { + type: 'bind', + source: vsdbgPath, + destination: '/vsdbg' + }, + entryPoint: { + command: 'bash', + args: [ + '-c', + 'curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg' + ] + } + }) + + return { debuggerPath: vsdbgPath } + } catch (err) { + channelLogger.info( + 'AWS.samcli.local.invoke.debugger.install.failed', + 'Error installing .NET Core Debugger: {0}', + err instanceof Error ? err as Error : String(err) + ) + + throw err + } +} diff --git a/src/shared/codelens/localLambdaRunner.ts b/src/shared/codelens/localLambdaRunner.ts index 5c1950fe102..25e378ece75 100644 --- a/src/shared/codelens/localLambdaRunner.ts +++ b/src/shared/codelens/localLambdaRunner.ts @@ -9,19 +9,24 @@ import * as path from 'path' import * as tcpPortUsed from 'tcp-port-used' import * as vscode from 'vscode' import { getLocalLambdaConfiguration } from '../../lambda/local/configureLocalLambda' -import { detectLocalLambdas } from '../../lambda/local/detectLocalLambdas' +import { detectLocalLambdas, LocalLambda } from '../../lambda/local/detectLocalLambdas' import { CloudFormation } from '../cloudformation/cloudformation' import { writeFile } from '../filesystem' import { makeTemporaryToolkitFolder } from '../filesystemUtilities' import { SamCliBuildInvocation, SamCliBuildInvocationArguments } from '../sam/cli/samCliBuild' import { SamCliProcessInvoker } from '../sam/cli/samCliInvokerUtils' -import { SamCliLocalInvokeInvocation, SamLocalInvokeCommand } from '../sam/cli/samCliLocalInvoke' +import { + SamCliLocalInvokeInvocation, + SamCliLocalInvokeInvocationArguments, + SamLocalInvokeCommand +} from '../sam/cli/samCliLocalInvoke' import { SettingsConfiguration } from '../settingsConfiguration' import { SamTemplateGenerator } from '../templates/sam/samTemplateGenerator' import { ExtensionDisposableFiles } from '../utilities/disposableFiles' import { generateDefaultHandlerConfig, HandlerConfig } from '../../lambda/config/templates' import { DebugConfiguration } from '../../lambda/local/debugConfiguration' +import { getFamily, SamLambdaRuntimeFamily } from '../../lambda/models/samLambdaRuntime' import { BasicLogger } from '../logger' import { TelemetryService } from '../telemetry/telemetryService' import { normalizeSeparator } from '../utilities/pathUtils' @@ -29,12 +34,12 @@ import { Timeout } from '../utilities/timeoutUtils' import { ChannelLogger, getChannelLogger } from '../utilities/vsCodeUtils' export interface LambdaLocalInvokeParams { - document: vscode.TextDocument, - range: vscode.Range, - handlerName: string, - isDebug: boolean, - workspaceFolder: vscode.WorkspaceFolder, - samTemplate: vscode.Uri, + document: vscode.TextDocument + range: vscode.Range + handlerName: string + isDebug: boolean + workspaceFolder: vscode.WorkspaceFolder + samTemplate: vscode.Uri } export interface SAMTemplateEnvironmentVariables { @@ -44,13 +49,13 @@ export interface SAMTemplateEnvironmentVariables { } export interface OnDidSamBuildParams { - buildDir: string, - debugPort: number, - handlerName: string, + buildDir: string + debugPort: number + handlerName: string isDebug: boolean } -const TEMPLATE_RESOURCE_NAME: string = 'awsToolkitSamLocalResource' +const TEMPLATE_RESOURCE_NAME = 'awsToolkitSamLocalResource' const SAM_LOCAL_PORT_CHECK_RETRY_INTERVAL_MILLIS: number = 125 const SAM_LOCAL_PORT_CHECK_RETRY_TIMEOUT_MILLIS_DEFAULT: number = 30000 const MAX_DEBUGGER_RETRIES_DEFAULT: number = 30 @@ -58,7 +63,6 @@ const ATTACH_DEBUGGER_RETRY_DELAY_MILLIS: number = 200 // TODO: Consider replacing LocalLambdaRunner use with associated duplicative functions export class LocalLambdaRunner { - private _baseBuildFolder?: string private readonly _debugPort?: number @@ -75,7 +79,7 @@ export class LocalLambdaRunner { private readonly codeRootDirectoryPath: string, private readonly telemetryService: TelemetryService, private readonly onDidSamBuild?: (params: OnDidSamBuildParams) => Promise, - private readonly channelLogger = getChannelLogger(outputChannel), + private readonly channelLogger = getChannelLogger(outputChannel) ) { if (localInvokeParams.isDebug && !debugPort) { throw new Error('Debug port must be provided when launching in debug mode') @@ -99,7 +103,6 @@ export class LocalLambdaRunner { const samBuildTemplate: string = await this.executeSamBuild(this.codeRootDirectoryPath, inputTemplate) await this.invokeLambdaFunction(samBuildTemplate) - } catch (err) { const error = err as Error this.channelLogger.error( @@ -110,7 +113,6 @@ export class LocalLambdaRunner { return } - } public get debugPort(): number { @@ -135,9 +137,7 @@ export class LocalLambdaRunner { * Create the SAM Template that will be passed in to sam build. * @returns Path to the generated template file */ - private async generateInputTemplate( - rootCodeFolder: string - ): Promise { + private async generateInputTemplate(rootCodeFolder: string): Promise { const buildFolder: string = await this.getBaseBuildFolder() const inputTemplatePath: string = path.join(buildFolder, 'input', 'input-template.yaml') @@ -147,10 +147,9 @@ export class LocalLambdaRunner { path.dirname(this.localInvokeParams.document.uri.fsPath) ) - const relativeFunctionHandler = path.join( - handlerFileRelativePath, - this.localInvokeParams.handlerName - ).replace('\\', '/') + const relativeFunctionHandler = path + .join(handlerFileRelativePath, this.localInvokeParams.handlerName) + .replace('\\', '/') const workspaceFolder = vscode.workspace.getWorkspaceFolder(this.localInvokeParams.workspaceFolder.uri) let existingTemplateResource: CloudFormation.Resource | undefined @@ -166,8 +165,11 @@ export class LocalLambdaRunner { .withResourceName(TEMPLATE_RESOURCE_NAME) .withRuntime(this.runtime) - if (existingTemplateResource && existingTemplateResource.Properties && - existingTemplateResource.Properties.Environment) { + if ( + existingTemplateResource && + existingTemplateResource.Properties && + existingTemplateResource.Properties.Environment + ) { newTemplate = newTemplate.withEnvironment(existingTemplateResource.Properties.Environment) } @@ -176,14 +178,8 @@ export class LocalLambdaRunner { return inputTemplatePath } - private async executeSamBuild( - rootCodeFolder: string, - inputTemplatePath: string - ): Promise { - this.channelLogger.info( - 'AWS.output.building.sam.application', - 'Building SAM Application...' - ) + private async executeSamBuild(rootCodeFolder: string, inputTemplatePath: string): Promise { + this.channelLogger.info('AWS.output.building.sam.application', 'Building SAM Application...') const samBuildOutputFolder = path.join(await this.getBaseBuildFolder(), 'output') @@ -195,10 +191,7 @@ export class LocalLambdaRunner { } await new SamCliBuildInvocation(samCliArgs).execute() - this.channelLogger.info( - 'AWS.output.building.sam.application.complete', - 'Build complete.' - ) + this.channelLogger.info('AWS.output.building.sam.application.complete', 'Build complete.') if (this.onDidSamBuild) { // Enable post build tasks if needed @@ -217,9 +210,7 @@ export class LocalLambdaRunner { * Runs `sam local invoke` against the provided template file * @param samTemplatePath sam template to run locally */ - private async invokeLambdaFunction( - samTemplatePath: string, - ): Promise { + private async invokeLambdaFunction(samTemplatePath: string): Promise { this.channelLogger.info( 'AWS.output.starting.sam.app.locally', 'Starting the SAM Application locally (see Terminal for output)' @@ -231,17 +222,14 @@ export class LocalLambdaRunner { const maxRetries: number = getAttachDebuggerMaxRetryLimit(this.configuration, MAX_DEBUGGER_RETRIES_DEFAULT) await writeFile(eventPath, JSON.stringify(config.event || {})) - await writeFile( - environmentVariablePath, - JSON.stringify(this.getEnvironmentVariables(config)) - ) + await writeFile(environmentVariablePath, JSON.stringify(this.getEnvironmentVariables(config))) const command = new SamCliLocalInvokeInvocation({ templateResourceName: TEMPLATE_RESOURCE_NAME, templatePath: samTemplatePath, eventPath, environmentVariablePath, - debugPort: (!!this._debugPort) ? this._debugPort.toString() : undefined, + debugPort: !!this._debugPort ? this._debugPort.toString() : undefined, invoker: this.localInvokeCommand }) @@ -249,7 +237,7 @@ export class LocalLambdaRunner { await command.execute(timer) if (this.localInvokeParams.isDebug) { - const isPortOpen: boolean = await waitForDebugPort({ + const isPortOpen = await waitForDebugPort({ debugPort: this.debugPort, configuration: this.configuration, channelLogger: this.channelLogger, @@ -269,22 +257,20 @@ export class LocalLambdaRunner { maxRetries, retryDelayMillis: ATTACH_DEBUGGER_RETRY_DELAY_MILLIS, channelLogger: this.channelLogger, - onRecordAttachDebuggerMetric: ( - attachResult: boolean | undefined, attempts: number - ): void => { + onRecordAttachDebuggerMetric: (attachResult: boolean | undefined, attempts: number): void => { recordAttachDebuggerMetric({ telemetryService: this.telemetryService, result: attachResult, attempts, durationMillis: timer.elapsedTime, - runtime: this.runtime, + runtime: this.runtime }) - }, + } }) if (attachResults.success) { await showDebugConsole({ - logger: this.channelLogger.logger, + logger: this.channelLogger.logger }) } } @@ -316,47 +302,6 @@ export class LocalLambdaRunner { } } // end class LocalLambdaRunner -export async function getRuntimeForLambda(params: { - handlerName: string, - templatePath: string, -}): Promise { - const samTemplateData: CloudFormation.Template = await CloudFormation.load(params.templatePath) - if (!samTemplateData.Resources) { - throw new Error( - `Please specify Resource for '${params.handlerName}' Lambda in SAM template: '${params.templatePath}'` - ) - } - const runtimes = new Set() - for (const resourceKey in samTemplateData.Resources) { - if (samTemplateData.Resources.hasOwnProperty(resourceKey)) { - const resource: CloudFormation.Resource | undefined = samTemplateData.Resources[resourceKey] - if (!resource) { - continue - } - if (resource.Type === 'AWS::Serverless::Function') { - if (!resource.Properties) { - continue - } - if (resource.Properties.Runtime) { - if (resource.Properties.Handler === params.handlerName) { - return resource.Properties.Runtime - } else { - runtimes.add(resource.Properties.Runtime) - } - } - - } - } - } - if (runtimes.size === 1) { - // If all lambdas have the same runtime... assume that will continue to be the case - return Array.from(runtimes)[0] - } - throw new Error( - `Please specify runtime for '${params.handlerName}' Lambda in SAM template: '${params.templatePath}'` - ) -} - export const makeBuildDir = async (): Promise => { const buildDir = await makeTemporaryToolkitFolder() ExtensionDisposableFiles.getInstance().addFolder(buildDir) @@ -364,167 +309,176 @@ export const makeBuildDir = async (): Promise => { return buildDir } -export async function makeInputTemplate(params: { - baseBuildDir: string, - codeDir: string, - documentUri: vscode.Uri - originalHandlerName: string, - handlerName: string, - runtime: string, - workspaceUri: vscode.Uri, -}): Promise { - const inputTemplatePath: string = path.join(params.baseBuildDir, 'input', 'input-template.yaml') - ExtensionDisposableFiles.getInstance().addFolder(inputTemplatePath) +export function getHandlerRelativePath(params: { codeRoot: string; filePath: string }): string { + return path.relative(params.codeRoot, path.dirname(params.filePath)) +} +export function getRelativeFunctionHandler(params: { + handlerName: string + runtime: string + handlerFileRelativePath: string +}): string { // Make function handler relative to baseDir - const handlerFileRelativePath = path.relative( - params.codeDir, - path.dirname(params.documentUri.fsPath) - ) + let relativeFunctionHandler: string + if (shouldAppendRelativePathToFunctionHandler(params.runtime)) { + relativeFunctionHandler = normalizeSeparator(path.join(params.handlerFileRelativePath, params.handlerName)) + } else { + relativeFunctionHandler = params.handlerName + } + + return relativeFunctionHandler +} +export async function getLambdaInfoFromExistingTemplate(params: { + workspaceUri: vscode.Uri + relativeOriginalFunctionHandler: string +}): Promise { const workspaceFolder = vscode.workspace.getWorkspaceFolder(params.workspaceUri) - let existingTemplateResource: CloudFormation.Resource | undefined + let existingLambda: LocalLambda | undefined if (workspaceFolder) { - - const relativeOriginalFunctionHandler = normalizeSeparator( - path.join( - handlerFileRelativePath, - params.originalHandlerName, - ) - ) - const lambdas = await detectLocalLambdas([workspaceFolder]) - const existingLambda = lambdas.find(lambda => lambda.handler === relativeOriginalFunctionHandler) - existingTemplateResource = existingLambda ? existingLambda.resource : undefined + existingLambda = lambdas.find(lambda => lambda.handler === params.relativeOriginalFunctionHandler) } - const relativeFunctionHandler = normalizeSeparator( - path.join( - handlerFileRelativePath, - params.handlerName, - ) - ) + return existingLambda +} - let newTemplate = new SamTemplateGenerator() - .withCodeUri(params.codeDir) - .withFunctionHandler(relativeFunctionHandler) +export async function makeInputTemplate(params: { + baseBuildDir: string + codeDir: string + relativeFunctionHandler: string + properties?: CloudFormation.ResourceProperties + runtime: string +}): Promise { + const newTemplate = new SamTemplateGenerator() + .withFunctionHandler(params.relativeFunctionHandler) .withResourceName(TEMPLATE_RESOURCE_NAME) .withRuntime(params.runtime) - - if (existingTemplateResource && existingTemplateResource.Properties && - existingTemplateResource.Properties.Environment) { - newTemplate = newTemplate.withEnvironment(existingTemplateResource.Properties.Environment) + .withCodeUri(params.codeDir) + if (params.properties && params.properties.Environment) { + newTemplate.withEnvironment(params.properties.Environment) } + const inputTemplatePath: string = path.join(params.baseBuildDir, 'input', 'input-template.yaml') + ExtensionDisposableFiles.getInstance().addFolder(inputTemplatePath) + await newTemplate.generate(inputTemplatePath) return inputTemplatePath } -export async function executeSamBuild(params: { - baseBuildDir: string, - channelLogger: ChannelLogger, - codeDir: string, - inputTemplatePath: string, - manifestPath?: string, - samProcessInvoker: SamCliProcessInvoker, -}): Promise { - params.channelLogger.info( - 'AWS.output.building.sam.application', - 'Building SAM Application...' - ) +export interface ExecuteSamBuildArguments { + baseBuildDir: string + channelLogger: Pick + codeDir: string + inputTemplatePath: string + manifestPath?: string + environmentVariables?: NodeJS.ProcessEnv + samProcessInvoker: SamCliProcessInvoker +} + +export async function executeSamBuild({ + baseBuildDir, + channelLogger, + codeDir, + inputTemplatePath, + manifestPath, + environmentVariables, + samProcessInvoker +}: ExecuteSamBuildArguments): Promise { + channelLogger.info('AWS.output.building.sam.application', 'Building SAM Application...') - const samBuildOutputFolder = path.join(params.baseBuildDir, 'output') + const samBuildOutputFolder = path.join(baseBuildDir, 'output') const samCliArgs: SamCliBuildInvocationArguments = { buildDir: samBuildOutputFolder, - baseDir: params.codeDir, - templatePath: params.inputTemplatePath, - invoker: params.samProcessInvoker, - manifestPath: params.manifestPath + baseDir: codeDir, + templatePath: inputTemplatePath, + invoker: samProcessInvoker, + manifestPath, + environmentVariables } await new SamCliBuildInvocation(samCliArgs).execute() - params.channelLogger.info( - 'AWS.output.building.sam.application.complete', - 'Build complete.' - ) + channelLogger.info('AWS.output.building.sam.application.complete', 'Build complete.') return path.join(samBuildOutputFolder, 'template.yaml') } -export const invokeLambdaFunction = async (params: { - baseBuildDir: string, - channelLogger: ChannelLogger, - configuration: SettingsConfiguration, - debugConfig: DebugConfiguration, - documentUri: vscode.Uri, - originalHandlerName: string, - handlerName: string, - isDebug?: boolean, - originalSamTemplatePath: string, - samTemplatePath: string, - samLocalInvokeCommand: SamLocalInvokeCommand, - telemetryService: TelemetryService, - runtime: string, -}): Promise => { - params.channelLogger.info( +export interface InvokeLambdaFunctionArguments { + baseBuildDir: string + documentUri: vscode.Uri + originalHandlerName: string + handlerName: string + originalSamTemplatePath: string + samTemplatePath: string + runtime: string + debugArgs?: DebugLambdaFunctionArguments +} + +export interface DebugLambdaFunctionArguments { + debugConfig: DebugConfiguration + debuggerPath?: string + debugPort: number +} + +export interface InvokeLambdaFunctionContext { + channelLogger: ChannelLogger + configuration: SettingsConfiguration + samLocalInvokeCommand: SamLocalInvokeCommand + telemetryService: TelemetryService +} + +export async function invokeLambdaFunction( + invokeArgs: InvokeLambdaFunctionArguments, + { channelLogger, configuration, samLocalInvokeCommand, telemetryService }: InvokeLambdaFunctionContext +): Promise { + channelLogger.info( 'AWS.output.starting.sam.app.locally', 'Starting the SAM Application locally (see Terminal for output)' ) - params.channelLogger.logger.debug(`localLambdaRunner.invokeLambdaFunction: ${JSON.stringify( - { - baseBuildDir: params.baseBuildDir, - configuration: params.configuration, - debugConfig: params.debugConfig, - documentUri: vscode.Uri, - handlerName: params.handlerName, - originalHandlerName: params.originalHandlerName, - isDebug: params.isDebug, - samTemplatePath: params.samTemplatePath, - originalSamTemplatePath: params.originalSamTemplatePath, - }, - undefined, - 2)}` - ) + channelLogger.logger.debug(`localLambdaRunner.invokeLambdaFunction: ${JSON.stringify(invokeArgs, undefined, 2)}`) - const eventPath: string = path.join(params.baseBuildDir, 'event.json') - const environmentVariablePath = path.join(params.baseBuildDir, 'env-vars.json') + const eventPath: string = path.join(invokeArgs.baseBuildDir, 'event.json') + const environmentVariablePath = path.join(invokeArgs.baseBuildDir, 'env-vars.json') const config = await getConfig({ - handlerName: params.originalHandlerName, - documentUri: params.documentUri, - samTemplate: vscode.Uri.file(params.originalSamTemplatePath), + handlerName: invokeArgs.originalHandlerName, + documentUri: invokeArgs.documentUri, + samTemplate: vscode.Uri.file(invokeArgs.originalSamTemplatePath) }) - const maxRetries: number = getAttachDebuggerMaxRetryLimit(params.configuration, MAX_DEBUGGER_RETRIES_DEFAULT) + const maxRetries: number = getAttachDebuggerMaxRetryLimit(configuration, MAX_DEBUGGER_RETRIES_DEFAULT) await writeFile(eventPath, JSON.stringify(config.event || {})) - await writeFile( - environmentVariablePath, - JSON.stringify(getEnvironmentVariables(config)) - ) + await writeFile(environmentVariablePath, JSON.stringify(getEnvironmentVariables(config))) - const command = new SamCliLocalInvokeInvocation({ + const localInvokeArgs: SamCliLocalInvokeInvocationArguments = { templateResourceName: TEMPLATE_RESOURCE_NAME, - templatePath: params.samTemplatePath, + templatePath: invokeArgs.samTemplatePath, eventPath, environmentVariablePath, - debugPort: (params.isDebug) ? params.debugConfig.port.toString() : undefined, - invoker: params.samLocalInvokeCommand, - }) + invoker: samLocalInvokeCommand + } + + const debugArgs = invokeArgs.debugArgs + if (debugArgs) { + localInvokeArgs.debugPort = debugArgs.debugPort.toString() + localInvokeArgs.debuggerPath = debugArgs.debuggerPath + } + const command = new SamCliLocalInvokeInvocation(localInvokeArgs) - const timer = createInvokeTimer(params.configuration) + const timer = createInvokeTimer(configuration) await command.execute(timer) - if (params.isDebug) { - const isPortOpen: boolean = await waitForDebugPort({ - debugPort: params.debugConfig.port, - configuration: params.configuration, - channelLogger: params.channelLogger, + if (debugArgs) { + const isPortOpen = await waitForDebugPort({ + debugPort: debugArgs.debugPort, + configuration, + channelLogger, timeoutDuration: timer.remainingTime }) if (!isPortOpen) { - params.channelLogger.warn( + channelLogger.warn( 'AWS.samcli.local.invoke.port.not.open', // tslint:disable-next-line:max-line-length "The debug port doesn't appear to be open. The debugger might not succeed when attaching to your SAM Application." @@ -532,26 +486,24 @@ export const invokeLambdaFunction = async (params: { } const attachResults = await attachDebugger({ - debugConfig: params.debugConfig, + debugConfig: debugArgs.debugConfig, maxRetries, retryDelayMillis: ATTACH_DEBUGGER_RETRY_DELAY_MILLIS, - channelLogger: params.channelLogger, - onRecordAttachDebuggerMetric: ( - attachResult: boolean | undefined, attempts: number - ): void => { + channelLogger, + onRecordAttachDebuggerMetric: (attachResult: boolean | undefined, attempts: number): void => { recordAttachDebuggerMetric({ - telemetryService: params.telemetryService, + telemetryService: telemetryService, result: attachResult, attempts, durationMillis: timer.elapsedTime, - runtime: params.runtime, + runtime: invokeArgs.runtime }) - }, + } }) if (attachResults.success) { await showDebugConsole({ - logger: params.channelLogger.logger, + logger: channelLogger.logger }) } } @@ -570,13 +522,15 @@ const getConfig = async (params: { const config: HandlerConfig = await getLocalLambdaConfiguration( workspaceFolder, params.handlerName, - params.samTemplate, + params.samTemplate ) return config } -const getEnvironmentVariables = (config: HandlerConfig): SAMTemplateEnvironmentVariables => { +const getEnvironmentVariables = ( + config: Pick +): SAMTemplateEnvironmentVariables => { if (!!config.environmentVariables) { return { [TEMPLATE_RESOURCE_NAME]: config.environmentVariables @@ -596,33 +550,30 @@ export interface AttachDebuggerContext { onWillRetry?(): Promise } -export async function attachDebugger( - { - retryDelayMillis = ATTACH_DEBUGGER_RETRY_DELAY_MILLIS, - onStartDebugging = vscode.debug.startDebugging, - onWillRetry = async (): Promise => { - await new Promise(resolve => { - setTimeout(resolve, retryDelayMillis) - }) - }, - ...params - }: AttachDebuggerContext -): Promise<{ success: boolean }> { +export async function attachDebugger({ + retryDelayMillis = ATTACH_DEBUGGER_RETRY_DELAY_MILLIS, + onStartDebugging = vscode.debug.startDebugging, + onWillRetry = async (): Promise => { + await new Promise(resolve => { + setTimeout(resolve, retryDelayMillis) + }) + }, + ...params +}: AttachDebuggerContext): Promise<{ success: boolean }> { const channelLogger = params.channelLogger const logger = params.channelLogger.logger - logger.debug(`localLambdaRunner.attachDebugger: startDebugging with debugConfig: ${JSON.stringify( - params.debugConfig, - undefined, - 2 - )}`) + logger.debug( + `localLambdaRunner.attachDebugger: startDebugging with debugConfig: ${JSON.stringify( + params.debugConfig, + undefined, + 2 + )}` + ) let isDebuggerAttached: boolean | undefined let retries = 0 - channelLogger.info( - 'AWS.output.sam.local.attaching', - 'Attaching debugger to SAM Application...', - ) + channelLogger.info('AWS.output.sam.local.attaching', 'Attaching debugger to SAM Application...') do { isDebuggerAttached = await onStartDebugging(undefined, params.debugConfig) @@ -649,10 +600,7 @@ export async function attachDebugger( } if (isDebuggerAttached) { - channelLogger.info( - 'AWS.output.sam.local.attach.success', - 'Debugger attached' - ) + channelLogger.info('AWS.output.sam.local.attach.success', 'Debugger attached') } else { channelLogger.error( 'AWS.output.sam.local.attach.failure', @@ -685,11 +633,7 @@ async function waitForDebugPort({ try { // this should not fail: if it hits this point, the port should be open // this function always attempts once no matter the timeoutDuration - await tcpPortUsed.waitUntilUsed( - debugPort, - SAM_LOCAL_PORT_CHECK_RETRY_INTERVAL_MILLIS, - timeoutDuration - ) + await tcpPortUsed.waitUntilUsed(debugPort, SAM_LOCAL_PORT_CHECK_RETRY_INTERVAL_MILLIS, timeoutDuration) return true } catch (err) { @@ -713,9 +657,7 @@ function recordAttachDebuggerMetric(params: RecordAttachDebuggerMetricContext) { const currTime = new Date() const namespace = params.result ? 'DebugAttachSuccess' : 'DebugAttachFailure' - const metadata = new Map([ - ['runtime', params.runtime], - ]) + const metadata = new Map([['runtime', params.runtime]]) params.telemetryService.record({ namespace: namespace, @@ -725,31 +667,37 @@ function recordAttachDebuggerMetric(params: RecordAttachDebuggerMetricContext) { name: 'attempts', value: params.attempts, unit: 'Count', - metadata, + metadata }, { name: 'duration', value: params.durationMillis, unit: 'Milliseconds', - metadata, + metadata } ] }) } -function getAttachDebuggerMaxRetryLimit( - configuration: SettingsConfiguration, - defaultValue: number, -): number { - return configuration.readSetting( - 'samcli.debug.attach.retry.maximum', - defaultValue - )! +function getAttachDebuggerMaxRetryLimit(configuration: SettingsConfiguration, defaultValue: number): number { + return configuration.readSetting('samcli.debug.attach.retry.maximum', defaultValue)! +} + +export function shouldAppendRelativePathToFunctionHandler(runtime: string): boolean { + // getFamily will throw an error if the runtime doesn't exist + switch (getFamily(runtime)) { + case SamLambdaRuntimeFamily.NodeJS: + case SamLambdaRuntimeFamily.Python: + return true + case SamLambdaRuntimeFamily.DotNetCore: + return false + // if the runtime exists but for some reason we forgot to cover it here, throw anyway so we remember to cover it + default: + throw new Error('localLambdaRunner can not determine if runtime requires a relative path.') + } } -function createInvokeTimer( - configuration: SettingsConfiguration, -): Timeout { +function createInvokeTimer(configuration: SettingsConfiguration): Timeout { const timelimit = configuration.readSetting( 'samcli.debug.attach.timeout.millis', SAM_LOCAL_PORT_CHECK_RETRY_TIMEOUT_MILLIS_DEFAULT @@ -774,9 +722,6 @@ async function showDebugConsole({ await executeVsCodeCommand('workbench.debug.action.toggleRepl') } catch (err) { // in case the vs code command changes or misbehaves, swallow error - params.logger.verbose( - 'Unable to switch to the Debug Console', - err as Error - ) + params.logger.verbose('Unable to switch to the Debug Console', err as Error) } } diff --git a/src/shared/codelens/pythonCodeLensProvider.ts b/src/shared/codelens/pythonCodeLensProvider.ts index 87a7e950939..ab7ff0e1d8b 100644 --- a/src/shared/codelens/pythonCodeLensProvider.ts +++ b/src/shared/codelens/pythonCodeLensProvider.ts @@ -8,35 +8,36 @@ import * as os from 'os' import * as path from 'path' import * as vscode from 'vscode' - import { PythonDebugConfiguration, PythonPathMapping } from '../../lambda/local/debugConfiguration' +import { CloudFormation } from '../cloudformation/cloudformation' import { unlink, writeFile } from '../filesystem' import { fileExists, readFileAsString } from '../filesystemUtilities' import { LambdaHandlerCandidate } from '../lambdaHandlerSearch' import { getLogger } from '../logger' +import { DefaultValidatingSamCliProcessInvoker } from '../sam/cli/defaultValidatingSamCliProcessInvoker' +import { DefaultSamLocalInvokeCommand, WAIT_FOR_DEBUGGER_MESSAGES } from '../sam/cli/samCliLocalInvoke' import { Datum, TelemetryNamespace } from '../telemetry/telemetryTypes' import { registerCommand } from '../telemetry/telemetryUtils' import { getChannelLogger, getDebugPort } from '../utilities/vsCodeUtils' - -import { DefaultValidatingSamCliProcessInvoker } from '../sam/cli/defaultValidatingSamCliProcessInvoker' -import { DefaultSamLocalInvokeCommand, WAIT_FOR_DEBUGGER_MESSAGES } from '../sam/cli/samCliLocalInvoke' import { CodeLensProviderParams, + DRIVE_LETTER_REGEX, getInvokeCmdKey, getMetricDatum, - makeCodeLenses, + makeCodeLenses } from './codeLensUtils' import { executeSamBuild, - getRuntimeForLambda, + getHandlerRelativePath, + getLambdaInfoFromExistingTemplate, + getRelativeFunctionHandler, invokeLambdaFunction, + InvokeLambdaFunctionArguments, LambdaLocalInvokeParams, makeBuildDir, - makeInputTemplate, + makeInputTemplate } from './localLambdaRunner' -const PATH_STARTS_WITH_DRIVE_LETTER_REGEX: RegExp = new RegExp(/^[a-zA-Z]\:/) - export const PYTHON_LANGUAGE = 'python' export const PYTHON_ALLFILES: vscode.DocumentFilter[] = [ { @@ -53,22 +54,19 @@ const getSamProjectDirPathForFile = async (filepath: string): Promise => const getLambdaHandlerCandidates = async ({ uri }: { uri: vscode.Uri }): Promise => { const logger = getLogger() const filename = uri.fsPath - const symbols: vscode.DocumentSymbol[] = ( - (await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - uri - )) || [] - ) + const symbols: vscode.DocumentSymbol[] = + (await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', uri)) || + [] return symbols .filter(sym => sym.kind === vscode.SymbolKind.Function) .map(symbol => { - logger.debug(`pythonCodeLensProviderFound.getLambdaHandlerCandidates: ${ - JSON.stringify({ + logger.debug( + `pythonCodeLensProviderFound.getLambdaHandlerCandidates: ${JSON.stringify({ filePath: uri.fsPath, handlerName: `${path.parse(filename).name}.${symbol.name}` - }) - }`) + })}` + ) return { filename, @@ -80,7 +78,7 @@ const getLambdaHandlerCandidates = async ({ uri }: { uri: vscode.Uri }): Promise // Add create debugging manifest/requirements.txt containing ptvsd const makePythonDebugManifest = async (params: { - samProjectCodeRoot: string, + samProjectCodeRoot: string outputDir: string }): Promise => { let manifestText = '' @@ -102,10 +100,10 @@ const makePythonDebugManifest = async (params: { // tslint:disable:no-trailing-whitespace const makeLambdaDebugFile = async (params: { - handlerName: string, - debugPort: number, + handlerName: string + debugPort: number outputDir: string -}): Promise<{ outFilePath: string, debugHandlerName: string }> => { +}): Promise<{ outFilePath: string; debugHandlerName: string }> => { if (!params.outputDir) { throw new Error('Must specify outputDir') } @@ -152,34 +150,31 @@ def ${debugHandlerFunctionName}(event, context): } export function getLocalRootVariants(filePath: string): string[] { - if (process.platform === 'win32') { - if (PATH_STARTS_WITH_DRIVE_LETTER_REGEX.test(filePath)) { - return [ - filePath.replace(PATH_STARTS_WITH_DRIVE_LETTER_REGEX, match => match.toLowerCase()), - filePath.replace(PATH_STARTS_WITH_DRIVE_LETTER_REGEX, match => match.toUpperCase()) - ] - } + if (process.platform === 'win32' && DRIVE_LETTER_REGEX.test(filePath)) { + return [ + filePath.replace(DRIVE_LETTER_REGEX, match => match.toLowerCase()), + filePath.replace(DRIVE_LETTER_REGEX, match => match.toUpperCase()) + ] } return [filePath] } -function makeDebugConfig( - { - debugPort, - samProjectCodeRoot - }: { - debugPort?: number, - samProjectCodeRoot: string, - }): PythonDebugConfiguration { - - const pathMappings: PythonPathMapping[] = getLocalRootVariants(samProjectCodeRoot) - .map(variant => { +function makeDebugConfig({ + debugPort, + samProjectCodeRoot +}: { + debugPort?: number + samProjectCodeRoot: string +}): PythonDebugConfiguration { + const pathMappings: PythonPathMapping[] = getLocalRootVariants(samProjectCodeRoot).map( + variant => { return { localRoot: variant, - remoteRoot: '/var/task', + remoteRoot: '/var/task' } - }) + } + ) return { type: PYTHON_LANGUAGE, @@ -191,7 +186,7 @@ function makeDebugConfig( // Disable redirectOutput to prevent the Python Debugger from automatically writing stdout/stderr text // to the Debug Console. We're taking the child process stdout/stderr and explicitly writing that to // the Debug Console. - redirectOutput: false, + redirectOutput: false } } @@ -199,28 +194,21 @@ export async function initialize({ configuration, outputChannel: toolkitOutputChannel, processInvoker = new DefaultValidatingSamCliProcessInvoker({}), - telemetryService: telemetryService, - localInvokeCommand, + telemetryService, + localInvokeCommand }: CodeLensProviderParams): Promise { const logger = getLogger() const channelLogger = getChannelLogger(toolkitOutputChannel) if (!localInvokeCommand) { - localInvokeCommand = new DefaultSamLocalInvokeCommand( - channelLogger, - [WAIT_FOR_DEBUGGER_MESSAGES.PYTHON] - ) + localInvokeCommand = new DefaultSamLocalInvokeCommand(channelLogger, [WAIT_FOR_DEBUGGER_MESSAGES.PYTHON]) } const invokeLambda = async (args: LambdaLocalInvokeParams & { runtime: string }) => { // Switch over to the output channel so the user has feedback that we're getting things ready channelLogger.channel.show(true) - channelLogger.info( - 'AWS.output.sam.local.start', - 'Preparing to run {0} locally...', - args.handlerName - ) + channelLogger.info('AWS.output.sam.local.start', 'Preparing to run {0} locally...', args.handlerName) let lambdaDebugFilePath: string | undefined @@ -237,7 +225,7 @@ export async function initialize({ const { debugHandlerName, outFilePath } = await makeLambdaDebugFile({ handlerName: args.handlerName, debugPort: debugPort, - outputDir: samProjectCodeRoot, + outputDir: samProjectCodeRoot }) lambdaDebugFilePath = outFilePath handlerName = debugHandlerName @@ -246,18 +234,44 @@ export async function initialize({ outputDir: baseBuildDir }) } + + const handlerFileRelativePath = getHandlerRelativePath({ + codeRoot: samProjectCodeRoot, + filePath: args.document.uri.fsPath + }) + + const relativeOriginalFunctionHandler = getRelativeFunctionHandler({ + handlerName: args.handlerName, + runtime: args.runtime, + handlerFileRelativePath + }) + + const relativeFunctionHandler = getRelativeFunctionHandler({ + handlerName: handlerName, + runtime: args.runtime, + handlerFileRelativePath + }) + + const lambdaInfo = await getLambdaInfoFromExistingTemplate({ + workspaceUri: args.workspaceFolder.uri, + relativeOriginalFunctionHandler + }) + const inputTemplatePath = await makeInputTemplate({ baseBuildDir, codeDir: samProjectCodeRoot, - documentUri: args.document.uri, - originalHandlerName: args.handlerName, - handlerName, - runtime: args.runtime, - workspaceUri: args.workspaceFolder.uri + relativeFunctionHandler, + properties: lambdaInfo && lambdaInfo.resource.Properties ? lambdaInfo.resource.Properties : undefined, + runtime: args.runtime }) - logger.debug(`pythonCodeLensProvider.invokeLambda: ${ - JSON.stringify({ samProjectCodeRoot, inputTemplatePath, handlerName, manifestPath }, undefined, 2) - }`) + + logger.debug( + `pythonCodeLensProvider.invokeLambda: ${JSON.stringify( + { samProjectCodeRoot, inputTemplatePath, handlerName, manifestPath }, + undefined, + 2 + )}` + ) const codeDir = samProjectCodeRoot const samTemplatePath: string = await executeSamBuild({ @@ -266,24 +280,31 @@ export async function initialize({ codeDir, inputTemplatePath, manifestPath, - samProcessInvoker: processInvoker, - + samProcessInvoker: processInvoker }) - const debugConfig: PythonDebugConfiguration = makeDebugConfig({ debugPort, samProjectCodeRoot }) - await invokeLambdaFunction({ + const invokeArgs: InvokeLambdaFunctionArguments = { baseBuildDir, - channelLogger, - configuration, - debugConfig, - samLocalInvokeCommand: localInvokeCommand!, originalSamTemplatePath: args.samTemplate.fsPath, samTemplatePath, documentUri: args.document.uri, originalHandlerName: args.handlerName, handlerName, - isDebug: args.isDebug, - runtime: args.runtime, + runtime: args.runtime + } + + if (args.isDebug) { + const debugConfig: PythonDebugConfiguration = makeDebugConfig({ debugPort, samProjectCodeRoot }) + invokeArgs.debugArgs = { + debugConfig, + debugPort: debugConfig.port + } + } + + await invokeLambdaFunction(invokeArgs, { + channelLogger, + configuration, + samLocalInvokeCommand: localInvokeCommand!, telemetryService }) } catch (err) { @@ -304,11 +325,11 @@ export async function initialize({ registerCommand({ command: command, callback: async (params: LambdaLocalInvokeParams): Promise<{ datum: Datum }> => { - - const runtime = await getRuntimeForLambda({ + const resource = await CloudFormation.getResourceFromTemplate({ handlerName: params.handlerName, templatePath: params.samTemplate.fsPath }) + const runtime = CloudFormation.getRuntime(resource) await invokeLambda({ runtime, @@ -318,7 +339,7 @@ export async function initialize({ return getMetricDatum({ isDebug: params.isDebug, command, - runtime, + runtime }) }, telemetryName: { @@ -340,7 +361,8 @@ async function deleteFile(filePath: string): Promise { export async function makePythonCodeLensProvider(): Promise { const logger = getLogger() - return { // CodeLensProvider + return { + // CodeLensProvider provideCodeLenses: async ( document: vscode.TextDocument, token: vscode.CancellationToken diff --git a/src/shared/codelens/typescriptCodeLensProvider.ts b/src/shared/codelens/typescriptCodeLensProvider.ts index 073cb9362ae..4c7cbf4b397 100644 --- a/src/shared/codelens/typescriptCodeLensProvider.ts +++ b/src/shared/codelens/typescriptCodeLensProvider.ts @@ -7,17 +7,17 @@ import * as path from 'path' import * as vscode from 'vscode' - import { NodejsDebugConfiguration } from '../../lambda/local/debugConfiguration' +import { CloudFormation } from '../cloudformation/cloudformation' import { findFileInParentPaths } from '../filesystemUtilities' import { LambdaHandlerCandidate } from '../lambdaHandlerSearch' +import { DefaultSamLocalInvokeCommand, WAIT_FOR_DEBUGGER_MESSAGES } from '../sam/cli/samCliLocalInvoke' import { Datum, TelemetryNamespace } from '../telemetry/telemetryTypes' import { registerCommand } from '../telemetry/telemetryUtils' import { TypescriptLambdaHandlerSearch } from '../typescriptLambdaHandlerSearch' import { getChannelLogger, getDebugPort, localize } from '../utilities/vsCodeUtils' import { DefaultValidatingSamCliProcessInvoker } from '../sam/cli/defaultValidatingSamCliProcessInvoker' -import { DefaultSamLocalInvokeCommand, WAIT_FOR_DEBUGGER_MESSAGES } from '../sam/cli/samCliLocalInvoke' import { CodeLensProviderParams, getInvokeCmdKey, @@ -25,13 +25,12 @@ import { makeCodeLenses, } from './codeLensUtils' import { - getRuntimeForLambda, LambdaLocalInvokeParams, LocalLambdaRunner, } from './localLambdaRunner' const unsupportedNodeJsRuntimes: Set = new Set([ - 'nodejs4.3', + 'nodejs4.3' ]) async function getSamProjectDirPathForFile(filepath: string): Promise { @@ -109,11 +108,11 @@ export function initialize({ registerCommand({ command: command, callback: async (params: LambdaLocalInvokeParams): Promise<{ datum: Datum }> => { - - const runtime = await getRuntimeForLambda({ + const resource = await CloudFormation.getResourceFromTemplate({ handlerName: params.handlerName, templatePath: params.samTemplate.fsPath }) + const runtime = CloudFormation.getRuntime(resource) if (params.isDebug && unsupportedNodeJsRuntimes.has(runtime)) { vscode.window.showErrorMessage( @@ -144,7 +143,7 @@ export function initialize({ } export function makeTypescriptCodeLensProvider(): vscode.CodeLensProvider { - return { // CodeLensProvider + return { provideCodeLenses: async ( document: vscode.TextDocument, token: vscode.CancellationToken diff --git a/src/shared/credentials/userCredentialsUtils.ts b/src/shared/credentials/userCredentialsUtils.ts index aeddb4c3809..4f4e0fc09bb 100644 --- a/src/shared/credentials/userCredentialsUtils.ts +++ b/src/shared/credentials/userCredentialsUtils.ts @@ -137,6 +137,7 @@ export class UserCredentialsUtils { sts?: StsClient ): Promise { const logger: Logger = getLogger() + if (!sts) { const transformedCredentials: ServiceConfigurationOptions = { credentials: { diff --git a/src/shared/defaultAwsContext.ts b/src/shared/defaultAwsContext.ts index 94746078cf1..94874d8dc5e 100644 --- a/src/shared/defaultAwsContext.ts +++ b/src/shared/defaultAwsContext.ts @@ -19,7 +19,6 @@ const localize = nls.loadMessageBundle() // Wraps an AWS context in terms of credential profile and zero or more regions. The // context listens for configuration updates and resets the context accordingly. export class DefaultAwsContext implements AwsContext { - public readonly onDidChangeContext: vscode.Event private readonly credentialsMru: CredentialsProfileMru private readonly _onDidChangeContext: vscode.EventEmitter @@ -38,7 +37,6 @@ export class DefaultAwsContext implements AwsContext { public context: vscode.ExtensionContext, private readonly credentialsManager: CredentialsManager = new CredentialsManager() ) { - this._onDidChangeContext = new vscode.EventEmitter() this.onDidChangeContext = this._onDidChangeContext.event @@ -59,19 +57,23 @@ export class DefaultAwsContext implements AwsContext { public async getCredentials(profileName?: string): Promise { const profile = profileName || this.profileName - if (!profile) { return undefined } + if (!profile) { + return undefined + } try { return await this.credentialsManager.getCredentials(profile) } catch (err) { const error = err as Error - vscode.window.showErrorMessage(localize( - 'AWS.message.credentials.error', - 'There was an issue trying to use credentials profile {0}: {1}', - profile, - error.message - )) + vscode.window.showErrorMessage( + localize( + 'AWS.message.credentials.error', + 'There was an issue trying to use credentials profile {0}: {1}', + profile, + error.message + ) + ) throw error } @@ -142,10 +144,8 @@ export class DefaultAwsContext implements AwsContext { } private emitEvent() { - this._onDidChangeContext.fire(new ContextChangeEventsArgs( - this.profileName, - this.accountId, - this.explorerRegions - )) + this._onDidChangeContext.fire( + new ContextChangeEventsArgs(this.profileName, this.accountId, this.explorerRegions) + ) } } diff --git a/src/shared/defaultAwsContextCommands.ts b/src/shared/defaultAwsContextCommands.ts index 6ad503b1ea5..33390fc64ae 100644 --- a/src/shared/defaultAwsContextCommands.ts +++ b/src/shared/defaultAwsContextCommands.ts @@ -21,10 +21,7 @@ import { promptToDefineCredentialsProfile } from './credentials/defaultCredentialSelectionDataProvider' import { DefaultCredentialsFileReaderWriter } from './credentials/defaultCredentialsFileReaderWriter' -import { - CredentialsValidationResult, - UserCredentialsUtils -} from './credentials/userCredentialsUtils' +import { CredentialsValidationResult, UserCredentialsUtils } from './credentials/userCredentialsUtils' import { ext } from './extensionGlobals' import { RegionInfo } from './regions/regionInfo' import { RegionProvider } from './regions/regionProvider' @@ -47,7 +44,7 @@ enum OnDefaultRegionMissingOperation { /** * Do nothing */ - Ignore = 'ignore', + Ignore = 'ignore' } class DefaultRegionMissingPromptItems { @@ -64,7 +61,6 @@ class DefaultRegionMissingPromptItems { } export class DefaultAWSContextCommands { - private readonly _awsContext: AwsContext private readonly _awsContextTrees: AwsContextTreeCollection private readonly _regionProvider: RegionProvider @@ -96,7 +92,6 @@ export class DefaultAWSContextCommands { } public async onCommandCreateCredentialsProfile(): Promise { - const credentialsFiles: string[] = await UserCredentialsUtils.findExistingCredentialsFilenames() if (credentialsFiles.length === 0) { @@ -106,6 +101,7 @@ export class DefaultAWSContextCommands { if (profileName) { const successfulLogin = await UserCredentialsUtils.addUserDataToContext(profileName, this._awsContext) if (!successfulLogin) { + // credentials are invalid. Prompt user and log out await this.onCommandLogout() await UserCredentialsUtils.notifyUserCredentialsAreBad(profileName) } @@ -134,7 +130,7 @@ export class DefaultAWSContextCommands { } public async onCommandHideRegion(regionCode?: string) { - const region = regionCode || await this.promptForRegion(await this._awsContext.getExplorerRegions()) + const region = regionCode || (await this.promptForRegion(await this._awsContext.getExplorerRegions())) if (region) { await this._awsContext.removeExplorerRegion(region) this.refresh() @@ -152,9 +148,7 @@ export class DefaultAWSContextCommands { * @returns The profile name, or undefined if user cancelled */ private async promptAndCreateNewCredentialsFile(): Promise { - while (true) { - const dataProvider = new DefaultCredentialSelectionDataProvider([], ext.context) const state: CredentialSelectionState = await promptToDefineCredentialsProfile(dataProvider) @@ -168,13 +162,11 @@ export class DefaultAWSContextCommands { if (validationResult.isValid) { await UserCredentialsUtils.generateCredentialDirectoryIfNonexistent() - await UserCredentialsUtils.generateCredentialsFile( - ext.context.extensionPath, - { - profileName: state.profileName, - accessKey: state.accesskey, - secretKey: state.secretKey - }) + await UserCredentialsUtils.generateCredentialsFile(ext.context.extensionPath, { + profileName: state.profileName, + accessKey: state.accesskey, + secretKey: state.secretKey + }) return state.profileName } @@ -195,7 +187,6 @@ export class DefaultAWSContextCommands { if (!response || response !== responseYes) { return undefined } - } // Keep asking until cancel or valid credentials are entered } @@ -208,7 +199,6 @@ export class DefaultAWSContextCommands { * editing their credentials file. */ private async getProfileNameFromUser(): Promise { - await new DefaultCredentialsFileReaderWriter().setCanUseConfigFileIfExists() const responseYes: string = localize('AWS.generic.response.yes', 'Yes') @@ -217,7 +207,6 @@ export class DefaultAWSContextCommands { const credentialsFiles: string[] = await UserCredentialsUtils.findExistingCredentialsFilenames() if (credentialsFiles.length === 0) { - const userResponse = await window.showInformationMessage( localize( 'AWS.message.prompt.credentials.create', @@ -227,11 +216,12 @@ export class DefaultAWSContextCommands { responseNo ) - if (userResponse !== responseYes) { return undefined } + if (userResponse !== responseYes) { + return undefined + } return await this.promptAndCreateNewCredentialsFile() } else { - const credentialReaderWriter = new DefaultCredentialsFileReaderWriter() const profileNames = await credentialReaderWriter.getProfileNames() @@ -271,19 +261,16 @@ export class DefaultAWSContextCommands { * @description Sets the user up to edit the credentials files. */ private async editCredentials(): Promise { - const credentialsFiles: string[] = await UserCredentialsUtils.findExistingCredentialsFilenames() let preserveFocus: boolean = false let viewColumn: ViewColumn = ViewColumn.Active for (const filename of credentialsFiles) { - await window.showTextDocument( - Uri.file(filename), - { - preserveFocus: preserveFocus, - preview: false, - viewColumn: viewColumn - }) + await window.showTextDocument(Uri.file(filename), { + preserveFocus: preserveFocus, + preview: false, + viewColumn: viewColumn + }) preserveFocus = true viewColumn = ViewColumn.Beside @@ -294,10 +281,11 @@ export class DefaultAWSContextCommands { const response = await window.showInformationMessage( localize( 'AWS.message.prompt.credentials.definition.help', - 'Would you like some information related to defining credentials?', + 'Would you like some information related to defining credentials?' ), responseYes, - responseNo) + responseNo + ) if (response && response === responseYes) { await opn(extensionConstants.aboutCredentialsFileUrl) @@ -326,19 +314,21 @@ export class DefaultAWSContextCommands { */ private async promptForRegion(regions?: string[]): Promise { const availableRegions = await this._regionProvider.getRegionData() - const regionsToShow = availableRegions.filter(r => { - if (regions) { - return regions.some(x => x === r.regionCode) - } + const regionsToShow = availableRegions + .filter(r => { + if (regions) { + return regions.some(x => x === r.regionCode) + } - return true - }).map(r => ({ - label: r.regionName, - detail: r.regionCode - })) + return true + }) + .map(r => ({ + label: r.regionName, + detail: r.regionCode + })) const input = await window.showQuickPick(regionsToShow, { placeHolder: localize('AWS.message.selectRegion', 'Select an AWS region'), - matchOnDetail: true, + matchOnDetail: true }) return input ? input.detail : undefined @@ -348,10 +338,14 @@ export class DefaultAWSContextCommands { const credentialReaderWriter = new DefaultCredentialsFileReaderWriter() const profileRegion = await credentialReaderWriter.getDefaultRegion(profileName) - if (!profileRegion) { return } + if (!profileRegion) { + return + } const explorerRegions = new Set(await this._awsContext.getExplorerRegions()) - if (explorerRegions.has(profileRegion)) { return } + if (explorerRegions.has(profileRegion)) { + return + } // Explorer does not contain the default region. See if we should add it. const config = workspace.getConfiguration(extensionConstants.extensionSettingsPrefix) @@ -382,14 +376,16 @@ export class DefaultAWSContextCommands { placeHolder: localize( 'AWS.message.prompt.defaultRegionHidden', "This profile's default region ({0}) is currently hidden. " + - 'Would you like to show it in the Explorer?', + 'Would you like to show it in the Explorer?', profileRegion - ), + ) } ) // User Cancelled - if (!regionHiddenResponse) { return } + if (!regionHiddenResponse) { + return + } switch (regionHiddenResponse) { case DefaultRegionMissingPromptItems.add: @@ -402,16 +398,19 @@ export class DefaultAWSContextCommands { case DefaultRegionMissingPromptItems.alwaysAdd: case DefaultRegionMissingPromptItems.alwaysIgnore: // User does not want to be prompted anymore - const action = regionHiddenResponse === DefaultRegionMissingPromptItems.alwaysAdd ? - OnDefaultRegionMissingOperation.Add : - OnDefaultRegionMissingOperation.Ignore + const action = + regionHiddenResponse === DefaultRegionMissingPromptItems.alwaysAdd + ? OnDefaultRegionMissingOperation.Add + : OnDefaultRegionMissingOperation.Ignore await config.update('onDefaultRegionMissing', action, !workspace.name) - window.showInformationMessage(localize( - 'AWS.message.prompt.defaultRegionHidden.suppressed', - "You will no longer be asked what to do when the current profile's default region is " + - "hidden from the Explorer. This behavior can be changed by modifying the '{0}' setting.", - 'aws.onDefaultRegionMissing' - )) + window.showInformationMessage( + localize( + 'AWS.message.prompt.defaultRegionHidden.suppressed', + "You will no longer be asked what to do when the current profile's default region is " + + "hidden from the Explorer. This behavior can be changed by modifying the '{0}' setting.", + 'aws.onDefaultRegionMissing' + ) + ) break } } diff --git a/src/shared/sam/cli/defaultValidatingSamCliProcessInvoker.ts b/src/shared/sam/cli/defaultValidatingSamCliProcessInvoker.ts index 52a4e0b9a46..0646f92c8b3 100644 --- a/src/shared/sam/cli/defaultValidatingSamCliProcessInvoker.ts +++ b/src/shared/sam/cli/defaultValidatingSamCliProcessInvoker.ts @@ -18,33 +18,34 @@ import { DefaultSamCliValidator, DefaultSamCliValidatorContext, SamCliValidator, - SamCliValidatorResult, + SamCliValidatorResult } from './samCliValidator' /** * Validates the SAM CLI version before making calls to the SAM CLI. */ export class DefaultValidatingSamCliProcessInvoker implements SamCliProcessInvoker { - private readonly invoker: SamCliProcessInvoker private readonly invokerContext: SamCliProcessInvokerContext private readonly validator: SamCliValidator public constructor(params: { - invoker?: SamCliProcessInvoker, - invokerContext?: SamCliProcessInvokerContext, - validator?: SamCliValidator, + invoker?: SamCliProcessInvoker + invokerContext?: SamCliProcessInvokerContext + validator?: SamCliValidator }) { this.invokerContext = resolveSamCliProcessInvokerContext(params.invokerContext) this.invoker = params.invoker || new DefaultSamCliProcessInvoker(this.invokerContext) // Regardless of the sam cli invoker provided, the default validator will always use the standard invoker - this.validator = params.validator || new DefaultSamCliValidator( - new DefaultSamCliValidatorContext( - this.invokerContext.cliConfig, - new DefaultSamCliProcessInvoker(this.invokerContext), + this.validator = + params.validator || + new DefaultSamCliValidator( + new DefaultSamCliValidatorContext( + this.invokerContext.cliConfig, + new DefaultSamCliProcessInvoker(this.invokerContext) + ) ) - ) } public invoke(options: SpawnOptions, ...args: string[]): Promise diff --git a/src/shared/sam/cli/samCliBuild.ts b/src/shared/sam/cli/samCliBuild.ts index 2ce4978744a..7a753e92b53 100644 --- a/src/shared/sam/cli/samCliBuild.ts +++ b/src/shared/sam/cli/samCliBuild.ts @@ -24,6 +24,10 @@ export interface SamCliBuildInvocationArguments { * Location of the SAM Template to build */ templatePath: string + /** + * Environment variables to set on the child process. + */ + environmentVariables?: NodeJS.ProcessEnv /** * Manages the sam cli execution. */ @@ -56,6 +60,7 @@ export interface FileFunctions { export class SamCliBuildInvocation { private readonly buildDir: string private readonly baseDir?: string + private readonly environmentVariables?: NodeJS.ProcessEnv private readonly templatePath: string private readonly invoker: SamCliProcessInvoker private readonly useContainer: boolean @@ -81,6 +86,7 @@ export class SamCliBuildInvocation { this.buildDir = params.buildDir this.baseDir = params.baseDir this.templatePath = params.templatePath + this.environmentVariables = params.environmentVariables this.invoker = invoker this.useContainer = useContainer this.dockerNetwork = params.dockerNetwork @@ -103,7 +109,13 @@ export class SamCliBuildInvocation { this.addArgumentIf(invokeArgs, !!this.skipPullImage, '--skip-pull-image') this.addArgumentIf(invokeArgs, !!this.manifestPath, '--manifest', this.manifestPath!) - const childProcessResult = await this.invoker.invoke( + const env: NodeJS.ProcessEnv = { + ...process.env, + ...this.environmentVariables + } + + const childProcessResult = await this.invoker.invoke( + { env }, ...invokeArgs ) diff --git a/src/shared/sam/cli/samCliInfo.ts b/src/shared/sam/cli/samCliInfo.ts index 5e9d7207523..f1c77992cb2 100644 --- a/src/shared/sam/cli/samCliInfo.ts +++ b/src/shared/sam/cli/samCliInfo.ts @@ -26,7 +26,6 @@ export class SamCliInfoInvocation { const childProcessResult = await this.invoker.invoke('--info') logAndThrowIfUnexpectedExitCode(childProcessResult, 0) - const response = this.convertOutput(childProcessResult.stdout) if (!response) { diff --git a/src/shared/sam/cli/samCliInvoker.ts b/src/shared/sam/cli/samCliInvoker.ts index 067d4bdbdfa..bc37697fca4 100644 --- a/src/shared/sam/cli/samCliInvoker.ts +++ b/src/shared/sam/cli/samCliInvoker.ts @@ -34,15 +34,12 @@ export function resolveSamCliProcessInvokerContext( return { cliConfig: params.cliConfig || defaults.cliConfig, - logger: params.logger || defaults.logger, + logger: params.logger || defaults.logger } } export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker { - - public constructor( - private readonly context: SamCliProcessInvokerContext = resolveSamCliProcessInvokerContext() - ) { } + public constructor(private readonly context: SamCliProcessInvokerContext = resolveSamCliProcessInvokerContext()) {} public invoke(options: SpawnOptions, ...args: string[]): Promise public invoke(...args: string[]): Promise diff --git a/src/shared/sam/cli/samCliLocalInvoke.ts b/src/shared/sam/cli/samCliLocalInvoke.ts index f1cb80b4ce0..8d7677519aa 100644 --- a/src/shared/sam/cli/samCliLocalInvoke.ts +++ b/src/shared/sam/cli/samCliLocalInvoke.ts @@ -18,13 +18,14 @@ const localize = nls.loadMessageBundle() export const WAIT_FOR_DEBUGGER_MESSAGES = { PYTHON: 'Waiting for debugger to attach...', NODEJS: 'Debugger listening on', + DOTNET: 'Waiting for the debugger to attach...' } export interface SamLocalInvokeCommandArgs { - command: string, - args: string[], - options?: child_process.SpawnOptions, - isDebug: boolean, + command: string + args: string[] + options?: child_process.SpawnOptions + isDebug: boolean timeout?: Timeout } @@ -32,7 +33,7 @@ export interface SamLocalInvokeCommandArgs { * Represents and manages the SAM CLI command that is run to locally invoke SAM Applications. */ export interface SamLocalInvokeCommand { - invoke({ }: SamLocalInvokeCommandArgs): Promise + invoke({ }: SamLocalInvokeCommandArgs): Promise } export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand { @@ -40,15 +41,11 @@ export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand { private readonly channelLogger: ChannelLogger, private readonly debuggerAttachCues: string[] = [ WAIT_FOR_DEBUGGER_MESSAGES.PYTHON, - WAIT_FOR_DEBUGGER_MESSAGES.NODEJS, - ], - ) { - } + WAIT_FOR_DEBUGGER_MESSAGES.NODEJS + ] + ) {} - public async invoke({ - options = {}, - ...params - }: SamLocalInvokeCommandArgs): Promise { + public async invoke({ options = {}, ...params }: SamLocalInvokeCommandArgs): Promise { this.channelLogger.info( 'AWS.running.command', 'Running command: {0}', @@ -56,60 +53,52 @@ export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand { ) const childProcess = new ChildProcess(params.command, options, ...params.args) - let debuggerPromiseClosed: boolean = false const debuggerPromise = new Promise(async (resolve, reject) => { let checkForDebuggerAttachCue: boolean = params.isDebug - await childProcess.start( - { - onStdout: (text: string): void => { - this.emitMessage(text) - }, - onStderr: (text: string): void => { - this.emitMessage(text) - if (checkForDebuggerAttachCue) { - // Look for messages like "Waiting for debugger to attach" before returning back to caller - if (this.debuggerAttachCues.some(cue => text.includes(cue))) { - checkForDebuggerAttachCue = false - this.channelLogger.logger.verbose( - 'Local SAM App should be ready for a debugger to attach now.' - ) - debuggerPromiseClosed = true - resolve() - } - } - }, - onClose: (code: number, signal: string): void => { - this.channelLogger.logger.verbose( - `The child process for sam local invoke closed with code ${code}` - ) - this.channelLogger.channel.appendLine( - localize( - 'AWS.samcli.local.invoke.ended', - 'Local invoke of SAM Application has ended.' + await childProcess.start({ + onStdout: (text: string): void => { + this.emitMessage(text) + }, + onStderr: (text: string): void => { + this.emitMessage(text) + if (checkForDebuggerAttachCue) { + // Look for messages like "Waiting for debugger to attach" before returning back to caller + if (this.debuggerAttachCues.some(cue => text.includes(cue))) { + checkForDebuggerAttachCue = false + this.channelLogger.logger.verbose( + 'Local SAM App should be ready for a debugger to attach now.' ) - ) - - // Handles scenarios where the process exited before we anticipated. - // Example: We didn't see an expected debugger attach cue, and the process or docker container - // was terminated by the user, or the user manually attached to the sam app. - if (!debuggerPromiseClosed) { debuggerPromiseClosed = true - reject(new Error('The SAM Application closed unexpectedly')) + resolve() } - }, - onError: (error: Error): void => { - this.channelLogger.error( - 'AWS.samcli.local.invoke.error', - 'Error encountered running local SAM Application', - error - ) + } + }, + onClose: (code: number, signal: string): void => { + this.channelLogger.logger.verbose(`The child process for sam local invoke closed with code ${code}`) + this.channelLogger.channel.appendLine( + localize('AWS.samcli.local.invoke.ended', 'Local invoke of SAM Application has ended.') + ) + + // Handles scenarios where the process exited before we anticipated. + // Example: We didn't see an expected debugger attach cue, and the process or docker container + // was terminated by the user, or the user manually attached to the sam app. + if (!debuggerPromiseClosed) { debuggerPromiseClosed = true - reject(error) - }, + reject(new Error('The SAM Application closed unexpectedly')) + } + }, + onError: (error: Error): void => { + this.channelLogger.error( + 'AWS.samcli.local.invoke.error', + 'Error encountered running local SAM Application', + error + ) + debuggerPromiseClosed = true + reject(error) } - ) + }) if (!params.isDebug) { this.channelLogger.logger.verbose('Local SAM App does not expect a debugger to attach.') @@ -120,7 +109,7 @@ export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand { const awaitedPromises = params.timeout ? [debuggerPromise, params.timeout.timer] : [debuggerPromise] - await Promise.race(awaitedPromises).catch( async () => { + await Promise.race(awaitedPromises).catch(async () => { // did debugger promise resolve/reject? if not, this was a timeout: kill the process // otherwise, process closed out on its own; no need to kill the process if (!debuggerPromiseClosed) { @@ -150,37 +139,41 @@ export interface SamCliLocalInvokeInvocationArguments { /** * The name of the resource in the SAM Template to be invoked. */ - templateResourceName: string, + templateResourceName: string /** * Location of the SAM Template to invoke locally against. */ - templatePath: string, + templatePath: string /** * Location of the file containing the Lambda Function event payload. */ - eventPath: string, + eventPath: string /** * Location of the file containing the environment variables to invoke the Lambda Function against. */ - environmentVariablePath: string, + environmentVariablePath: string /** * When specified, starts the Lambda function container in debug mode and exposes this port on the local host. */ - debugPort?: string, + debugPort?: string /** * Manages the sam cli execution. */ - invoker: SamLocalInvokeCommand, + invoker: SamLocalInvokeCommand /** * Specifies the name or id of an existing Docker network to Lambda Docker containers should connect to, * along with the default bridge network. * If not specified, the Lambda containers will only connect to the default bridge Docker network. */ - dockerNetwork?: string, + dockerNetwork?: string /** * Specifies whether the command should skip pulling down the latest Docker image for Lambda runtime. */ - skipPullImage?: boolean, + skipPullImage?: boolean + /** + * Host path to a debugger that will be mounted into the Lambda container. + */ + debuggerPath?: string } export class SamCliLocalInvokeInvocation { @@ -192,16 +185,13 @@ export class SamCliLocalInvokeInvocation { private readonly invoker: SamLocalInvokeCommand private readonly dockerNetwork?: string private readonly skipPullImage: boolean + private readonly debuggerPath?: string /** * @see SamCliLocalInvokeInvocationArguments for parameter info * skipPullImage - Defaults to false (the latest Docker image will be pulled down if necessary) */ - public constructor({ - skipPullImage = false, - ...params - }: SamCliLocalInvokeInvocationArguments - ) { + public constructor({ skipPullImage = false, ...params }: SamCliLocalInvokeInvocationArguments) { this.templateResourceName = params.templateResourceName this.templatePath = params.templatePath this.eventPath = params.eventPath @@ -210,6 +200,7 @@ export class SamCliLocalInvokeInvocation { this.invoker = params.invoker this.dockerNetwork = params.dockerNetwork this.skipPullImage = skipPullImage + this.debuggerPath = params.debuggerPath } public async execute(timeout?: Timeout): Promise { @@ -230,6 +221,7 @@ export class SamCliLocalInvokeInvocation { this.addArgumentIf(args, !!this.debugPort, '-d', this.debugPort!) this.addArgumentIf(args, !!this.dockerNetwork, '--docker-network', this.dockerNetwork!) this.addArgumentIf(args, !!this.skipPullImage, '--skip-pull-image') + this.addArgumentIf(args, !!this.debuggerPath, '--debugger-path', this.debuggerPath!) await this.invoker.invoke({ command: 'sam', @@ -244,11 +236,11 @@ export class SamCliLocalInvokeInvocation { throw new Error('template resource name is missing or empty') } - if (!await fileExists(this.templatePath)) { + if (!(await fileExists(this.templatePath))) { throw new Error(`template path does not exist: ${this.templatePath}`) } - if (!await fileExists(this.eventPath)) { + if (!(await fileExists(this.eventPath))) { throw new Error(`event path does not exist: ${this.eventPath}`) } } diff --git a/src/shared/telemetry/defaultTelemetryClient.ts b/src/shared/telemetry/defaultTelemetryClient.ts index 1d01aef6a14..795df13a876 100644 --- a/src/shared/telemetry/defaultTelemetryClient.ts +++ b/src/shared/telemetry/defaultTelemetryClient.ts @@ -21,10 +21,7 @@ export class DefaultTelemetryClient implements TelemetryClient { private static readonly PRODUCT_NAME = 'AWS Toolkit For VS Code' - private constructor( - private readonly clientId: string, - private readonly client: ClientTelemetry, - ) {} + private constructor(private readonly clientId: string, private readonly client: ClientTelemetry) {} /** * Returns failed events @@ -32,16 +29,18 @@ export class DefaultTelemetryClient implements TelemetryClient { */ public async postMetrics(batch: TelemetryEvent[]): Promise { try { - await this.client.postMetrics({ - AWSProduct: DefaultTelemetryClient.PRODUCT_NAME, - AWSProductVersion: constants.pluginVersion, - ClientID: this.clientId, - OS: os.platform(), - OSVersion: os.release(), - ParentProduct: vscode.env.appName, - ParentProductVersion: vscode.version, - MetricData: toMetricData(batch) - }).promise() + await this.client + .postMetrics({ + AWSProduct: DefaultTelemetryClient.PRODUCT_NAME, + AWSProductVersion: constants.pluginVersion, + ClientID: this.clientId, + OS: os.platform(), + OSVersion: os.release(), + ParentProduct: vscode.env.appName, + ParentProductVersion: vscode.version, + MetricData: toMetricData(batch) + }) + .promise() console.info(`Successfully sent a telemetry batch of ${batch.length}`) } catch (err) { console.error(`Batch error: ${err}`) @@ -53,24 +52,20 @@ export class DefaultTelemetryClient implements TelemetryClient { public static async createDefaultClient( clientId: string, region: string, - credentials: Credentials, + credentials: Credentials ): Promise { - await credentials.getPromise() return new DefaultTelemetryClient( clientId, - await ext.sdkClientBuilder.createAndConfigureServiceClient( - opts => new Service(opts), - { - // @ts-ignore: apiConfig is internal and not in the TS declaration file - apiConfig: apiConfig, - region: region, - credentials: credentials, - correctClockSkew: true, - endpoint: DefaultTelemetryClient.DEFAULT_TELEMETRY_ENDPOINT - } - ), + await ext.sdkClientBuilder.createAndConfigureServiceClient(opts => new Service(opts), { + // @ts-ignore: apiConfig is internal and not in the TS declaration file + apiConfig: apiConfig, + region: region, + credentials: credentials, + correctClockSkew: true, + endpoint: DefaultTelemetryClient.DEFAULT_TELEMETRY_ENDPOINT + }) ) } } diff --git a/src/shared/telemetry/telemetryEvent.ts b/src/shared/telemetry/telemetryEvent.ts index 6f040d24abd..33e9a516f56 100644 --- a/src/shared/telemetry/telemetryEvent.ts +++ b/src/shared/telemetry/telemetryEvent.ts @@ -20,7 +20,6 @@ export interface TelemetryEvent { export function toMetricData(array: TelemetryEvent[]): MetricDatum[] { return ([] as MetricDatum[]).concat( ...array.map(metricEvent => { - const namespace = metricEvent.namespace.replace(REMOVE_UNDERSCORES_REGEX, '') if (metricEvent.data !== undefined) { diff --git a/src/shared/telemetry/telemetryUtils.ts b/src/shared/telemetry/telemetryUtils.ts index 1658a6fe1a6..33d45cb3ba4 100644 --- a/src/shared/telemetry/telemetryUtils.ts +++ b/src/shared/telemetry/telemetryUtils.ts @@ -25,13 +25,13 @@ export function registerCommand({ namespace: 'Command', name: command }, - callback, + callback }: { command: string thisArg?: any register?: typeof vscode.commands.registerCommand telemetryName?: TelemetryName - callback(...args: any[]): (Promise) + callback(...args: any[]): Promise }): vscode.Disposable { return register( command, @@ -54,7 +54,8 @@ export function registerCommand({ setMetadataIfNotExists( datum.metadata, METADATA_FIELD_NAME.RESULT, - hasException ? MetadataResult.Fail.toString() : MetadataResult.Pass.toString()) + hasException ? MetadataResult.Fail.toString() : MetadataResult.Pass.toString() + ) setMetadataIfNotExists(datum.metadata, 'duration', `${endTime.getTime() - startTime.getTime()}`) ext.telemetry.record({ diff --git a/src/shared/templates/sam/samTemplateGenerator.ts b/src/shared/templates/sam/samTemplateGenerator.ts index f0e2501bd65..97a8333da79 100644 --- a/src/shared/templates/sam/samTemplateGenerator.ts +++ b/src/shared/templates/sam/samTemplateGenerator.ts @@ -53,7 +53,7 @@ export class SamTemplateGenerator { const template: CloudFormation.Template = { Resources: { [this.resourceName!]: { - Type: 'AWS::Serverless::Function', + Type: CloudFormation.SERVERLESS_FUNCTION_TYPE, Properties: CloudFormation.validateProperties(this.properties) } } diff --git a/src/shared/utilities/pathUtils.ts b/src/shared/utilities/pathUtils.ts index 58cf2d86f1f..6f10ef53a38 100644 --- a/src/shared/utilities/pathUtils.ts +++ b/src/shared/utilities/pathUtils.ts @@ -14,3 +14,12 @@ export function getNormalizedRelativePath(from: string, to: string): string { export function normalizeSeparator(path: string) { return path.split(_path.sep).join(_path.posix.sep) } + +export function dirnameWithTrailingSlash(path: string): string { + let dirname = _path.dirname(path) + if (!dirname.endsWith(_path.sep)) { + dirname += _path.sep + } + + return dirname +} diff --git a/src/shared/utilities/vsCodeUtils.ts b/src/shared/utilities/vsCodeUtils.ts index 541fe33a033..fb6e6cc976e 100644 --- a/src/shared/utilities/vsCodeUtils.ts +++ b/src/shared/utilities/vsCodeUtils.ts @@ -82,7 +82,7 @@ export interface ChannelLogger { * Wrapper around normal logger that writes to output channel and normal logs. * Avoids making two log statements when writing to output channel and improves consistency */ -export function getChannelLogger(channel: vscode.OutputChannel, logger: BasicLogger = getLogger()) { +export function getChannelLogger(channel: vscode.OutputChannel, logger: BasicLogger = getLogger()): ChannelLogger { return Object.freeze({ channel, logger, diff --git a/src/test/lambda/lambdaTreeDataProvider.test.ts b/src/test/lambda/lambdaTreeDataProvider.test.ts index 6761c91afd6..10c97bbea8f 100644 --- a/src/test/lambda/lambdaTreeDataProvider.test.ts +++ b/src/test/lambda/lambdaTreeDataProvider.test.ts @@ -40,7 +40,6 @@ describe('LambdaProvider', () => { }) it('displays region nodes with user-friendly region names', async () => { - const awsContext = new FakeAwsContext() const regionProvider = new FakeRegionProvider() const awsContextTreeCollection = new AwsContextTreeCollection() diff --git a/src/test/lambda/local/debugConfiguration.test.ts b/src/test/lambda/local/debugConfiguration.test.ts new file mode 100644 index 00000000000..18fa55ac865 --- /dev/null +++ b/src/test/lambda/local/debugConfiguration.test.ts @@ -0,0 +1,71 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as assert from 'assert' +import * as os from 'os' +import * as path from 'path' + +import { + makeCoreCLRDebugConfiguration, + MakeCoreCLRDebugConfigurationArguments +} from '../../../lambda/local/debugConfiguration' + +describe('makeCoreCLRDebugConfiguration', async () => { + function makeConfig({ + codeUri = path.join('foo', 'bar'), + port = 42, + }: Partial) { + return makeCoreCLRDebugConfiguration({ codeUri, port }) + } + + it('uses the specified codeUri', async () => { + const config = makeConfig({}) + + assert.strictEqual( + config.sourceFileMap['/var/task'], + path.join('foo', 'bar') + ) + }) + + describe('windows', async () => { + if (os.platform() === 'win32') { + it('massages drive letters to uppercase', async () => { + const config = makeConfig({ codeUri: 'c:\\foo\\bar' }) + + assert.strictEqual( + config.windows.pipeTransport.pipeCwd, + 'C:\\foo\\bar' + ) + }) + } + + it('uses powershell', async () => { + const config = makeConfig({}) + + assert.strictEqual(config.windows.pipeTransport.pipeProgram, 'powershell') + }) + + it('uses the specified port', async () => { + const config = makeConfig({ port: 538 }) + + assert.strictEqual(config.windows.pipeTransport.pipeArgs.some(arg => arg.includes('538')), true) + }) + }) + describe('*nix', async () => { + it('uses the default shell', async () => { + const config = makeConfig({}) + + assert.strictEqual(config.pipeTransport.pipeProgram, 'sh') + }) + + it('uses the specified port', async () => { + const config = makeConfig({ port: 538 }) + + assert.strictEqual(config.pipeTransport.pipeArgs.some(arg => arg.includes('538')), true) + }) + }) +}) diff --git a/src/test/lambda/local/util.ts b/src/test/lambda/local/util.ts index 477adba4eef..330e27a2ded 100644 --- a/src/test/lambda/local/util.ts +++ b/src/test/lambda/local/util.ts @@ -8,6 +8,7 @@ import * as os from 'os' import * as path from 'path' import { Uri, WorkspaceFolder } from 'vscode' +import { CloudFormation } from '../../../shared/cloudformation/cloudformation' import { writeFile } from '../../../shared/filesystem' import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' @@ -30,7 +31,7 @@ export async function createWorkspaceFolder(prefix: string): Promise<{ export async function saveTemplate(templatePath: string, runtime: string, ...functionNames: string[]) { const functionResources = functionNames.map( functionName => ` ${functionName}: - Type: AWS::Serverless::Function + Type: ${CloudFormation.SERVERLESS_FUNCTION_TYPE} Properties: CodeUri: hello_world/ Handler: app.lambdaHandler diff --git a/src/test/lambda/utilities/getMainSourceFile.test.ts b/src/test/lambda/utilities/getMainSourceFile.test.ts deleted file mode 100644 index 6f2de2f0c3f..00000000000 --- a/src/test/lambda/utilities/getMainSourceFile.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -/*! - * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -import '../../../shared/utilities/asyncIteratorShim' - -import * as assert from 'assert' -import * as os from 'os' -import * as path from 'path' -import * as vscode from 'vscode' -import { getMainSourceFileUri } from '../../../lambda/utilities/getMainSourceFile' -import { CloudFormation } from '../../../shared/cloudformation/cloudformation' - -async function* toAsyncIterable(array: T[]): AsyncIterable { - yield *array -} - -describe('getMainSourceFile', async () => { - it('throws when no template is found', async () => { - const root = vscode.Uri.file('/dir') - try { - await getMainSourceFileUri({ - root, - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([]), - }) - } catch (err) { - assert.strictEqual( - String(err), - `Error: Invalid project format: '${root.fsPath}' does not contain a SAM template.` - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - - it('throws when template is empty', async () => { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => ({}), - fileExists: async p => ['.ts'].indexOf(path.extname(p)) >= 0 - }) - } catch (err) { - assert.strictEqual( - String(err), - `Error: SAM Template '${templateUri.fsPath}' does not contain any resources` - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - - it('throws when template only contains non-lambda resources', async () => { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => ({ - Resources: { - HelloWorld: { - Type: 'AWS::Serverless:NotAFunction' - } as any as CloudFormation.Resource - } - }), - fileExists: async p => ['.ts'].indexOf(path.extname(p)) >= 0 - }) - } catch (err) { - assert.strictEqual( - String(err), - `Error: SAM Template '${templateUri.fsPath}' does not contain any lambda resources` - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - - it('throws when lambda resource has no properties', async () => { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => ({ - Resources: { - HelloWorld: { - Type: 'AWS::Serverless::Function' - } - } - }) - }) - } catch (err) { - assert.strictEqual( - String(err), - // JSON.stringify always uses `\n` regardless of os.EOL. - // tslint:disable-next-line:prefer-template - `Error: Lambda resource is missing the 'Properties' property:${os.EOL}` + - '{\n' + - ' "Type": "AWS::Serverless::Function"\n' + - '}' - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - - it('throws when runtime is unknown or unsupported', async () => { - async function test(runtime: string | undefined, message: string): Promise { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => ({ - Resources: { - HelloWorld: { - Type: 'AWS::Serverless::Function', - Properties: { - Handler: '', - CodeUri: '', - Runtime: runtime, - } - } - } - }) - }) - } catch (err) { - assert.strictEqual(String(err), message) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - } - - await test(undefined, 'Error: Unrecognized runtime: \'undefined\'') - await test('fakeruntime', 'Error: Unrecognized runtime: \'fakeruntime\'') - await test('go', 'Error: Lambda resource \'\' has unknown runtime \'go\'') - }) - - describe('nodejs', async () => { - function createTestTemplate(): CloudFormation.Template { - return { - Resources: { - HelloWorld: { - Type: 'AWS::Serverless::Function', - Properties: { - Handler: 'app.handler', - CodeUri: 'my_app', - Runtime: 'nodejs' - } - } - } - } - } - - it('returns the URI of the main source file for a valid template', async () => { - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => ['.ts'].indexOf(path.extname(p)) >= 0 - }) - - assert.strictEqual(actual.fsPath, path.join(path.sep, 'dir', 'my_app', 'app.ts')) - }) - - it('recognizes all NodeJS runtimes', async () => { - async function test(runtime?: string): Promise { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => { - const template = createTestTemplate() - template.Resources!.HelloWorld!.Properties!.Runtime = runtime - - return template - }, - fileExists: async p => ['.ts'].indexOf(path.extname(p)) >= 0 - }) - - assert.strictEqual(actual.fsPath, path.join('/dir', 'my_app', 'app.ts')) - } - - await test('nodejs') - await test('nodejs6.10') - await test('nodejs8.10') - }) - - it('prefers TypeScript files over javascript files', async () => { - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => ['.ts', '.js'].indexOf(path.extname(p)) >= 0 - }) - - assert.strictEqual(actual.fsPath, path.join(path.sep, 'dir', 'my_app', 'app.ts')) - }) - - it('prefers JSX files over javascript files', async () => { - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => ['.jsx', '.js'].indexOf(path.extname(p)) >= 0 - }) - - assert.strictEqual(actual.fsPath, path.join(path.sep, 'dir', 'my_app', 'app.jsx')) - }) - - it('finds javascript file if no TS or JSX file exists', async () => { - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => ['.js'].indexOf(path.extname(p)) >= 0 - }) - - assert.strictEqual(actual.fsPath, path.join(path.sep, 'dir', 'my_app', 'app.js')) - }) - - it('fails when no source file is found at the expected location', async () => { - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => false - }) - } catch (err) { - const expectedBasePath = path.join('/dir', 'my_app', 'app') - assert.strictEqual( - String(err), - `Error: Javascript file expected at ${expectedBasePath}.(ts|jsx|js), but no file was found` - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - }) - - describe('python', async () => { - function createTestTemplate(): CloudFormation.Template { - return { - Resources: { - HelloWorld: { - Type: 'AWS::Serverless::Function', - Properties: { - Handler: 'app.handler', - CodeUri: 'my_app', - Runtime: 'python' - } - } - } - } - } - - it('returns the URI of the main source file for a valid template', async () => { - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => path.extname(p) === '.py' - }) - - assert.strictEqual(actual.fsPath, path.join(path.sep, 'dir', 'my_app', 'app.py')) - }) - - it('recognizes all Python runtimes', async () => { - async function test(runtime?: string): Promise { - const templateUri = vscode.Uri.file(path.join('/dir', 'template.yaml')) - const actual: vscode.Uri = await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([templateUri]), - loadSamTemplate: async uri => { - const template = createTestTemplate() - template.Resources!.HelloWorld!.Properties!.Runtime = runtime - - return template - }, - fileExists: async p => path.extname(p) === '.py' - }) - - assert.strictEqual(actual.fsPath, path.join('/dir', 'my_app', 'app.py')) - } - - await test('python') - await test('python2.7') - await test('python3.6') - }) - - it('fails when no source file is found at the expected location', async () => { - try { - await getMainSourceFileUri({ - root: vscode.Uri.file('/dir'), - getLocalTemplates: (...workspaceUris: vscode.Uri[]) => toAsyncIterable([ - vscode.Uri.file('/dir/template.yaml') - ]), - loadSamTemplate: async uri => createTestTemplate(), - fileExists: async p => false - }) - } catch (err) { - const expectedBasePath = path.join('/dir', 'my_app', 'app') - assert.strictEqual( - String(err), - `Error: Python file expected at ${expectedBasePath}.py, but no file was found` - ) - - return - } - - assert.fail('Expected an exception, but none was thrown.') - }) - }) -}) diff --git a/src/test/shared/clients/dockerClient.test.ts b/src/test/shared/clients/dockerClient.test.ts new file mode 100644 index 00000000000..a365a269159 --- /dev/null +++ b/src/test/shared/clients/dockerClient.test.ts @@ -0,0 +1,165 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as assert from 'assert' +import * as path from 'path' + +import { + DefaultDockerClient, + DockerInvokeArguments +} from '../../../shared/clients/dockerClient' +import { MockOutputChannel } from '../../mockOutputChannel' + +describe('DefaultDockerClient', async () => { + const outputChannel = new MockOutputChannel() + + function makeInvokeArgs({ + command = 'run', + image = 'myimage', + ...rest + }: Partial): DockerInvokeArguments { + return { + command, + image, + ...rest + } + } + + describe('invoke', async () => { + it('uses the specified command', async () => { + let spawnCount = 0 + const client = new DefaultDockerClient( + outputChannel, + { + async run(args): Promise { + spawnCount++ + assert.ok(args) + assert.ok(args!.length) + assert.strictEqual(args![0], 'run') + } + }) + + await client.invoke(makeInvokeArgs({})) + + assert.strictEqual(spawnCount, 1) + }) + + it('uses the specified image', async () => { + let spawnCount = 0 + const client = new DefaultDockerClient( + outputChannel, + { + async run(args): Promise { + spawnCount++ + assert.strictEqual(args && args.some(arg => arg === 'myimage'), true) + } + + }) + + await client.invoke(makeInvokeArgs({})) + + assert.strictEqual(spawnCount, 1) + }) + + it('includes the --rm flag if specified', async () => { + let spawnCount = 0 + const client = new DefaultDockerClient( + outputChannel, + { + async run(args): Promise { + spawnCount++ + assert.strictEqual(args && args.some(arg => arg === '--rm'), true) + } + }) + + await client.invoke(makeInvokeArgs({ + removeOnExit: true + })) + + assert.strictEqual(spawnCount, 1) + }) + + it('includes the --mount flag if specified', async () => { + const source = path.join('my', 'src') + const destination = path.join('my', 'dst') + + let spawnCount = 0 + const client = new DefaultDockerClient( + outputChannel, + { + async run(args): Promise { + spawnCount++ + + assert.ok(args) + + const flagIndex = args!.findIndex(value => value === '--mount') + assert.notStrictEqual(flagIndex, -1) + + const flagValueIndex = flagIndex + 1 + assert.ok(flagValueIndex < args!.length) + assert.strictEqual( + args![flagValueIndex], + `type=bind,src=${source},dst=${destination}` + ) + } + }) + + await client.invoke(makeInvokeArgs({ + mount: { + type: 'bind', + source, + destination + } + })) + + assert.strictEqual(spawnCount, 1) + }) + + it('includes the --entryPoint flag if specified', async () => { + const entryPointArgs = [ + 'myArg1', + 'myArg2' + ] + let spawnCount = 0 + const client = new DefaultDockerClient( + outputChannel, + { + async run(args): Promise { + spawnCount++ + + assert.ok(args) + + const flagIndex = args!.findIndex(value => value === '--entrypoint') + assert.notStrictEqual(flagIndex, -1) + + const flagCommandIndex = flagIndex + 1 + assert.ok(flagCommandIndex < args!.length) + assert.strictEqual( + args![flagCommandIndex], + 'mycommand' + ) + + const endIndex = (args!.length - 1) + entryPointArgs.reverse().forEach((value, index) => { + const argIndex = endIndex - index + assert.ok(argIndex < args!.length) + assert.strictEqual(args![argIndex], value) + }) + } + }) + + await client.invoke(makeInvokeArgs({ + entryPoint: { + command: 'mycommand', + args: entryPointArgs + } + })) + + assert.strictEqual(spawnCount, 1) + }) + }) +}) diff --git a/src/test/shared/cloudformation/cloudformation.test.ts b/src/test/shared/cloudformation/cloudformation.test.ts index ba1a8121ef9..2d93dca86f8 100644 --- a/src/test/shared/cloudformation/cloudformation.test.ts +++ b/src/test/shared/cloudformation/cloudformation.test.ts @@ -43,7 +43,7 @@ describe ('CloudFormation', () => { function createBaseResource(): CloudFormation.Resource { return { - Type: 'AWS::Serverless::Function', + Type: CloudFormation.SERVERLESS_FUNCTION_TYPE, Properties: { Handler: 'handler', CodeUri: 'codeuri', @@ -62,11 +62,12 @@ describe ('CloudFormation', () => { await writeFile(file, str, 'utf8') } - it ('can successfully load a file', async () => { - const yamlStr: string = + describe('load', async () => { + it ('can successfully load a file', async () => { + const yamlStr: string = `Resources: TestResource: - Type: AWS::Serverless::Function + Type: ${CloudFormation.SERVERLESS_FUNCTION_TYPE} Properties: Handler: handler CodeUri: codeuri @@ -76,13 +77,13 @@ describe ('CloudFormation', () => { Variables: ENVVAR: envvar` - await strToYamlFile(yamlStr, filename) - const loadedTemplate = await CloudFormation.load(filename) - assert.deepStrictEqual(loadedTemplate, createBaseTemplate()) - }) + await strToYamlFile(yamlStr, filename) + const loadedTemplate = await CloudFormation.load(filename) + assert.deepStrictEqual(loadedTemplate, createBaseTemplate()) + }) - it ('can successfully load a file with parameters', async () => { - const yamlStr: string = + it ('can successfully load a file with parameters', async () => { + const yamlStr: string = `Parameters: MyParam1: Type: String @@ -97,28 +98,28 @@ describe ('CloudFormation', () => { MyParam6: Type: AWS::SSM::Parameter::Value` - await strToYamlFile(yamlStr, filename) - const loadedTemplate = await CloudFormation.load(filename) - const expectedTemplate: CloudFormation.Template = { - Parameters: { - MyParam1: { Type: 'String' }, - MyParam2: { Type: 'Number' }, - MyParam3: { Type: 'List' }, - MyParam4: { Type: 'CommaDelimitedList' }, - MyParam5: { Type: 'AWS::EC2::AvailabilityZone::Name' }, - MyParam6: { Type: 'AWS::SSM::Parameter::Value' }, + await strToYamlFile(yamlStr, filename) + const loadedTemplate = await CloudFormation.load(filename) + const expectedTemplate: CloudFormation.Template = { + Parameters: { + MyParam1: { Type: 'String' }, + MyParam2: { Type: 'Number' }, + MyParam3: { Type: 'List' }, + MyParam4: { Type: 'CommaDelimitedList' }, + MyParam5: { Type: 'AWS::EC2::AvailabilityZone::Name' }, + MyParam6: { Type: 'AWS::SSM::Parameter::Value' }, + } } - } - assert.deepStrictEqual(loadedTemplate, expectedTemplate) - }) + assert.deepStrictEqual(loadedTemplate, expectedTemplate) + }) - it ('only loads YAML with valid types', async () => { - // timeout is not a number - const badYamlStr: string = + it ('only loads YAML with valid types', async () => { + // timeout is not a number + const badYamlStr: string = `Resources: TestResource: - Type: AWS::Serverless::Function + Type: ${CloudFormation.SERVERLESS_FUNCTION_TYPE} Properties: Handler: handler CodeUri: codeuri @@ -127,16 +128,16 @@ describe ('CloudFormation', () => { Environment: Variables: ENVVAR: envvar` - await strToYamlFile(badYamlStr, filename) - await assertRejects(async () => await CloudFormation.load(filename)) - }) + await strToYamlFile(badYamlStr, filename) + await assertRejects(async () => await CloudFormation.load(filename)) + }) - it ('only loads valid YAML', async () => { - // same as above, minus the handler - const badYamlStr: string = + it ('only loads valid YAML', async () => { + // same as above, minus the handler + const badYamlStr: string = `Resources: TestResource: - Type: AWS::Serverless::Function + Type: ${CloudFormation.SERVERLESS_FUNCTION_TYPE} Properties: CodeUri: codeuri Runtime: runtime @@ -144,48 +145,162 @@ describe ('CloudFormation', () => { Environment: Variables: ENVVAR: envvar` - await strToYamlFile(badYamlStr, filename) - await assertRejects(async () => await CloudFormation.load(filename)) + await strToYamlFile(badYamlStr, filename) + await assertRejects(async () => await CloudFormation.load(filename)) + }) }) - it ('can successfully save a file', async() => { - await CloudFormation.save(createBaseTemplate(), filename) - assert.strictEqual(await SystemUtilities.fileExists(filename), true) + describe('save', async () => { + it ('can successfully save a file', async() => { + await CloudFormation.save(createBaseTemplate(), filename) + assert.strictEqual(await SystemUtilities.fileExists(filename), true) + }) + + it ('can successfully save a file to YAML and load the file as a CloudFormation.Template', async () => { + const baseTemplate = createBaseTemplate() + await CloudFormation.save(baseTemplate, filename) + assert.strictEqual(await SystemUtilities.fileExists(filename), true) + const loadedYaml: CloudFormation.Template = await CloudFormation.load(filename) + assert.deepStrictEqual(loadedYaml, baseTemplate) + }) }) - it ('can successfully save a file to YAML and load the file as a CloudFormation.Template', async () => { - const baseTemplate = createBaseTemplate() - await CloudFormation.save(baseTemplate, filename) - assert.strictEqual(await SystemUtilities.fileExists(filename), true) - const loadedYaml: CloudFormation.Template = await CloudFormation.load(filename) - assert.deepStrictEqual(loadedYaml, baseTemplate) + describe('validateTemplate', async () => { + it ('can successfully validate a valid template', () => { + assert.doesNotThrow(() => CloudFormation.validateTemplate(createBaseTemplate())) + }) + + it ('can detect an invalid template', () => { + const badTemplate = createBaseTemplate() + delete badTemplate.Resources!.TestResource!.Type + assert.throws( + () => CloudFormation.validateTemplate(badTemplate), + Error, + 'Template does not contain any Lambda resources' + ) + }) }) - it ('can successfully validate a valid template', () => { - assert.doesNotThrow(() => CloudFormation.validateTemplate(createBaseTemplate())) + describe('validateResource', async () => { + it ('can successfully validate a valid resource', () => { + assert.doesNotThrow(() => CloudFormation.validateResource(createBaseResource())) + }) + + it ('can detect an invalid resource', () => { + const badResource = createBaseResource() + delete badResource.Properties!.CodeUri + assert.throws( + () => CloudFormation.validateResource(badResource), + Error, + 'Missing or invalid value in Template for key: CodeUri' + ) + }) }) - it ('can successfully validate a valid resource', () => { - assert.doesNotThrow(() => CloudFormation.validateResource(createBaseResource())) + describe('getResourceFromTemplate', async () => { + const testData = [ + { + title: 'existing lambda, single runtime', + handlerName: 'app.lambda_handler', + templateFileName: 'template_python2.7.yaml', + expectedRuntime: 'python2.7' + }, + { + title: 'non-existing lambda, single runtime', + handlerName: 'app.handler_that_does_not_exist', + templateFileName: 'template_python2.7.yaml', + expectedRuntime: undefined + }, + { + title: '2nd existing lambda, multiple runtimes', + handlerName: 'app.lambda_handler2', + templateFileName: 'template_python_mixed.yaml', + expectedRuntime: 'python2.7' + }, + { + title: '1st existing lambda, multiple runtimes', + handlerName: 'app.lambda_handler3', + templateFileName: 'template_python_mixed.yaml', + expectedRuntime: 'python3.6' + }, + { + title: 'non-existing lambda, multiple runtimes', + handlerName: 'app.handler_that_does_not_exist', + templateFileName: 'template_python_mixed.yaml', + expectedRuntime: undefined + }, + ] + + for (const data of testData) { + it(`should ${data.expectedRuntime ? 'resolve runtime' : 'throw'} for ${data.title}`, async () => { + const templatePath = path.join(path.dirname(__filename), 'yaml', data.templateFileName) + const expectedRuntime = data.expectedRuntime + if (data.expectedRuntime === undefined) { + await assertRejects(async () => { + const resource = await CloudFormation.getResourceFromTemplate({ + templatePath, + handlerName: data.handlerName + }) + CloudFormation.getRuntime(resource) + }) + } else { + const resource = await CloudFormation.getResourceFromTemplate({ + templatePath, + handlerName: data.handlerName + }) + const runtime = CloudFormation.getRuntime(resource) + assert( + expectedRuntime === runtime, + JSON.stringify({ expectedRuntime, runtime }) + ) + } + }) + } }) - it ('can detect an invalid template', () => { - const badTemplate = createBaseTemplate() - delete badTemplate.Resources!.TestResource!.Type - assert.throws( - () => CloudFormation.validateTemplate(badTemplate), - Error, - 'Template does not contain any Lambda resources' - ) + describe('getRuntime', async () => { + it('throws if resource does not specify properties', async () => { + const resource = createBaseResource() + delete resource.Properties + + assert.throws(() => CloudFormation.getRuntime(resource)) + }) + + it('throws if resource does not specify a runtime', async () => { + const resource = createBaseResource() + delete resource.Properties!.Runtime + + assert.throws(() => CloudFormation.getRuntime(resource)) + }) + + it('returns runtime if specified', async () => { + const resource = createBaseResource() + const runtime = CloudFormation.getRuntime(resource) + + assert.strictEqual(runtime, 'runtime') + }) }) - it ('can detect an invalid resource', () => { - const badResource = createBaseResource() - delete badResource.Properties!.CodeUri - assert.throws( - () => CloudFormation.validateResource(badResource), - Error, - 'Missing or invalid value in Template for key: CodeUri' - ) + describe('getCodeUri', async () => { + it('throws if resource does not specify properties', async () => { + const resource = createBaseResource() + delete resource.Properties + + assert.throws(() => CloudFormation.getCodeUri(resource)) + }) + + it('throws if resource does not specify a code uri', async () => { + const resource = createBaseResource() + delete resource.Properties!.CodeUri + + assert.throws(() => CloudFormation.getCodeUri(resource)) + }) + + it('returns code uri if specified', async () => { + const resource = createBaseResource() + const codeUri = CloudFormation.getCodeUri(resource) + + assert.strictEqual(codeUri, 'codeuri') + }) }) }) diff --git a/src/test/shared/codelens/yaml/template_no_lambdas.yaml b/src/test/shared/cloudformation/yaml/template_no_lambdas.yaml similarity index 100% rename from src/test/shared/codelens/yaml/template_no_lambdas.yaml rename to src/test/shared/cloudformation/yaml/template_no_lambdas.yaml diff --git a/src/test/shared/codelens/yaml/template_python2.7.yaml b/src/test/shared/cloudformation/yaml/template_python2.7.yaml similarity index 100% rename from src/test/shared/codelens/yaml/template_python2.7.yaml rename to src/test/shared/cloudformation/yaml/template_python2.7.yaml diff --git a/src/test/shared/codelens/yaml/template_python_mixed.yaml b/src/test/shared/cloudformation/yaml/template_python_mixed.yaml similarity index 100% rename from src/test/shared/codelens/yaml/template_python_mixed.yaml rename to src/test/shared/cloudformation/yaml/template_python_mixed.yaml diff --git a/src/test/shared/codelens/csharpCodeLensProvider.test.ts b/src/test/shared/codelens/csharpCodeLensProvider.test.ts new file mode 100644 index 00000000000..6eb1d7afa0e --- /dev/null +++ b/src/test/shared/codelens/csharpCodeLensProvider.test.ts @@ -0,0 +1,283 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as assert from 'assert' +import * as os from 'os' +import * as path from 'path' +import * as vscode from 'vscode' +import * as sampleDotNetSamProgram from './sampleDotNetSamProgram' + +import { + DotNetLambdaHandlerComponents, + findParentProjectFile, + generateDotNetLambdaHandler, + getLambdaHandlerComponents, + isPublicClassSymbol, + isPublicMethodSymbol, +} from '../../../shared/codelens/csharpCodeLensProvider' +import { writeFile } from '../../../shared/filesystem' +import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' + +const fakeRange = new vscode.Range(0, 0, 0, 0) + +describe('findParentProjectFile', async () => { + const sourceCodeUri = vscode.Uri.file(path.join('code', 'someproject', 'src', 'Program.cs')) + const projectInSameFolderUri = vscode.Uri.file(path.join('code', 'someproject', 'src', 'App.csproj')) + const projectInParentFolderUri = vscode.Uri.file(path.join('code', 'someproject', 'App.csproj')) + const projectInParentParentFolderUri = vscode.Uri.file(path.join('code', 'App.csproj')) + const projectOutOfParentChainUri = vscode.Uri.file(path.join('code', 'someotherproject', 'App.csproj')) + + const testScenarios = [ + { + scenario: 'locates project in same folder', + findFilesResult: [projectInSameFolderUri], + expectedResult: projectInSameFolderUri, + }, + { + scenario: 'locates project in parent folder', + findFilesResult: [projectInParentFolderUri], + expectedResult: projectInParentFolderUri, + }, + { + scenario: 'locates project two parent folders up', + findFilesResult: [projectInParentParentFolderUri], + expectedResult: projectInParentParentFolderUri, + }, + { + scenario: 'selects project in same folder over parent folder', + findFilesResult: [projectInSameFolderUri, projectInParentFolderUri], + expectedResult: projectInSameFolderUri, + }, + { + scenario: 'returns undefined when no project files are located', + findFilesResult: [], + expectedResult: undefined, + }, + { + scenario: 'returns undefined when no project files are located in parent chain', + findFilesResult: [projectOutOfParentChainUri], + expectedResult: undefined, + }, + ] + + testScenarios.forEach((test) => { + it(test.scenario, async () => { + const projectFile = await findParentProjectFile( + sourceCodeUri, + async (): Promise => test.findFilesResult, + ) + assert.strictEqual(projectFile, test.expectedResult, 'Project file was not the expected one') + }) + }) +}) + +describe('getLambdaHandlerComponents', async () => { + it('Detects a public function symbol', async () => { + const folder = await makeTemporaryToolkitFolder() + const programFile = path.join(folder, 'program.cs') + await writeFile(programFile, sampleDotNetSamProgram.getFunctionText()) + + const textDoc = await vscode.workspace.openTextDocument(programFile) + const documentSymbols = sampleDotNetSamProgram.getDocumentSymbols() + const assembly = 'myAssembly' + + const componentsArray = getLambdaHandlerComponents( + textDoc, + documentSymbols, + assembly, + ) + + assert.ok(componentsArray) + assert.strictEqual(componentsArray.length, 1, 'Expected only one set of Lambda Handler components') + const components = componentsArray[0] + assert.strictEqual(components.assembly, 'myAssembly', 'Unexpected Lambda Handler assembly') + assert.strictEqual(components.namespace, 'HelloWorld', 'Unexpected Lambda Handler Namespace') + assert.strictEqual(components.class, 'Function', 'Unexpected Lambda Handler Class') + assert.strictEqual(components.method, 'FunctionHandler', 'Unexpected Lambda Handler Function') + }) +}) + +describe('isPublicClassSymbol', async () => { + const sampleClassSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol( + 'HelloWorld.Function', + '', + vscode.SymbolKind.Class, fakeRange, fakeRange + ) + + it('returns true for a public class', async () => { + const doc = { + getText: (range?: vscode.Range): string => { + return 'public class Function {}' + } + } + + const isPublic = isPublicClassSymbol(doc, sampleClassSymbol) + assert.strictEqual(isPublic, true, 'Expected symbol to be a public class') + }) + + it('returns false when symbol is not of type Class', async () => { + const symbol = new vscode.DocumentSymbol( + sampleClassSymbol.name, sampleClassSymbol.detail, vscode.SymbolKind.Method, + sampleClassSymbol.range, sampleClassSymbol.selectionRange + ) + + const doc = { + getText: (range?: vscode.Range): string => { + return 'public class Function {}' + } + } + + const isPublic = isPublicClassSymbol(doc, symbol) + assert.strictEqual(isPublic, false, 'Expected symbol not to be a public class') + }) + + it('returns false when class is not public', async () => { + const doc = { + getText: (range?: vscode.Range): string => 'private class ' + } + + const isPublic = isPublicClassSymbol(doc, sampleClassSymbol) + assert.strictEqual(isPublic, false, 'Expected symbol not to be a public class') + }) +}) + +describe('isPublicMethodSymbol', async () => { + const sampleMethodSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol( + 'FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)', + '', + vscode.SymbolKind.Method, fakeRange, fakeRange + ) + + const validPublicMethodTests = [ + { + scenario: 'signature all on one line', + functionSignature: generateFunctionSignature('public', 'FunctionHandler') + }, + { + scenario: 'signature across many lines', + functionSignature: generateFunctionSignature('public', 'FunctionHandler', true, true, true) + }, + { + scenario: 'method name on another line', + functionSignature: generateFunctionSignature('public', 'FunctionHandler', true) + }, + { + scenario: 'args on many lines', + functionSignature: generateFunctionSignature('public', 'FunctionHandler', false, true) + }, + ] + + validPublicMethodTests.forEach((test) => { + it(`returns true for a public method symbol when ${test.scenario}`, async () => { + const doc = { + getText: (range?: vscode.Range): string => + generateFunctionDeclaration(test.functionSignature) + } + + const isPublic = isPublicMethodSymbol(doc, sampleMethodSymbol) + assert.strictEqual(isPublic, true, 'Expected symbol to be a public method') + }) + }) + + it('returns false for a symbol that is not a method', async () => { + const symbol = new vscode.DocumentSymbol( + sampleMethodSymbol.name, sampleMethodSymbol.detail, vscode.SymbolKind.Class, + sampleMethodSymbol.range, sampleMethodSymbol.selectionRange + ) + + const doc = { + getText: (range?: vscode.Range): string => { + throw new Error('getText is unused') + } + } + + const isPublic = isPublicMethodSymbol(doc, symbol) + assert.strictEqual(isPublic, false, 'Expected symbol not to be a public method') + }) + + it('returns false when the method is not public', async () => { + const doc = { + getText: (range?: vscode.Range): string => + generateFunctionDeclaration(generateFunctionSignature('private', 'FunctionHandler')) + } + + const isPublic = isPublicMethodSymbol(doc, sampleMethodSymbol) + assert.strictEqual(isPublic, false, 'Expected symbol not to be a public method') + }) + + it('returns false when a private method name contains the word public in it', async () => { + const symbol = new vscode.DocumentSymbol( + 'notpublicmethod', sampleMethodSymbol.detail, vscode.SymbolKind.Method, + sampleMethodSymbol.range, sampleMethodSymbol.selectionRange + ) + + const doc = { + getText: (range?: vscode.Range): string => + generateFunctionDeclaration(generateFunctionSignature('private', symbol.name)) + } + + const isPublic = isPublicMethodSymbol(doc, symbol) + assert.strictEqual(isPublic, false, 'Expected symbol not to be a public method') + }) + + /** + * Simulates the contents of a TextDocument that corresponds to a Method Symbol's range + */ + function generateFunctionDeclaration(functionSignature: string): string { + return `${functionSignature} + { + string location = GetCallingIP().Result; + Dictionary body = new Dictionary + { + { "message", "hello world" }, + { "location", location }, + }; + + return new APIGatewayProxyResponse + { + Body = JsonConvert.SerializeObject(body), + StatusCode = 200, + Headers = new Dictionary { { "Content-Type", "application/json" } } + }; + } +` + } + + /** + * Simulates the Function signature portion of the contents of a TextDocument that corresponds + * to a Method Symbol's range. Used with generateFunctionDeclaration. + */ + function generateFunctionSignature( + access: 'public' | 'private', + functionName: string, + beforeFunctionName: boolean = false, + beforeArgument: boolean = false, + afterSignature: boolean = false, + ): string { + const beforeFunctionText = beforeFunctionName ? os.EOL : '' + const beforeArgumentText = beforeArgument ? os.EOL : '' + const afterSignatureText = afterSignature ? os.EOL : '' + + // tslint:disable-next-line:max-line-length + return `${access} APIGatewayProxyResponse ${beforeFunctionText}${functionName}(${beforeArgumentText}APIGatewayProxyRequest apigProxyEvent, ${beforeArgumentText}ILambdaContext context)${afterSignatureText}` + } +}) + +describe('generateDotNetLambdaHandler', async () => { + it('produces a handler name', async () => { + const components: DotNetLambdaHandlerComponents = { + assembly: 'myAssembly', + namespace: 'myNamespace', + class: 'myClass', + method: 'foo', + handlerRange: undefined!, + } + + const handlerName = generateDotNetLambdaHandler(components) + assert.strictEqual(handlerName, 'myAssembly::myNamespace.myClass::foo', 'Handler name mismatch') + }) +}) diff --git a/src/test/shared/codelens/localLambdaRunner.test.ts b/src/test/shared/codelens/localLambdaRunner.test.ts index e6d463daefe..78edd2fa083 100644 --- a/src/test/shared/codelens/localLambdaRunner.test.ts +++ b/src/test/shared/codelens/localLambdaRunner.test.ts @@ -6,12 +6,18 @@ 'use strict' import * as assert from 'assert' +import * as del from 'del' import * as path from 'path' import * as vscode from 'vscode' import { DebugConfiguration } from '../../../lambda/local/debugConfiguration' import * as localLambdaRunner from '../../../shared/codelens/localLambdaRunner' +import * as fs from '../../../shared/filesystem' +import * as fsUtils from '../../../shared/filesystemUtilities' import { BasicLogger, ErrorOrString } from '../../../shared/logger' +import { ChildProcessResult } from '../../../shared/utilities/childProcess' +import { ExtensionDisposableFiles } from '../../../shared/utilities/disposableFiles' import { ChannelLogger } from '../../../shared/utilities/vsCodeUtils' +import { FakeExtensionContext } from '../../fakeExtensionContext' import { assertRejects } from '../utilities/assertUtils' class FakeChannelLogger implements Pick { @@ -53,6 +59,17 @@ class FakeBasicLogger implements BasicLogger { } describe('localLambdaRunner', async () => { + + let tempDir: string + before(async () => { + tempDir = await fsUtils.makeTemporaryToolkitFolder() + await ExtensionDisposableFiles.initialize(new FakeExtensionContext()) + }) + + after(async () => { + await del(tempDir, { force: true }) + }) + describe('attachDebugger', async () => { let actualRetries: number = 0 let channelLogger: FakeChannelLogger @@ -319,61 +336,54 @@ describe('localLambdaRunner', async () => { }) }) - describe('getRuntimeForLambda', () => { - const testData = [ - { - title: 'existing lambda, single runtime', - handlerName: 'app.lambda_handler', - templateFileName: 'template_python2.7.yaml', - expectedRuntime: 'python2.7' - }, - { - title: 'non-existing lambda, single runtime', - handlerName: 'app.handler_that_does_not_exist', - templateFileName: 'template_python2.7.yaml', - expectedRuntime: 'python2.7' - }, - { - title: '2nd existing lambda, multiple runtimes', - handlerName: 'app.lambda_handler2', - templateFileName: 'template_python_mixed.yaml', - expectedRuntime: 'python2.7' - }, - { - title: '1st existing lambda, multiple runtimes', - handlerName: 'app.lambda_handler3', - templateFileName: 'template_python_mixed.yaml', - expectedRuntime: 'python3.6' - }, - { - title: 'non-existing lambda, multiple runtimes', - handlerName: 'app.handler_that_does_not_exist', - templateFileName: 'template_python_mixed.yaml', - expectedRuntime: undefined - }, - ] - for (const data of testData) { - it(`should ${data.expectedRuntime ? 'resolve runtime' : 'throw'} for ${data.title}`, async () => { - const templatePath = path.join(path.dirname(__filename), 'yaml', data.templateFileName) - const expectedRuntime = data.expectedRuntime - if (data.expectedRuntime === undefined) { - await assertRejects(async () => { - await localLambdaRunner.getRuntimeForLambda({ - templatePath, - handlerName: data.handlerName - }) - }) - } else { - const runtime = await localLambdaRunner.getRuntimeForLambda({ - templatePath, - handlerName: data.handlerName - }) - assert( - expectedRuntime === runtime, - JSON.stringify({ expectedRuntime, runtime }) - ) + describe('makeBuildDir', () => { + it ('creates a temp directory', async () => { + const dir = await localLambdaRunner.makeBuildDir() + assert.ok(dir) + assert.strictEqual(await fsUtils.fileExists(dir), true) + const fsDir = await fs.readdir(dir) + assert.strictEqual(fsDir.length, 0) + await del(dir, { force: true }) + }) + }) + + describe('executeSamBuild', () => { + const failedChildProcess: ChildProcessResult = { + exitCode: 1, + error: new Error('you are already dead'), + stdout: 'friendly failure message', + stderr: 'big ugly failure message' + } + + const successfulChildProcess: ChildProcessResult = { + exitCode: 0, + error: undefined, + stdout: 'everything sunny all the time always', + stderr: 'nothing to report' + } + + const generateSamBuildParams = (isSuccessfulBuild: boolean) => { + return { + baseBuildDir: tempDir, + codeDir: tempDir, + inputTemplatePath: tempDir, + channelLogger: new FakeChannelLogger(), + // not needed for testing + manifestPath: undefined, + samProcessInvoker: { + invoke: async (): Promise => + isSuccessfulBuild ? successfulChildProcess : failedChildProcess } - }) + } } + + it ('fails when the child process returns a nonzero exit code', async () => { + await assertRejects(async () => localLambdaRunner.executeSamBuild(generateSamBuildParams(false))) + }) + + it ('succeeds when the child process returns with an exit code of 0', async () => { + const samBuildResult = await localLambdaRunner.executeSamBuild(generateSamBuildParams(true)) + assert.strictEqual(samBuildResult, path.join(tempDir, 'output', 'template.yaml')) + }) }) }) diff --git a/src/test/shared/codelens/sampleDotNetSamProgram.ts b/src/test/shared/codelens/sampleDotNetSamProgram.ts new file mode 100644 index 00000000000..74089beb9a6 --- /dev/null +++ b/src/test/shared/codelens/sampleDotNetSamProgram.ts @@ -0,0 +1,84 @@ +/*! + * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +import * as vscode from 'vscode' + +// tslint:disable:max-line-length + +/** + * This file emits the Program.cs file contents from the stock SAM CLI app for dotnet, as well as + * the corresponding DocumentSymbols generated by VS Code. + */ + +export function getDocumentSymbols(): vscode.DocumentSymbol[] { + const namespaceSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol('HelloWorld', '', vscode.SymbolKind.Namespace, new vscode.Range(14, 0, 51, 1), new vscode.Range(14, 10, 14, 20)) + const classSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol('HelloWorld.Function', '', vscode.SymbolKind.Class, new vscode.Range(17, 4, 50, 5), new vscode.Range(17, 17, 17, 25)) + const privateMethodSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol('GetCallingIP()', '', vscode.SymbolKind.Method, new vscode.Range(22, 8, 31, 9), new vscode.Range(22, 42, 22, 54)) + const publicMethodSymbol: vscode.DocumentSymbol = new vscode.DocumentSymbol('FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)', '', vscode.SymbolKind.Method, new vscode.Range(33, 8, 49, 9), new vscode.Range(33, 39, 33, 54)) + + namespaceSymbol.children.push(classSymbol) + classSymbol.children.push(privateMethodSymbol) + classSymbol.children.push(publicMethodSymbol) + + return [namespaceSymbol] +} + +export function getFunctionText(): string { + return String.raw`using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Net.Http; +using System.Net.Http.Headers; +using Newtonsoft.Json; + +using Amazon.Lambda.Core; +using Amazon.Lambda.APIGatewayEvents; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace HelloWorld +{ + + public class Function + { + + private static readonly HttpClient client = new HttpClient(); + + private static async Task GetCallingIP() + { + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("User-Agent", "AWS Lambda .Net Client"); + + var stringTask = client.GetStringAsync("http://checkip.amazonaws.com/").ConfigureAwait(continueOnCapturedContext:false); + + var msg = await stringTask; + return msg.Replace("\n",""); + } + + public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + + string location = GetCallingIP().Result; + Dictionary body = new Dictionary + { + { "message", "hello world" }, + { "location", location }, + }; + + return new APIGatewayProxyResponse + { + Body = JsonConvert.SerializeObject(body), + StatusCode = 200, + Headers = new Dictionary { { "Content-Type", "application/json" } } + }; + } + } +} +` +} diff --git a/src/test/shared/credentials/userCredentialsUtils.test.ts b/src/test/shared/credentials/userCredentialsUtils.test.ts index a3e239d3954..c74e453f0f4 100644 --- a/src/test/shared/credentials/userCredentialsUtils.test.ts +++ b/src/test/shared/credentials/userCredentialsUtils.test.ts @@ -12,14 +12,8 @@ import * as fs from 'fs' import * as path from 'path' import { promisify } from 'util' -import { - loadSharedConfigFiles, - SharedConfigFiles -} from '../../../shared/credentials/credentialsFile' -import { - CredentialsValidationResult, - UserCredentialsUtils, -} from '../../../shared/credentials/userCredentialsUtils' +import { loadSharedConfigFiles, SharedConfigFiles } from '../../../shared/credentials/credentialsFile' +import { CredentialsValidationResult, UserCredentialsUtils } from '../../../shared/credentials/userCredentialsUtils' import { EnvironmentVariables } from '../../../shared/environmentVariables' import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' import { TestLogger } from '../../../shared/loggerUtils' @@ -28,13 +22,12 @@ import { MockStsClient } from '../clients/mockClients' import { assertThrowsError } from '../utilities/assertUtils' describe('UserCredentialsUtils', () => { - let tempFolder: string let logger: TestLogger const fakeCredentials = new AWS.Credentials('fakeaccess', 'fakesecret') - before( async () => { + before(async () => { // Make a temp folder for all these tests // Stick some temp credentials files in there to load from logger = await TestLogger.createTestLogger() @@ -164,14 +157,14 @@ describe('UserCredentialsUtils', () => { assert(typeof profiles === 'object', 'profiles should be an object') assert(profiles[profileName], 'profiles should be truthy') assert.strictEqual( - profiles[profileName].aws_access_key_id, - creds.accessKey, - `creds.accessKey: "${profiles[profileName].aws_access_key_id}" !== "${creds.accessKey}"` + profiles[profileName].aws_access_key_id, + creds.accessKey, + `creds.accessKey: "${profiles[profileName].aws_access_key_id}" !== "${creds.accessKey}"` ) assert.strictEqual( - profiles[profileName].aws_secret_access_key, - creds.secretKey, - `creds.secretKey: "${profiles[profileName].aws_access_key_id}" !== "${creds.secretKey}"` + profiles[profileName].aws_secret_access_key, + creds.secretKey, + `creds.secretKey: "${profiles[profileName].aws_access_key_id}" !== "${creds.secretKey}"` ) const access = promisify(fs.access) await access(credentialsFilename, fs.constants.R_OK).catch(err => assert(false, 'Should be readable')) @@ -181,7 +174,6 @@ describe('UserCredentialsUtils', () => { describe('validateCredentials', () => { it('returns a valid result if getCallerIdentity resolves', async () => { - let timesCalled: number = 0 const mockResponse: AWS.STS.GetCallerIdentityResponse = { @@ -196,7 +188,8 @@ describe('UserCredentialsUtils', () => { return mockResponse } - })) + }) + ) assert.strictEqual(timesCalled, 1) assert.strictEqual(result.isValid, true) @@ -204,7 +197,6 @@ describe('UserCredentialsUtils', () => { }) it('returns an invalid result if getCallerIdentity returns undefined', async () => { - let timesCalled: number = 0 const mockResponse: AWS.STS.GetCallerIdentityResponse = { @@ -227,7 +219,6 @@ describe('UserCredentialsUtils', () => { }) it('returns an invalid result if getCallerIdentity throws', async () => { - let timesCalled: number = 0 const result: CredentialsValidationResult = await UserCredentialsUtils.validateCredentials( @@ -246,19 +237,13 @@ describe('UserCredentialsUtils', () => { assert.strictEqual(result.invalidMessage, 'Simulating error with explicit throw') }) - it ( - 'throws an error if STS is not defined and ext.toolkitClientBuilder cannot create an STS client', - async () => { - await assertThrowsError(async () => await UserCredentialsUtils.validateCredentials( - fakeCredentials - )) - } - ) + it('throws an error if STS is not defined and toolkitClientBuilder cannot create an STS client', async () => { + await assertThrowsError(async () => await UserCredentialsUtils.validateCredentials(fakeCredentials)) + }) }) describe('addUserDataToContext', async () => { - it ('adds profile data to the context if the profile is valid', async () => { - + it('adds profile data to the context if the profile is valid', async () => { const testProfile = 'testprofile' const testAccount = 'testaccount' const mockSts = new MockStsClient({ @@ -270,11 +255,9 @@ describe('UserCredentialsUtils', () => { } } }) - const mockAws = new FakeAwsContext( - { - credentials: new AWS.Credentials('access', 'secret') - } - ) + const mockAws = new FakeAwsContext({ + credentials: new AWS.Credentials('access', 'secret') + }) assert.strictEqual(mockAws.getCredentialProfileName(), DEFAULT_TEST_PROFILE_NAME) assert.strictEqual(mockAws.getCredentialAccountId(), DEFAULT_TEST_ACCOUNT_ID) @@ -284,19 +267,16 @@ describe('UserCredentialsUtils', () => { assert.strictEqual(mockAws.getCredentialAccountId(), testAccount) }) - it ('returns false if credentials are invalid', async () => { - + it('returns false if credentials are invalid', async () => { const testProfile = 'testprofile' const mockSts = new MockStsClient({ getCallerIdentity: async () => { throw new AWS.AWSError() } }) - const mockAws = new FakeAwsContext( - { - credentials: new AWS.Credentials('access', 'secret') - } - ) + const mockAws = new FakeAwsContext({ + credentials: new AWS.Credentials('access', 'secret') + }) const returnValue = await UserCredentialsUtils.addUserDataToContext(testProfile, mockAws, mockSts) assert.strictEqual(returnValue, false) @@ -326,5 +306,4 @@ aws_secret_access_key = FAKESECRET fs.writeFileSync(filename, fileContents) } - }) diff --git a/src/test/shared/defaultAwsClientBuilder.test.ts b/src/test/shared/defaultAwsClientBuilder.test.ts index 74ddfa2d5a7..34dacd5f46a 100644 --- a/src/test/shared/defaultAwsClientBuilder.test.ts +++ b/src/test/shared/defaultAwsClientBuilder.test.ts @@ -12,7 +12,6 @@ import { DefaultAWSClientBuilder } from '../../shared/awsClientBuilder' import { FakeAwsContext } from '../utilities/fakeAwsContext' describe('DefaultAwsClientBuilder', () => { - describe('createAndConfigureSdkClient', () => { class FakeService extends Service { public constructor(config?: ServiceConfigurationOptions) { diff --git a/src/test/shared/defaultAwsContext.test.ts b/src/test/shared/defaultAwsContext.test.ts index ede8cafc408..186e3c71037 100644 --- a/src/test/shared/defaultAwsContext.test.ts +++ b/src/test/shared/defaultAwsContext.test.ts @@ -15,7 +15,6 @@ import { TestSettingsConfiguration } from '../utilities/testSettingsConfiguratio import { assertThrowsError } from './utilities/assertUtils' describe('DefaultAwsContext', () => { - const testRegion1Value: string = 're-gion-1' const testRegion2Value: string = 're-gion-2' const testProfileValue: string = 'profile1' @@ -29,17 +28,14 @@ describe('DefaultAwsContext', () => { } public async writeSetting( - settingKey: string, value: T | undefined, + settingKey: string, + value: T | undefined, target: ConfigurationTarget - ): Promise { - } -} + ): Promise {} + } class TestCredentialsManager extends CredentialsManager { - public constructor( - private readonly expectedName?: string, - private readonly reportedCredentials?: Credentials - ) { + public constructor(private readonly expectedName?: string, private readonly reportedCredentials?: Credentials) { super() } @@ -90,16 +86,15 @@ describe('DefaultAwsContext', () => { new FakeExtensionContext(), new TestCredentialsManager(overrideProfile) ) - await assertThrowsError( async () => { await testContext.getCredentials(testProfileValue) } ) + await assertThrowsError(async () => { + await testContext.getCredentials(testProfileValue) + }) }) it('returns undefined if no profile is provided and no profile was previously saved to settings', async () => { const settingsConfig = new TestSettingsConfiguration() - const testContext = new DefaultAwsContext( - settingsConfig, - new FakeExtensionContext() - ) + const testContext = new DefaultAwsContext(settingsConfig, new FakeExtensionContext()) const creds = await testContext.getCredentials() assert.strictEqual(creds, undefined) }) @@ -113,7 +108,6 @@ describe('DefaultAwsContext', () => { }) it('gets single region from config on startup', async () => { - const fakeMementoStorage: FakeMementoStorage = {} fakeMementoStorage[regionSettingKey] = [testRegion1Value] @@ -128,7 +122,6 @@ describe('DefaultAwsContext', () => { }) it('gets multiple regions from config on startup', async () => { - const fakeMementoStorage: FakeMementoStorage = {} fakeMementoStorage[regionSettingKey] = [testRegion1Value, testRegion2Value] @@ -144,7 +137,6 @@ describe('DefaultAwsContext', () => { }) it('updates config on single region change', async () => { - class TestConfiguration extends ContextTestsSettingsConfigurationBase { public async writeSetting(settingKey: string, value: T, target: ConfigurationTarget): Promise { const array: string[] = value as any @@ -160,12 +152,11 @@ describe('DefaultAwsContext', () => { }) it('updates config on multiple region change', async () => { - class TestConfiguration extends ContextTestsSettingsConfigurationBase { public async writeSetting(settingKey: string, value: T, target: ConfigurationTarget): Promise { assert.strictEqual(settingKey, regionSettingKey) assert(value instanceof Array) - const values = value as any as string[] + const values = (value as any) as string[] assert.strictEqual(values[0], testRegion1Value) assert.strictEqual(values[1], testRegion2Value) assert.strictEqual(target, ConfigurationTarget.Global) @@ -177,18 +168,17 @@ describe('DefaultAwsContext', () => { }) it('updates on region removal', async () => { - class TestConfiguration extends ContextTestsSettingsConfigurationBase { public readSetting(settingKey: string, defaultValue?: T): T | undefined { if (settingKey === regionSettingKey) { - return [ testRegion1Value, testRegion2Value ] as any as T + return ([testRegion1Value, testRegion2Value] as any) as T } return super.readSetting(settingKey, defaultValue) } public async writeSetting(settingKey: string, value: T, target: ConfigurationTarget): Promise { assert.strictEqual(settingKey, regionSettingKey) - assert.deepStrictEqual(value, [ testRegion1Value ]) + assert.deepStrictEqual(value, [testRegion1Value]) assert.strictEqual(target, ConfigurationTarget.Global) } } @@ -198,7 +188,6 @@ describe('DefaultAwsContext', () => { }) it('updates config on profile change', async () => { - class TestConfiguration extends ContextTestsSettingsConfigurationBase { public async writeSetting(settingKey: string, value: T, target: ConfigurationTarget): Promise { assert.strictEqual(settingKey, profileSettingKey) @@ -221,14 +210,13 @@ describe('DefaultAwsContext', () => { }) it('fires event on single region change', async () => { - const testContext = new DefaultAwsContext( new ContextTestsSettingsConfigurationBase(), new FakeExtensionContext() ) let invocationCount = 0 - testContext.onDidChangeContext((c) => { + testContext.onDidChangeContext(c => { assert.strictEqual(c.regions.length, 1) assert.strictEqual(c.regions[0], testRegion1Value) invocationCount++ @@ -240,14 +228,13 @@ describe('DefaultAwsContext', () => { }) it('fires event on multi region change', async () => { - const testContext = new DefaultAwsContext( new ContextTestsSettingsConfigurationBase(), new FakeExtensionContext() ) let invocationCount = 0 - testContext.onDidChangeContext((c) => { + testContext.onDidChangeContext(c => { assert.strictEqual(c.regions.length, 2) assert.strictEqual(c.regions[0], testRegion1Value) assert.strictEqual(c.regions[1], testRegion2Value) @@ -260,14 +247,13 @@ describe('DefaultAwsContext', () => { }) it('fires event on profile change', async () => { - const testContext = new DefaultAwsContext( new ContextTestsSettingsConfigurationBase(), new FakeExtensionContext() ) let invocationCount = 0 - testContext.onDidChangeContext((c) => { + testContext.onDidChangeContext(c => { assert.strictEqual(c.profileName, testProfileValue) invocationCount++ }) @@ -278,14 +264,13 @@ describe('DefaultAwsContext', () => { }) it('fires event on accountId change', async () => { - const testContext = new DefaultAwsContext( new ContextTestsSettingsConfigurationBase(), new FakeExtensionContext() ) let invocationCount = 0 - testContext.onDidChangeContext((c) => { + testContext.onDidChangeContext(c => { assert.strictEqual(c.accountId, testAccountIdValue) invocationCount++ }) diff --git a/src/test/shared/sam/cli/samCliDeploy.test.ts b/src/test/shared/sam/cli/samCliDeploy.test.ts index 6c2d21ca379..fd0f0569bbb 100644 --- a/src/test/shared/sam/cli/samCliDeploy.test.ts +++ b/src/test/shared/sam/cli/samCliDeploy.test.ts @@ -42,12 +42,10 @@ describe('runSamCliDeploy', async () => { }) it('does not include --parameter-overrides if there are no overrides', async () => { - const invoker = new MockSamCliProcessInvoker( - args => { - invokeCount++ - assertArgNotPresent(args, '--parameter-overrides') - } - ) + const invoker = new MockSamCliProcessInvoker(args => { + invokeCount++ + assertArgNotPresent(args, '--parameter-overrides') + }) await runSamCliDeploy( { @@ -55,7 +53,7 @@ describe('runSamCliDeploy', async () => { parameterOverrides: new Map(), region: fakeRegion, stackName: fakeStackName, - templateFile: fakeTemplateFile, + templateFile: fakeTemplateFile }, invoker ) @@ -64,28 +62,23 @@ describe('runSamCliDeploy', async () => { }) it('includes overrides as a string of key=value pairs', async () => { - const invoker = new MockSamCliProcessInvoker( - args => { - invokeCount++ - assertArgIsPresent(args, '--parameter-overrides') - const overridesIndex = args.findIndex(arg => arg === '--parameter-overrides') - assert.strictEqual(overridesIndex > -1, true) - assert.strictEqual(args.length >= overridesIndex + 3, true) - assert.strictEqual(args[overridesIndex + 1], 'key1=value1') - assert.strictEqual(args[overridesIndex + 2], 'key2=value2') - } - ) + const invoker = new MockSamCliProcessInvoker(args => { + invokeCount++ + assertArgIsPresent(args, '--parameter-overrides') + const overridesIndex = args.findIndex(arg => arg === '--parameter-overrides') + assert.strictEqual(overridesIndex > -1, true) + assert.strictEqual(args.length >= overridesIndex + 3, true) + assert.strictEqual(args[overridesIndex + 1], 'key1=value1') + assert.strictEqual(args[overridesIndex + 2], 'key2=value2') + }) await runSamCliDeploy( { profile: fakeProfile, - parameterOverrides: new Map([ - ['key1', 'value1'], - ['key2', 'value2'], - ]), + parameterOverrides: new Map([['key1', 'value1'], ['key2', 'value2']]), region: fakeRegion, stackName: fakeStackName, - templateFile: fakeTemplateFile, + templateFile: fakeTemplateFile }, invoker ) @@ -94,15 +87,13 @@ describe('runSamCliDeploy', async () => { }) it('includes a template, stack name, region, and profile ', async () => { - const invoker = new MockSamCliProcessInvoker( - args => { - invokeCount++ - assertArgsContainArgument(args, '--template-file', fakeTemplateFile) - assertArgsContainArgument(args, '--stack-name', fakeStackName) - assertArgsContainArgument(args, '--region', fakeRegion) - assertArgsContainArgument(args, '--profile', fakeProfile) - } - ) + const invoker = new MockSamCliProcessInvoker(args => { + invokeCount++ + assertArgsContainArgument(args, '--template-file', fakeTemplateFile) + assertArgsContainArgument(args, '--stack-name', fakeStackName) + assertArgsContainArgument(args, '--region', fakeRegion) + assertArgsContainArgument(args, '--profile', fakeProfile) + }) await runSamCliDeploy( { @@ -110,7 +101,7 @@ describe('runSamCliDeploy', async () => { parameterOverrides: new Map(), region: fakeRegion, stackName: fakeStackName, - templateFile: fakeTemplateFile, + templateFile: fakeTemplateFile }, invoker ) @@ -129,7 +120,7 @@ describe('runSamCliDeploy', async () => { parameterOverrides: new Map(), region: fakeRegion, stackName: fakeStackName, - templateFile: fakeTemplateFile, + templateFile: fakeTemplateFile }, badExitCodeProcessInvoker ) diff --git a/src/test/shared/sam/cli/samCliLocalInvoke.test.ts b/src/test/shared/sam/cli/samCliLocalInvoke.test.ts index c09a4e8b8ee..04ff45c6e93 100644 --- a/src/test/shared/sam/cli/samCliLocalInvoke.test.ts +++ b/src/test/shared/sam/cli/samCliLocalInvoke.test.ts @@ -178,7 +178,6 @@ describe('SamCliLocalInvokeInvocation', async () => { it('Passes docker network to sam cli', async () => { const expectedDockerNetwork = 'hello-world' - const taskInvoker: SamLocalInvokeCommand = new TestSamLocalInvokeCommand( (invokeArgs: SamLocalInvokeCommandArgs) => { assertArgsContainArgument(invokeArgs.args, '--docker-network', expectedDockerNetwork) @@ -262,4 +261,39 @@ describe('SamCliLocalInvokeInvocation', async () => { skipPullImage: undefined, }).execute() }) + + it('Passes debuggerPath to sam cli', async () => { + const expectedDebuggerPath = path.join('foo', 'bar') + + const taskInvoker: SamLocalInvokeCommand = new TestSamLocalInvokeCommand( + (invokeArgs: SamLocalInvokeCommandArgs) => { + assertArgsContainArgument(invokeArgs.args, '--debugger-path', expectedDebuggerPath) + } + ) + + await new SamCliLocalInvokeInvocation({ + templateResourceName: nonRelevantArg, + templatePath: placeholderTemplateFile, + eventPath: placeholderEventFile, + environmentVariablePath: nonRelevantArg, + invoker: taskInvoker, + debuggerPath: expectedDebuggerPath + }).execute() + }) + + it('Does not pass debuggerPath to sam cli when undefined', async () => { + const taskInvoker: SamLocalInvokeCommand = new TestSamLocalInvokeCommand( + (invokeArgs: SamLocalInvokeCommandArgs) => { + assertArgNotPresent(invokeArgs.args, '--debugger-path') + } + ) + + await new SamCliLocalInvokeInvocation({ + templateResourceName: nonRelevantArg, + templatePath: placeholderTemplateFile, + eventPath: placeholderEventFile, + environmentVariablePath: nonRelevantArg, + invoker: taskInvoker, + }).execute() + }) }) diff --git a/src/test/shared/sam/cli/samCliTestUtils.ts b/src/test/shared/sam/cli/samCliTestUtils.ts index e060937be8d..1add58e6c51 100644 --- a/src/test/shared/sam/cli/samCliTestUtils.ts +++ b/src/test/shared/sam/cli/samCliTestUtils.ts @@ -11,10 +11,7 @@ import { SamCliProcessInvoker } from '../../../../shared/sam/cli/samCliInvokerUt import { ChildProcessResult } from '../../../../shared/utilities/childProcess' export class MockSamCliProcessInvoker implements SamCliProcessInvoker { - public constructor( - private readonly validateArgs: (args: string[]) => void - ) { - } + public constructor(private readonly validateArgs: (args: string[]) => void) {} public invoke(options: SpawnOptions, ...args: string[]): Promise public invoke(...args: string[]): Promise @@ -22,41 +19,23 @@ export class MockSamCliProcessInvoker implements SamCliProcessInvoker { const args: string[] = typeof first === 'string' ? [first, ...rest] : rest this.validateArgs(args) - return { + return ({ exitCode: 0 - } as any as ChildProcessResult + } as any) as ChildProcessResult } } -export function assertArgsContainArgument( - args: any[], - argOfInterest: string, - expectedArgValue: string -) { +export function assertArgsContainArgument(args: any[], argOfInterest: string, expectedArgValue: string) { const argPos = args.indexOf(argOfInterest) assert.notStrictEqual(argPos, -1, `Expected arg '${argOfInterest}' was not found`) assert.ok(args.length >= argPos + 2, `Args does not contain a value for '${argOfInterest}'`) assert.strictEqual(args[argPos + 1], expectedArgValue, `Arg '${argOfInterest}' did not have expected value`) } -export function assertArgIsPresent( - args: any[], - argOfInterest: string, -) { - assert.notStrictEqual( - args.indexOf(argOfInterest), - -1, - `Expected '${argOfInterest}' arg` - ) +export function assertArgIsPresent(args: any[], argOfInterest: string) { + assert.notStrictEqual(args.indexOf(argOfInterest), -1, `Expected '${argOfInterest}' arg`) } -export function assertArgNotPresent( - args: any[], - argOfInterest: string, -) { - assert.strictEqual( - args.indexOf(argOfInterest), - -1, - `Did not expect '${argOfInterest}' arg` - ) +export function assertArgNotPresent(args: any[], argOfInterest: string) { + assert.strictEqual(args.indexOf(argOfInterest), -1, `Did not expect '${argOfInterest}' arg`) } diff --git a/src/test/shared/telemetry/telemetryUtils.test.ts b/src/test/shared/telemetry/telemetryUtils.test.ts index 9689dd3f156..5469e7c5b52 100644 --- a/src/test/shared/telemetry/telemetryUtils.test.ts +++ b/src/test/shared/telemetry/telemetryUtils.test.ts @@ -59,6 +59,7 @@ describe('telemetryUtils', () => { ) assert.strictEqual(mockService.lastEvent!.namespace, 'Command') assert.strictEqual(mockService.lastEvent!.data![0].name, 'command') + done() }).catch(err => { throw err diff --git a/src/test/shared/utilities/childProcess.test.ts b/src/test/shared/utilities/childProcess.test.ts index bfa7c89b74c..1843a1032b9 100644 --- a/src/test/shared/utilities/childProcess.test.ts +++ b/src/test/shared/utilities/childProcess.test.ts @@ -14,7 +14,6 @@ import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' import { ChildProcess, ChildProcessResult } from '../../../shared/utilities/childProcess' describe('ChildProcess', async () => { - let tempFolder: string beforeEach(async () => { @@ -33,9 +32,7 @@ describe('ChildProcess', async () => { const batchFile = path.join(tempFolder, 'test-script.bat') writeBatchFile(batchFile) - const childProcess = new ChildProcess( - batchFile - ) + const childProcess = new ChildProcess(batchFile) const result = await childProcess.run() @@ -54,9 +51,7 @@ describe('ChildProcess', async () => { writeWindowsCommandFile(command) - const childProcess = new ChildProcess( - command - ) + const childProcess = new ChildProcess(command) const result = await childProcess.run() @@ -71,9 +66,7 @@ describe('ChildProcess', async () => { const batchFile = path.join(tempFolder, 'test-script.bat') writeBatchFile(batchFile) - const childProcess = new ChildProcess( - batchFile - ) + const childProcess = new ChildProcess(batchFile) // We want to verify that the error is thrown even if the first // invocation is still in progress, so we don't await the promise. @@ -88,16 +81,12 @@ describe('ChildProcess', async () => { assert.fail('Expected exception, but none was thrown.') }) - } // END Windows only tests - - if (process.platform !== 'win32') { + } else { it('runs and captures stdout - unix', async () => { const scriptFile = path.join(tempFolder, 'test-script.sh') writeShellFile(scriptFile) - const childProcess = new ChildProcess( - scriptFile - ) + const childProcess = new ChildProcess(scriptFile) const result = await childProcess.run() @@ -112,9 +101,7 @@ describe('ChildProcess', async () => { const scriptFile = path.join(tempFolder, 'test-script.sh') writeShellFile(scriptFile) - const childProcess = new ChildProcess( - scriptFile - ) + const childProcess = new ChildProcess(scriptFile) // We want to verify that the error is thrown even if the first // invocation is still in progress, so we don't await the promise. @@ -145,9 +132,7 @@ describe('ChildProcess', async () => { writeShellFile(command) } - const childProcess = new ChildProcess( - command - ) + const childProcess = new ChildProcess(command) const result = await childProcess.run() @@ -161,9 +146,7 @@ describe('ChildProcess', async () => { it('reports error for missing executable', async () => { const batchFile = path.join(tempFolder, 'nonExistentScript') - const childProcess = new ChildProcess( - batchFile - ) + const childProcess = new ChildProcess(batchFile) const result = await childProcess.run() @@ -176,8 +159,8 @@ describe('ChildProcess', async () => { expectedExitCode, expectedOutput }: { - childProcessResult: ChildProcessResult, - expectedExitCode: number, + childProcessResult: ChildProcessResult + expectedExitCode: number expectedOutput: string }) { assert.strictEqual( @@ -204,7 +187,7 @@ describe('ChildProcess', async () => { onClose: (code, signal) => { assert.strictEqual(code, 0, 'Unexpected close code') resolve() - }, + } }) }) } @@ -313,7 +296,7 @@ describe('ChildProcess', async () => { onClose: (code, signal) => { assert.notStrictEqual(code, 0, 'Expected an error close code') resolve() - }, + } }) }) }) @@ -322,13 +305,11 @@ describe('ChildProcess', async () => { describe('kill & killed', async () => { if (process.platform === 'win32') { // tslint:disable-next-line:max-line-length - it ('detects running processes and successfully sends a kill signal to a running process - Windows', async () => { + it('detects running processes and successfully sends a kill signal to a running process - Windows', async () => { const batchFile = path.join(tempFolder, 'test-script.bat') writeBatchFileWithDelays(batchFile) - const childProcess = new ChildProcess( - batchFile - ) + const childProcess = new ChildProcess(batchFile) // awaiting this means that the script finishes before it can be killed // worst case scenario, this script will take 2 seconds to run its course @@ -336,7 +317,7 @@ describe('ChildProcess', async () => { childProcess.run() assert.strictEqual(childProcess.killed, false) childProcess.kill() - await new Promise((resolve) => { + await new Promise(resolve => { setTimeout( () => { assert.strictEqual(childProcess.killed, true) @@ -347,20 +328,18 @@ describe('ChildProcess', async () => { }) }) - it ('can not kill previously killed processes - Windows', async () => { + it('can not kill previously killed processes - Windows', async () => { const batchFile = path.join(tempFolder, 'test-script.bat') writeBatchFileWithDelays(batchFile) - const childProcess = new ChildProcess( - batchFile - ) + const childProcess = new ChildProcess(batchFile) // awaiting this means that the script finishes before it can be killed // worst case scenario, this script will take 2 seconds to run its course // tslint:disable-next-line: no-floating-promises childProcess.run() childProcess.kill() - await new Promise( (resolve) => { + await new Promise(resolve => { setTimeout( () => { assert.strictEqual(childProcess.killed, true) @@ -369,10 +348,12 @@ describe('ChildProcess', async () => { 100 ) }) - await new Promise( (resolve) => { + await new Promise(resolve => { setTimeout( () => { - assert.throws( () => { childProcess.kill() }) + assert.throws(() => { + childProcess.kill() + }) resolve() }, 100 @@ -383,13 +364,11 @@ describe('ChildProcess', async () => { if (process.platform !== 'win32') { // tslint:disable-next-line:max-line-length - it ('detects running processes and successfully sends a kill signal to a running process - Unix', async () => { + it('detects running processes and successfully sends a kill signal to a running process - Unix', async () => { const scriptFile = path.join(tempFolder, 'test-script.sh') writeShellFileWithDelays(scriptFile) - const childProcess = new ChildProcess( - scriptFile - ) + const childProcess = new ChildProcess(scriptFile) // awaiting this means that the script finishes before it can be killed // worst case scenario, this script will take 2 seconds to run its course @@ -397,7 +376,7 @@ describe('ChildProcess', async () => { childProcess.run() assert.strictEqual(childProcess.killed, false) childProcess.kill() - await new Promise((resolve) => { + await new Promise(resolve => { setTimeout( () => { assert.strictEqual(childProcess.killed, true) @@ -408,20 +387,18 @@ describe('ChildProcess', async () => { }) }) - it ('can not kill previously killed processes - Unix', async () => { + it('can not kill previously killed processes - Unix', async () => { const scriptFile = path.join(tempFolder, 'test-script.sh') writeShellFileWithDelays(scriptFile) - const childProcess = new ChildProcess( - scriptFile - ) + const childProcess = new ChildProcess(scriptFile) // awaiting this means that the script finishes before it can be killed // worst case scenario, this script will take 2 seconds to run its course // tslint:disable-next-line: no-floating-promises childProcess.run() childProcess.kill() - await new Promise( (resolve) => { + await new Promise(resolve => { setTimeout( () => { assert.strictEqual(childProcess.killed, true) @@ -430,10 +407,12 @@ describe('ChildProcess', async () => { 100 ) }) - await new Promise( (resolve) => { + await new Promise(resolve => { setTimeout( () => { - assert.throws( () => { childProcess.kill() }) + assert.throws(() => { + childProcess.kill() + }) resolve() }, 100 diff --git a/src/test/shared/utilities/pathUtils.test.ts b/src/test/shared/utilities/pathUtils.test.ts index 681cb10cea7..9f1021aacf7 100644 --- a/src/test/shared/utilities/pathUtils.test.ts +++ b/src/test/shared/utilities/pathUtils.test.ts @@ -7,7 +7,11 @@ import * as assert from 'assert' import * as path from 'path' -import { getNormalizedRelativePath, normalizeSeparator } from '../../../shared/utilities/pathUtils' +import { + dirnameWithTrailingSlash, + getNormalizedRelativePath, + normalizeSeparator, +} from '../../../shared/utilities/pathUtils' describe('getNormalizedRelativePath', async () => { it('returns expected path', async () => { @@ -28,3 +32,13 @@ describe('normalizeSeparator', async () => { assert.strictEqual(actual, 'a/b/c') }) }) + +describe('dirnameWithTrailingSlash', async () => { + it('Adds a trailing slash to a parent folder', async () => { + const expectedResult = path.join('src', 'processors') + path.sep + const input = path.join(expectedResult, 'app.js') + const actualResult = dirnameWithTrailingSlash(input) + + assert.strictEqual(actualResult, expectedResult, 'Expected path to contain trailing slash') + }) +})