Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
siddharthsalot authored Jan 25, 2022
2 parents 9814a7f + 16db963 commit 953d1c4
Show file tree
Hide file tree
Showing 117 changed files with 3,087 additions and 2,211 deletions.
75 changes: 73 additions & 2 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ This matcher can be combined with any of the other matchers.
// The following will NOT throw an assertion error
template.hasResourceProperties('Foo::Bar', {
Fred: {
Wobble: [ Match.anyValue(), "Flip" ],
Wobble: [ Match.anyValue(), Match.anyValue() ],
},
});

Expand Down Expand Up @@ -400,7 +400,7 @@ template.hasResourceProperties('Foo::Bar', {

## Capturing Values

This matcher APIs documented above allow capturing values in the matching entry
The matcher APIs documented above allow capturing values in the matching entry
(Resource, Output, Mapping, etc.). The following code captures a string from a
matching resource.

Expand Down Expand Up @@ -492,3 +492,74 @@ fredCapture.asString(); // returns "Flob"
fredCapture.next(); // returns true
fredCapture.asString(); // returns "Quib"
```

## Asserting Annotations

In addition to template matching, we provide an API for annotation matching.
[Annotations](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Annotations.html)
can be added via the [Aspects](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html)
API. You can learn more about Aspects [here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html).

Say you have a `MyAspect` and a `MyStack` that uses `MyAspect`:

```ts nofixture
import * as cdk from '@aws-cdk/core';
import { Construct, IConstruct } from 'constructs';

class MyAspect implements cdk.IAspect {
public visit(node: IConstruct): void {
if (node instanceof cdk.CfnResource && node.cfnResourceType === 'Foo::Bar') {
this.error(node, 'we do not want a Foo::Bar resource');
}
}

protected error(node: IConstruct, message: string): void {
cdk.Annotations.of(node).addError(message);
}
}

class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);

const stack = new cdk.Stack();
new cdk.CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: {
Fred: 'Thud',
},
});
cdk.Aspects.of(stack).add(new MyAspect());
}
}
```

We can then assert that the stack contains the expected Error:

```ts
// import { Annotations } from '@aws-cdk/assertions';

