-
Notifications
You must be signed in to change notification settings - Fork 825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: templategen command e2e integration tests #13984
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import path from 'node:path'; | ||
import assert from 'node:assert'; | ||
import { createNewProjectDir, npmInstall, deleteS3Bucket } from '@aws-amplify/amplify-e2e-core'; | ||
import { assertDefaultGen1Setup } from '../assertions'; | ||
import { setupAndPushDefaultGen1Project, runCodegenCommand, runGen2SandboxCommand, cleanupProjects } from '..'; | ||
import { copyFunctionFile } from '../function_utils'; | ||
import { copyGen1Schema } from '../api_utils'; | ||
import { updatePackageDependency } from '../updatePackageJson'; | ||
import { createS3Bucket } from '../sdk_calls'; | ||
import { runTemplategenCommand, stackRefactor } from '../templategen'; | ||
|
||
void describe('Templategen E2E tests', () => { | ||
void describe('Full Migration Templategen Flow', () => { | ||
let projRoot: string; | ||
let projName: string; | ||
let bucketName: string; | ||
|
||
beforeEach(async () => { | ||
const baseDir = process.env.INIT_CWD ?? process.cwd(); | ||
projRoot = await createNewProjectDir('templategen_e2e_flow_test', path.join(baseDir, '..', '..')); | ||
projName = `test${Math.floor(Math.random() * 1000000)}`; | ||
bucketName = `testbucket${Math.floor(Math.random() * 1000000)}`; | ||
}); | ||
|
||
afterEach(async () => { | ||
await cleanupProjects(projRoot); | ||
await deleteS3Bucket(bucketName); | ||
}); | ||
|
||
void it('should init a project & add auth, function, storage, api with defaults & perform full migration templategen flow', async () => { | ||
await setupAndPushDefaultGen1Project(projRoot, projName); | ||
const { gen1StackName, gen1FunctionName, gen1Region } = await assertDefaultGen1Setup(projRoot); | ||
await createS3Bucket(bucketName, gen1Region); | ||
assert(gen1StackName); | ||
await runCodegenCommand(projRoot); | ||
await copyFunctionFile(projRoot, 'function', gen1FunctionName); | ||
await copyGen1Schema(projRoot, projName); | ||
// TODO: replace below line with correct package version | ||
await updatePackageDependency(projRoot, '@aws-amplify/backend'); | ||
await npmInstall(projRoot); | ||
const gen2StackName = await runGen2SandboxCommand(projRoot); | ||
assert(gen2StackName); | ||
await runTemplategenCommand(projRoot, gen1StackName, gen2StackName); | ||
await stackRefactor(projRoot, 'auth', bucketName); | ||
await stackRefactor(projRoot, 'storage', bucketName); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import execa from 'execa'; | ||
import path from 'node:path'; | ||
import * as fs from 'fs-extra'; | ||
import { getNpxPath, retry, RetrySettings } from '@aws-amplify/amplify-e2e-core'; | ||
import { runGen2SandboxCommand } from '.'; | ||
|
||
export type EnvVariableAction = 'SET' | 'DELETE'; | ||
export type RefactorCategory = 'auth' | 'storage'; | ||
|
||
const RETRY_CONFIG: RetrySettings = { | ||
times: 50, | ||
delayMS: 1000, // 1 second | ||
timeoutMS: 1000 * 60 * 5, // 5 minutes | ||
stopOnError: true, | ||
}; | ||
|
||
const STATUS_COMPLETE = 'COMPLETE'; | ||
const STATUS_IN_PROGRESS = 'IN_PROGRESS'; | ||
const STATUS_FAILED = 'FAILED'; | ||
|
||
export function runTemplategenCommand(cwd: string, gen1StackName: string, gen2StackName: string) { | ||
const parentDir = path.resolve(cwd, '..'); | ||
const processResult = execa.sync( | ||
getNpxPath(), | ||
['--prefix', parentDir, '@aws-amplify/migrate', 'to-gen-2', 'generate-templates', '--from', gen1StackName, '--to', gen2StackName], | ||
{ | ||
cwd, | ||
env: { ...process.env, npm_config_user_agent: 'npm' }, | ||
encoding: 'utf-8', | ||
}, | ||
); | ||
|
||
if (processResult.exitCode !== 0) { | ||
throw new Error(`Templategen command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`); | ||
} | ||
} | ||
|
||
function uncommentBucketNameLineFromBackendFile(projRoot: string) { | ||
const backendFilePath = path.join(projRoot, 'amplify', 'backend.ts'); | ||
const backendFileContent = fs.readFileSync(backendFilePath, 'utf8'); | ||
const regex = /^\s*\/\/\s*(s3Bucket\.bucketName)/m; | ||
const updatedBackendFileContent = backendFileContent.replace(regex, '$1'); | ||
fs.writeFileSync(backendFilePath, updatedBackendFileContent); | ||
} | ||
|
||
function toggleEnvVariable(name: string, option: EnvVariableAction, value?: string) { | ||
if (option === 'SET') { | ||
process.env[name] = value; | ||
} else if (option === 'DELETE') { | ||
delete process.env[name]; | ||
} | ||
} | ||
|
||
function extractContent(readmeContent: string, startRegex: string, endRegex: string) { | ||
const pattern = new RegExp(`${startRegex}([\\s\\S]*?)${endRegex}`, 'i'); | ||
const match = readmeContent.match(pattern); | ||
|
||
if (match && match[1]) { | ||
return match[1].trim(); | ||
} | ||
throw new Error('README file parsing failed to get the stack refactor commands'); | ||
} | ||
|
||
function extractCommands(readmeContent: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
const pattern = /```([\s\S]*?)```/g; | ||
const matches = readmeContent.matchAll(pattern); | ||
const commands = []; | ||
|
||
for (const match of matches) { | ||
if (match[1]) { | ||
commands.push(match[1].trim()); | ||
} | ||
} | ||
if (commands.length === 0) { | ||
throw new Error('README file parsing failed to get the stack refactor commands'); | ||
} | ||
return commands; | ||
} | ||
|
||
function getCommandsFromReadme(readmeContent: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the README parsing logic is difficult to follow here. Unless a code reviewer knows a logic beforehand, it is not trivial to know what command 1,2,3 are. Some options here:
I would suggest the black box testing for simplicity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a great idea. We can do both kinds of testing based on what we are testing. For the happy path, we can do black box testing since we only care about the end state. We already have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, will keep the logic for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah in the black box mode, we don't need the names. Iterating them one by one would suffice. |
||
const step1Content = extractContent(readmeContent, '### STEP 1', '#### Rollback step'); | ||
const step2Content = extractContent(readmeContent, '### STEP 2', '#### Rollback step'); | ||
const step3Content = extractContent(readmeContent, '### STEP 3', '#### Rollback step'); | ||
const step1Commands = extractCommands(step1Content); | ||
const step2commands = extractCommands(step2Content); | ||
const step3Commands = extractCommands(step3Content); | ||
return { step1Commands, step2commands, step3Commands }; | ||
} | ||
|
||
async function executeCommand(command: string, cwd?: string) { | ||
cwd = cwd ?? process.cwd(); | ||
const processResult = execa.sync(command, { | ||
cwd, | ||
env: { ...process.env, npm_config_user_agent: 'npm' }, | ||
encoding: 'utf-8', | ||
shell: true, | ||
}); | ||
|
||
if (processResult.exitCode === 0) { | ||
return processResult.stdout; | ||
} else { | ||
throw new Error(`Command exit code: ${processResult.exitCode}, message: ${processResult.stderr}`); | ||
} | ||
} | ||
|
||
async function executeCreateStackRefactorCallCommand(command: string, cwd: string) { | ||
const processResult = JSON.parse(await executeCommand(command, cwd)); | ||
const stackRefactorId = processResult.StackRefactorId; | ||
return stackRefactorId; | ||
} | ||
|
||
async function assertStepCompletion(command: string) { | ||
const processResult = JSON.parse(await executeCommand(command)); | ||
return processResult.Stacks[0].StackStatus; | ||
} | ||
|
||
async function assertRefactorStepCompletion(command: string) { | ||
const processResult = JSON.parse(await executeCommand(command)); | ||
return processResult.Status; | ||
} | ||
|
||
async function executeStep1(cwd: string, commands: string[]) { | ||
await executeCommand(commands[0], cwd); | ||
await retry( | ||
() => assertStepCompletion(commands[1]), | ||
(status) => status.includes(STATUS_COMPLETE) && !status.includes(STATUS_IN_PROGRESS), | ||
RETRY_CONFIG, | ||
(status) => status.includes(STATUS_FAILED), | ||
); | ||
} | ||
|
||
async function executeStep2(cwd: string, commands: string[]) { | ||
await executeCommand(commands[0], cwd); | ||
await retry( | ||
() => assertStepCompletion(commands[1]), | ||
(status) => status.includes(STATUS_COMPLETE) && !status.includes(STATUS_IN_PROGRESS), | ||
RETRY_CONFIG, | ||
(status) => status.includes(STATUS_FAILED), | ||
); | ||
} | ||
|
||
async function executeStep3(cwd: string, commands: string[], bucketName: string) { | ||
toggleEnvVariable('BUCKET_NAME', 'SET', bucketName); | ||
await executeCommand(commands[1], cwd); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we name the commands instead of accessing by indices for easier understanding: e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we follow the Black Box Testing approach, this approach is out of scope then? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup! |
||
await executeCommand(commands[2], cwd); | ||
const stackRefactorId = await executeCreateStackRefactorCallCommand(commands[3], cwd); | ||
toggleEnvVariable('STACK_REFACTOR_ID', 'SET', stackRefactorId); | ||
await retry( | ||
() => assertRefactorStepCompletion(commands[5]), | ||
(status) => status.includes(STATUS_COMPLETE) && !status.includes(STATUS_IN_PROGRESS), | ||
RETRY_CONFIG, | ||
(status) => status.includes(STATUS_FAILED), | ||
); | ||
await executeCommand(commands[6], cwd); | ||
await retry( | ||
() => assertRefactorStepCompletion(commands[7]), | ||
(status) => status.includes(STATUS_COMPLETE) && !status.includes(STATUS_IN_PROGRESS), | ||
RETRY_CONFIG, | ||
(status) => status.includes(STATUS_FAILED), | ||
); | ||
} | ||
|
||
export async function stackRefactor(projRoot: string, category: RefactorCategory, bucketName: string) { | ||
const readmeFilePath = path.join(projRoot, '.amplify', 'migration', 'templates', category, 'MIGRATION_README.md'); | ||
const readmeContent = fs.readFileSync(readmeFilePath, 'utf-8'); | ||
const { step1Commands, step2commands, step3Commands } = getCommandsFromReadme(readmeContent); | ||
|
||
await executeStep1(projRoot, step1Commands); | ||
await executeStep2(projRoot, step2commands); | ||
await executeStep3(projRoot, step3Commands, bucketName); | ||
|
||
if (category == 'storage') { | ||
await uncommentBucketNameLineFromBackendFile(projRoot); | ||
} | ||
|
||
await runGen2SandboxCommand(projRoot); | ||
|
||
toggleEnvVariable('BUCKET_NAME', 'DELETE'); | ||
toggleEnvVariable('STACK_REFACTOR_ID', 'DELETE'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!