Skip to content

Commit 49b5afd

Browse files
authored
chore(cli): update tests for future monorepo (#32185)
A number of our tests depend on the version of the `cloud-assembly-schema`, and some of our tests use `aws-cdk-lib` to generate Cloud Assemblies which has its own copy of `cloud-assembly-schema` built in. Because `cloud-assembly-schema` currently takes its schema version from its `package.json` version, and because the version of `cloud-assembly-schema/package.json` will be `0.0.0` once it has been moved into our new monorepo, these tests need to be changed to deal with this situation. The solution we're currently taking is to rewrite `manifest.json` and change the `version` in there to a different one. This PR also renames `expect(...).toBeCalledWith()` to `expect(...).toHaveBeenCalledWith()` because the jest linter requires is. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3e7ba32 commit 49b5afd

File tree

7 files changed

+149
-67
lines changed

7 files changed

+149
-67
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as fs from 'fs';
2+
import * as cxapi from '@aws-cdk/cx-api';
3+
import { CloudAssembly } from '../../lib/api/cxapp/cloud-assembly';
4+
5+
/**
6+
* The cloud-assembly-schema in the new monorepo will use its own package version as the schema version, which is always `0.0.0` when tests are running.
7+
*
8+
* If we want to test the CLI's behavior when presented with specific schema versions, we will have to
9+
* mutate `manifest.json` on disk after writing it, and write the schema version that we want to test for in there.
10+
*
11+
* After we raise the schema version in the file on disk from `0.0.0` to
12+
* `30.0.0`, `cx-api` will refuse to load `manifest.json` back, because the
13+
* version is higher than its own package version ("Maximum schema version
14+
* supported is 0.x.x, but found 30.0.0"), so we have to turn on `skipVersionCheck`.
15+
*/
16+
export function cxapiAssemblyWithForcedVersion(asm: cxapi.CloudAssembly, version: string) {
17+
rewriteManifestVersion(asm.directory, version);
18+
return new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true });
19+
}
20+
21+
/**
22+
* The CLI has its own CloudAssembly class which wraps the cxapi CloudAssembly class
23+
*/
24+
export function cliAssemblyWithForcedVersion(asm: CloudAssembly, version: string) {
25+
rewriteManifestVersion(asm.directory, version);
26+
return new CloudAssembly(new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true }));
27+
}
28+
29+
export function rewriteManifestVersion(directory: string, version: string) {
30+
const manifestFile = `${directory}/manifest.json`;
31+
const contents = JSON.parse(fs.readFileSync(`${directory}/manifest.json`, 'utf-8'));
32+
contents.version = version;
33+
fs.writeFileSync(manifestFile, JSON.stringify(contents, undefined, 2));
34+
}

packages/aws-cdk/test/api/cloud-assembly.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
33
import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
44
import { MockCloudExecutable } from '../util';
5+
import { cliAssemblyWithForcedVersion } from './assembly-versions';
56

67
// behave like v2
78
process.env.CXAPI_DISABLE_SELECT_BY_ID = '1';
@@ -261,5 +262,6 @@ async function testNestedCloudAssembly({ env }: { env?: string; versionReporting
261262
}],
262263
});
263264

264-
return cloudExec.synthesize();
265+
const asm = await cloudExec.synthesize();
266+
return cliAssemblyWithForcedVersion(asm, '30.0.0');
265267
}

packages/aws-cdk/test/api/cloud-executable.test.ts

+60-54
Original file line numberDiff line numberDiff line change
@@ -5,64 +5,76 @@ import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
55
import { registerContextProvider } from '../../lib/context-providers';
66
import { MockCloudExecutable } from '../util';
77