Annotations.fromStack(stack).hasError(
'/Default/Foo',
'we do not want a Foo::Bar resource',
);
```

Here are the available APIs for `Annotations`:

- `hasError()` and `findError()`
- `hasWarning()` and `findWarning()`
- `hasInfo()` and `findInfo()`

The corresponding `findXxx()` API is complementary to the `hasXxx()` API, except instead
of asserting its presence, it returns the set of matching messages.

In addition, this suite of APIs is compatable with `Matchers` for more fine-grained control.
For example, the following assertion works as well:

```ts
Annotations.fromStack(stack).hasError(
'/Default/Foo',
Match.stringLikeRegexp('.*Foo::Bar.*'),
);
```
129 changes: 129 additions & 0 deletions packages/@aws-cdk/assertions/lib/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Stack, Stage } from '@aws-cdk/core';
import { SynthesisMessage } from '@aws-cdk/cx-api';
import { Messages } from './private/message';
import { findMessage, hasMessage } from './private/messages';

/**
* Suite of assertions that can be run on a CDK Stack.
* Focused on asserting annotations.
*/
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));
}

private readonly _messages: Messages;

private constructor(messages: SynthesisMessage[]) {
this._messages = convertArrayToMessagesType(messages);
}

/**
* Assert that an error with the given message exists in the synthesized CDK `Stack`.
*
* @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template.
* @param message the error message as should be expected. This should be a string or Matcher object.
*/
public hasError(constructPath: string, message: any): void {
const matchError = hasMessage(this._messages, constructPath, constructMessage('error', message));
if (matchError) {
throw new Error(matchError);
}
}

/**
* Get the set of matching errors of a given construct path and message.
*
* @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template.
* @param message the error message as should be expected. This should be a string or Matcher object.
*/
public findError(constructPath: string, message: any): SynthesisMessage[] {
return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('error', message)) as Messages);
}

/**
* Assert that an warning with the given message exists in the synthesized CDK `Stack`.
*
* @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template.
* @param message the warning message as should be expected. This should be a string or Matcher object.
*/
public hasWarning(constructPath: string, message: any): void {
const matchError = hasMessage(this._messages, constructPath, constructMessage('warning', message));
if (matchError) {
throw new Error(matchError);
}
}

/**
* Get the set of matching warning of a given construct path and message.
*
* @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template.
* @param message the warning message as should be expected. This should be a string or Matcher object.
*/
public findWarning(constructPath: string, message: any): SynthesisMessage[] {
return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('warning', message)) as Messages);
}

/**
* Assert that an info with the given message exists in the synthesized CDK `Stack`.
*
* @param constructPath the construct path to the info. Provide `'*'` to match all info in the template.
* @param message the info message as should be expected. This should be a string or Matcher object.
*/
public hasInfo(constructPath: string, message: any): void {
const matchError = hasMessage(this._messages, constructPath, constructMessage('info', message));
if (matchError) {
throw new Error(matchError);
}
}

/**
* Get the set of matching infos of a given construct path and message.
*
* @param constructPath the construct path to the info. Provide `'*'` to match all infos in the template.
* @param message the info message as should be expected. This should be a string or Matcher object.
*/
public findInfo(constructPath: string, message: any): SynthesisMessage[] {
return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('info', message)) as Messages);
}
}

function constructMessage(type: 'info' | 'warning' | 'error', message: any): {[key:string]: any } {
return {
level: type,
entry: {
data: message,
},
};
}

function convertArrayToMessagesType(messages: SynthesisMessage[]): Messages {
return messages.reduce((obj, item) => {
return {
...obj,
[item.id]: item,
};
}, {}) as Messages;
}

function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] {
return Object.values(messages) as SynthesisMessage[];
}

function toMessages(stack: Stack): 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;

const assembly = root.synth({ force });

return assembly.getStackArtifact(stack.artifactId).messages;
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/assertions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './capture';
export * from './template';
export * from './match';
export * from './matcher';
export * from './matcher';
export * from './annotations';
5 changes: 5 additions & 0 deletions packages/@aws-cdk/assertions/lib/private/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SynthesisMessage } from '@aws-cdk/cx-api';

export type Messages = {
[logicalId: string]: SynthesisMessage;
}
41 changes: 41 additions & 0 deletions packages/@aws-cdk/assertions/lib/private/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MatchResult } from '../matcher';
import { Messages } from './message';
import { filterLogicalId, formatFailure, matchSection } from './section';

export function findMessage(messages: Messages, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section: { [key: string]: {} } = messages;
const result = matchSection(filterLogicalId(section, logicalId), props);

if (!result.match) {
return {};
}

return result.matches;
}

export function hasMessage(messages: Messages, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = messages;
const result = matchSection(filterLogicalId(section, logicalId), props);

if (result.match) {
return;
}

if (result.closestResult === undefined) {
return 'No messages found in the stack';
}

return [
`Stack has ${result.analyzedCount} messages, but none match as expected.`,
formatFailure(formatMessage(result.closestResult)),
].join('\n');
}

// We redact the stack trace by default because it is unnecessarily long and unintelligible.
// If there is a use case for rendering the trace, we can add it later.
function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult {
if (!renderTrace) {
match.target.entry.trace = 'redacted';
}
return match;
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/assertions/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export class Template {
* @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template.
* @param props by default, matches all Parameters in the template.
* When a literal object is provided, performs a partial match via `Match.objectLike()`.
* Use the `Match` APIs to configure a different behaviour. */
* Use the `Match` APIs to configure a different behaviour.
*/
public findParameters(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
return findParameters(this.template, logicalId, props);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/assertions/rosetta/default.ts-fixture
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Fixture with packages imported, but nothing else
import { Construct } from 'constructs';
import { Stack } from '@aws-cdk/core';
import { Capture, Match, Template } from '@aws-cdk/assertions';
import { Aspects, CfnResource, Stack } from '@aws-cdk/core';
import { Annotations, Capture, Match, Template } from '@aws-cdk/assertions';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
Expand Down
Loading

0 comments on commit 953d1c4

Please sign in to comment.