From 3c56353420f2505fbd866b7d65523e3a2251d06d Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Wed, 11 Oct 2023 22:51:54 +0200 Subject: [PATCH 1/6] chore: upgrade integration test-suite to enable performance testing and testing sandbox process loop --- .../execa_process_manager.ts | 22 +++ .../process-controller/predicated_action.ts | 56 ++++++ .../predicated_action_macros.ts | 62 +++++++ .../predicated_action_queue_builder.ts | 163 ++++++++++++++++++ .../process-controller/process_controller.ts | 65 +++---- .../process-controller/stdio_interaction.ts | 12 -- .../stdio_interaction_macros.ts | 27 --- .../stdio_interaction_queue_builder.ts | 86 --------- .../src/test-e2e/deployment.test.ts | 65 ++++++- .../update-1/data/resource.ts | 15 ++ 10 files changed, 407 insertions(+), 166 deletions(-) create mode 100644 packages/integration-tests/src/process-controller/execa_process_manager.ts create mode 100644 packages/integration-tests/src/process-controller/predicated_action.ts create mode 100644 packages/integration-tests/src/process-controller/predicated_action_macros.ts create mode 100644 packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts delete mode 100644 packages/integration-tests/src/process-controller/stdio_interaction.ts delete mode 100644 packages/integration-tests/src/process-controller/stdio_interaction_macros.ts delete mode 100644 packages/integration-tests/src/process-controller/stdio_interaction_queue_builder.ts create mode 100644 packages/integration-tests/test-projects/data-storage-auth-with-triggers/update-1/data/resource.ts diff --git a/packages/integration-tests/src/process-controller/execa_process_manager.ts b/packages/integration-tests/src/process-controller/execa_process_manager.ts new file mode 100644 index 0000000000..0f55e04c6f --- /dev/null +++ b/packages/integration-tests/src/process-controller/execa_process_manager.ts @@ -0,0 +1,22 @@ +import { ExecaChildProcess, execa } from 'execa'; + +/** + * Kills the given process (equivalent of sending CTRL-C) + * @param processInstance an instance of execa child process + */ +export const killExecaProcess = async (processInstance: ExecaChildProcess) => { + if (process.platform.startsWith('win')) { + if (typeof processInstance.pid !== 'number') { + throw new Error('Cannot kill the process that does not have pid'); + } + // Wait X milliseconds before sending kill in hopes of draining the node event queue + await new Promise((resolve) => setTimeout(resolve, 1500)); + // turns out killing child process on Windows is a huge PITA + // https://stackoverflow.com/questions/23706055/why-can-i-not-kill-my-child-process-in-nodejs-on-windows + // https://github.com/sindresorhus/execa#killsignal-options + // eslint-disable-next-line spellcheck/spell-checker + await execa('taskkill', ['/pid', `${processInstance.pid}`, '/f', '/t']); + } else { + processInstance.kill('SIGINT'); + } +}; diff --git a/packages/integration-tests/src/process-controller/predicated_action.ts b/packages/integration-tests/src/process-controller/predicated_action.ts new file mode 100644 index 0000000000..30f3040831 --- /dev/null +++ b/packages/integration-tests/src/process-controller/predicated_action.ts @@ -0,0 +1,56 @@ +import { ExecaChildProcess } from 'execa'; + +/** + * Type of actions a user can take with their app. + */ +export enum ActionType { + SEND_INPUT_TO_PROCESS, + MAKE_CODE_CHANGES, + ASSERT_ON_PROCESS_OUTPUT, +} + +type SendInputToProcessAction = { + actionType: ActionType.SEND_INPUT_TO_PROCESS; + action: (execaProcess: ExecaChildProcess) => Promise; +}; +type MakeCodeChangesAction = { + actionType: ActionType.MAKE_CODE_CHANGES; + action: () => Promise; +}; +type AssertOnProcessOutputAction = { + actionType: ActionType.ASSERT_ON_PROCESS_OUTPUT; + action: (processOutputLine: string) => void; +}; + +export type Action = + | SendInputToProcessAction + | MakeCodeChangesAction + | AssertOnProcessOutputAction; + +/** + * Type of predicates based on which to execute actions. An action can only be associated with a predicate + */ +export enum PredicateType { + MATCHES_STRING_PREDICATE, +} + +type MatchesStringPredicate = { + predicateType: PredicateType.MATCHES_STRING_PREDICATE; + predicate: (line: string) => boolean; +}; + +export type Predicate = MatchesStringPredicate; + +/** + * Contains a predicate that the process controller should evaluate, + * then an optional action is executed by the process controller if predicate is true. IFTTT + */ +export type PredicatedAction = { + ifThis: Predicate; + /** + * String that should be sent once the predicate is true + * + * If we need to do things like send multiple keystrokes in response to a single prompt, we will likely need to expand this to an array of values to send + */ + then?: Action; +}; diff --git a/packages/integration-tests/src/process-controller/predicated_action_macros.ts b/packages/integration-tests/src/process-controller/predicated_action_macros.ts new file mode 100644 index 0000000000..da3c56cd08 --- /dev/null +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -0,0 +1,62 @@ +import { PredicatedActionBuilder } from './predicated_action_queue_builder.js'; + +/** + * Convenience predicated actions that can be used to build up more complex CLI flows. + * By composing flows from reusable macros we will hopefully avoid the situation in the + * classic CLI E2E tests where changing one CLI prompt requires updates to 97742 different E2E prompts + */ + +/** + * Reusable predicates: Wait for sandbox to finish and emit "✨ Total time: xx.xxs" + */ +export const waitForSandboxDeployment = () => + new PredicatedActionBuilder().waitForLineIncludes('Total time'); + +/** + * Reusable predicates: Wait for sandbox to become idle and emit "Watching for file changes..." + */ +export const waitForSandboxToBecomeIdle = () => + new PredicatedActionBuilder().waitForLineIncludes( + 'Watching for file changes...' + ); + +/** + * Reusable predicated action: Wait for sandbox delete to prompt to delete all the resource and respond with yes + */ +export const confirmDeleteSandbox = () => + new PredicatedActionBuilder() + .waitForLineIncludes( + 'Are you sure you want to delete all the resources in your sandbox environment' + ) + .sendYes(); + +/** + * Reusable predicated action: Wait for sandbox to prompt on quitting to delete all the resource and respond with no + */ +export const rejectCleanupSandbox = () => + new PredicatedActionBuilder() + .waitForLineIncludes( + 'Would you like to delete all the resources in your sandbox environment' + ) + .sendNo(); + +/** + * Reusable predicated action: Wait for sandbox to become idle and then update the + * backend code which should trigger sandbox again + */ +export const updateBackendCode = (from: string, to: string) => { + return waitForSandboxToBecomeIdle().updateBackendCode(from, to); +}; + +/** + * Reusable predicated action: Wait for sandbox to become idle and then quit it (CTRL-C) + */ +export const interruptSandbox = () => waitForSandboxToBecomeIdle().sendCtrlC(); + +/** + * Reusable predicated action: Wait for sandbox to finish deployment and assert that the deployment time is less + * than the threshold. + */ +export const ensureDeploymentTimeLessThan = (seconds: number) => { + return waitForSandboxDeployment().ensureDeploymentTimeLessThan(seconds); +}; diff --git a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts new file mode 100644 index 0000000000..7f8025420f --- /dev/null +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -0,0 +1,163 @@ +import { + ActionType, + PredicateType, + PredicatedAction, +} from './predicated_action.js'; +import os from 'os'; +import fs from 'fs/promises'; + +import { killExecaProcess } from './execa_process_manager.js'; + +export const CONTROL_C = '\x03'; +/** + * Builder for a queue of Actions + */ +export class PredicatedActionBuilder { + private readonly predicatedActionQueue: PredicatedAction[] = []; + + /** + * Append the action queue from another builder to this builder + */ + append = (builder: PredicatedActionBuilder) => { + this.predicatedActionQueue.push(...builder.getPredicatedActionQueue()); + return this; + }; + + /** + * Add a new predicated action to the queue with a predicate that matches a given string + */ + waitForLineIncludes = (str: string) => { + this.predicatedActionQueue.push({ + ifThis: { + predicateType: PredicateType.MATCHES_STRING_PREDICATE, + predicate: (line) => line.includes(str), + }, + }); + return this; + }; + + /** + * Adds a wait for ms milliseconds + */ + waitFor = async (ms: number) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + }; + + /** + * Update the last predicated action to send str to the running process with no newline + */ + send = (str: string) => { + this.getLastPredicatedAction().then = { + actionType: ActionType.SEND_INPUT_TO_PROCESS, + action: async (execaProcess) => { + if (str === CONTROL_C) { + await killExecaProcess(execaProcess); + } else { + execaProcess.stdin?.write(str); + } + }, + }; + return this; + }; + + /** + * Update the last predicated action to update backend code by copying files from + * `from` location to `to` location. + */ + updateBackendCode = (from: string, to: string) => { + this.getLastPredicatedAction().then = { + actionType: ActionType.MAKE_CODE_CHANGES, + action: async () => { + await fs.cp(from, to, { + recursive: true, + }); + }, + }; + return this; + }; + + /** + * Update the last predicated action to validate that the deployment time is less than the one specified + */ + ensureDeploymentTimeLessThan = (seconds: number) => { + this.getLastPredicatedAction().then = { + actionType: ActionType.ASSERT_ON_PROCESS_OUTPUT, + action: (strWithDeploymentTime: string) => { + const regex = /^✨ {2}Total time: (\d*.\d*).*$/; + const deploymentTime = strWithDeploymentTime.match(regex); + if ( + deploymentTime && + deploymentTime.length > 1 && + !isNaN(+deploymentTime[1]) + ) { + if (+deploymentTime[1] <= seconds) { + return; + } + throw new Error( + `Deployment time ${+deploymentTime[1]} seconds is higher than the threshold of ${seconds}` + ); + } else { + throw new Error( + `Could not determine the deployment time. String was ${strWithDeploymentTime}` + ); + } + }, + }; + return this; + }; + + /** + * Send line with a newline at the end + */ + sendLine = (line: string) => { + this.send(`${line}${os.EOL}`); + return this; + }; + + /** + * Send `N\n` + */ + sendNo = () => { + this.sendLine('N'); + return this; + }; + + /** + * Send `Y\n` + */ + sendYes = () => { + this.sendLine('Y'); + return this; + }; + + /** + * Send SIGINT to the child process + */ + sendCtrlC = () => { + this.send(CONTROL_C); + return this; + }; + + /** + * Get the currently queued actions + */ + getPredicatedActionQueue = (): PredicatedAction[] => { + return this.predicatedActionQueue; + }; + + getLastPredicatedAction = () => { + if (this.predicatedActionQueue.length === 0) { + throw new Error('Must have a predicate to execute the action'); + } + // this assertion is safe because we checked the length above + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lastPredicatedAction = this.predicatedActionQueue.at(-1)!; + if (typeof lastPredicatedAction.then === 'function') { + throw new Error( + 'An action is already registered to the last predicate in the queue. Update the same action.' + ); + } + + return lastPredicatedAction; + }; +} diff --git a/packages/integration-tests/src/process-controller/process_controller.ts b/packages/integration-tests/src/process-controller/process_controller.ts index 7082f644f6..485d52b247 100644 --- a/packages/integration-tests/src/process-controller/process_controller.ts +++ b/packages/integration-tests/src/process-controller/process_controller.ts @@ -1,8 +1,8 @@ import { Options, execa } from 'execa'; import readline from 'readline'; -import { CONTROL_C } from './stdio_interaction_macros.js'; -import { StdioInteractionQueueBuilder } from './stdio_interaction_queue_builder.js'; - +import { PredicatedActionBuilder } from './predicated_action_queue_builder.js'; +import { ActionType } from './predicated_action.js'; +import { killExecaProcess } from './execa_process_manager.js'; /** * Provides an abstractions for sending and receiving data on stdin/out of a child process * @@ -17,8 +17,8 @@ import { StdioInteractionQueueBuilder } from './stdio_interaction_queue_builder. * then send "yes" on stdin of the process */ export class ProcessController { - private readonly interactions: StdioInteractionQueueBuilder = - new StdioInteractionQueueBuilder(); + private readonly interactions: PredicatedActionBuilder = + new PredicatedActionBuilder(); /** * Initialize a process controller for the specified command and args. * @@ -32,7 +32,7 @@ export class ProcessController { private readonly options?: Pick ) {} - do = (interactions: StdioInteractionQueueBuilder) => { + do = (interactions: PredicatedActionBuilder) => { this.interactions.append(interactions); return this; }; @@ -41,13 +41,13 @@ export class ProcessController { * Execute the sequence of actions queued on the process */ run = async () => { - const interactionQueue = this.interactions.getStdioInteractionQueue(); + const interactionQueue = this.interactions.getPredicatedActionQueue(); const execaProcess = execa(this.command, this.args, { reject: false, ...this.options, }); - const pid = execaProcess.pid; - if (typeof pid !== 'number') { + let errorThrownFromActions = undefined; + if (typeof execaProcess.pid !== 'number') { throw new Error('Could not determine child process id'); } @@ -64,40 +64,41 @@ export class ProcessController { } const reader = readline.createInterface(execaProcess.stdout); - let expectKilled = false; - for await (const line of reader) { const currentInteraction = interactionQueue[0]; - if (!currentInteraction?.predicate(line)) { - continue; - } - // if we got here, the line matched the predicate - // now we need to send the payload of the action (if any) - if (typeof currentInteraction.payload === 'string') { - if (currentInteraction.payload === CONTROL_C) { - if (process.platform.startsWith('win')) { - // Wait X milliseconds before sending kill in hopes of draining the node event queue - await new Promise((resolve) => setTimeout(resolve, 1500)); - // turns out killing child process on Windows is a huge PITA - // https://stackoverflow.com/questions/23706055/why-can-i-not-kill-my-child-process-in-nodejs-on-windows - // https://github.com/sindresorhus/execa#killsignal-options - // eslint-disable-next-line spellcheck/spell-checker - await execa('taskkill', ['/pid', `${pid}`, '/f', '/t']); - } else { - execaProcess.kill('SIGINT'); + try { + // For now we only have one predicate type. If we add more predicate types in the future, we will have to + // turn this into a predicate executor (Similar to the switch-case for actions below) + if (currentInteraction?.ifThis.predicate(line)) { + switch (currentInteraction.then?.actionType) { + case ActionType.SEND_INPUT_TO_PROCESS: + await currentInteraction.then.action(execaProcess); + break; + case ActionType.MAKE_CODE_CHANGES: + await currentInteraction.then.action(); + break; + case ActionType.ASSERT_ON_PROCESS_OUTPUT: + currentInteraction.then.action(line); + break; + default: + break; } - expectKilled = true; } else { - execaProcess.stdin?.write(currentInteraction.payload); + continue; } + } catch (error) { + await killExecaProcess(execaProcess); + execaProcess.stdin?.write('N'); + errorThrownFromActions = error; } // advance the queue interactionQueue.shift(); } const result = await execaProcess; - if (expectKilled) { - return; + + if (errorThrownFromActions) { + throw errorThrownFromActions; } else if (result.failed) { throw new Error(result.stdout); } diff --git a/packages/integration-tests/src/process-controller/stdio_interaction.ts b/packages/integration-tests/src/process-controller/stdio_interaction.ts deleted file mode 100644 index 4992c4fa6f..0000000000 --- a/packages/integration-tests/src/process-controller/stdio_interaction.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Contains a predicate that the process controller should wait for on stdout, then an optional string that should be sent on stdin after the predicate matches - */ -export type StdioInteraction = { - predicate: (line: string) => boolean; - /** - * String that should be sent once the predicate is true - * - * If we need to do things like send multiple keystrokes in response to a single prompt, we will likely need to expand this to an array of values to send - */ - payload?: string; -}; diff --git a/packages/integration-tests/src/process-controller/stdio_interaction_macros.ts b/packages/integration-tests/src/process-controller/stdio_interaction_macros.ts deleted file mode 100644 index 19d64ed2d3..0000000000 --- a/packages/integration-tests/src/process-controller/stdio_interaction_macros.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { StdioInteractionQueueBuilder } from './stdio_interaction_queue_builder.js'; - -export const CONTROL_C = '\x03'; - -/** - * Convenience actions that can be used to build up more complex CLI flows. - * By composing flows from reusable macros we will hopefully avoid the situation in the classic CLI E2E tests where changing one CLI prompt requires updates to 97742 different E2E prompts - */ - -export const confirmDeleteSandbox = new StdioInteractionQueueBuilder() - .waitForLineIncludes( - 'Are you sure you want to delete all the resources in your sandbox environment' - ) - .sendYes(); - -export const rejectCleanupSandbox = new StdioInteractionQueueBuilder() - .waitForLineIncludes( - 'Would you like to delete all the resources in your sandbox environment' - ) - .sendNo(); - -export const waitForSandboxDeployment = - new StdioInteractionQueueBuilder().waitForLineIncludes('Total time'); - -export const interruptSandbox = new StdioInteractionQueueBuilder() - .waitForLineIncludes('[Sandbox] Watching for file changes') - .sendCtrlC(); diff --git a/packages/integration-tests/src/process-controller/stdio_interaction_queue_builder.ts b/packages/integration-tests/src/process-controller/stdio_interaction_queue_builder.ts deleted file mode 100644 index 79083128f6..0000000000 --- a/packages/integration-tests/src/process-controller/stdio_interaction_queue_builder.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { StdioInteraction } from './stdio_interaction.js'; -import os from 'os'; -import { CONTROL_C } from './stdio_interaction_macros.js'; - -/** - * Builder for a queue of LineActions - */ -export class StdioInteractionQueueBuilder { - private readonly stdioInteractionQueue: StdioInteraction[] = []; - - /** - * Append the action queue from another builder to this builder - */ - append = (builder: StdioInteractionQueueBuilder) => { - this.stdioInteractionQueue.push(...builder.getStdioInteractionQueue()); - return this; - }; - - /** - * Add a new action to the queue to wait for a line that includes str - */ - waitForLineIncludes = (str: string) => { - this.stdioInteractionQueue.push({ - predicate: (line) => line.includes(str), - }); - return this; - }; - - /** - * Send str with no newline - */ - send = (str: string) => { - if (this.stdioInteractionQueue.length === 0) { - throw new Error('Must wait for a line before sending'); - } - // this assertion is safe because we checked the length above - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const lastInteraction = this.stdioInteractionQueue.at(-1)!; - if (typeof lastInteraction.payload === 'string') { - throw new Error( - 'A string to send is already registered to the last action in the queue. Add a new action to the queue.' - ); - } - lastInteraction.payload = str; - return this; - }; - - /** - * Send line with a newline at the end - */ - sendLine = (line: string) => { - this.send(`${line}${os.EOL}`); - return this; - }; - - /** - * Send `N\n` - */ - sendNo = () => { - this.sendLine('N'); - return this; - }; - - /** - * Send `Y\n` - */ - sendYes = () => { - this.sendLine('Y'); - return this; - }; - - /** - * Send SIGINT to the child process - */ - sendCtrlC = () => { - this.send(CONTROL_C); - return this; - }; - - /** - * Get the currently queued actions - */ - getStdioInteractionQueue = (): StdioInteraction[] => { - return this.stdioInteractionQueue; - }; -} diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index 4c69686547..84ba0666be 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -5,10 +5,12 @@ import { amplifyCli } from '../process-controller/process_controller.js'; import assert from 'node:assert'; import { confirmDeleteSandbox, + ensureDeploymentTimeLessThan, interruptSandbox, rejectCleanupSandbox, + updateBackendCode, waitForSandboxDeployment, -} from '../process-controller/stdio_interaction_macros.js'; +} from '../process-controller/predicated_action_macros.js'; import { createEmptyAmplifyProject } from '../create_empty_amplify_project.js'; import { createTestDirectoryBeforeAndCleanupAfter, @@ -39,10 +41,20 @@ void describe('amplify deploys', () => { const testProjects = [ { name: 'data-storage-auth-with-triggers', - initialAmplifyDirPath: new URL( + amplifyPath: new URL( '../../test-projects/data-storage-auth-with-triggers/amplify', import.meta.url ), + updates: [ + { + amplifyPath: new URL( + '../../test-projects/data-storage-auth-with-triggers/update-1', + import.meta.url + ), + fileToUpdate: 'data/resource.ts', + deploymentThresholdInSeconds: 80, + }, + ], assertions: async () => { const { default: clientConfig } = await import( pathToFileURL( @@ -67,21 +79,56 @@ void describe('amplify deploys', () => { void describe('sandbox', () => { afterEach(async () => { await amplifyCli(['sandbox', 'delete'], testProjectRoot) - .do(confirmDeleteSandbox) + .do(confirmDeleteSandbox()) .run(); await fs.rm(testProjectRoot, { recursive: true }); }); testProjects.forEach((testProject) => { - void it(testProject.name, async () => { - await fs.cp(testProject.initialAmplifyDirPath, testAmplifyDir, { + void it(`${testProject.name} deploys with sandbox on startup`, async () => { + await fs.cp(testProject.amplifyPath, testAmplifyDir, { recursive: true, }); await amplifyCli(['sandbox'], testProjectRoot) - .do(waitForSandboxDeployment) - .do(interruptSandbox) - .do(rejectCleanupSandbox) + .do(waitForSandboxDeployment()) + .do(interruptSandbox()) + .do(rejectCleanupSandbox()) + .run(); + + await testProject.assertions(); + }); + }); + + testProjects.forEach((testProject) => { + void it(`${testProject.name} hot swaps a change`, async () => { + await fs.cp(testProject.amplifyPath, testAmplifyDir, { + recursive: true, + }); + + const processController = amplifyCli( + ['sandbox', '--dirToWatch', 'amplify'], + testProjectRoot + ).do(waitForSandboxDeployment()); + + for (const update of testProject.updates) { + const fileToUpdate = path.join( + update.amplifyPath.pathname, + update.fileToUpdate + ); + const updateSource = path.join(testAmplifyDir, update.fileToUpdate); + + processController + .do(updateBackendCode(fileToUpdate, updateSource)) + .do( + ensureDeploymentTimeLessThan(update.deploymentThresholdInSeconds) + ); + } + + // Execute the process. + await processController + .do(interruptSandbox()) + .do(rejectCleanupSandbox()) .run(); await testProject.assertions(); @@ -109,7 +156,7 @@ void describe('amplify deploys', () => { testProjects.forEach((testProject) => { void it(testProject.name, async () => { - await fs.cp(testProject.initialAmplifyDirPath, testAmplifyDir, { + await fs.cp(testProject.amplifyPath, testAmplifyDir, { recursive: true, }); diff --git a/packages/integration-tests/test-projects/data-storage-auth-with-triggers/update-1/data/resource.ts b/packages/integration-tests/test-projects/data-storage-auth-with-triggers/update-1/data/resource.ts new file mode 100644 index 0000000000..c89ae4cc0e --- /dev/null +++ b/packages/integration-tests/test-projects/data-storage-auth-with-triggers/update-1/data/resource.ts @@ -0,0 +1,15 @@ +import { defineData } from '@aws-amplify/backend-graphql'; + +const schema = ` + input AMPLIFY {globalAuthRule: AuthRule = { allow: public }} # FOR TESTING ONLY! + + type Todo @model { + id: ID! + name: String! + description: String + otherField: String + newFieldAdded: String + } +`; + +export const data = defineData({ schema }); From d858bad692e20a31f82d65865904d47dc2fb3f53 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Wed, 11 Oct 2023 23:11:04 +0200 Subject: [PATCH 2/6] add changeset --- .changeset/smooth-teachers-invent.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/smooth-teachers-invent.md diff --git a/.changeset/smooth-teachers-invent.md b/.changeset/smooth-teachers-invent.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/smooth-teachers-invent.md @@ -0,0 +1,2 @@ +--- +--- From 16fb11e99e4967a03ffaa73a7446750f6579f6ff Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 12 Oct 2023 14:28:58 +0200 Subject: [PATCH 3/6] attempt to fix windows path creation --- .../src/process-controller/predicated_action_macros.ts | 2 +- .../predicated_action_queue_builder.ts | 2 +- .../integration-tests/src/test-e2e/deployment.test.ts | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/integration-tests/src/process-controller/predicated_action_macros.ts b/packages/integration-tests/src/process-controller/predicated_action_macros.ts index da3c56cd08..8a91686391 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_macros.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -44,7 +44,7 @@ export const rejectCleanupSandbox = () => * Reusable predicated action: Wait for sandbox to become idle and then update the * backend code which should trigger sandbox again */ -export const updateBackendCode = (from: string, to: string) => { +export const updateBackendCode = (from: URL, to: URL) => { return waitForSandboxToBecomeIdle().updateBackendCode(from, to); }; diff --git a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts index 7f8025420f..9027e35f8e 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -64,7 +64,7 @@ export class PredicatedActionBuilder { * Update the last predicated action to update backend code by copying files from * `from` location to `to` location. */ - updateBackendCode = (from: string, to: string) => { + updateBackendCode = (from: URL, to: URL) => { this.getLastPredicatedAction().then = { actionType: ActionType.MAKE_CODE_CHANGES, action: async () => { diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index 84ba0666be..1a87ecf33a 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -112,11 +112,12 @@ void describe('amplify deploys', () => { ).do(waitForSandboxDeployment()); for (const update of testProject.updates) { - const fileToUpdate = path.join( - update.amplifyPath.pathname, - update.fileToUpdate + const fileToUpdate = pathToFileURL( + path.join(update.amplifyPath.pathname, update.fileToUpdate) + ); + const updateSource = pathToFileURL( + path.join(testAmplifyDir, update.fileToUpdate) ); - const updateSource = path.join(testAmplifyDir, update.fileToUpdate); processController .do(updateBackendCode(fileToUpdate, updateSource)) From 9ae6a1243538e97bd01cc6cefded1479297e04c7 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 12 Oct 2023 15:26:04 +0200 Subject: [PATCH 4/6] attempt to fix windows path creation --- .../src/process-controller/predicated_action.ts | 6 ++++++ .../process-controller/predicated_action_queue_builder.ts | 8 ++++++-- .../src/process-controller/process_controller.ts | 7 ++++++- .../integration-tests/src/test-e2e/deployment.test.ts | 6 +++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/integration-tests/src/process-controller/predicated_action.ts b/packages/integration-tests/src/process-controller/predicated_action.ts index 30f3040831..36fba75556 100644 --- a/packages/integration-tests/src/process-controller/predicated_action.ts +++ b/packages/integration-tests/src/process-controller/predicated_action.ts @@ -7,12 +7,17 @@ export enum ActionType { SEND_INPUT_TO_PROCESS, MAKE_CODE_CHANGES, ASSERT_ON_PROCESS_OUTPUT, + KILL_PROCESS, } type SendInputToProcessAction = { actionType: ActionType.SEND_INPUT_TO_PROCESS; action: (execaProcess: ExecaChildProcess) => Promise; }; +type KillProcess = { + actionType: ActionType.KILL_PROCESS; + action: (execaProcess: ExecaChildProcess) => Promise; +}; type MakeCodeChangesAction = { actionType: ActionType.MAKE_CODE_CHANGES; action: () => Promise; @@ -24,6 +29,7 @@ type AssertOnProcessOutputAction = { export type Action = | SendInputToProcessAction + | KillProcess | MakeCodeChangesAction | AssertOnProcessOutputAction; diff --git a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts index 9027e35f8e..80e58dcee8 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -7,6 +7,7 @@ import os from 'os'; import fs from 'fs/promises'; import { killExecaProcess } from './execa_process_manager.js'; +import { ExecaChildProcess } from 'execa'; export const CONTROL_C = '\x03'; /** @@ -48,8 +49,11 @@ export class PredicatedActionBuilder { */ send = (str: string) => { this.getLastPredicatedAction().then = { - actionType: ActionType.SEND_INPUT_TO_PROCESS, - action: async (execaProcess) => { + actionType: + str === CONTROL_C + ? ActionType.KILL_PROCESS + : ActionType.SEND_INPUT_TO_PROCESS, + action: async (execaProcess: ExecaChildProcess) => { if (str === CONTROL_C) { await killExecaProcess(execaProcess); } else { diff --git a/packages/integration-tests/src/process-controller/process_controller.ts b/packages/integration-tests/src/process-controller/process_controller.ts index 485d52b247..06a197431a 100644 --- a/packages/integration-tests/src/process-controller/process_controller.ts +++ b/packages/integration-tests/src/process-controller/process_controller.ts @@ -47,6 +47,7 @@ export class ProcessController { ...this.options, }); let errorThrownFromActions = undefined; + let expectKilled = false; if (typeof execaProcess.pid !== 'number') { throw new Error('Could not determine child process id'); } @@ -74,6 +75,10 @@ export class ProcessController { case ActionType.SEND_INPUT_TO_PROCESS: await currentInteraction.then.action(execaProcess); break; + case ActionType.KILL_PROCESS: + expectKilled = true; + await currentInteraction.then.action(execaProcess); + break; case ActionType.MAKE_CODE_CHANGES: await currentInteraction.then.action(); break; @@ -99,7 +104,7 @@ export class ProcessController { if (errorThrownFromActions) { throw errorThrownFromActions; - } else if (result.failed) { + } else if (result.failed && !expectKilled) { throw new Error(result.stdout); } }; diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index 1a87ecf33a..2f577cbd8b 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -16,7 +16,7 @@ import { createTestDirectoryBeforeAndCleanupAfter, getTestDir, } from '../setup_test_directory.js'; -import { pathToFileURL } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; import { shortUuid } from '../short_uuid.js'; import { CloudFormationClient, @@ -113,10 +113,10 @@ void describe('amplify deploys', () => { for (const update of testProject.updates) { const fileToUpdate = pathToFileURL( - path.join(update.amplifyPath.pathname, update.fileToUpdate) + path.join(fileURLToPath(update.amplifyPath), update.fileToUpdate) ); const updateSource = pathToFileURL( - path.join(testAmplifyDir, update.fileToUpdate) + path.join(fileURLToPath(testAmplifyDir), update.fileToUpdate) ); processController From 967f7a9736d8fa2e4614e097271b661a89553e22 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 12 Oct 2023 15:43:38 +0200 Subject: [PATCH 5/6] typo fix --- packages/integration-tests/src/test-e2e/deployment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index 2f577cbd8b..a4af0ee89c 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -116,7 +116,7 @@ void describe('amplify deploys', () => { path.join(fileURLToPath(update.amplifyPath), update.fileToUpdate) ); const updateSource = pathToFileURL( - path.join(fileURLToPath(testAmplifyDir), update.fileToUpdate) + path.join(testAmplifyDir, update.fileToUpdate) ); processController From 7706134608b9e79e5cdad0a0c78672c4b637f0c8 Mon Sep 17 00:00:00 2001 From: Praveen Gupta Date: Thu, 12 Oct 2023 16:42:19 +0200 Subject: [PATCH 6/6] PR upadtes --- ...xeca_process_manager.ts => execa_process_killer.ts} | 0 .../src/process-controller/predicated_action.ts | 8 ++++---- .../src/process-controller/predicated_action_macros.ts | 10 ++++++---- .../predicated_action_queue_builder.ts | 6 +++--- .../src/process-controller/process_controller.ts | 4 ++-- .../integration-tests/src/test-e2e/deployment.test.ts | 10 +++++----- 6 files changed, 20 insertions(+), 18 deletions(-) rename packages/integration-tests/src/process-controller/{execa_process_manager.ts => execa_process_killer.ts} (100%) diff --git a/packages/integration-tests/src/process-controller/execa_process_manager.ts b/packages/integration-tests/src/process-controller/execa_process_killer.ts similarity index 100% rename from packages/integration-tests/src/process-controller/execa_process_manager.ts rename to packages/integration-tests/src/process-controller/execa_process_killer.ts diff --git a/packages/integration-tests/src/process-controller/predicated_action.ts b/packages/integration-tests/src/process-controller/predicated_action.ts index 36fba75556..c63316970c 100644 --- a/packages/integration-tests/src/process-controller/predicated_action.ts +++ b/packages/integration-tests/src/process-controller/predicated_action.ts @@ -5,7 +5,7 @@ import { ExecaChildProcess } from 'execa'; */ export enum ActionType { SEND_INPUT_TO_PROCESS, - MAKE_CODE_CHANGES, + UPDATE_FILE_CONTENT, ASSERT_ON_PROCESS_OUTPUT, KILL_PROCESS, } @@ -18,8 +18,8 @@ type KillProcess = { actionType: ActionType.KILL_PROCESS; action: (execaProcess: ExecaChildProcess) => Promise; }; -type MakeCodeChangesAction = { - actionType: ActionType.MAKE_CODE_CHANGES; +type UpdateFileContentAction = { + actionType: ActionType.UPDATE_FILE_CONTENT; action: () => Promise; }; type AssertOnProcessOutputAction = { @@ -30,7 +30,7 @@ type AssertOnProcessOutputAction = { export type Action = | SendInputToProcessAction | KillProcess - | MakeCodeChangesAction + | UpdateFileContentAction | AssertOnProcessOutputAction; /** diff --git a/packages/integration-tests/src/process-controller/predicated_action_macros.ts b/packages/integration-tests/src/process-controller/predicated_action_macros.ts index 8a91686391..700bc79980 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_macros.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -9,7 +9,7 @@ import { PredicatedActionBuilder } from './predicated_action_queue_builder.js'; /** * Reusable predicates: Wait for sandbox to finish and emit "✨ Total time: xx.xxs" */ -export const waitForSandboxDeployment = () => +export const waitForSandboxDeploymentToPrintTotalTime = () => new PredicatedActionBuilder().waitForLineIncludes('Total time'); /** @@ -44,8 +44,8 @@ export const rejectCleanupSandbox = () => * Reusable predicated action: Wait for sandbox to become idle and then update the * backend code which should trigger sandbox again */ -export const updateBackendCode = (from: URL, to: URL) => { - return waitForSandboxToBecomeIdle().updateBackendCode(from, to); +export const updateFileContent = (from: URL, to: URL) => { + return waitForSandboxToBecomeIdle().updateFileContent(from, to); }; /** @@ -58,5 +58,7 @@ export const interruptSandbox = () => waitForSandboxToBecomeIdle().sendCtrlC(); * than the threshold. */ export const ensureDeploymentTimeLessThan = (seconds: number) => { - return waitForSandboxDeployment().ensureDeploymentTimeLessThan(seconds); + return waitForSandboxDeploymentToPrintTotalTime().ensureDeploymentTimeLessThan( + seconds + ); }; diff --git a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts index 80e58dcee8..c489136e47 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -6,7 +6,7 @@ import { import os from 'os'; import fs from 'fs/promises'; -import { killExecaProcess } from './execa_process_manager.js'; +import { killExecaProcess } from './execa_process_killer.js'; import { ExecaChildProcess } from 'execa'; export const CONTROL_C = '\x03'; @@ -68,9 +68,9 @@ export class PredicatedActionBuilder { * Update the last predicated action to update backend code by copying files from * `from` location to `to` location. */ - updateBackendCode = (from: URL, to: URL) => { + updateFileContent = (from: URL, to: URL) => { this.getLastPredicatedAction().then = { - actionType: ActionType.MAKE_CODE_CHANGES, + actionType: ActionType.UPDATE_FILE_CONTENT, action: async () => { await fs.cp(from, to, { recursive: true, diff --git a/packages/integration-tests/src/process-controller/process_controller.ts b/packages/integration-tests/src/process-controller/process_controller.ts index 06a197431a..e04b175b53 100644 --- a/packages/integration-tests/src/process-controller/process_controller.ts +++ b/packages/integration-tests/src/process-controller/process_controller.ts @@ -2,7 +2,7 @@ import { Options, execa } from 'execa'; import readline from 'readline'; import { PredicatedActionBuilder } from './predicated_action_queue_builder.js'; import { ActionType } from './predicated_action.js'; -import { killExecaProcess } from './execa_process_manager.js'; +import { killExecaProcess } from './execa_process_killer.js'; /** * Provides an abstractions for sending and receiving data on stdin/out of a child process * @@ -79,7 +79,7 @@ export class ProcessController { expectKilled = true; await currentInteraction.then.action(execaProcess); break; - case ActionType.MAKE_CODE_CHANGES: + case ActionType.UPDATE_FILE_CONTENT: await currentInteraction.then.action(); break; case ActionType.ASSERT_ON_PROCESS_OUTPUT: diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index a4af0ee89c..e13203e998 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -8,8 +8,8 @@ import { ensureDeploymentTimeLessThan, interruptSandbox, rejectCleanupSandbox, - updateBackendCode, - waitForSandboxDeployment, + updateFileContent, + waitForSandboxDeploymentToPrintTotalTime, } from '../process-controller/predicated_action_macros.js'; import { createEmptyAmplifyProject } from '../create_empty_amplify_project.js'; import { @@ -91,7 +91,7 @@ void describe('amplify deploys', () => { }); await amplifyCli(['sandbox'], testProjectRoot) - .do(waitForSandboxDeployment()) + .do(waitForSandboxDeploymentToPrintTotalTime()) .do(interruptSandbox()) .do(rejectCleanupSandbox()) .run(); @@ -109,7 +109,7 @@ void describe('amplify deploys', () => { const processController = amplifyCli( ['sandbox', '--dirToWatch', 'amplify'], testProjectRoot - ).do(waitForSandboxDeployment()); + ).do(waitForSandboxDeploymentToPrintTotalTime()); for (const update of testProject.updates) { const fileToUpdate = pathToFileURL( @@ -120,7 +120,7 @@ void describe('amplify deploys', () => { ); processController - .do(updateBackendCode(fileToUpdate, updateSource)) + .do(updateFileContent(fileToUpdate, updateSource)) .do( ensureDeploymentTimeLessThan(update.deploymentThresholdInSeconds) );