Skip to content

Commit 1f56a5f

Browse files
author
Kamil Sobol
authored
Add hotswap to canaries (#2289)
* Add hotswap to canaries * try this * try this * try this * try this * try this * change this * pr feedback * pr feedback
1 parent cfdc854 commit 1f56a5f

File tree

13 files changed

+242
-3
lines changed

13 files changed

+242
-3
lines changed

.changeset/twenty-baboons-rule.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.eslint_dictionary.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@
7575
"homedir",
7676
"hotfix",
7777
"hotswap",
78+
"hotswappable",
7879
"hotswapped",
80+
"hotswapping",
7981
"iamv2",
8082
"identitypool",
8183
"idps",

packages/integration-tests/src/process-controller/predicated_action_macros.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export const waitForSandboxToBecomeIdle = () =>
3030
'Watching for file changes...'
3131
);
3232

33+
/**
34+
* Reusable predicates: Wait for sandbox to indicate that it's executing hotswap deployment, i.e. "hotswapping resources:"
35+
*/
36+
export const waitForSandboxToBeginHotswappingResources = () =>
37+
new PredicatedActionBuilder().waitForLineIncludes('hotswapping resources:');
38+
3339
/**
3440
* Reusable predicated action: Wait for sandbox delete to prompt to delete all the resource and respond with yes
3541
*/

