forked from aws/aws-cdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(assertions): support assertions on stack messages (aws#18521)
Looking to unblock users who want to use assertions with annotations added on a stack. Fixes aws#18347 Example: ```ts class MyAspect implements IAspect { public visit(node: IConstruct): void { if (node instanceof CfnResource) { this.warn(node, 'insert message here', } } protected warn(node: IConstruct, message: string): void { Annotations.of(node).addWarning(message); } } const app = new App(); const stack = new Stack(app); new CfnResource(stack, 'Foo', { type: 'Foo::Bar', properties: { Baz: 'Qux', }, }); Aspects.of(stack).add(new MyAspect()); AssertAnnotations.fromStack(stack).hasMessage({ level: 'warning', entry: { data: 'insert message here', }, }); ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
8 changed files
with
404 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.