8+
// Apps on this version of the cxschema don't emit their own metadata resources
9+
// yet, so rely on the CLI to add the Metadata resource in.
10+
const SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF = '2.0.0';
11+
812
describe('AWS::CDK::Metadata', () => {
913
test('is generated for relocatable stacks from old frameworks', async () => {
10-
await withFakeCurrentCxVersion('2.0.0', async () => {
11-
const cx = await testCloudExecutable({ env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`, versionReporting: true });
12-
const cxasm = await cx.synthesize();
13-
14-
const result = cxasm.stackById('withouterrors').firstStack;
15-
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
16-
expect(metadata).toEqual({
17-
Type: 'AWS::CDK::Metadata',
18-
Properties: {
19-
// eslint-disable-next-line @typescript-eslint/no-require-imports
20-
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
21-
},
22-
Condition: 'CDKMetadataAvailable',
23-
});
24-
25-
expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
14+
const cx = await testCloudExecutable({
15+
env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`,
16+
versionReporting: true,
17+
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
18+
});
19+
const cxasm = await cx.synthesize();
20+
21+
const result = cxasm.stackById('withouterrors').firstStack;
22+
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
23+
expect(metadata).toEqual({
24+
Type: 'AWS::CDK::Metadata',
25+
Properties: {
26+
// eslint-disable-next-line @typescript-eslint/no-require-imports
27+
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
28+
},
29+
Condition: 'CDKMetadataAvailable',
2630
});
31+
32+
expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
2733
});
2834

2935
test('is generated for stacks in supported regions from old frameworks', async () => {
30-
await withFakeCurrentCxVersion('2.0.0', async () => {
31-
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
32-
const cxasm = await cx.synthesize();
33-
34-
const result = cxasm.stackById('withouterrors').firstStack;
35-
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
36-
expect(metadata).toEqual({
37-
Type: 'AWS::CDK::Metadata',
38-
Properties: {
39-
// eslint-disable-next-line @typescript-eslint/no-require-imports
40-
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
41-
},
42-
});
36+
const cx = await testCloudExecutable({
37+
env: 'aws://012345678912/us-east-1',
38+
versionReporting: true,
39+
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
40+
});
41+
const cxasm = await cx.synthesize();
42+
43+
const result = cxasm.stackById('withouterrors').firstStack;
44+
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
45+
expect(metadata).toEqual({
46+
Type: 'AWS::CDK::Metadata',
47+
Properties: {
48+
// eslint-disable-next-line @typescript-eslint/no-require-imports
49+
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
50+
},
4351
});
4452
});
4553

4654
test('is not generated for stacks in unsupported regions from old frameworks', async () => {
47-
await withFakeCurrentCxVersion('2.0.0', async () => {
48-
const cx = await testCloudExecutable({ env: 'aws://012345678912/bermuda-triangle-1337', versionReporting: true });
49-
const cxasm = await cx.synthesize();
50-
51-
const result = cxasm.stackById('withouterrors').firstStack;
52-
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
53-
expect(metadata).toBeUndefined();
55+
const cx = await testCloudExecutable({
56+
env: 'aws://012345678912/bermuda-triangle-1337',
57+
versionReporting: true,
58+
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
5459
});
60+
const cxasm = await cx.synthesize();
61+
62+
const result = cxasm.stackById('withouterrors').firstStack;
63+
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
64+
expect(metadata).toBeUndefined();
5565
});
5666

5767
test('is not generated for new frameworks', async () => {
58-
await withFakeCurrentCxVersion('8.0.0', async () => {
59-
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
60-
const cxasm = await cx.synthesize();
61-
62-
const result = cxasm.stackById('withouterrors').firstStack;
63-
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
64-
expect(metadata).toBeUndefined();
68+
const cx = await testCloudExecutable({
69+
env: 'aws://012345678912/us-east-1',
70+
versionReporting: true,
71+
schemaVersion: '8.0.0',
6572
});
73+
const cxasm = await cx.synthesize();
74+
75+
const result = cxasm.stackById('withouterrors').firstStack;
76+
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
77+
expect(metadata).toBeUndefined();
6678
});
6779
});
6880

@@ -109,7 +121,10 @@ test('fails if lookups are disabled and missing context is synthesized', async (
109121
await expect(cloudExecutable.synthesize()).rejects.toThrow(/Context lookups have been disabled/);
110122
});
111123

112-
async function testCloudExecutable({ env, versionReporting = true }: { env?: string; versionReporting?: boolean } = {}) {
124+
async function testCloudExecutable(
125+
{ env, versionReporting = true, schemaVersion }:
126+
{ env?: string; versionReporting?: boolean; schemaVersion?: string } = {},
127+
) {
113128
const cloudExec = new MockCloudExecutable({
114129
stacks: [{
115130
stackName: 'withouterrors',
@@ -129,18 +144,9 @@ async function testCloudExecutable({ env, versionReporting = true }: { env?: str
129144
],
130145
},
131146
}],
147+
schemaVersion,
132148
});
133149
cloudExec.configuration.settings.set(['versionReporting'], versionReporting);
134150

135151
return cloudExec;
136152
}
137-
138-
async function withFakeCurrentCxVersion<A>(version: string, block: () => Promise<A>): Promise<A> {
139-
const currentVersionFn = cxschema.Manifest.version;
140-
cxschema.Manifest.version = () => version;
141-
try {
142-
return await block();
143-
} finally {
144-
cxschema.Manifest.version = currentVersionFn;
145-
}
146-
}

packages/aws-cdk/test/api/exec.test.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable import/order */
22
jest.mock('child_process');
33
import { bockfs } from '@aws-cdk/cdk-build-tools';
4-
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
4+
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
55
import * as cdk from 'aws-cdk-lib';
66
import * as semver from 'semver';
77
import * as sinon from 'sinon';
@@ -13,6 +13,7 @@ import { testAssembly } from '../util';
1313
import { mockSpawn } from '../util/mock-child_process';
1414
import { MockSdkProvider } from '../util/mock-sdk';
1515
import { RWLock } from '../../lib/api/util/rwlock';
16+
import { rewriteManifestVersion } from './assembly-versions';
1617

1718
let sdkProvider: MockSdkProvider;
1819
let config: Configuration;
@@ -76,6 +77,8 @@ test('cli throws when manifest version > schema version', async () => {
7677
mockVersionNumber.restore();
7778
}
7879

80+
rewriteManifestVersion('cdk.out', `${mockManifestVersion}`);
81+
7982
const expectedError = 'This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.'
8083
+ `\n(Cloud assembly schema version mismatch: Maximum schema version supported is ${semver.major(currentSchemaVersion)}.x.x, but found ${mockManifestVersion})`;
8184

@@ -90,20 +93,31 @@ test('cli does not throw when manifest version = schema version', async () => {
9093
const app = createApp();
9194
app.synth();
9295

96+
rewriteManifestVersionToOurs();
97+
9398
config.settings.set(['app'], 'cdk.out');
9499

95100
const { lock } = await execProgram(sdkProvider, config);
96101
await lock.release();
97102

98103
}, TEN_SECOND_TIMEOUT);
99104

100-
test('cli does not throw when manifest version < schema version', async () => {
105+
// Why do we have to do something here at all? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
106+
// which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
107+
// uses.
108+
//
109+
// Since our Cloud Assembly Schema version will be `0.0.0` and there is no such thing as `-1.0.0`, this test doesn't
110+
// make any sense anymore.
111+
// eslint-disable-next-line jest/no-disabled-tests
112+
test.skip('cli does not throw when manifest version < schema version', async () => {
101113

102114
const app = createApp();
103115
const currentSchemaVersion = cxschema.Manifest.version();
104116

105117
app.synth();
106118

119+
rewriteManifestVersionToOurs();
120+
107121
config.settings.set(['app'], 'cdk.out');
108122

109123
// this mock will cause the cli to think its exepcted schema version is
@@ -130,6 +144,7 @@ test('bypasses synth when app points to a cloud assembly', async () => {
130144
// GIVEN
131145
config.settings.set(['app'], 'cdk.out');
132146
writeOutputAssembly();
147+
rewriteManifestVersionToOurs();
133148

134149
// WHEN
135150
const { assembly: cloudAssembly, lock } = await execProgram(sdkProvider, config);
@@ -259,4 +274,18 @@ function writeOutputAssembly() {
259274
stacks: [],
260275
});
261276
bockfs.write('/home/project/cdk.out/manifest.json', JSON.stringify(asm.manifest));
277+
rewriteManifestVersionToOurs(bockfs.path('/home/project/cdk.out'));
262278
}
279+
280+
/**
281+
* Rewrite the manifest schema version in the given directory to match the version number we expect (probably `0.0.0`).
282+
*
283+
* Why do we have to do this? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
284+
* which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
285+
* uses.
286+
*
287+
* If we don't do this, every time we load a Cloud Assembly the code will say "Maximum schema version supported is 0.x.x, but found 30.0.0".0
288+
*/
289+
function rewriteManifestVersionToOurs(dir: string = 'cdk.out') {
290+
rewriteManifestVersion(dir, cxschema.Manifest.version());
291+
}

packages/aws-cdk/test/build.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ describe('buildAllStackAssets', () => {
2222
.toBeUndefined();
2323

2424
expect(buildStackAssets).toBeCalledTimes(3);
25-
expect(buildStackAssets).toBeCalledWith(A);
26-
expect(buildStackAssets).toBeCalledWith(B);
27-
expect(buildStackAssets).toBeCalledWith(C);
25+
expect(buildStackAssets).toHaveBeenCalledWith(A);
26+
expect(buildStackAssets).toHaveBeenCalledWith(B);
27+
expect(buildStackAssets).toHaveBeenCalledWith(C);
2828
});
2929

3030
test('errors', async () => {

packages/aws-cdk/test/cdk-toolkit.test.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ describe('watch', () => {
10621062
});
10631063
fakeChokidarWatcherOn.readyCallback();
10641064

1065-
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 }));
1065+
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ concurrency: 3 }));
10661066
});
10671067

10681068
describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => {
@@ -1078,7 +1078,7 @@ describe('watch', () => {
10781078
});
10791079
fakeChokidarWatcherOn.readyCallback();
10801080

1081-
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
1081+
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
10821082
});
10831083
});
10841084

@@ -1094,7 +1094,7 @@ describe('watch', () => {
10941094
});
10951095
fakeChokidarWatcherOn.readyCallback();
10961096

1097-
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
1097+
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
10981098
});
10991099

11001100
test('respects HotswapMode.FALL_BACK', async () => {
@@ -1109,7 +1109,7 @@ describe('watch', () => {
11091109
});
11101110
fakeChokidarWatcherOn.readyCallback();
11111111

1112-
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
1112+
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
11131113
});
11141114

11151115
test('respects HotswapMode.FULL_DEPLOYMENT', async () => {
@@ -1124,7 +1124,7 @@ describe('watch', () => {
11241124
});
11251125
fakeChokidarWatcherOn.readyCallback();
11261126

1127-
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
1127+
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
11281128
});
11291129

11301130
describe('with file change events', () => {
@@ -1677,7 +1677,13 @@ class FakeCloudFormation extends Deployments {
16771677
expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]);
16781678
}
16791679

1680-
expect(options.notificationArns).toEqual(this.expectedNotificationArns);
1680+
// In these tests, we don't make a distinction here between `undefined` and `[]`.
1681+
//
1682+
// In tests `deployStack` itself we do treat `undefined` and `[]` differently,
1683+
// and in `aws-cdk-lib` we emit them under different conditions. But this test
1684+
// without normalization depends on a version of `aws-cdk-lib` that hasn't been
1685+
// released yet.
1686+
expect(options.notificationArns ?? []).toEqual(this.expectedNotificationArns ?? []);
16811687
return Promise.resolve({
16821688
type: 'did-deploy-stack',
16831689
stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`,

packages/aws-cdk/test/util.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { type CloudAssembly, CloudAssemblyBuilder, type CloudFormationStackArtif
55
import { MockSdkProvider } from './util/mock-sdk';
66
import { CloudExecutable } from '../lib/api/cxapp/cloud-executable';
77
import { Configuration } from '../lib/settings';
8+
import { cxapiAssemblyWithForcedVersion } from './api/assembly-versions';
89

910
export const DEFAULT_FAKE_TEMPLATE = { No: 'Resources' };
1011

12+
const SOME_RECENT_SCHEMA_VERSION = '30.0.0';
13+
1114
export interface TestStackArtifact {
1215
stackName: string;
1316
template?: any;
@@ -30,6 +33,7 @@ export interface TestAssembly {
3033
stacks: TestStackArtifact[];
3134
missing?: MissingContext[];
3235
nestedAssemblies?: TestAssembly[];
36+
schemaVersion?: string;
3337
}
3438

3539
export class MockCloudExecutable extends CloudExecutable {
@@ -136,7 +140,8 @@ export function testAssembly(assembly: TestAssembly): CloudAssembly {
136140
});
137141
}
138142

139-
return builder.buildAssembly();
143+
const asm = builder.buildAssembly();
144+
return cxapiAssemblyWithForcedVersion(asm, assembly.schemaVersion ?? SOME_RECENT_SCHEMA_VERSION);
140145
}
141146

142147
/**

0 commit comments

Comments
 (0)