packages/integration-tests/src/test-e2e/sandbox/sandbox.test.template.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
interruptSandbox,
1414
replaceFiles,
1515
waitForConfigUpdateAfterDeployment,
16+
waitForSandboxToBeginHotswappingResources,
1617
} from '../../process-controller/predicated_action_macros.js';
1718
import { BackendIdentifier } from '@aws-amplify/plugin-types';
1819
import { testConcurrencyLevel } from '../test_concurrency.js';
@@ -93,7 +94,12 @@ export const defineSandboxTest = (testProjectCreator: TestProjectCreator) => {
9394
for (const update of updates) {
9495
processController
9596
.do(replaceFiles(update.replacements))
96-
.do(ensureDeploymentTimeLessThan(update.deployThresholdSec));
97+
.do(waitForSandboxToBeginHotswappingResources());
98+
if (update.deployThresholdSec) {
99+
processController.do(
100+
ensureDeploymentTimeLessThan(update.deployThresholdSec)
101+
);
102+
}
97103
}
98104

99105
// Execute the process.

packages/integration-tests/src/test-live-dependency-health-checks/health_checks.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { afterEach, before, beforeEach, describe, it } from 'node:test';
22
import fs from 'fs/promises';
33
import path from 'path';
4-
import os from 'os';
4+
import os, { userInfo } from 'os';
55
import { execa } from 'execa';
66
import { ampxCli } from '../process-controller/process_controller.js';
77
import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js';
@@ -14,11 +14,15 @@ import {
1414
import {
1515
confirmDeleteSandbox,
1616
interruptSandbox,
17+
replaceFiles,
1718
waitForSandboxDeploymentToPrintTotalTime,
19+
waitForSandboxToBeginHotswappingResources,
1820
} from '../process-controller/predicated_action_macros.js';
1921
import { BackendIdentifierConversions } from '@aws-amplify/platform-core';
2022
import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js';
2123
import { amplifyAtTag } from '../constants.js';
24+
import { FunctionCodeHotswapTestProjectCreator } from '../test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.js';
25+
import { BackendIdentifier } from '@aws-amplify/plugin-types';
2226

2327
const cfnClient = new CloudFormationClient(e2eToolingClientConfig);
2428

@@ -134,4 +138,47 @@ void describe('Live dependency health checks', { concurrency: true }, () => {
134138
.run();
135139
});
136140
});
141+
142+
void describe('sandbox hotswap', () => {
143+
let tempDir: string;
144+
145+
beforeEach(async () => {
146+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-amplify'));
147+
});
148+
149+
afterEach(async () => {
150+
await fs.rm(tempDir, { recursive: true });
151+
});
152+
153+
void it('can hotswap function code', async () => {
154+
const projectCreator = new FunctionCodeHotswapTestProjectCreator();
155+
const testProject = await projectCreator.createProject(tempDir);
156+
157+
const sandboxBackendIdentifier: BackendIdentifier = {
158+
type: 'sandbox',
159+
namespace: testProject.name,
160+
name: userInfo().username,
161+
};
162+
163+
await testProject.deploy(sandboxBackendIdentifier);
164+
165+
const processController = ampxCli(
166+
['sandbox', '--dirToWatch', 'amplify'],
167+
testProject.projectDirPath
168+
);
169+
const updates = await testProject.getUpdates();
170+
for (const update of updates) {
171+
processController
172+
.do(replaceFiles(update.replacements))
173+
.do(waitForSandboxToBeginHotswappingResources())
174+
.do(waitForSandboxDeploymentToPrintTotalTime());
175+
}
176+
177+
// Execute the process.
178+
await processController.do(interruptSandbox()).run();
179+
180+
// Clean up
181+
await testProject.tearDown(sandboxBackendIdentifier);
182+
});
183+
});
137184
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Projects in this directory are meant for `live-dependency-health-checks` (aka canaries).
2+
3+
1. These projects must not be used in e2e tests to provide deep functional coverage.
4+
2. These projects must be lightweight to provide fast runtime and stability.
5+
3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`).
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import fs from 'fs/promises';
2+
import { createEmptyAmplifyProject } from '../create_empty_amplify_project.js';
3+
import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
4+
import { TestProjectBase, TestProjectUpdate } from '../test_project_base.js';
5+
import { fileURLToPath, pathToFileURL } from 'node:url';
6+
import path from 'path';
7+
import { TestProjectCreator } from '../test_project_creator.js';
8+
import { AmplifyClient } from '@aws-sdk/client-amplify';
9+
import { e2eToolingClientConfig } from '../../e2e_tooling_client_config.js';
10+
import { execa } from 'execa';
11+
12+
/**
13+
* Creates test projects with function hotswap.
14+
*/
15+
export class FunctionCodeHotswapTestProjectCreator
16+
implements TestProjectCreator
17+
{
18+
readonly name = 'function-code-hotswap';
19+
20+
/**
21+
* Creates project creator.
22+
*/
23+
constructor(
24+
private readonly cfnClient: CloudFormationClient = new CloudFormationClient(
25+
e2eToolingClientConfig
26+
),
27+
private readonly amplifyClient: AmplifyClient = new AmplifyClient(
28+
e2eToolingClientConfig
29+
)
30+
) {}
31+
32+
createProject = async (e2eProjectDir: string): Promise<TestProjectBase> => {
33+
const { projectName, projectRoot, projectAmplifyDir } =
34+
await createEmptyAmplifyProject(this.name, e2eProjectDir);
35+
36+
const project = new FunctionCodeHotswapTestTestProject(
37+
projectName,
38+
projectRoot,
39+
projectAmplifyDir,
40+
this.cfnClient,
41+
this.amplifyClient
42+
);
43+
await fs.cp(
44+
project.sourceProjectAmplifyDirURL,
45+
project.projectAmplifyDirPath,
46+
{
47+
recursive: true,
48+
}
49+
);
50+
51+
// we're not starting from create flow. install latest versions of dependencies.
52+
await execa(
53+
'npm',
54+
[
55+
'install',
56+
'@aws-amplify/backend',
57+
'@aws-amplify/backend-cli',
58+
'aws-cdk@^2',
59+
'aws-cdk-lib@^2',
60+
'constructs@^10.0.0',
61+
'typescript@^5.0.0',
62+
'tsx',
63+
'esbuild',
64+
],
65+
{
66+
cwd: projectRoot,
67+
stdio: 'inherit',
68+
}
69+
);
70+
71+
return project;
72+
};
73+
}
74+
75+
/**
76+
* Test project with function hotswap.
77+
*/
78+
class FunctionCodeHotswapTestTestProject extends TestProjectBase {
79+
// Note that this is pointing to the non-compiled project directory
80+
// This allows us to test that we are able to deploy js, cjs, ts, etc. without compiling with tsc first
81+
readonly sourceProjectRootPath =
82+
'../../../src/test-projects/live-dependency-health-checks-projects/function-code-hotswap';
83+
84+
readonly sourceProjectRootURL: URL = new URL(
85+
this.sourceProjectRootPath,
86+
import.meta.url
87+
);
88+
89+
readonly sourceProjectAmplifyDirURL: URL = new URL(
90+
`${this.sourceProjectRootPath}/amplify`,
91+
import.meta.url
92+
);
93+
94+
private readonly sourceProjectUpdateDirURL: URL = new URL(
95+
`${this.sourceProjectRootPath}/hotswap-update-files`,
96+
import.meta.url
97+
);
98+
99+
/**
100+
* Create a test project instance.
101+
*/
102+
constructor(
103+
name: string,
104+
projectDirPath: string,
105+
projectAmplifyDirPath: string,
106+
cfnClient: CloudFormationClient,
107+
amplifyClient: AmplifyClient
108+
) {
109+
super(
110+
name,
111+
projectDirPath,
112+
projectAmplifyDirPath,
113+
cfnClient,
114+
amplifyClient
115+
);
116+
}
117+
118+
/**
119+
* @inheritdoc
120+
*/
121+
override async getUpdates(): Promise<TestProjectUpdate[]> {
122+
return [
123+
{
124+
replacements: [
125+
this.getUpdateReplacementDefinition('func-src/handler.ts'),
126+
],
127+
},
128+
];
129+
}
130+
131+
private getUpdateReplacementDefinition = (suffix: string) => ({
132+
source: this.getSourceProjectUpdatePath(suffix),
133+
destination: this.getTestProjectPath(suffix),
134+
});
135+
136+
private getSourceProjectUpdatePath = (suffix: string) =>
137+
pathToFileURL(
138+
path.join(fileURLToPath(this.sourceProjectUpdateDirURL), suffix)
139+
);
140+
141+
private getTestProjectPath = (suffix: string) =>
142+
pathToFileURL(path.join(this.projectAmplifyDirPath, suffix));
143+
}

packages/integration-tests/src/test-project-setup/test_project_base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export type TestProjectUpdate = {
4545
* Windows has a separate threshold because it is consistently slower than other platforms
4646
* https://github.com/microsoft/Windows-Dev-Performance/issues/17
4747
*/
48-
deployThresholdSec: PlatformDeploymentThresholds;
48+
deployThresholdSec?: PlatformDeploymentThresholds;
4949
};
5050

5151
/**
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Projects in this directory are meant for `live-dependency-health-checks` (aka canaries).
2+
3+
1. These projects must not be used in e2e tests to provide deep functional coverage.
4+
2. These projects must be lightweight to provide fast runtime and stability.
5+
3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { defineBackend } from '@aws-amplify/backend';
2+
import { nodeFunc } from './function.js';
3+
4+
defineBackend({ nodeFunc });

0 commit comments

Comments
 (0)