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

WIP: feat(assertions): cleanup stacks after test #30931

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ describe('Scope based Associations with Application with Cross Region/Account',
associateStage: true,
});
app.synth();
Template.fromStack(application.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(application.appRegistryApplication.stack, { skipClean: true }).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(pipelineStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class EqualsAssertion extends Construct {

new CfnOutput(this, 'AssertionResults', {
value: this.result,
}).overrideLogicalId(`AssertionResults${id}${md5hash({ actual: props.actual.result, expected: props.expected.result })}`);
key: `AssertionResults${id}${md5hash({ actual: props.actual.result, expected: props.expected.result })}`,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export class HttpApiCall extends ApiCallBase {

new CfnOutput(node, 'AssertionResults', {
value: result,
}).overrideLogicalId(`AssertionResults${id.replace(/[\W_]+/g, '')}`);
key: `AssertionResults${id.replace(/[\W_]+/g, '')}`,
});
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/integ-tests-alpha/lib/assertions/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export class AwsApiCall extends ApiCallBase {
// Remove the at sign, slash, and hyphen because when using the v3 package name or client name as the service name,
// the `id` includes them, but they are not allowed in the `CfnOutput` logical id
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html#outputs-section-syntax
}).overrideLogicalId(`AssertionResults${id}`.replace(/[\@\/\-]/g, ''));
key: `AssertionResults${id}`.replace(/[\@\/\-]/g, ''),
});
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ describe('User provided assertions stack', () => {
integ.assertions.awsApiCall('Service', 'Api', { Reference: cr.ref });

// THEN
const integTemplate = Template.fromStack(integStack);
const integTemplate = Template.fromStack(integStack, { skipClean: true });
const assertionTemplate = Template.fromStack(assertionStack);
integTemplate.resourceCountIs('Custom::Bar', 1);
assertionTemplate.resourceCountIs('Custom::DeployAssert@SdkCallServiceApi', 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ describe('AwsApiCall', () => {
});

// THEN
Template.fromStack(deplossert.scope).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', {
const template = Template.fromStack(deplossert.scope);
template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', {
service: 'MyService',
api: 'MyApi',
parameters: {
Expand All @@ -180,7 +181,7 @@ describe('AwsApiCall', () => {
},
expected: JSON.stringify({ $ObjectLike: { Key: 'Value' } }),
});
Template.fromStack(deplossert.scope).findResources('AWS::IAM::Role', {
template.findResources('AWS::IAM::Role', {
SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73: {
Properties: {
Policies: [
Expand Down
32 changes: 25 additions & 7 deletions packages/aws-cdk-lib/assertions/lib/annotations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'fs-extra';
import { Messages } from './private/message';
import { findMessage, hasMessage, hasNoMessage } from './private/messages';
import { Stack, Stage } from '../../core';
Expand All @@ -12,8 +13,8 @@ export class Annotations {
* Base your assertions on the messages returned by a synthesized CDK `Stack`.
* @param stack the CDK Stack to run assertions on
*/
public static fromStack(stack: Stack): Annotations {
return new Annotations(toMessages(stack));
public static fromStack(stack: Stack, skipClean?: boolean): Annotations {
return new Annotations(toMessages(stack, skipClean));
}

private readonly _messages: Messages;
Expand Down Expand Up @@ -153,16 +154,33 @@ function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] {
return Object.values(messages) as SynthesisMessage[];
}

function toMessages(stack: Stack): any {
function toMessages(stack: Stack, skipClean?: boolean): any {
const root = stack.node.root;
if (!Stage.isStage(root)) {
throw new Error('unexpected: all stacks must be part of a Stage or an App');
}

// to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()")
const force = true;
// We may have deleted all of this in a prior run so check and remake it if
// that is the case.
const outdir = root!.outdir;
const assetOutdir = root!.assetOutdir;

const assembly = root.synth({ force });
if (!fs.existsSync(outdir)) {
fs.mkdirSync(outdir, { recursive: true });
}

if (!fs.existsSync(assetOutdir)) {
fs.mkdirSync(assetOutdir, { recursive: true });
}

const assembly = root.synth({ force: true });
const messages = assembly.getStackArtifact(stack.artifactId).messages;

if (skipClean !== true) {
// Now clean up after yourself
fs.rmSync(outdir, { recursive: true, force: true });
fs.rmSync(assetOutdir, { recursive: true, force: true });
}

return assembly.getStackArtifact(stack.artifactId).messages;
return messages;
}
26 changes: 22 additions & 4 deletions packages/aws-cdk-lib/assertions/lib/tags.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'fs-extra';
import { Match } from './match';
import { Matcher } from './matcher';
import { Stack, Stage } from '../../core';
Expand Down Expand Up @@ -76,16 +77,33 @@ export class Tags {
}
}

function getManifestTags(stack: Stack): ManifestTags {
function getManifestTags(stack: Stack, skipClean?: boolean): ManifestTags {
const root = stack.node.root;
if (!Stage.isStage(root)) {
throw new Error('unexpected: all stacks must be part of a Stage or an App');
}

// synthesis is not forced: the stack will only be synthesized once regardless
// of the number of times this is called.
const assembly = root.synth();
// We may have deleted all of this in a prior run so check and remake it if
// that is the case.
const outdir = root!.outdir;
const assetOutdir = root!.assetOutdir;

if (!fs.existsSync(outdir)) {
fs.mkdirSync(outdir, { recursive: true });
}

if (!fs.existsSync(assetOutdir)) {
fs.mkdirSync(assetOutdir, { recursive: true });
}

const assembly = root.synth({ force: true });
const artifact = assembly.getStackArtifact(stack.artifactId);

if (skipClean !== true) {
// Now clean up after yourself
fs.rmSync(outdir, { recursive: true, force: true });
fs.rmSync(assetOutdir, { recursive: true, force: true });
}

return artifact.tags;
}
65 changes: 58 additions & 7 deletions packages/aws-cdk-lib/assertions/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Template {
* dependencies.
*/
public static fromStack(stack: Stack, templateParsingOptions?: TemplateParsingOptions): Template {
return new Template(toTemplate(stack), templateParsingOptions);
return new Template(toTemplate(stack, templateParsingOptions?.skipClean), templateParsingOptions);
}

/**
Expand All @@ -50,6 +50,21 @@ export class Template {
return new Template(JSON.parse(template), templateParsingOptions);
}

/**
* Cleans up the stack created by this test
* @param stack
*/
public static clean(stack: Stack): void {
const stage = Stage.of(stack);

if (!Stage.isStage(stage)) {
throw new Error('unexpected: all stacks must be part of a Stage or an App');
}

fs.rmSync(stage.outdir, { recursive: true, force: true });
fs.rmSync(stage.assetOutdir, { recursive: true, force: true });
}

private readonly template: TemplateType;

private constructor(template: { [key: string]: any }, templateParsingOptions: TemplateParsingOptions = {}) {
Expand Down Expand Up @@ -293,18 +308,54 @@ export interface TemplateParsingOptions {
* @default false
*/
readonly skipCyclicalDependenciesCheck?: boolean;

/**
* If set to true, will skip cleanup of the synthesized app generated by the
*
* @default false
*/
readonly skipClean?: boolean;
}

function toTemplate(stack: Stack): any {
function toTemplate(stack: Stack, skipClean?: boolean): any {
const stage = Stage.of(stack);

if (!Stage.isStage(stage)) {
throw new Error('unexpected: all stacks must be part of a Stage or an App');
}

const assembly = stage.synth();
if (stack.nestedStackParent) {
// if this is a nested stack (it has a parent), then just read the template as a string
return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8'));
// We may have deleted all of this in a prior run so check and remake it if
// that is the case.
const outdir = stage!.outdir;
const assetOutdir = stage!.assetOutdir;

if (!fs.existsSync(outdir)) {
fs.mkdirSync(outdir, { recursive: true });
}

if (!fs.existsSync(assetOutdir)) {
fs.mkdirSync(assetOutdir, { recursive: true });
}
return assembly.getStackArtifact(stack.artifactId).template;

let assembly;
try {
// The only case this will fail for is integ-test-alpha's use of custom synthesis
assembly = stage.synth({ force: true });
} catch (e) {
assembly = stage.synth();
}
const template = stack.nestedStackParent ?
JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')) :
assembly.getStackArtifact(stack.artifactId).template;

if (skipClean !== true) {
// Now clean up after yourself
fs.rmSync(outdir, { recursive: true, force: true });
fs.rmSync(assetOutdir, { recursive: true, force: true });
}

return template;
}

// https://github.com/aws/aws-cdk/pull/30836
// https://github.com/aws/aws-cdk/pull/30758
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ describeDeprecated('NestedStack', () => {
const assembly = app.synth();

// nested stack should output this value as if it was referenced by the parent (without the export)
Template.fromStack(nestedUnderStack1).templateMatches({
Template.fromStack(nestedUnderStack1, { skipClean: true }).templateMatches({
Resources: {
ResourceInNestedStack: {
Type: 'MyResource',
Expand All @@ -487,7 +487,7 @@ describeDeprecated('NestedStack', () => {
});

// consuming stack should use ImportValue to import the value from the parent stack
Template.fromStack(stack2).templateMatches({
Template.fromStack(stack2, { skipClean: true }).templateMatches({
Resources: {
ResourceInStack2: {
Type: 'JustResource',
Expand All @@ -506,6 +506,8 @@ describeDeprecated('NestedStack', () => {
expect(stack1Artifact.dependencies.length).toEqual(0);
expect(stack2Artifact.dependencies.length).toEqual(1);
expect(stack2Artifact.dependencies[0]).toEqual(stack1Artifact);
Template.clean(stack1);
Template.clean(stack2);
});

test('references between sibling nested stacks should output from one and getAtt from the other', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-iam/test/precreated-role.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('precreatedRole report created', () => {
],
});

Template.fromStack(otherStack).resourceCountIs('AWS::IAM::Role', 0);
Template.fromStack(otherStack, { skipClean: true }).resourceCountIs('AWS::IAM::Role', 0);
const assembly = app.synth();
const filePath = path.join(assembly.directory, 'iam-policy-report.json');
const file = fs.readFileSync(filePath, { encoding: 'utf-8' });
Expand All @@ -73,6 +73,7 @@ describe('precreatedRole report created', () => {
}],
}],
});
Template.clean(otherStack);
});

test('with managed policies', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-lambda/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2639,7 +2639,7 @@ describe('function', () => {
],
});

Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
AWS_CODEGURU_PROFILER_GROUP_NAME: 'profiler_group',
Expand All @@ -2651,6 +2651,7 @@ describe('function', () => {
});

Annotations.fromStack(stack).hasWarning('/Default/MyLambda', Match.stringLikeRegexp('AWS_CODEGURU_PROFILER_GROUP_NAME, AWS_CODEGURU_PROFILER_GROUP_ARN, AWS_CODEGURU_PROFILER_TARGET_REGION, and AWS_CODEGURU_PROFILER_ENABLED should not be set when profiling options enabled'));
Template.clean(stack);
});

test('default function with client provided Profiling Group and client provided env vars', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/aws-cdk-lib/aws-synthetics/test/code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ describe(synthetics.Code.fromAsset, () => {
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', {
Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Synthetics::Canary', {
Code: {
Handler: 'canary.handler',
S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.bucketName),
S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.objectKey),
},
});
Template.clean(stack);
});

test('fromAsset works for python runtimes', () => {
Expand All @@ -79,13 +80,14 @@ describe(synthetics.Code.fromAsset, () => {
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', {
Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Synthetics::Canary', {
Code: {
Handler: 'canary.handler',
S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.bucketName),
S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.objectKey),
},
});
Template.clean(stack);
});

test('only one Asset object gets created even if multiple canaries use the same AssetCode', () => {
Expand Down
Loading
Loading