Skip to content
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

chore(cli): update tests for future monorepo #32185

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/aws-cdk/test/api/assembly-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as fs from 'fs';
import * as cxapi from '@aws-cdk/cx-api';
import { CloudAssembly } from '../../lib/api/cxapp/cloud-assembly';

/**
* 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.
*
* If we want to test the CLI's behavior when presented with specific schema versions, we will have to
* mutate `manifest.json` on disk after writing it, and write the schema version that we want to test for in there.
*
* After we raise the schema version in the file on disk from `0.0.0` to
* `30.0.0`, `cx-api` will refuse to load `manifest.json` back, because the
* version is higher than its own package version ("Maximum schema version
* supported is 0.x.x, but found 30.0.0"), so we have to turn on `skipVersionCheck`.
*/
export function cxapiAssemblyWithForcedVersion(asm: cxapi.CloudAssembly, version: string) {
rewriteManifestVersion(asm.directory, version);
return new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true });
}

/**
* The CLI has its own CloudAssembly class which wraps the cxapi CloudAssembly class
*/
export function cliAssemblyWithForcedVersion(asm: CloudAssembly, version: string) {
rewriteManifestVersion(asm.directory, version);
return new CloudAssembly(new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true }));
}

export function rewriteManifestVersion(directory: string, version: string) {
const manifestFile = `${directory}/manifest.json`;
const contents = JSON.parse(fs.readFileSync(`${directory}/manifest.json`, 'utf-8'));
contents.version = version;
fs.writeFileSync(manifestFile, JSON.stringify(contents, undefined, 2));
}
4 changes: 3 additions & 1 deletion packages/aws-cdk/test/api/cloud-assembly.test.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
import { MockCloudExecutable } from '../util';
import { cliAssemblyWithForcedVersion } from './assembly-versions';

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

return cloudExec.synthesize();
const asm = await cloudExec.synthesize();
return cliAssemblyWithForcedVersion(asm, '30.0.0');
}
114 changes: 60 additions & 54 deletions packages/aws-cdk/test/api/cloud-executable.test.ts
Original file line number Diff line number Diff line change
@@ -5,64 +5,76 @@ import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
import { registerContextProvider } from '../../lib/context-providers';
import { MockCloudExecutable } from '../util';

// Apps on this version of the cxschema don't emit their own metadata resources
// yet, so rely on the CLI to add the Metadata resource in.
const SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF = '2.0.0';

