From 6bdc9eb52608329f1e66c420cb6c61aa942d17b0 Mon Sep 17 00:00:00 2001 From: joe-king-sh Date: Tue, 15 Mar 2022 09:02:59 +0900 Subject: [PATCH] feat(assertions): Add the `hasNoXXX` methods. (#19330) This PR adds `hasNoError`, `hasNoWarning`, and `hasNoInfo` methods to the Annotations assertions toolkit. Fixes #18874 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 6 +-- .../@aws-cdk/assertions/lib/annotations.ts | 41 ++++++++++++++++++- .../assertions/lib/private/messages.ts | 16 +++++++- .../assertions/lib/private/section.ts | 6 +++ .../assertions/test/annotations.test.ts | 33 +++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index d23c37a08b099..8692ecc7d66ce 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -550,9 +550,9 @@ Annotations.fromStack(stack).hasError( Here are the available APIs for `Annotations`: -- `hasError()` and `findError()` -- `hasWarning()` and `findWarning()` -- `hasInfo()` and `findInfo()` +- `hasError()`, `hasNoError()`, and `findError()` +- `hasWarning()`, `hasNoWarning()`, and `findWarning()` +- `hasInfo()`, `hasNoInfo()`, 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. diff --git a/packages/@aws-cdk/assertions/lib/annotations.ts b/packages/@aws-cdk/assertions/lib/annotations.ts index 09f4309044e4a..2068b58ccb480 100644 --- a/packages/@aws-cdk/assertions/lib/annotations.ts +++ b/packages/@aws-cdk/assertions/lib/annotations.ts @@ -1,7 +1,7 @@ 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'; +import { findMessage, hasMessage, hasNoMessage } from './private/messages'; /** * Suite of assertions that can be run on a CDK Stack. @@ -35,6 +35,19 @@ export class Annotations { } } + /** + * Assert that an error with the given message does not exist 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 hasNoError(constructPath: string, message: any): void { + const matchError = hasNoMessage(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. * @@ -58,6 +71,19 @@ export class Annotations { } } + /** + * Assert that an warning with the given message does not exist 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 hasNoWarning(constructPath: string, message: any): void { + const matchError = hasNoMessage(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. * @@ -81,6 +107,19 @@ export class Annotations { } } + /** + * Assert that an info with the given message does not exist 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 hasNoInfo(constructPath: string, message: any): void { + const matchError = hasNoMessage(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. * diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts index 95e898b687183..f152ebcefb0e5 100644 --- a/packages/@aws-cdk/assertions/lib/private/messages.ts +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -1,6 +1,6 @@ import { SynthesisMessage } from '@aws-cdk/cx-api'; import { Messages } from './message'; -import { formatFailure, matchSection } from './section'; +import { formatAllMatches, formatFailure, matchSection } from './section'; export function findMessage(messages: Messages, constructPath: string, props: any = {}): { [key: string]: { [key: string]: any } } { const section: { [key: string]: SynthesisMessage } = messages; @@ -32,6 +32,20 @@ export function hasMessage(messages: Messages, constructPath: string, props: any ].join('\n'); } +export function hasNoMessage(messages: Messages, constructPath: string, props: any): string | void { + const section: { [key: string]: SynthesisMessage } = messages; + const result = matchSection(filterPath(section, constructPath), props); + + if (!result.match) { + return; + } + + return [ + `Expected no matches, but stack has ${Object.keys(result.matches).length} messages as follows:`, + formatAllMatches(result.matches), + ].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 handleTrace(match: any, redact: boolean = true): void { diff --git a/packages/@aws-cdk/assertions/lib/private/section.ts b/packages/@aws-cdk/assertions/lib/private/section.ts index d2dd96800da08..2468e47f33c1c 100644 --- a/packages/@aws-cdk/assertions/lib/private/section.ts +++ b/packages/@aws-cdk/assertions/lib/private/section.ts @@ -43,6 +43,12 @@ function eachEntryInSection( } } +export function formatAllMatches(matches: {[key: string]: any}): string { + return [ + leftPad(JSON.stringify(matches, undefined, 2)), + ].join('\n'); +} + export function formatFailure(closestResult: MatchResult): string { return [ 'The closest result is:', diff --git a/packages/@aws-cdk/assertions/test/annotations.test.ts b/packages/@aws-cdk/assertions/test/annotations.test.ts index 2b1c0c0274ead..ebc1ca081d868 100644 --- a/packages/@aws-cdk/assertions/test/annotations.test.ts +++ b/packages/@aws-cdk/assertions/test/annotations.test.ts @@ -50,6 +50,17 @@ describe('Messages', () => { }); }); + describe('hasNoError', () => { + test('match', () => { + annotations.hasNoError('/Default/Fred', Match.anyValue()); + }); + + test('no match', () => { + expect(() => annotations.hasNoError('/Default/Foo', 'this is an error')) + .toThrowError(/Expected no matches, but stack has 1 messages as follows:/); + }); + }); + describe('findError', () => { test('match', () => { const result = annotations.findError('*', Match.anyValue()); @@ -72,6 +83,17 @@ describe('Messages', () => { }); }); + describe('hasNoWarning', () => { + test('match', () => { + annotations.hasNoWarning('/Default/Foo', Match.anyValue()); + }); + + test('no match', () => { + expect(() => annotations.hasNoWarning('/Default/Fred', 'this is a warning')) + .toThrowError(/Expected no matches, but stack has 1 messages as follows:/); + }); + }); + describe('findWarning', () => { test('match', () => { const result = annotations.findWarning('*', Match.anyValue()); @@ -94,6 +116,17 @@ describe('Messages', () => { }); }); + describe('hasNoInfo', () => { + test('match', () => { + annotations.hasNoInfo('/Default/Qux', 'this info is incorrect'); + }); + + test('no match', () => { + expect(() => annotations.hasNoInfo('/Default/Qux', 'this is an info')) + .toThrowError(/Expected no matches, but stack has 1 messages as follows:/); + }); + }); + describe('findInfo', () => { test('match', () => { const result = annotations.findInfo('/Default/Qux', 'this is an info');