diff --git a/packages/core/src/shared/sam/build.ts b/packages/core/src/shared/sam/build.ts index c3513363993..49cc78fad8a 100644 --- a/packages/core/src/shared/sam/build.ts +++ b/packages/core/src/shared/sam/build.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' -import { TemplateItem, createTemplatePrompter, getSamCliPathAndVersion, runInTerminal } from './sync' +import { TemplateItem, createTemplatePrompter } from './sync' import { ChildProcess } from '../utilities/processUtils' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { Wizard } from '../wizards/wizard' @@ -19,8 +19,9 @@ import globals from '../extensionGlobals' import { TreeNode } from '../treeview/resourceTreeDataProvider' import { telemetry } from '../telemetry/telemetry' import { getSpawnEnv } from '../env/resolveEnv' -import { getProjectRoot, isDotnetRuntime } from './utils' +import { getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils' import { getConfigFileUri, validateSamBuildConfig } from './config' +import { runInTerminal } from './processTerminal' export interface BuildParams { readonly template: TemplateItem diff --git a/packages/core/src/shared/sam/deploy.ts b/packages/core/src/shared/sam/deploy.ts index 6154e2e3def..3763dcab83d 100644 --- a/packages/core/src/shared/sam/deploy.ts +++ b/packages/core/src/shared/sam/deploy.ts @@ -23,15 +23,9 @@ import { CancellationError } from '../utilities/timeoutUtils' import { Wizard } from '../wizards/wizard' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { validateSamDeployConfig, SamConfig, writeSamconfigGlobal } from './config' -import { - TemplateItem, - createStackPrompter, - createBucketPrompter, - createTemplatePrompter, - getSamCliPathAndVersion, - runInTerminal, -} from './sync' -import { getProjectRoot, getSource } from './utils' +import { TemplateItem, createStackPrompter, createBucketPrompter, createTemplatePrompter } from './sync' +import { getProjectRoot, getSamCliPathAndVersion, getSource } from './utils' +import { runInTerminal } from './processTerminal' export interface DeployParams { readonly paramsSource: ParamsSource diff --git a/packages/core/src/shared/sam/processTerminal.ts b/packages/core/src/shared/sam/processTerminal.ts new file mode 100644 index 00000000000..e383b956d52 --- /dev/null +++ b/packages/core/src/shared/sam/processTerminal.ts @@ -0,0 +1,146 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' + +import { ToolkitError, UnknownError } from '../errors' +import globals from '../extensionGlobals' +import { isCloud9 } from '../extensionUtilities' +import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' +import { CancellationError } from '../utilities/timeoutUtils' +import { getLogger } from '../logger' +import { removeAnsi } from '../utilities/textUtilities' +import { isAutomation } from '../vscode/env' + +let oldTerminal: ProcessTerminal | undefined +export async function runInTerminal(proc: ChildProcess, cmd: string) { + const handleResult = (result?: ChildProcessResult) => { + if (result && result.exitCode !== 0) { + const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}` + if (result.stderr.includes('is up to date')) { + throw ToolkitError.chain(result.error, message, { + code: 'NoUpdateExitCode', + }) + } + throw ToolkitError.chain(result.error, message, { + code: 'NonZeroExitCode', + }) + } + } + + // `createTerminal` doesn't work on C9 so we use the output channel instead + if (isCloud9()) { + globals.outputChannel.show() + + const result = proc.run({ + onStdout: (text) => globals.outputChannel.append(removeAnsi(text)), + onStderr: (text) => globals.outputChannel.append(removeAnsi(text)), + }) + await proc.send('\n') + + return handleResult(await result) + } + + // The most recent terminal won't get garbage collected until the next run + if (oldTerminal?.stopped === true) { + oldTerminal.close() + } + const pty = (oldTerminal = new ProcessTerminal(proc)) + const terminal = vscode.window.createTerminal({ pty, name: `SAM ${cmd}` }) + terminal.sendText('\n') + terminal.show() + + const result = await new Promise((resolve) => pty.onDidExit(resolve)) + if (pty.cancelled) { + throw result.error !== undefined + ? ToolkitError.chain(result.error, 'SAM CLI was cancelled before exiting', { cancelled: true }) + : new CancellationError('user') + } else { + return handleResult(result) + } +} + +// This is a decent improvement over using the output channel but it isn't a tty/pty +// SAM CLI uses `click` which has reduced functionality if `os.isatty` returns false +// Historically, Windows lack of a pty-equivalent is why it's not available in libuv +// Maybe it's doable now with the ConPTY API? https://github.com/libuv/libuv/issues/2640 +class ProcessTerminal implements vscode.Pseudoterminal { + private readonly onDidCloseEmitter = new vscode.EventEmitter() + private readonly onDidWriteEmitter = new vscode.EventEmitter() + private readonly onDidExitEmitter = new vscode.EventEmitter() + public readonly onDidWrite = this.onDidWriteEmitter.event + public readonly onDidClose = this.onDidCloseEmitter.event + public readonly onDidExit = this.onDidExitEmitter.event + + public constructor(private readonly process: ChildProcess) { + // Used in integration tests + if (isAutomation()) { + // Disable because it is a test. + // eslint-disable-next-line aws-toolkits/no-console-log + this.onDidWrite((text) => console.log(text.trim())) + } + } + + #cancelled = false + public get cancelled() { + return this.#cancelled + } + + public get stopped() { + return this.process.stopped + } + + public open(initialDimensions: vscode.TerminalDimensions | undefined): void { + this.process + .run({ + onStdout: (text) => this.mapStdio(text), + onStderr: (text) => this.mapStdio(text), + }) + .then((result) => this.onDidExitEmitter.fire(result)) + .catch((err) => + this.onDidExitEmitter.fire({ error: UnknownError.cast(err), exitCode: -1, stderr: '', stdout: '' }) + ) + .finally(() => this.onDidWriteEmitter.fire('\r\nPress any key to close this terminal')) + } + + public close(): void { + this.process.stop() + this.onDidCloseEmitter.fire() + } + + public handleInput(data: string) { + // ETX + if (data === '\u0003' || this.process.stopped) { + this.#cancelled ||= data === '\u0003' + return this.close() + } + + // enter + if (data === '\u000D') { + this.process.send('\n').then(undefined, (e) => { + getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) + }) + this.onDidWriteEmitter.fire('\r\n') + } else { + this.process.send(data).then(undefined, (e) => { + getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) + }) + this.onDidWriteEmitter.fire(data) + } + } + + private mapStdio(text: string): void { + const lines = text.split('\n') + const first = lines.shift() + + if (first) { + this.onDidWriteEmitter.fire(first) + } + + for (const line of lines) { + this.onDidWriteEmitter.fire('\r\n') + this.onDidWriteEmitter.fire(line) + } + } +} diff --git a/packages/core/src/shared/sam/sync.ts b/packages/core/src/shared/sam/sync.ts index a0d691dc981..e21d89899f9 100644 --- a/packages/core/src/shared/sam/sync.ts +++ b/packages/core/src/shared/sam/sync.ts @@ -17,25 +17,20 @@ import * as CloudFormation from '../cloudformation/cloudformation' import { DefaultEcrClient } from '../clients/ecrClient' import { createRegionPrompter } from '../ui/common/region' import { CancellationError } from '../utilities/timeoutUtils' -import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' +import { ChildProcess } from '../utilities/processUtils' import { keys, selectFrom } from '../utilities/tsUtils' import { AWSTreeNodeBase } from '../treeview/nodes/awsTreeNodeBase' -import { ToolkitError, UnknownError } from '../errors' +import { ToolkitError } from '../errors' import { telemetry } from '../telemetry/telemetry' import { createCommonButtons } from '../ui/buttons' import { ToolkitPromptSettings } from '../settings' import { getLogger } from '../logger' -import { getSamInitDocUrl, isCloud9 } from '../extensionUtilities' -import { removeAnsi } from '../utilities/textUtilities' +import { getSamInitDocUrl } from '../extensionUtilities' import { createExitPrompter } from '../ui/common/exitPrompter' import { StackSummary } from 'aws-sdk/clients/cloudformation' -import { SamCliSettings } from './cli/samCliSettings' import { getConfigFileUri, SamConfig, validateSamSyncConfig, writeSamconfigGlobal } from './config' import { cast, Optional } from '../utilities/typeConstructors' import { pushIf, toRecord } from '../utilities/collectionUtils' -import { SamCliInfoInvocation } from './cli/samCliInfo' -import { parse } from 'semver' -import { isAutomation } from '../vscode/env' import { getOverriddenParameters } from '../../lambda/config/parameterUtils' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { samSyncParamUrl, samSyncUrl, samUpgradeUrl } from '../constants' @@ -46,7 +41,8 @@ import { IamConnection } from '../../auth/connection' import { CloudFormationTemplateRegistry } from '../fs/templateRegistry' import { TreeNode } from '../treeview/resourceTreeDataProvider' import { getSpawnEnv } from '../env/resolveEnv' -import { getProjectRoot, getProjectRootUri, getSource } from './utils' +import { getProjectRoot, getProjectRootUri, getSamCliPathAndVersion, getSource } from './utils' +import { runInTerminal } from './processTerminal' const localize = nls.loadMessageBundle() @@ -459,71 +455,6 @@ export async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boun return { boundArgs } } -export async function getSamCliPathAndVersion() { - const { path: samCliPath } = await SamCliSettings.instance.getOrDetectSamCli() - if (samCliPath === undefined) { - throw new ToolkitError('SAM CLI could not be found', { code: 'MissingExecutable' }) - } - - const info = await new SamCliInfoInvocation(samCliPath).execute() - const parsedVersion = parse(info.version) - telemetry.record({ version: info.version }) - - if (parsedVersion?.compare('1.53.0') === -1) { - throw new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) - } - - return { path: samCliPath, parsedVersion } -} - -let oldTerminal: ProcessTerminal | undefined -export async function runInTerminal(proc: ChildProcess, cmd: string) { - const handleResult = (result?: ChildProcessResult) => { - if (result && result.exitCode !== 0) { - const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}` - if (result.stderr.includes('is up to date')) { - throw ToolkitError.chain(result.error, message, { - code: 'NoUpdateExitCode', - }) - } - throw ToolkitError.chain(result.error, message, { - code: 'NonZeroExitCode', - }) - } - } - - // `createTerminal` doesn't work on C9 so we use the output channel instead - if (isCloud9()) { - globals.outputChannel.show() - - const result = proc.run({ - onStdout: (text) => globals.outputChannel.append(removeAnsi(text)), - onStderr: (text) => globals.outputChannel.append(removeAnsi(text)), - }) - await proc.send('\n') - - return handleResult(await result) - } - - // The most recent terminal won't get garbage collected until the next run - if (oldTerminal?.stopped === true) { - oldTerminal.close() - } - const pty = (oldTerminal = new ProcessTerminal(proc)) - const terminal = vscode.window.createTerminal({ pty, name: `SAM ${cmd}` }) - terminal.sendText('\n') - terminal.show() - - const result = await new Promise((resolve) => pty.onDidExit(resolve)) - if (pty.cancelled) { - throw result.error !== undefined - ? ToolkitError.chain(result.error, 'SAM CLI was cancelled before exiting', { cancelled: true }) - : new CancellationError('user') - } else { - return handleResult(result) - } -} - async function loadLegacyParameterOverrides(template: TemplateItem) { try { const params = await getOverriddenParameters(template.uri) @@ -581,6 +512,7 @@ export async function runSamSync(args: SyncParams) { env: await getSpawnEnv(process.env, { promptForInvalidCredential: true }), }), }) + await runInTerminal(sam, 'sync') const { paramsSource, stackName, region, projectRoot } = args const shouldWriteSyncSamconfigGlobal = paramsSource !== ParamsSource.SamConfig && !!stackName && !!region @@ -660,6 +592,7 @@ export async function prepareSyncParams( } else if (arg instanceof vscode.Uri) { if (arg.path.endsWith('samconfig.toml')) { // "Deploy" command was invoked on a samconfig.toml file. + // TODO: add step to verify samconfig content to skip param source prompter const config = await SamConfig.fromConfigFileUri(arg) const params = getSyncParamsFromConfig(config) const projectRoot = vscode.Uri.joinPath(config.location, '..') @@ -675,7 +608,7 @@ export async function prepareSyncParams( // Always use the dependency layer if the user specified to do so const skipDependencyLayer = !config.getCommandParam('sync', 'dependency_layer') - return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } + return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } as SyncParams } // "Deploy" command was invoked on a template.yaml file. @@ -777,90 +710,6 @@ Confirm that you are synchronizing a development stack. } } -// This is a decent improvement over using the output channel but it isn't a tty/pty -// SAM CLI uses `click` which has reduced functionality if `os.isatty` returns false -// Historically, Windows lack of a pty-equivalent is why it's not available in libuv -// Maybe it's doable now with the ConPTY API? https://github.com/libuv/libuv/issues/2640 -class ProcessTerminal implements vscode.Pseudoterminal { - private readonly onDidCloseEmitter = new vscode.EventEmitter() - private readonly onDidWriteEmitter = new vscode.EventEmitter() - private readonly onDidExitEmitter = new vscode.EventEmitter() - public readonly onDidWrite = this.onDidWriteEmitter.event - public readonly onDidClose = this.onDidCloseEmitter.event - public readonly onDidExit = this.onDidExitEmitter.event - - public constructor(private readonly process: ChildProcess) { - // Used in integration tests - if (isAutomation()) { - // Disable because it is a test. - // eslint-disable-next-line aws-toolkits/no-console-log - this.onDidWrite((text) => console.log(text.trim())) - } - } - - #cancelled = false - public get cancelled() { - return this.#cancelled - } - - public get stopped() { - return this.process.stopped - } - - public open(initialDimensions: vscode.TerminalDimensions | undefined): void { - this.process - .run({ - onStdout: (text) => this.mapStdio(text), - onStderr: (text) => this.mapStdio(text), - }) - .then((result) => this.onDidExitEmitter.fire(result)) - .catch((err) => - this.onDidExitEmitter.fire({ error: UnknownError.cast(err), exitCode: -1, stderr: '', stdout: '' }) - ) - .finally(() => this.onDidWriteEmitter.fire('\r\nPress any key to close this terminal')) - } - - public close(): void { - this.process.stop() - this.onDidCloseEmitter.fire() - } - - public handleInput(data: string) { - // ETX - if (data === '\u0003' || this.process.stopped) { - this.#cancelled ||= data === '\u0003' - return this.close() - } - - // enter - if (data === '\u000D') { - this.process.send('\n').then(undefined, (e) => { - getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) - }) - this.onDidWriteEmitter.fire('\r\n') - } else { - this.process.send(data).then(undefined, (e) => { - getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message) - }) - this.onDidWriteEmitter.fire(data) - } - } - - private mapStdio(text: string): void { - const lines = text.split('\n') - const first = lines.shift() - - if (first) { - this.onDidWriteEmitter.fire(first) - } - - for (const line of lines) { - this.onDidWriteEmitter.fire('\r\n') - this.onDidWriteEmitter.fire(line) - } - } -} - function resolveSyncArgConflict(boundArgs: string[]): string[] { const boundArgsSet = new Set(boundArgs) if (boundArgsSet.has('--watch')) { diff --git a/packages/core/src/shared/sam/utils.ts b/packages/core/src/shared/sam/utils.ts index f95f408b797..a0c700657d2 100644 --- a/packages/core/src/shared/sam/utils.ts +++ b/packages/core/src/shared/sam/utils.ts @@ -4,12 +4,17 @@ */ import * as vscode from 'vscode' -import path from 'path' +import * as path from 'path' import { AWSTreeNodeBase } from '../treeview/nodes/awsTreeNodeBase' import { TreeNode, isTreeNode } from '../treeview/resourceTreeDataProvider' import * as CloudFormation from '../cloudformation/cloudformation' import { TemplateItem } from './sync' import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime' +import { telemetry } from '../telemetry' +import { ToolkitError } from '../errors' +import { SamCliSettings } from './cli/samCliSettings' +import { SamCliInfoInvocation } from './cli/samCliInfo' +import { parse } from 'semver' /** * @description determines the root directory of the project given Template Item @@ -61,3 +66,20 @@ export async function isDotnetRuntime(templateUri: vscode.Uri, contents?: string const globalRuntime = samTemplate.template.Globals?.Function?.Runtime as string return globalRuntime ? getFamily(globalRuntime) === RuntimeFamily.DotNet : false } + +export async function getSamCliPathAndVersion() { + const { path: samCliPath } = await SamCliSettings.instance.getOrDetectSamCli() + if (samCliPath === undefined) { + throw new ToolkitError('SAM CLI could not be found', { code: 'MissingExecutable' }) + } + + const info = await new SamCliInfoInvocation(samCliPath).execute() + const parsedVersion = parse(info.version) + telemetry.record({ version: info.version }) + + if (parsedVersion?.compare('1.53.0') === -1) { + throw new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) + } + + return { path: samCliPath, parsedVersion } +} diff --git a/packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts similarity index 92% rename from packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts rename to packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts index dbac6f573e8..b675b2452f8 100644 --- a/packages/core/src/test/awsService/appBuilder/detectSamProjects.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { TestFolder } from '../../testUtil' -import { detectSamProjects, getFiles } from '../../../awsService/appBuilder/explorer/detectSamProjects' +import { TestFolder } from '../../../testUtil' +import { detectSamProjects, getFiles } from '../../../../awsService/appBuilder/explorer/detectSamProjects' import assert from 'assert' import * as sinon from 'sinon' import path from 'path' -import { ToolkitError } from '../../../shared' -import { assertLogsContain } from '../../globalSetup.test' +import { ToolkitError } from '../../../../shared' +import { assertLogsContain } from '../../../globalSetup.test' describe('detectSamProjects', () => { let sandbox: sinon.SinonSandbox diff --git a/packages/core/src/test/awsService/appBuilder/samProject.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts similarity index 93% rename from packages/core/src/test/awsService/appBuilder/samProject.test.ts rename to packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts index 1d884ddd694..b093e6bb847 100644 --- a/packages/core/src/test/awsService/appBuilder/samProject.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts @@ -3,22 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { getApp, getStackName, SamAppLocation } from '../../../awsService/appBuilder/explorer/samProject' +import { getApp, getStackName, SamAppLocation } from '../../../../awsService/appBuilder/explorer/samProject' import * as sinon from 'sinon' import assert from 'assert' -import { ToolkitError } from '../../../shared' -import * as CloudformationModule from '../../../shared/cloudformation/cloudformation' +import { ToolkitError } from '../../../../shared' +import * as CloudformationModule from '../../../../shared/cloudformation/cloudformation' import path from 'path' -import { TestFolder } from '../../testUtil' +import { TestFolder } from '../../../testUtil' import { generateSamconfigData, samconfigCompleteData, samconfigInvalidData, validTemplateData, -} from '../../shared/sam/samTestUtils' -import { assertLogsContain } from '../../globalSetup.test' -import { getTestWindow } from '../../shared/vscode/window' +} from '../../../shared/sam/samTestUtils' +import { assertLogsContain } from '../../../globalSetup.test' +import { getTestWindow } from '../../../shared/vscode/window' describe('samProject', () => { let sandbox: sinon.SinonSandbox diff --git a/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts new file mode 100644 index 00000000000..86292ebe856 --- /dev/null +++ b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts @@ -0,0 +1,114 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +import { PrompterTester } from '../../../shared/wizards/prompterTester' +import assert from 'assert' +import { DeployTypeWizard } from '../../../../awsService/appBuilder/wizards/deployTypeWizard' +import { getSyncWizard } from '../../../../shared/sam/sync' +import { TestFolder } from '../../../testUtil' +import { samconfigCompleteData, validTemplateData } from '../../../shared/sam/samTestUtils' +import { getDeployWizard } from '../../../../shared/sam/deploy' + +describe('DeployTypeWizard', function () { + let testFolder: TestFolder + let templateFile: vscode.Uri + let syncWizard: any + let deployWizard: any + + let deployTypeWizard: DeployTypeWizard + + before(async () => { + testFolder = await TestFolder.create() + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + await testFolder.write('samconfig.toml', samconfigCompleteData) + + deployWizard = await getDeployWizard(templateFile) + syncWizard = await getSyncWizard('infra', templateFile, undefined, false) + }) + + it('customer abort wizard should not call any command function', async function () { + // Given + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.dispose() + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert(!choices) + prompterTester.assertCall('Select deployment command', 1) + }) + + it('deploy is selected', async function () { + /** + * This test focus on that deploy wizard get triggered when customer choose to use sam deploy + * Selection for deploy wizard speficy here focus on only one case + * More cases are test in Deploy.test.ts + * + */ + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.acceptItem(picker.items[1]) + }) + .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert.strictEqual(choices?.choice, 'deploy') + prompterTester.assertCallAll() + }) + + it('sync is selected', async function () { + /** + * This test focus on that sync wizard get triggered when customer choose to use sam sync + * Selection for deploy wizard speficy here focus on only one case + * More cases are test in Sync.test.ts + * + */ + const prompterTester = PrompterTester.init() + .handleQuickPick('Select deployment command', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'Sync') + assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + deployTypeWizard = new DeployTypeWizard(syncWizard, deployWizard) + const choices = await deployTypeWizard.run() + // Then + assert.strictEqual(choices?.choice, 'sync') + prompterTester.assertCallAll() + }) +}) diff --git a/packages/core/src/test/shared/sam/deploy.test.ts b/packages/core/src/test/shared/sam/deploy.test.ts index 1da271fbdfb..01eb6df3423 100644 --- a/packages/core/src/test/shared/sam/deploy.test.ts +++ b/packages/core/src/test/shared/sam/deploy.test.ts @@ -14,6 +14,7 @@ import { getDeployWizard, runDeploy, } from '../../../shared/sam/deploy' +import * as UtilsModule from '../../../shared/sam/utils' import { globals, ToolkitError } from '../../../shared' import sinon from 'sinon' import { samconfigCompleteData, samconfigInvalidData, validTemplateData } from './samTestUtils' @@ -29,8 +30,8 @@ import { DefaultS3Client } from '../../../shared/clients/s3Client' import * as CloudFormationClientModule from '../../../shared/clients/cloudFormationClient' import * as S3ClientModule from '../../../shared/clients/s3Client' import * as ProcessUtilsModule from '../../../shared/utilities/processUtils' +import * as ProcessTerminalModule from '../../../shared/sam/processTerminal' import * as ResolveEnvModule from '../../../shared/env/resolveEnv' -import * as SyncModule from '../../../shared/sam/sync' import * as SamConfiModule from '../../../shared/sam/config' import { RequiredProps } from '../../../shared/utilities/tsUtils' import { UserAgent as __UserAgent } from '@smithy/types' @@ -92,7 +93,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigInvalidData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -142,6 +143,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.region, 'us-west-2') assert.strictEqual(parameters.stackName, 'stack1') assert.strictEqual(parameters.bucketSource, 0) + prompterTester.assertCallAll() }) it('happy path with valid samconfig.toml', async () => { @@ -162,7 +164,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -193,6 +195,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -300,7 +303,7 @@ describe('DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -330,6 +333,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.region, 'us-west-2') assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -361,7 +365,7 @@ describe('DeployWizard', async function () { * - bucketName : [Skip] automatically set for bucketSource option 1 */ - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -411,6 +415,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.stackName, 'stack2') assert.strictEqual(parameters.bucketSource, 0) assert(!parameters.bucketName) + prompterTester.assertCallAll() }) it('happy path with valid samconfig.toml', async () => { @@ -431,7 +436,7 @@ describe('DeployWizard', async function () { // generate samconfig.toml in temporary test folder await testFolder.write('samconfig.toml', samconfigCompleteData) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) @@ -462,6 +467,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) @@ -486,7 +492,7 @@ describe('DeployWizard', async function () { const templateFile2 = vscode.Uri.file(await testFolder2.write('template.yaml', validTemplateData)) await (await globals.templateRegistry).addItem(templateFile2) - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -543,6 +549,7 @@ describe('DeployWizard', async function () { assert.strictEqual(parameters.stackName, 'stack3') assert.strictEqual(parameters.bucketSource, 1) assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + prompterTester.assertCallAll() }) it('happy path with samconfig.toml', async () => { @@ -568,7 +575,7 @@ describe('DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) // Simulate return of deployed stacks - PrompterTester.init() + const prompterTester = PrompterTester.init() .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file await quickPick.untilReady() @@ -595,6 +602,7 @@ describe('DeployWizard', async function () { assert(!parameters.region) assert(!parameters.stackName) assert(!parameters.bucketSource) + prompterTester.assertCallAll() }) }) }) @@ -647,13 +655,13 @@ describe('SAM Deploy', () => { describe(':) path', () => { beforeEach(() => { mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) mockRunInTerminal = sandbox.stub().resolves(Promise.resolve()) - sandbox.stub(SyncModule, 'runInTerminal').callsFake(mockRunInTerminal) + sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake(mockRunInTerminal) }) it('[ParamsSource.SamConfig] should instantiate the correct ChildProcess', async () => { @@ -927,7 +935,7 @@ describe('SAM Deploy', () => { // Break point mockGetSamCliPath = sandbox - .stub(SyncModule, 'getSamCliPathAndVersion') + .stub(UtilsModule, 'getSamCliPathAndVersion') .rejects( new ToolkitError('SAM CLI version 1.53.0 or higher is required', { code: 'VersionTooLow' }) ) @@ -936,7 +944,7 @@ describe('SAM Deploy', () => { mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) mockRunInTerminal = sandbox.stub().resolves(Promise.resolve()) - sandbox.stub(SyncModule, 'runInTerminal').callsFake(mockRunInTerminal) + sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake(mockRunInTerminal) await runDeploy(appNode) assert.fail('Should have thrown an Error') @@ -955,13 +963,13 @@ describe('SAM Deploy', () => { // Happy Stub sandbox.stub(DeployWizard.prototype, 'run').resolves(mockDeployParams) mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) // Breaking point mockRunInTerminal = sandbox - .stub(SyncModule, 'runInTerminal') + .stub(ProcessTerminalModule, 'runInTerminal') .rejects(new ToolkitError('SAM CLI was cancelled before exiting', { cancelled: true })) await runDeploy(appNode) @@ -981,12 +989,12 @@ describe('SAM Deploy', () => { // Happy Stub sandbox.stub(DeployWizard.prototype, 'run').resolves(mockDeployParams) mockGetSamCliPath = sandbox.stub().resolves({ path: 'sam-cli-path' }) - sandbox.stub(SyncModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) + sandbox.stub(UtilsModule, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) mockChildProcess = sandbox.stub().resolves({}) sandbox.stub(ProcessUtilsModule, 'ChildProcess').callsFake(mockChildProcess) // Breaking point - mockRunInTerminal = sandbox.stub(SyncModule, 'runInTerminal').callsFake((input, cmd) => { + mockRunInTerminal = sandbox.stub(ProcessTerminalModule, 'runInTerminal').callsFake((input, cmd) => { if (cmd === 'deploy') { throw new ToolkitError('The stack is up to date', { code: 'NoUpdateExitCode', diff --git a/packages/core/src/test/shared/sam/sync.test.ts b/packages/core/src/test/shared/sam/sync.test.ts index dfe73dac9cb..10bff92a0f9 100644 --- a/packages/core/src/test/shared/sam/sync.test.ts +++ b/packages/core/src/test/shared/sam/sync.test.ts @@ -4,12 +4,19 @@ */ import * as vscode from 'vscode' -import * as sync from '../../../shared/sam/sync' +import * as SyncModule from '../../../shared/sam/sync' +import * as UtilsModule from '../../../shared/sam/utils' +import * as ProcessTerminalUtils from '../../../shared/sam/processTerminal' import * as awsConsole from '../../../shared/awsConsole' import * as S3ClientModule from '../../../shared/clients/s3Client' +import * as SamConfigModule from '../../../shared/sam/config' +import * as ResolveEnvModule from '../../../shared/env/resolveEnv' +import * as ProcessUtilsModule from '../../../shared/utilities/processUtils' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' import * as CloudFormationClientModule from '../../../shared/clients/cloudFormationClient' import * as buttons from '../../../shared/ui/buttons' -import assert from 'assert' +import assert, { fail } from 'assert' + import { createBucketPrompter, createEcrPrompter, @@ -18,9 +25,11 @@ import { createTemplatePrompter, ensureBucket, getSyncParamsFromConfig, + getSyncWizard, ParamsSource, paramsSourcePrompter, prepareSyncParams, + runSync, saveAndBindArgs, syncFlagItems, SyncParams, @@ -32,8 +41,6 @@ import { createBaseTemplate, makeSampleSamTemplateYaml, } from '../cloudformation/cloudformationTestUtils' -import * as deploySamApplication from '../../../shared/sam/deploy' -import * as syncSam from '../../../shared/sam/sync' import { createWizardTester } from '../wizards/wizardTestUtils' import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' @@ -60,10 +67,19 @@ import { AppNode } from '../../../awsService/appBuilder/explorer/nodes/appNode' import * as Cfn from '../../../shared/cloudformation/cloudformation' import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry' import { WatchedItem } from '../../../shared/fs/watchedFiles' -import { validTemplateData } from '../../shared/sam/samTestUtils' +import { samconfigCompleteData, samconfigInvalidData, validTemplateData } from '../../shared/sam/samTestUtils' //import { beforeEach } from 'mocha' -import { assertEqualPaths, getWorkspaceFolder, TestFolder } from '../../testUtil' +import { + assertEqualPaths, + assertTelemetry, + assertTelemetryCurried, + getWorkspaceFolder, + TestFolder, +} from '../../testUtil' import { samSyncUrl } from '../../../shared/constants' +import { PrompterTester } from '../wizards/prompterTester' +import { createTestRegionProvider } from '../regions/testUtil' +import { ToolkitPromptSettings } from '../../../shared/settings' describe('SyncWizard', async function () { const createTester = async (params?: Partial) => @@ -383,7 +399,7 @@ describe('createBucketPrompter', () => { const stub = sandbox.stub(s3Client, 'listBucketsIterable').callsFake(() => { return buckets }) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) // Mock recent bucket + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Mock recent bucket // Act const prompter = createBucketPrompter(s3Client) @@ -407,7 +423,7 @@ describe('createBucketPrompter', () => { const stub = sandbox.stub(s3Client, 'listBucketsIterable').callsFake(() => { return [] as unknown as AsyncCollection & { readonly region: string }> }) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) // Mock recent bucket + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Mock recent bucket // Act const prompter = createBucketPrompter(s3Client) @@ -481,7 +497,7 @@ describe('createStackPrompter', () => { }, ] const listAllStacksStub = sandbox.stub(cfnClient, 'listAllStacks').returns(intoCollection(stackSummaries)) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -508,7 +524,7 @@ describe('createStackPrompter', () => { it('should include no items found message if no stacks exist', async () => { const listAllStacksStub = sandbox.stub(cfnClient, 'listAllStacks').returns(intoCollection([])) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -592,7 +608,7 @@ describe('createEcrPrompter', () => { }, ] const listAllRepositoriesStub = sandbox.stub(ecrClient, 'listAllRepositories').returns(intoCollection(ecrRepos)) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -619,7 +635,7 @@ describe('createEcrPrompter', () => { it('should include no items found message if no repos exist', async () => { const listAllStacksStub = sandbox.stub(ecrClient, 'listAllRepositories').returns(intoCollection([])) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) const createCommonButtonsStub = sandbox.stub(buttons, 'createCommonButtons') sandbox .stub(awsConsole, 'getAwsConsoleUrl') @@ -682,7 +698,7 @@ describe('createEnvironmentPrompter', () => { const envs: Environment[] = [defaultEnv, stagingEnv, prodEnv] listEnvironmentsStub.returns(envs) - sandbox.stub(sync, 'getRecentResponse').returns(undefined) + sandbox.stub(SyncModule, 'getRecentResponse').returns(undefined) // Act const prompter = createEnvironmentPrompter(config) @@ -734,7 +750,7 @@ describe('createTemplatePrompter', () => { it('should create quick pick items from registry items', () => { // Arrange const recentTemplatePathStub = sinon.stub().returns(undefined) - sandbox.replace(sync, 'getRecentResponse', recentTemplatePathStub) + sandbox.replace(SyncModule, 'getRecentResponse', recentTemplatePathStub) const workspaceFolder = vscode.workspace.workspaceFolders?.[0] assert.ok(workspaceFolder) @@ -883,6 +899,7 @@ describe('SyncWizard', async () => { let mockDefaultCFNClient: sinon.SinonStubbedInstance let mockDefaultS3Client: sinon.SinonStubbedInstance + let registry: CloudFormationTemplateRegistry beforeEach(async () => { testFolder = await TestFolder.create() @@ -902,147 +919,1038 @@ describe('SyncWizard', async () => { // generate template.yaml in temporary test folder and add to registery templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) - await (await globals.templateRegistry).addItem(templateFile) + registry = await globals.templateRegistry + await registry.addItem(templateFile) }) afterEach(() => { sandbox.restore() + registry.reset() }) - describe('deploy sync prompt', function () { - let sandbox: sinon.SinonSandbox - beforeEach(function () { - sandbox = sinon.createSandbox() + describe('entry: template file', () => { + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 1. 'stack1' + * - bucketName : [Select] 1. 'stack-1-bucket' + * - syncFlags : [Select] ["--dependency-layer","--use-container","--save-params"] + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigInvalidData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (quickPick) => { + // The prompt will need some time to generate option + await quickPick.untilReady() + + assert.strictEqual(quickPick.items.length, 3) + assert.strictEqual(quickPick.items[0].label, 'stack1') + assert.strictEqual(quickPick.items[1].label, 'stack2') + assert.strictEqual(quickPick.items[2].label, 'stack3') + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + const saveParam = picker.items.filter((item) => item.label === 'Save parameters')[0] + picker.acceptItems(dependencyLayer, useContainer, saveParam) + }) + .build() + + const parameters = await (await getSyncWizard('infra', templateFile, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack1') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.bucketName, 'stack-1-bucket') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--save-params"]') + prompterTester.assertCallAll() }) - afterEach(function () { - sandbox.restore() + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] null; will use 'us-west-2' from samconfig + * - stackName : [Skip] null; will use 'project-1' from samconfig + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (quickPick) => { + // Need time to check samconfig.toml file and generate options + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 3) + assert.strictEqual(quickPick.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(quickPick.items[1].label, 'Specify required parameters') + assert.strictEqual(quickPick.items[2].label, 'Use default values from samconfig') + quickPick.acceptItem(quickPick.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', templateFile, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert(!parameters.region) + assert(!parameters.stackName) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.bucketName) + assert.strictEqual(parameters.skipDependencyLayer, true) + assert(!parameters.syncFlags) + prompterTester.assertCallAll() }) + }) + + describe('entry: appBuilder', () => { + let appNode: AppNode - it('customer exit should not call any function', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + beforeEach(async () => { + const expectedSamAppLocation = { + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, + } + appNode = new AppNode(expectedSamAppLocation) + }) + + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 2. ('Specify required parameters') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 3. stack-3-bucket + * - syncFlags : [Select] ["--save-params"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) - picker.dispose() - } - }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.notCalled) - assert(sync.notCalled) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', async (picker) => { + await picker.untilReady() + const select = picker.items.filter((i) => i.detail === 'us-west-2')[0] + picker.acceptItem(select || picker.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const saveParam = picker.items.filter((item) => item.label === 'Save parameters')[0] + picker.acceptItems(saveParam) + }) + .build() + + const parameters = await (await getSyncWizard('infra', appNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.path, templateFile.path) + assert.strictEqual(parameters.projectRoot.path, projectRoot.path) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--save-params"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Skip] automatically set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] null; will use value from samconfig file + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', appNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.path, templateFile.path) + assert.strictEqual(parameters.projectRoot.path, projectRoot.path) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.region) + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() }) + }) - it('deploy is selected', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + describe('entry: region node', () => { + const expectedRegionId = 'us-west-2' + let regionNode: RegionNode + + beforeEach(async () => { + // Create RegionNode as entry point + regionNode = new RegionNode( + { id: expectedRegionId, name: 'US West (N. California)' }, + createTestRegionProvider() + ) + }) + + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 2. ('Specify required parameters') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 2. stack-2-bucket + * - syncFlags : [Select] ["--dependency-layer","--use-container"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') picker.acceptItem(picker.items[1]) - } else { + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + const parameters = await (await getSyncWizard('infra', regionNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-2-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template.yaml + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', regionNode, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.region, 'us-west-2') + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() + }) + }) + + describe('entry: samconfig file context menu', () => { + it('sad path with invalid samconfig.toml should throw parsing config file error', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigInvalidData)) + try { + await (await getSyncWizard('infra', samconfigFile, false, false)).run() + } catch (error: any) { + assert.strictEqual(error.code, 'samConfigParseError') + } + }) + + it('happy path with valid samconfig.toml', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', samconfigFile, false, false)).run() + assert(parameters) + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'project-1') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.bucketName, 'aws-sam-cli-managed-default-samclisourcebucket-lftqponsaxsr') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert(!parameters.syncFlags) + prompterTester.assertCallAll() + }) + + it('happy path with empty samconfig.toml', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', '')) + /** + * Selection: + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 2. 'stack2' + * - bucketName : [select] 2. stack-2-bucket + * - syncFlags : [Select] ["--dependency-layer","--use-container","--watch"] + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + const watch = picker.items.filter((item) => item.label === 'Watch')[0] + picker.acceptItems(dependencyLayer, useContainer, watch) + }) + .build() + + const parameters = await (await getSyncWizard('infra', samconfigFile, false, false)).run() + assert(parameters) + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketName, 'stack-2-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--watch"]') + prompterTester.assertCallAll() + }) + }) + + describe('entry: command palette', () => { + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 3. 'stack3' + * - bucketName : [select] 3. stack-3-bucket + * - syncFlags : [Select] all + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') picker.acceptItem(picker.items[0]) - } + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SpecifyAndSave) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack3') + assert.strictEqual(parameters.bucketName, 'stack-3-bucket') + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + prompterTester.assertCallAll() + }) + + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template.yaml + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.region) + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert(!parameters.syncFlags) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() + }) + }) +}) + +describe('SAM Sync', () => { + let sandbox: sinon.SinonSandbox + let testFolder: TestFolder + let projectRoot: vscode.Uri + let workspaceFolder: vscode.WorkspaceFolder + let templateFile: vscode.Uri + + let mockGetSpawnEnv: sinon.SinonStub + let mockGetSamCliPath: sinon.SinonStub + let mockChildProcessClass: sinon.SinonStub + let mockSamSyncChildProcess: sinon.SinonStub + + let spyWriteSamconfigGlobal: sinon.SinonSpy + let spyRunInterminal: sinon.SinonSpy + + let mockDefaultCFNClient: sinon.SinonStubbedInstance + let mockDefaultS3Client: sinon.SinonStubbedInstance + let registry: CloudFormationTemplateRegistry + + // Dependency clients + beforeEach(async function () { + testFolder = await TestFolder.create() + projectRoot = vscode.Uri.file(testFolder.path) + workspaceFolder = getWorkspaceFolder(testFolder.path) + sandbox = sinon.createSandbox() + registry = await globals.templateRegistry + + // Create template.yaml in temporary test folder and add to registery + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + + await registry.addItem(templateFile) + + // Simulate return of deployed stacks + mockDefaultCFNClient = sandbox.createStubInstance(CloudFormationClientModule.DefaultCloudFormationClient) + sandbox.stub(CloudFormationClientModule, 'DefaultCloudFormationClient').returns(mockDefaultCFNClient) + mockDefaultCFNClient.listAllStacks.returns(intoCollection(stackSummaries)) + + // Simulate return of list bucket + mockDefaultS3Client = sandbox.createStubInstance(S3ClientModule.DefaultS3Client) + sandbox.stub(S3ClientModule, 'DefaultS3Client').returns(mockDefaultS3Client) + mockDefaultS3Client.listBucketsIterable.returns(intoCollection(s3BucketListSummary)) + + // Create Spy for validation + spyWriteSamconfigGlobal = sandbox.spy(SamConfigModule, 'writeSamconfigGlobal') + spyRunInterminal = sandbox.spy(ProcessTerminalUtils, 'runInTerminal') + + // generate template.yaml in temporary test folder and add to registery + templateFile = vscode.Uri.file(await testFolder.write('template.yaml', validTemplateData)) + await (await globals.templateRegistry).addItem(templateFile) + + mockGetSpawnEnv = sandbox.stub(ResolveEnvModule, 'getSpawnEnv').callsFake( + sandbox.stub().resolves({ + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.called) - assert(sync.notCalled) + ) + }) + + afterEach(() => { + sandbox.restore() + registry.reset() + }) + + describe(':) path', () => { + beforeEach(() => { + mockGetSamCliPath = sandbox + .stub(UtilsModule, 'getSamCliPathAndVersion') + .callsFake(sandbox.stub().resolves({ path: 'sam-cli-path' })) + + // Confirm confirmDevStack message + // getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) + getTestWindow().onDidShowMessage((message) => { + message.selectItem("OK, and don't show this again") + }) + + // Mock child process with required properties that get called in ProcessTerminal + mockSamSyncChildProcess = Object.create(ProcessUtilsModule.ChildProcess.prototype, { + stopped: { get: sandbox.stub().returns(false) }, + stop: { value: sandbox.stub().resolves({}) }, + run: { + value: sandbox.stub().resolves({ + exitCode: 0, + stdout: 'Mock successful sync command execution ', + stderr: '', + }), + }, + }) + mockChildProcessClass = sandbox.stub(ProcessUtilsModule, 'ChildProcess').returns(mockSamSyncChildProcess) }) - it('sync is selected', async function () { - // Given - const deploy = sandbox.stub(deploySamApplication, 'runDeploy').resolves() - const sync = sandbox.stub(syncSam, 'runSync').resolves() - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Select deployment command') { + afterEach(() => { + sandbox.restore() + }) + + it('[entry: command palette] specify and save flag should instantiate correct process in terminal', async () => { + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options await picker.untilReady() - assert.strictEqual(picker.items[0].label, 'Sync') - assert.strictEqual(picker.items[1].label, 'Deploy') - assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') picker.acceptItem(picker.items[0]) - } else { + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + // Invoke sync command from command palette + await runSync('code', undefined) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--code', + '--template', + `${templateFile.fsPath}`, + '--s3-bucket', + 'stack-3-bucket', + '--stack-name', + 'stack3', + '--region', + 'us-west-2', + '--no-dependency-layer', + '--save-params', + '--dependency-layer', + '--use-container', + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.calledOnce) + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: undefined }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'CodeOnly', + source: undefined, + }) + prompterTester.assertCallAll() + }) + + it('[entry: template file] specify flag should instantiate correct process in terminal', async () => { + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (quickPick) => { + // The prompt will need some time to generate option + await quickPick.untilReady() + assert.strictEqual(quickPick.items[0].label, 'stack1') + quickPick.acceptItem(quickPick.items[0]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { await picker.untilReady() + assert.strictEqual(picker.items[0].label, 'stack-1-bucket') picker.acceptItem(picker.items[0]) - } + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + await runSync('infra', templateFile) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--template', + `${templateFile.fsPath}`, + '--s3-bucket', + 'stack-1-bucket', + '--stack-name', + 'stack1', + '--region', + 'us-west-2', + '--no-dependency-layer', + '--dependency-layer', + '--use-container', + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.calledOnce) + + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: 'template' }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'AllResources', + source: 'template', + }) + prompterTester.assertCallAll() + }) + + it('[entry: appBuilder] use samconfig should instantiate correct process in terminal', async () => { + const expectedSamAppLocation = { + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, + } + const appNode = new AppNode(expectedSamAppLocation) + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + await runSync('infra', appNode) + + assert(mockGetSamCliPath.calledOnce) + assert(mockChildProcessClass.calledOnce) + assert.deepEqual(mockChildProcessClass.getCall(0).args, [ + 'sam-cli-path', + [ + 'sync', + '--template', + `${templateFile.fsPath}`, + '--no-dependency-layer', + '--config-file', + `${samconfigFile.fsPath}`, + ], + { + spawnOptions: { + cwd: projectRoot?.fsPath, + env: { + AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', + SAM_CLI_TELEMETRY: '0', + }, + }, + }, + ]) + assert(mockGetSpawnEnv.calledOnce) + assert(spyRunInterminal.calledOnce) + assert.deepEqual(spyRunInterminal.getCall(0).args, [mockSamSyncChildProcess, 'sync']) + assert(spyWriteSamconfigGlobal.notCalled) + + // Check telementry + assertTelemetry('sam_sync', { result: 'Succeeded', source: 'appBuilderDeploy' }) + assertTelemetryCurried('sam_sync')({ + syncedResources: 'AllResources', + source: 'appBuilderDeploy', }) - await vscode.commands.executeCommand('aws.appBuilder.deploy') - // Then - assert(deploy.notCalled) - assert(sync.called) + prompterTester.assertCallAll() }) - }), - describe('appBuilder', () => { - let appNode: AppNode - beforeEach(async () => { - const expectedSamAppLocation = { - workspaceFolder: workspaceFolder, - samTemplateUri: templateFile, - projectRoot: projectRoot, - } - appNode = new AppNode(expectedSamAppLocation) + }) + + describe(':( path', () => { + let appNode: AppNode + beforeEach(async () => { + mockGetSamCliPath = sandbox + .stub(UtilsModule, 'getSamCliPathAndVersion') + .callsFake(sandbox.stub().resolves({ path: 'sam-cli-path' })) + + appNode = new AppNode({ + workspaceFolder: workspaceFolder, + samTemplateUri: templateFile, + projectRoot: projectRoot, }) - afterEach(() => { - sandbox.restore() + await testFolder.write('samconfig.toml', samconfigCompleteData) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should abort when customer cancel sync dev stack agreement', async () => { + // Set the + await ToolkitPromptSettings.instance.update('samcliConfirmDevStack', false) + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((message) => { + message.dispose() }) - it('should return correct params from quickPicks', async () => { - getTestWindow().onDidShowQuickPick(async (picker) => { - if (picker.title === 'Specify parameters for deploy') { - assert.strictEqual(picker.items.length, 2) - assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') - assert.strictEqual(picker.items[1].label, 'Specify required parameters') - picker.acceptItem(picker.items[1]) - } else if (picker.title === 'Select a region') { - await picker.untilReady() - const select = picker.items.filter((i) => i.detail === 'us-west-2')[0] - picker.acceptItem(select || picker.items[0]) - } else if (picker.title === 'Select a CloudFormation Stack') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'stack1') - assert.strictEqual(picker.items[1].label, 'stack2') - assert.strictEqual(picker.items[2].label, 'stack3') - picker.acceptItem(picker.items[1]) - } else if (picker.title === 'Select an S3 Bucket') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'stack-1-bucket') - assert.strictEqual(picker.items[1].label, 'stack-2-bucket') - assert.strictEqual(picker.items[2].label, 'stack-3-bucket') - picker.acceptItem(picker.items[0]) - } else if (picker.title === 'Specify parameters for sync') { - await picker.untilReady() - assert.strictEqual(picker.items.length, 9) - picker.acceptDefault() - } - }) + try { + await runSync('infra', appNode) + fail('should have thrown CancellationError') + } catch (error: any) { + assert(error instanceof CancellationError) + assert.strictEqual(error.agent, 'user') + } + }) - const parameters = await new SyncWizard( - { deployType: 'infra', template: { uri: appNode.resource.samTemplateUri, data: {} } }, - await globals.templateRegistry - ).run() + it('should abort when customer cancel sync wizard', async () => { + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) + sandbox.stub(SyncModule.SyncWizard.prototype, 'run').resolves(undefined) + + try { + await runSync('infra', appNode) + fail('should have thrown CancellationError') + } catch (error: any) { + assert(error instanceof CancellationError) + assert.strictEqual(error.agent, 'user') + } + }) - assert(parameters) + it('should throw ToolkitError when sync command fail', async () => { + // Confirm confirmDevStack message + getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) - assert.strictEqual(parameters.template.uri.path, templateFile.path) - assert.strictEqual(parameters.projectRoot.path, projectRoot.path) - assert.strictEqual(parameters.paramsSource, ParamsSource.Flags) - assert.strictEqual(parameters.region, 'us-west-2') - assert.strictEqual(parameters.stackName, 'stack2') - assert.strictEqual(parameters.bucketName, 'stack-1-bucket') + const prompterTester = PrompterTester.init() + .handleQuickPick('Specify parameters for deploy', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + // Mock child process with required properties that get called in ProcessTerminal + mockSamSyncChildProcess = Object.create(ProcessUtilsModule.ChildProcess.prototype, { + stopped: { get: sandbox.stub().returns(false) }, + stop: { value: sandbox.stub().resolves({}) }, + run: { + value: sandbox.stub().resolves({ + exitCode: -1, + stdout: 'Mock sync command execution failure', + stderr: '', + }), + }, }) + mockChildProcessClass = sandbox.stub(ProcessUtilsModule, 'ChildProcess').returns(mockSamSyncChildProcess) + + try { + await runSync('infra', appNode) + fail('should have thrown ToolkitError') + } catch (error: any) { + assert(error instanceof ToolkitError) + assert.strictEqual(error.message, 'Failed to sync SAM application') + } + prompterTester.assertCallAll() }) + }) }) describe('saveAndBindArgs', () => { @@ -1054,7 +1962,7 @@ describe('saveAndBindArgs', () => { getConfigFileUriStub = sandbox.stub() // Replace the real implementations with stubs - sandbox.stub(sync, 'updateRecentResponse').resolves() + sandbox.stub(SyncModule, 'updateRecentResponse').resolves() }) afterEach(() => { diff --git a/packages/core/src/test/shared/wizards/prompterTester.ts b/packages/core/src/test/shared/wizards/prompterTester.ts index f9e34002405..432944ce947 100644 --- a/packages/core/src/test/shared/wizards/prompterTester.ts +++ b/packages/core/src/test/shared/wizards/prompterTester.ts @@ -11,8 +11,8 @@ export class PrompterTester { private quickPickHandlers: Map void> = new Map() private inputBoxHanlder: Map void> = new Map() private testWindow: TestWindow - private calledOrder = 0 - private report = new Map() + private callLog = Array() + private callLogCount = new Map() private constructor(testWindow?: TestWindow) { this.testWindow = testWindow || getTestWindow() @@ -24,30 +24,76 @@ export class PrompterTester { handleQuickPick(titlePattern: string, handler: (input: TestQuickPick) => void): PrompterTester { this.quickPickHandlers.set(titlePattern, handler) + this.callLogCount.set(titlePattern, 0) return this } handleInputBox(titlePattern: string, handler: (input: TestInputBox) => void): PrompterTester { this.inputBoxHanlder.set(titlePattern, handler) + this.callLogCount.set(titlePattern, 0) return this } - build(): void { + build(): PrompterTester { this.testWindow.onDidShowQuickPick((input) => { return this.handle(input, this.quickPickHandlers) }) this.testWindow.onDidShowInputBox((input) => { return this.handle(input, this.inputBoxHanlder) }) + return this } private record(title: string): void { - this.calledOrder++ - this.report.set(title, this.calledOrder) + this.callLog.push(title) + this.callLogCount.set(title, (this.callLogCount.get(title) ?? 0) + 1) + } + + /** + * Asserts that a specific prompter handler has been called the expected number of times. + * + * @param title - The title prompter to check. + * @param expectedCall - The expected number of times the prompted handler should have been called. + * @throws AssertionError if the actual number of calls doesn't match the expected number. + */ + assertCall(title: string, expectedCall: number) { + assert.strictEqual(this.callLogCount.get(title), expectedCall, title) + } + + /** + * Asserts that a specific prompter handler was called in the expected order. + * + * @param title - The title or identifier of the handler to check. + * @param expectedOrder - The expected position in the call order (one-based index). + * @throws AssertionError if the handler wasn't called in the expected order. + */ + assertCallOrder(title: string, expectedOrder: number) { + assert.strictEqual(this.callLog[expectedOrder - 1], title) } - public assertOrder(title: string, expectedOrder: number) { - assert.strictEqual(this.report.get(title) || 0, expectedOrder) + /** + * Asserts that all specified prompter handlers were called in the expected number of times. + * + * @param titles - The array of propmter handler titles to check. + * If not provided, all registered handler titles are used + * @param expectedCall - The expected number of times all specified handlers should have been called. + * If not provided, it defaults to 1. + * @throws AssertionError if the actual number of calls doesn't match the expected number. + */ + assertCallAll(titles: string[] = this.getHandlers(), expectedOrder: number = 1) { + titles.every((handler) => { + this.assertCall(handler, expectedOrder) + }) + } + + /** + * Retrieves all registered handler titles. + * + * @returns An array of strings containing all handler titles, including both + * quick pick handlers and input box handlers. + */ + getHandlers(): string[] { + return [...this.quickPickHandlers.keys(), ...this.inputBoxHanlder.keys()] } private handle(input: any, handlers: any) { @@ -55,7 +101,14 @@ export class PrompterTester { if (input.title?.includes(pattern)) { handler(input) this.record(pattern) + return } } + this.handleUnknownPrompter(input) + } + + private handleUnknownPrompter(input: any) { + input.dispose() + throw assert.fail(`Unexpected prompter titled: "${input.title}"`) } } diff --git a/packages/core/src/testInteg/sam.test.ts b/packages/core/src/testInteg/sam.test.ts index 03d00165e22..f2181e4ba77 100644 --- a/packages/core/src/testInteg/sam.test.ts +++ b/packages/core/src/testInteg/sam.test.ts @@ -25,7 +25,7 @@ import { AwsSamDebuggerConfiguration } from '../shared/sam/debugger/awsSamDebugC import { AwsSamTargetType } from '../shared/sam/debugger/awsSamDebugConfiguration' import { insertTextIntoFile } from '../shared/utilities/textUtilities' import globals from '../shared/extensionGlobals' -import { assertTelemetry, closeAllEditors, getWorkspaceFolder } from '../test/testUtil' +import { closeAllEditors, getWorkspaceFolder } from '../test/testUtil' import { ToolkitError } from '../shared/errors' import { SamAppLocation } from '../awsService/appBuilder/explorer/samProject' import { AppNode } from '../awsService/appBuilder/explorer/nodes/appNode' @@ -34,15 +34,6 @@ import { getTestWindow } from '../test/shared/vscode/window' import { ParamsSource, runBuild } from '../shared/sam/build' import { DataQuickPickItem } from '../shared/ui/pickerPrompter' import fs from '../shared/fs/fs' -import { BucketSource, DeployParams, DeployWizard, runDeploy } from '../shared/sam/deploy' -import * as sync from '../shared/sam/sync' -import * as deploy from '../shared/sam/deploy' -import * as processUtils from '../shared/utilities/processUtils' -import * as resolveEnv from '../shared/env/resolveEnv' -import * as cfnClient from '../shared/clients/cloudFormationClient' -import { runSync, SyncParams, SyncWizard, TemplateItem } from '../shared/sam/sync' -import { AsyncCollection } from '../shared/utilities/asyncCollection' -import { StackSummary } from 'aws-sdk/clients/cloudformation' const projectFolder = testUtils.getTestWorkspaceFolder() @@ -742,285 +733,6 @@ describe('SAM Integration Tests', async function () { }) }) - describe('SAM Deploy', () => { - let sandbox: sinon.SinonSandbox - let workspaceFolder: vscode.WorkspaceFolder - let childProcessStub: sinon.SinonStub - // We're testing only one case (python 3.10 (ZIP)) on the integration tests. More niche cases are handled as unit tests. - const scenarioIndex = 2 - const scenario = scenarios[scenarioIndex] - let testRoot: string - let testDir: string - let cfnTemplatePath: string - let mockDeployParams: DeployParams - let mockGetSpawnEnv: sinon.SinonStub - let mockGetSamCliPath: sinon.SinonStub - let mockRunInTerminal: sinon.SinonStub - let cfnClientStub: sinon.SinonStub - let appNode: AppNode - - before(async function () { - workspaceFolder = getWorkspaceFolder(testSuiteRoot) - testRoot = path.join(testSuiteRoot, scenario.runtime) - await fs.mkdir(testRoot) - - testDir = mkdtempSync(path.join(testRoot, 'samapp-')) - console.log(`testDir: ${testDir}`) - - await createSamApplication(testDir, scenario) - - cfnTemplatePath = path.join(testDir, samApplicationName, 'template.yaml') - - mockDeployParams = { - paramsSource: deploy.ParamsSource.Specify, - region: 'us-east-1', - stackName: 'my-stack', - bucketName: 'my-bucket-name', - template: { uri: vscode.Uri.file(cfnTemplatePath), data: {} } as TemplateItem, - bucketSource: BucketSource.UserProvided, - projectRoot: vscode.Uri.file(path.join(testDir, samApplicationName)), - } - }) - - after(async function () { - await tryRemoveFolder(testSuiteRoot) - // don't clean up after java tests so the java language server doesn't freak out - if (scenario.language !== 'java') { - await tryRemoveFolder(testRoot) - } - }) - - beforeEach(() => { - sandbox = sinon.createSandbox() - childProcessStub = sandbox.stub().returns({}) - cfnClientStub = sandbox.stub().returns({ - listAllStacks: () => - Promise.resolve([ - [ - { - StackName: 'my-stack', - StackStatus: 'CREATE_COMPLETE', - }, - ], - ] as unknown as AsyncCollection), - }) - sandbox.stub(cfnClient, 'DefaultCloudFormationClient').returns(cfnClientStub) - sandbox.stub(processUtils, 'ChildProcess').callsFake(childProcessStub) - mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - - const samAppLocation = { - samTemplateUri: vscode.Uri.file(cfnTemplatePath), - workspaceFolder: workspaceFolder, - } as SamAppLocation - appNode = new AppNode(samAppLocation) - }) - - afterEach(() => { - sandbox.restore() - }) - - it('should instantiate ChildProcess with the correct arguments for build', async () => { - // Stubbing necessary functions - sandbox.stub(DeployWizard.prototype, 'run').returns(Promise.resolve(mockDeployParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - - // Execute the function - await runDeploy(appNode) - - // Check that ChildProcess is instantiated correctly - assert.strictEqual(childProcessStub.callCount, 2) // ChildProcess should be called twice' - assert.deepEqual(childProcessStub.getCall(0).args, [ - 'sam-cli-path', - ['build', '--cached'], - { - spawnOptions: { - cwd: mockDeployParams.projectRoot?.fsPath, - env: { - AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', - SAM_CLI_TELEMETRY: '0', - }, - }, - }, - ]) - - // Check that runInTerminal is called with the correct arguments for build - assert.strictEqual(mockRunInTerminal.callCount, 2) - const buildProcess = childProcessStub.getCall(0).returnValue // Get the instance from the first call - assert.deepEqual(mockRunInTerminal.getCall(0).args, [buildProcess, 'build']) - assertTelemetry('sam_deploy', { result: 'Succeeded', source: 'appBuilderDeploy' }) - }) - - it('should instantiate ChildProcess with the correct arguments for deploy', async () => { - const mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - const mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - const mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - - // Stubbing necessary functions - sandbox.stub(DeployWizard.prototype, 'run').returns(Promise.resolve(mockDeployParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - - // Execute the function - await runDeploy(appNode) - - // Check that ChildProcess is instantiated correctly for deploy - assert.strictEqual(childProcessStub.callCount, 2) // ChildProcess should be called twice' - assert.deepEqual(childProcessStub.getCall(1).args[0], 'sam-cli-path') - assert.deepEqual(childProcessStub.getCall(1).args[1], [ - 'deploy', - '--no-confirm-changeset', - '--region', - `${mockDeployParams.region}`, - '--stack-name', - `${mockDeployParams.stackName}`, - '--s3-bucket', - `${mockDeployParams.bucketName}`, - '--capabilities', - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - ]) - - // Check that runInTerminal is called with the correct arguments for deploy - const deployProcess = childProcessStub.getCall(1).returnValue // Get the instance from the second call - assert.ok( - mockRunInTerminal.calledTwice, - 'runInTerminal should be called twice, once for build and once for deploy' - ) - assert.deepEqual(mockRunInTerminal.getCall(1).args, [deployProcess, 'deploy']) - assertTelemetry('sam_deploy', { result: 'Succeeded', source: 'appBuilderDeploy' }) - }) - }) - - describe('SAM Sync', () => { - let sandbox: sinon.SinonSandbox - let childProcessStub: sinon.SinonStub - let workspaceFolder: vscode.WorkspaceFolder - // We're testing only one case (python 3.10 (ZIP)) on the integration tests. More niche cases are handled as unit tests. - const scenarioIndex = 2 - const scenario = scenarios[scenarioIndex] - let testRoot: string - let testDir: string - let cfnTemplatePath: string - let mockSyncParams: SyncParams - let mockGetSpawnEnv: sinon.SinonStub - let mockGetSamCliPath: sinon.SinonStub - let mockRunInTerminal: sinon.SinonStub - - before(async function () { - workspaceFolder = getWorkspaceFolder(testSuiteRoot) - testRoot = path.join(testSuiteRoot, scenario.runtime) - await fs.mkdir(testRoot) - - testDir = mkdtempSync(path.join(testRoot, 'samapp-')) - console.log(`testDir: ${testDir}`) - - await createSamApplication(testDir, scenario) - - cfnTemplatePath = path.join(testDir, samApplicationName, 'template.yaml') - - mockSyncParams = { - paramsSource: sync.ParamsSource.SpecifyAndSave, - region: 'us-east-1', - stackName: 'my-stack', - bucketName: 'my-bucket-name', - deployType: 'code', - skipDependencyLayer: true, - connection: { - id: '', - type: 'iam', - label: '', - getCredentials: () => Promise.resolve({ accessKeyId: 'AAAAA', secretAccessKey: 'XXXXXX' }), - }, - template: { uri: vscode.Uri.file(cfnTemplatePath), data: {} } as TemplateItem, - bucketSource: BucketSource.UserProvided, - projectRoot: vscode.Uri.file(path.join(testDir, samApplicationName)), - } - }) - - after(async function () { - await tryRemoveFolder(testSuiteRoot) - // don't clean up after java tests so the java language server doesn't freak out - if (scenario.language !== 'java') { - await tryRemoveFolder(testRoot) - } - }) - - beforeEach(() => { - sandbox = sinon.createSandbox() - childProcessStub = sandbox.stub().returns({}) - sandbox.stub(processUtils, 'ChildProcess').callsFake(childProcessStub) - mockGetSpawnEnv = sandbox.stub().returns(Promise.resolve({})) - mockGetSamCliPath = sandbox.stub().returns(Promise.resolve({ path: 'sam-cli-path' })) - mockRunInTerminal = sandbox.stub().returns(Promise.resolve()) - }) - - afterEach(() => { - sandbox.restore() - }) - - it('should instantiate ChildProcess with the correct arguments for sync', async () => { - // Stubbing necessary functions - sandbox.stub(SyncWizard.prototype, 'run').returns(Promise.resolve(mockSyncParams)) - sandbox.stub(resolveEnv, 'getSpawnEnv').callsFake(mockGetSpawnEnv) - sandbox.stub(sync, 'getSamCliPathAndVersion').callsFake(mockGetSamCliPath) - sandbox.stub(sync, 'runInTerminal').callsFake(mockRunInTerminal) - const mockConfirmDevStack = sandbox.stub().returns(Promise.resolve()) - sandbox.stub(sync, 'confirmDevStack').callsFake(mockConfirmDevStack) - - const samAppLocation = { - samTemplateUri: vscode.Uri.file(cfnTemplatePath), - workspaceFolder: workspaceFolder, - } as SamAppLocation - const appNode = new AppNode(samAppLocation) - - // Execute the function - await runSync('infra', appNode) - - // Check that ChildProcess is instantiated correctly - assert.strictEqual(childProcessStub.callCount, 1) // ChildProcess should be called once' - assert.deepEqual(childProcessStub.getCall(0).args, [ - 'sam-cli-path', - [ - 'sync', - '--codeOnly', - '--template', - `${mockSyncParams.template.uri.fsPath}`, - '--s3-bucket', - `${mockSyncParams.bucketName}`, - '--stack-name', - `${mockSyncParams.stackName}`, - '--region', - `${mockSyncParams.region}`, - '--no-dependency-layer', - ], - { - spawnOptions: { - cwd: mockSyncParams.projectRoot?.fsPath, - env: { - AWS_TOOLING_USER_AGENT: 'AWS-Toolkit-For-VSCode/testPluginVersion', - SAM_CLI_TELEMETRY: '0', - }, - }, - }, - ]) - - // Check that runInTerminal is called with the correct arguments for build - assert.strictEqual(mockRunInTerminal.callCount, 1) - const syncProcess = childProcessStub.getCall(0).returnValue // Get the instance from the first call - assert.deepEqual(mockRunInTerminal.getCall(0).args, [syncProcess, 'sync']) - assertTelemetry('sam_sync', { - result: 'Succeeded', - syncedResources: 'AllResources', - source: 'appBuilderSync', - }) - }) - }) - async function createSamApplication(location: string, scenario: TestScenario): Promise { const initArguments: SamCliInitArgs = { name: samApplicationName,