describe('AWS::CDK::Metadata', () => {
test('is generated for relocatable stacks from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`, versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
Condition: 'CDKMetadataAvailable',
});

expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
const cx = await testCloudExecutable({
env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`,
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
Condition: 'CDKMetadataAvailable',
});

expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
});

test('is generated for stacks in supported regions from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
});
const cx = await testCloudExecutable({
env: 'aws://012345678912/us-east-1',
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
});
});

test('is not generated for stacks in unsupported regions from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/bermuda-triangle-1337', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
const cx = await testCloudExecutable({
env: 'aws://012345678912/bermuda-triangle-1337',
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
});

test('is not generated for new frameworks', async () => {
await withFakeCurrentCxVersion('8.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
const cx = await testCloudExecutable({
env: 'aws://012345678912/us-east-1',
versionReporting: true,
schemaVersion: '8.0.0',
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
});
});

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

async function testCloudExecutable({ env, versionReporting = true }: { env?: string; versionReporting?: boolean } = {}) {
async function testCloudExecutable(
{ env, versionReporting = true, schemaVersion }:
{ env?: string; versionReporting?: boolean; schemaVersion?: string } = {},
) {
const cloudExec = new MockCloudExecutable({
stacks: [{
stackName: 'withouterrors',
@@ -129,18 +144,9 @@ async function testCloudExecutable({ env, versionReporting = true }: { env?: str
],
},
}],
schemaVersion,
});
cloudExec.configuration.settings.set(['versionReporting'], versionReporting);

return cloudExec;
}

async function withFakeCurrentCxVersion<A>(version: string, block: () => Promise<A>): Promise<A> {
const currentVersionFn = cxschema.Manifest.version;
cxschema.Manifest.version = () => version;
try {
return await block();
} finally {
cxschema.Manifest.version = currentVersionFn;
}
}
33 changes: 31 additions & 2 deletions packages/aws-cdk/test/api/exec.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable import/order */
jest.mock('child_process');
import { bockfs } from '@aws-cdk/cdk-build-tools';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cdk from 'aws-cdk-lib';
import * as semver from 'semver';
import * as sinon from 'sinon';
@@ -13,6 +13,7 @@ import { testAssembly } from '../util';
import { mockSpawn } from '../util/mock-child_process';
import { MockSdkProvider } from '../util/mock-sdk';
import { RWLock } from '../../lib/api/util/rwlock';
import { rewriteManifestVersion } from './assembly-versions';

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

rewriteManifestVersion('cdk.out', `${mockManifestVersion}`);

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

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

rewriteManifestVersionToOurs();

config.settings.set(['app'], 'cdk.out');

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

}, TEN_SECOND_TIMEOUT);

test('cli does not throw when manifest version < schema version', async () => {
// Why do we have to do something here at all? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
// which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
// uses.
//
// 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
// make any sense anymore.
// eslint-disable-next-line jest/no-disabled-tests
test.skip('cli does not throw when manifest version < schema version', async () => {

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

app.synth();

rewriteManifestVersionToOurs();

config.settings.set(['app'], 'cdk.out');

// 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 () => {
// GIVEN
config.settings.set(['app'], 'cdk.out');
writeOutputAssembly();
rewriteManifestVersionToOurs();

// WHEN
const { assembly: cloudAssembly, lock } = await execProgram(sdkProvider, config);
@@ -259,4 +274,18 @@ function writeOutputAssembly() {
stacks: [],
});
bockfs.write('/home/project/cdk.out/manifest.json', JSON.stringify(asm.manifest));
rewriteManifestVersionToOurs(bockfs.path('/home/project/cdk.out'));
}

/**
* Rewrite the manifest schema version in the given directory to match the version number we expect (probably `0.0.0`).
*
* Why do we have to do this? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
* which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
* uses.
*
* 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
*/
function rewriteManifestVersionToOurs(dir: string = 'cdk.out') {
rewriteManifestVersion(dir, cxschema.Manifest.version());
}
6 changes: 3 additions & 3 deletions packages/aws-cdk/test/build.test.ts
Original file line number Diff line number Diff line change
@@ -22,9 +22,9 @@ describe('buildAllStackAssets', () => {
.toBeUndefined();

expect(buildStackAssets).toBeCalledTimes(3);
expect(buildStackAssets).toBeCalledWith(A);
expect(buildStackAssets).toBeCalledWith(B);
expect(buildStackAssets).toBeCalledWith(C);
expect(buildStackAssets).toHaveBeenCalledWith(A);
expect(buildStackAssets).toHaveBeenCalledWith(B);
expect(buildStackAssets).toHaveBeenCalledWith(C);
});

test('errors', async () => {
18 changes: 12 additions & 6 deletions packages/aws-cdk/test/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
@@ -1062,7 +1062,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ concurrency: 3 }));
});

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

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
});
});

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

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
});

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

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
});

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

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
});

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

expect(options.notificationArns).toEqual(this.expectedNotificationArns);
// In these tests, we don't make a distinction here between `undefined` and `[]`.
//
// In tests `deployStack` itself we do treat `undefined` and `[]` differently,
// and in `aws-cdk-lib` we emit them under different conditions. But this test
// without normalization depends on a version of `aws-cdk-lib` that hasn't been
// released yet.
expect(options.notificationArns ?? []).toEqual(this.expectedNotificationArns ?? []);
return Promise.resolve({
type: 'did-deploy-stack',
stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`,
7 changes: 6 additions & 1 deletion packages/aws-cdk/test/util.ts
Original file line number Diff line number Diff line change
@@ -5,9 +5,12 @@ import { type CloudAssembly, CloudAssemblyBuilder, type CloudFormationStackArtif
import { MockSdkProvider } from './util/mock-sdk';
import { CloudExecutable } from '../lib/api/cxapp/cloud-executable';
import { Configuration } from '../lib/settings';
import { cxapiAssemblyWithForcedVersion } from './api/assembly-versions';

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

const SOME_RECENT_SCHEMA_VERSION = '30.0.0';

export interface TestStackArtifact {
stackName: string;
template?: any;
@@ -30,6 +33,7 @@ export interface TestAssembly {
stacks: TestStackArtifact[];
missing?: MissingContext[];
nestedAssemblies?: TestAssembly[];
schemaVersion?: string;
}

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

return builder.buildAssembly();
const asm = builder.buildAssembly();
return cxapiAssemblyWithForcedVersion(asm, assembly.schemaVersion ?? SOME_RECENT_SCHEMA_VERSION);
}

/**
Loading