From 1886f1490cc6309ba982ab0130ab28c81dec697c Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 4 Oct 2021 18:54:08 +0100 Subject: [PATCH 1/2] feat(assertions): Matcher support for `templateMatches()` API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `templateMatches()` API behaved differently from the rest of the `hasXxx()` and `findXxx()` APIs in that it did not accept a Matcher. This functionality is generally useful to perform partial matching on the full template. Further, users can get confused and assume that the `templateMatches()` API do support Matchers, as this is the only one that is an exception. Align this API with the rest of the module's behaviour. A nice side effect of this is that this module no longer needs to vendor in changes from the 'assert' module and brings this in line with the other modules in this repo. nozem can work again! 🙌 --- packages/@aws-cdk/assertions/README.md | 6 +- packages/@aws-cdk/assertions/lib/match.ts | 3 +- .../assertions/lib/private/mappings.ts | 10 ++-- .../assertions/lib/private/outputs.ts | 10 ++-- .../assertions/lib/private/resources.ts | 22 +++---- .../assertions/lib/private/template.ts | 16 ++++++ packages/@aws-cdk/assertions/lib/template.ts | 41 +++++++------ packages/@aws-cdk/assertions/package.json | 26 --------- .../@aws-cdk/assertions/test/template.test.ts | 10 ++-- packages/@aws-cdk/assertions/vendor-in.sh | 57 ------------------- 10 files changed, 72 insertions(+), 129 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/private/template.ts delete mode 100755 packages/@aws-cdk/assertions/vendor-in.sh diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 0c2d9d1ecdadf..ce480bc152dc4 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -144,9 +144,9 @@ The APIs `hasMapping()` and `findMappings()` provide similar functionalities. ## Special Matchers -The expectation provided to the `hasXXX()` and `findXXX()` methods, besides -carrying literal values, as seen in the above examples, also accept special -matchers. +The expectation provided to the `hasXxx()`, `findXxx()` and `templateMatches()` +APIs, besides carrying literal values, as seen in the above examples, also accept +special matchers. They are available as part of the `Match` class. diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 4fea0ed0f713e..9b18400abcbf6 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -1,6 +1,7 @@ import { Matcher, MatchResult } from './matcher'; import { getType } from './private/type'; -import { ABSENT } from './vendored/assert'; + +const ABSENT = '{{ABSENT}}'; /** * Partial and special matching during template assertions. diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts index e19afe541d204..e080843dd87f8 100644 --- a/packages/@aws-cdk/assertions/lib/private/mappings.ts +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -1,8 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; -export function findMappings(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = inspector.value.Mappings; +export function findMappings(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string] : {} } = template.Mappings; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -12,8 +12,8 @@ export function findMappings(inspector: StackInspector, logicalId: string, props return result.matches; } -export function hasMapping(inspector: StackInspector, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = inspector.value.Mappings; +export function hasMapping(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = template.Mappings; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts index 320016d22a8eb..f00f05bc9bb0f 100644 --- a/packages/@aws-cdk/assertions/lib/private/outputs.ts +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -1,8 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; -export function findOutputs(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = inspector.value.Outputs; +export function findOutputs(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section = template.Outputs; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -12,8 +12,8 @@ export function findOutputs(inspector: StackInspector, logicalId: string, props: return result.matches; } -export function hasOutput(inspector: StackInspector, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = inspector.value.Outputs; +export function hasOutput(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = template.Outputs; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts index 81b77346f61f5..aeb2037d81ad4 100644 --- a/packages/@aws-cdk/assertions/lib/private/resources.ts +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -1,13 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { formatFailure, matchSection } from './section'; +import { Resource, Template } from './template'; -// Partial type for CloudFormation Resource -type Resource = { - Type: string; -} - -export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : Resource } = inspector.value.Resources; +export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section = template.Resources; const result = matchSection(filterType(section, type), props); if (!result.match) { @@ -17,8 +12,8 @@ export function findResources(inspector: StackInspector, type: string, props: an return result.matches; } -export function hasResource(inspector: StackInspector, type: string, props: any): string | void { - const section: { [key: string]: Resource } = inspector.value.Resources; +export function hasResource(template: Template, type: string, props: any): string | void { + const section = template.Resources; const result = matchSection(filterType(section, type), props); if (result.match) { @@ -35,6 +30,13 @@ export function hasResource(inspector: StackInspector, type: string, props: any) ].join('\n'); } +export function countResources(template: Template, type: string): number { + const section = template.Resources; + const types = filterType(section, type); + + return Object.entries(types).length; +} + function filterType(section: { [key: string]: Resource }, type: string): { [key: string]: Resource } { return Object.entries(section ?? {}) .filter(([_, v]) => v.Type === type) diff --git a/packages/@aws-cdk/assertions/lib/private/template.ts b/packages/@aws-cdk/assertions/lib/private/template.ts new file mode 100644 index 0000000000000..3b44368138435 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/template.ts @@ -0,0 +1,16 @@ +// Partial types for CloudFormation Template + +export type Template = { + Resources: { [logicalId: string]: Resource }, + Outputs: { [logicalId: string]: Output }, + Mappings: { [logicalId: string]: Mapping } +} + +export type Resource = { + Type: string; + [key: string]: any; +} + +export type Output = { [key: string]: any }; + +export type Mapping = { [key: string]: any }; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index e6f721d928576..d3871c6cda36b 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -3,8 +3,8 @@ import { Match } from './match'; import { Matcher } from './matcher'; import { findMappings, hasMapping } from './private/mappings'; import { findOutputs, hasOutput } from './private/outputs'; -import { findResources, hasResource } from './private/resources'; -import * as assert from './vendored/assert'; +import { countResources, findResources, hasResource } from './private/resources'; +import { Template as TemplateType } from './private/template'; /** * Suite of assertions that can be run on a CDK stack. @@ -39,12 +39,10 @@ export class Template { return new Template(JSON.parse(template)); } - private readonly template: { [key: string]: any }; - private readonly inspector: assert.StackInspector; + private readonly template: TemplateType; private constructor(template: { [key: string]: any }) { - this.template = template; - this.inspector = new assert.StackInspector(template); + this.template = template as TemplateType; } /** @@ -61,8 +59,10 @@ export class Template { * @param count number of expected instances */ public resourceCountIs(type: string, count: number): void { - const assertion = assert.countResources(type, count); - assertion.assertOrThrow(this.inspector); + const counted = countResources(this.template, type); + if (counted !== count) { + throw new Error(`Expected ${count} resources of type ${type} but found ${counted}`); + } } /** @@ -88,7 +88,7 @@ export class Template { * @param props the entire defintion of the resource as should be expected in the template. */ public hasResource(type: string, props: any): void { - const matchError = hasResource(this.inspector, type, props); + const matchError = hasResource(this.template, type, props); if (matchError) { throw new Error(matchError); } @@ -102,7 +102,7 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findResources(type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findResources(this.inspector, type, props); + return findResources(this.template, type, props); } /** @@ -113,7 +113,7 @@ export class Template { * @param props the output as should be expected in the template. */ public hasOutput(logicalId: string, props: any): void { - const matchError = hasOutput(this.inspector, logicalId, props); + const matchError = hasOutput(this.template, logicalId, props); if (matchError) { throw new Error(matchError); } @@ -127,7 +127,7 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findOutputs(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findOutputs(this.inspector, logicalId, props); + return findOutputs(this.template, logicalId, props); } /** @@ -138,7 +138,7 @@ export class Template { * @param props the output as should be expected in the template. */ public hasMapping(logicalId: string, props: any): void { - const matchError = hasMapping(this.inspector, logicalId, props); + const matchError = hasMapping(this.template, logicalId, props); if (matchError) { throw new Error(matchError); } @@ -152,16 +152,23 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findMappings(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findMappings(this.inspector, logicalId, props); + return findMappings(this.template, logicalId, props); } /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. */ - public templateMatches(expected: {[key: string]: any}): void { - const assertion = assert.matchTemplate(expected); - assertion.assertOrThrow(this.inspector); + public templateMatches(expected: any): void { + const matcher = Matcher.isMatcher(expected) ? expected : Match.objectLike(expected); + const result = matcher.test(this.template); + + if (result.hasFailed()) { + throw new Error([ + 'Template did not match as expected. The following mismatches were found:', + ...result.toHumanStrings().map(s => `\t${s}`), + ].join('\n')); + } } } diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index e3c0612bf486d..58420966a5bfe 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -48,11 +48,6 @@ }, "projectReferences": true }, - "cdk-build": { - "pre": [ - "./vendor-in.sh" - ] - }, "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", @@ -61,7 +56,6 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", - "@aws-cdk/cfnspec": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^26.0.24", "constructs": "^3.3.69", @@ -106,26 +100,6 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "nozem": { - "ostools": [ - "dirname", - "cd", - "bash", - "rm", - "xargs", - "sed", - "mkdir", - "rsync", - "cat", - "find" - ], - "additionalDirs": [ - "../cfnspec/lib", - "ARTIFACTS:../cfnspec/spec", - "../cloudformation-diff", - "../assert-internal" - ] - }, "stability": "experimental", "maturity": "experimental", "publishConfig": { diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 7c3221763446c..97e7e4ca241a6 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -82,10 +82,10 @@ describe('Template', () => { const inspect = Template.fromStack(stack); inspect.resourceCountIs('Foo::Bar', 1); - expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow(/has 1 resource of type Foo::Bar/); - expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow(/has 1 resource of type Foo::Bar/); + expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow('Expected 0 resources of type Foo::Bar but found 1'); + expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow('Expected 2 resources of type Foo::Bar but found 1'); - expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow(/has 0 resource of type Foo::Baz/); + expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow('Expected 1 resources of type Foo::Baz but found 0'); }); test('no resource', () => { @@ -94,7 +94,7 @@ describe('Template', () => { const inspect = Template.fromStack(stack); inspect.resourceCountIs('Foo::Bar', 0); - expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow(/has 0 resource of type Foo::Bar/); + expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow('Expected 1 resources of type Foo::Bar but found 0'); }); }); @@ -132,7 +132,7 @@ describe('Template', () => { Properties: { baz: 'waldo' }, }, }, - })).toThrowError(); + })).toThrowError(/Expected waldo but received qux at \/Resources\/Foo\/Properties\/baz/); }); }); diff --git a/packages/@aws-cdk/assertions/vendor-in.sh b/packages/@aws-cdk/assertions/vendor-in.sh deleted file mode 100755 index 803ff1a7a28ff..0000000000000 --- a/packages/@aws-cdk/assertions/vendor-in.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -set -e - -porta_sed() { - local lookup=$1 - local replacement=$2 - local file=$3 - - # inplace replacement of sed is not portable across BSD and GNU. Use backup extension and then delete. - sed -i'.del' "s^${lookup}^${replacement}^g" $file - rm $file.del -} -export -f porta_sed - -echo "⏳ Vendoring in modules..." - -scriptdir=$(cd $(dirname $0) && pwd) -cd $scriptdir - -if [[ "$PHASE" == "transform" ]]; then - # Make the script short-circuit when running in the packages/individual-packages build context. - # That's required because the build done by individual-pkg-gen does import re-writing when copying the TS files - # (because it needs to know which modules are also unstable to do the rewriting correctly), - # but the vendor.sh script runs only after the 'build' script for this package has been invoked, - # which means any TS files copied by it successfully would not have their imports re-written. - exit 0 -fi - -set -euo pipefail -dest="lib/vendored" -mkdir -p $dest - -# cfnspec -mkdir -p $dest/cfnspec -rsync -a --exclude '*.d.ts' --exclude '*.js' ../cfnspec/lib/ $dest/cfnspec/lib -rsync -a ../cfnspec/spec/ $dest/cfnspec/spec - -# cloudformation-diff -rsync -a --exclude '*.d.ts' --exclude '*.js' ../cloudformation-diff/lib/ $dest/cloudformation-diff -find $dest/cloudformation-diff -name '*.ts' | xargs -n1 bash -c 'porta_sed "$@"' _ '@aws-cdk/cfnspec' '../../cfnspec/lib' - -# assert-internal -rsync -a --exclude '*.d.ts' --exclude '*.js' ../assert-internal/lib/ $dest/assert -find $dest/assert -name '*.ts' | xargs -n1 bash -c 'porta_sed "$@"' _ '@aws-cdk/cloudformation-diff' '../../cloudformation-diff' - -# readme -cat > $dest/README.md < Date: Tue, 5 Oct 2021 10:11:09 +0100 Subject: [PATCH 2/2] fix ivs --- packages/@aws-cdk/aws-ivs/test/ivs.test.ts | 100 +++++---------------- 1 file changed, 20 insertions(+), 80 deletions(-) diff --git a/packages/@aws-cdk/aws-ivs/test/ivs.test.ts b/packages/@aws-cdk/aws-ivs/test/ivs.test.ts index 82e3b41e79e64..6e4cd765576e4 100644 --- a/packages/@aws-cdk/aws-ivs/test/ivs.test.ts +++ b/packages/@aws-cdk/aws-ivs/test/ivs.test.ts @@ -1,4 +1,4 @@ -import { Template } from '@aws-cdk/assertions'; +import { Match, Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; import * as ivs from '../lib'; @@ -22,12 +22,8 @@ beforeEach( () => { test('channel default properties', () => { new ivs.Channel(stack, 'Channel'); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - }, - }, + Template.fromStack(stack).hasResource('AWS::IVS::Channel', { + Properties: Match.absentProperty(), }); }); @@ -36,15 +32,8 @@ test('channel name', () => { name: 'CarrotsAreTasty', }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Name: 'CarrotsAreTasty', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Name: 'CarrotsAreTasty', }); }); @@ -53,15 +42,8 @@ test('channel is authorized', () => { authorized: true, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Authorized: 'true', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Authorized: true, }); }); @@ -70,15 +52,8 @@ test('channel type', () => { type: ivs.ChannelType.BASIC, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Type: 'BASIC', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Type: 'BASIC', }); }); @@ -87,15 +62,8 @@ test('channel latency mode', () => { latencyMode: ivs.LatencyMode.NORMAL, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - LatencyMode: 'NORMAL', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + LatencyMode: 'NORMAL', }); }); @@ -116,15 +84,8 @@ test('playback key pair mandatory properties', () => { publicKeyMaterial: publicKey, }); - Template.fromStack(stack).templateMatches({ - Resources: { - PlaybackKeyPairBE17315B: { - Type: 'AWS::IVS::PlaybackKeyPair', - Properties: { - PublicKeyMaterial: publicKey, - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::PlaybackKeyPair', { + PublicKeyMaterial: publicKey, }); }); @@ -134,16 +95,9 @@ test('playback key pair name', () => { name: 'CarrotsAreNutritious', }); - Template.fromStack(stack).templateMatches({ - Resources: { - PlaybackKeyPairBE17315B: { - Type: 'AWS::IVS::PlaybackKeyPair', - Properties: { - PublicKeyMaterial: publicKey, - Name: 'CarrotsAreNutritious', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::PlaybackKeyPair', { + PublicKeyMaterial: publicKey, + Name: 'CarrotsAreNutritious', }); }); @@ -159,15 +113,8 @@ test('stream key mandatory properties', () => { channel: ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'), }); - Template.fromStack(stack).templateMatches({ - Resources: { - StreamKey9F296F4F: { - Type: 'AWS::IVS::StreamKey', - Properties: { - ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::StreamKey', { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', }); }); @@ -194,15 +141,8 @@ test('stream key from channel reference', () => { const channel = ivs.Channel.fromChannelArn(stack, 'Channel', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'); channel.addStreamKey('StreamKey'); - Template.fromStack(stack).templateMatches({ - Resources: { - ChannelStreamKey60BDC2BE: { - Type: 'AWS::IVS::StreamKey', - Properties: { - ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::StreamKey', { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', }); });