Skip to content

Commit 9a67ce7

Browse files
author
Niranjan Jayakar
authoredDec 2, 2021
feat(assertions): major improvements to the capture feature (#17713)
There are three major changes around the capture feature of assertions. Firstly, when there are multiple targets (say, Resource in the CloudFormation template) that matches the given condition, any `Capture` defined in the condition will contain only the last matched resource. Convert the `Capture` class into an iterable so all matching values can be retrieved. Secondly, add support to allow sub-patterns to be specified to the `Capture` class. This allows further conditions be specified, via Matchers or literals, when a value is to be captured. Finally, this fixes a bug with the current implementation where `Capture` contains the results of the last matched section, irrespective of whether that section matched with the rest of the matcher or not. fixes #17009 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b284eba commit 9a67ce7

File tree

9 files changed

+372
-69
lines changed

9 files changed

+372
-69
lines changed
 

‎packages/@aws-cdk/assertions/README.md

+64
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,67 @@ template.hasResourceProperties('Foo::Bar', {
399399
fredCapture.asArray(); // returns ["Flob", "Cat"]
400400
waldoCapture.asString(); // returns "Qux"
401401
```
402+
403+
With captures, a nested pattern can also be specified, so that only targets
404+
that match the nested pattern will be captured. This pattern can be literals or
405+
further Matchers.
406+
407+
```ts
408+
// Given a template -
409+
// {
410+
// "Resources": {
411+
// "MyBar1": {
412+
// "Type": "Foo::Bar",
413+
// "Properties": {
414+
// "Fred": ["Flob", "Cat"],
415+
// }
416+
// }
417+
// "MyBar2": {
418+
// "Type": "Foo::Bar",
419+
// "Properties": {
420+
// "Fred": ["Qix", "Qux"],
421+
// }
422+
// }
423+
// }
424+
// }
425+
426+
const capture = new Capture(Match.arrayWith(['Cat']));
427+
template.hasResourceProperties('Foo::Bar', {
428+
Fred: capture,
429+
});
430+
431+
capture.asArray(); // returns ['Flob', 'Cat']
432+
```
433+
434+
When multiple resources match the given condition, each `Capture` defined in
435+
the condition will capture all matching values. They can be paged through using
436+
the `next()` API. The following example illustrates this -
437+
438+
```ts
439+
// Given a template -
440+
// {
441+
// "Resources": {
442+
// "MyBar": {
443+
// "Type": "Foo::Bar",
444+
// "Properties": {
445+
// "Fred": "Flob",
446+
// }
447+
// },
448+
// "MyBaz": {
449+
// "Type": "Foo::Bar",
450+
// "Properties": {
451+
// "Fred": "Quib",
452+
// }
453+
// }
454+
// }
455+
// }
456+
457+
const fredCapture = new Capture();
458+
template.hasResourceProperties('Foo::Bar', {
459+
Fred: fredCapture,
460+
});
461+
462+
fredCapture.asString(); // returns "Flob"
463+
fredCapture.next(); // returns true
464+
fredCapture.asString(); // returns "Quib"
465+
```
+61-24
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Match } from '.';
12
import { Matcher, MatchResult } from './matcher';
23
import { Type, getType } from './private/type';
34

@@ -8,31 +9,63 @@ import { Type, getType } from './private/type';
89
*/
910
export class Capture extends Matcher {
1011
public readonly name: string;
11-
private value: any = null;
12+
/** @internal */
13+
public _captured: any[] = [];
14+
private idx = 0;
1215

13-
constructor() {
16+
/**
17+
* Initialize a new capture
18+
* @param pattern a nested pattern or Matcher.
19+
* If a nested pattern is provided `objectLike()` matching is applied.
20+
*/
21+
constructor(private readonly pattern?: any) {
1422
super();
1523
this.name = 'Capture';
1624
}
1725

1826
public test(actual: any): MatchResult {
19-
this.value = actual;
20-
2127
const result = new MatchResult(actual);
2228
if (actual == null) {
23-
result.push(this, [], `Can only capture non-nullish values. Found ${actual}`);
29+
return result.recordFailure({
30+
matcher: this,
31+
path: [],
32+
message: `Can only capture non-nullish values. Found ${actual}`,
33+
});
2434
}
35+
36+
if (this.pattern !== undefined) {
37+
const innerMatcher = Matcher.isMatcher(this.pattern) ? this.pattern : Match.objectLike(this.pattern);
38+
const innerResult = innerMatcher.test(actual);
39+
if (innerResult.hasFailed()) {
40+
return innerResult;
41+
}
42+
}
43+
44+
result.recordCapture({ capture: this, value: actual });
2545
return result;
2646
}
2747

48+
/**
49+
* When multiple results are captured, move the iterator to the next result.
50+
* @returns true if another capture is present, false otherwise
51+
*/
52+
public next(): boolean {
53+
if (this.idx < this._captured.length - 1) {
54+
this.idx++;
55+
return true;
56+
}
57+
return false;
58+
}
59+
2860
/**
2961
* Retrieve the captured value as a string.
3062
* An error is generated if no value is captured or if the value is not a string.
3163
*/
3264
public asString(): string {
33-
this.checkNotNull();
34-
if (getType(this.value) === 'string') {
35-
return this.value;
65+
this.validate();
66+
const val = this._captured[this.idx];
67+
if (getType(val) === 'string') {
68+
return val;
3669
}
3770
this.reportIncorrectType('string');
3871
}
@@ -42,9 +75,10 @@ export class Capture extends Matcher {
4275
* An error is generated if no value is captured or if the value is not a number.
4376
*/
4477
public asNumber(): number {
45-
this.checkNotNull();
46-
if (getType(this.value) === 'number') {
47-
return this.value;
78+
this.validate();
79+
const val = this._captured[this.idx];
80+
if (getType(val) === 'number') {
81+
return val;
4882
}
4983
this.reportIncorrectType('number');
5084
}
@@ -54,9 +88,10 @@ export class Capture extends Matcher {
5488
* An error is generated if no value is captured or if the value is not a boolean.
5589
*/
5690
public asBoolean(): boolean {
57-
this.checkNotNull();
58-
if (getType(this.value) === 'boolean') {
59-
return this.value;
91+
this.validate();
92+
const val = this._captured[this.idx];
93+
if (getType(val) === 'boolean') {
94+
return val;
6095
}
6196
this.reportIncorrectType('boolean');
6297
}
@@ -66,9 +101,10 @@ export class Capture extends Matcher {
66101
* An error is generated if no value is captured or if the value is not an array.
67102
*/
68103
public asArray(): any[] {
69-
this.checkNotNull();
70-
if (getType(this.value) === 'array') {
71-
return this.value;
104+
this.validate();
105+
const val = this._captured[this.idx];
106+
if (getType(val) === 'array') {
107+
return val;
72108
}
73109
this.reportIncorrectType('array');
74110
}
@@ -78,21 +114,22 @@ export class Capture extends Matcher {
78114
* An error is generated if no value is captured or if the value is not an object.
79115
*/
80116
public asObject(): { [key: string]: any } {
81-
this.checkNotNull();
82-
if (getType(this.value) === 'object') {
83-
return this.value;
117+
this.validate();
118+
const val = this._captured[this.idx];
119+
if (getType(val) === 'object') {
120+
return val;
84121
}
85122
this.reportIncorrectType('object');
86123
}
87124

88-
private checkNotNull(): void {
89-
if (this.value == null) {
125+
private validate(): void {
126+
if (this._captured.length === 0) {
90127
throw new Error('No value captured');
91128
}
92129
}
93130

94131
private reportIncorrectType(expected: Type): never {
95-
throw new Error(`Captured value is expected to be ${expected} but found ${getType(this.value)}. ` +
96-
`Value is ${JSON.stringify(this.value, undefined, 2)}`);
132+
throw new Error(`Captured value is expected to be ${expected} but found ${getType(this._captured[this.idx])}. ` +
133+
`Value is ${JSON.stringify(this._captured[this.idx], undefined, 2)}`);
97134
}
98135
}

‎packages/@aws-cdk/assertions/lib/match.ts

+60-12
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,20 @@ class LiteralMatch extends Matcher {
124124

125125
const result = new MatchResult(actual);
126126
if (typeof this.pattern !== typeof actual) {
127-
result.push(this, [], `Expected type ${typeof this.pattern} but received ${getType(actual)}`);
127+
result.recordFailure({
128+
matcher: this,
129+
path: [],
130+
message: `Expected type ${typeof this.pattern} but received ${getType(actual)}`,
131+
});
128132
return result;
129133
}
130134

131135
if (actual !== this.pattern) {
132-
result.push(this, [], `Expected ${this.pattern} but received ${actual}`);
136+
result.recordFailure({
137+
matcher: this,
138+
path: [],
139+
message: `Expected ${this.pattern} but received ${actual}`,
140+
});
133141
}
134142

135143
return result;
@@ -166,10 +174,18 @@ class ArrayMatch extends Matcher {
166174

167175
public test(actual: any): MatchResult {
168176
if (!Array.isArray(actual)) {
169-
return new MatchResult(actual).push(this, [], `Expected type array but received ${getType(actual)}`);
177+
return new MatchResult(actual).recordFailure({
178+
matcher: this,
179+
path: [],
180+
message: `Expected type array but received ${getType(actual)}`,
181+
});
170182
}
171183
if (!this.subsequence && this.pattern.length !== actual.length) {
172-
return new MatchResult(actual).push(this, [], `Expected array of length ${this.pattern.length} but received ${actual.length}`);
184+
return new MatchResult(actual).recordFailure({
185+
matcher: this,
186+
path: [],
187+
message: `Expected array of length ${this.pattern.length} but received ${actual.length}`,
188+
});
173189
}
174190

175191
let patternIdx = 0;
@@ -200,7 +216,11 @@ class ArrayMatch extends Matcher {
200216
for (; patternIdx < this.pattern.length; patternIdx++) {
201217
const pattern = this.pattern[patternIdx];
202218
const element = (Matcher.isMatcher(pattern) || typeof pattern === 'object') ? ' ' : ` [${pattern}] `;
203-
result.push(this, [], `Missing element${element}at pattern index ${patternIdx}`);
219+
result.recordFailure({
220+
matcher: this,
221+
path: [],
222+
message: `Missing element${element}at pattern index ${patternIdx}`,
223+
});
204224
}
205225

206226
return result;
@@ -236,21 +256,33 @@ class ObjectMatch extends Matcher {
236256

237257
public test(actual: any): MatchResult {
238258
if (typeof actual !== 'object' || Array.isArray(actual)) {
239-
return new MatchResult(actual).push(this, [], `Expected type object but received ${getType(actual)}`);
259+
return new MatchResult(actual).recordFailure({
260+
matcher: this,
261+
path: [],
262+
message: `Expected type object but received ${getType(actual)}`,
263+
});
240264
}
241265

242266
const result = new MatchResult(actual);
243267
if (!this.partial) {
244268
for (const a of Object.keys(actual)) {
245269
if (!(a in this.pattern)) {
246-
result.push(this, [`/${a}`], 'Unexpected key');
270+
result.recordFailure({
271+
matcher: this,
272+
path: [`/${a}`],
273+
message: 'Unexpected key',
274+
});
247275
}
248276
}
249277
}
250278

251279
for (const [patternKey, patternVal] of Object.entries(this.pattern)) {
252280
if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) {
253-
result.push(this, [`/${patternKey}`], 'Missing key');
281+
result.recordFailure({
282+
matcher: this,
283+
path: [`/${patternKey}`],
284+
message: 'Missing key',
285+
});
254286
continue;
255287
}
256288
const matcher = Matcher.isMatcher(patternVal) ?
@@ -275,15 +307,23 @@ class SerializedJson extends Matcher {
275307
public test(actual: any): MatchResult {
276308
const result = new MatchResult(actual);
277309
if (getType(actual) !== 'string') {
278-
result.push(this, [], `Expected JSON as a string but found ${getType(actual)}`);
310+
result.recordFailure({
311+
matcher: this,
312+
path: [],
313+
message: `Expected JSON as a string but found ${getType(actual)}`,
314+
});
279315
return result;
280316
}
281317
let parsed;
282318
try {
283319
parsed = JSON.parse(actual);
284320
} catch (err) {
285321
if (err instanceof SyntaxError) {
286-
result.push(this, [], `Invalid JSON string: ${actual}`);
322+
result.recordFailure({
323+
matcher: this,
324+
path: [],
325+
message: `Invalid JSON string: ${actual}`,
326+
});
287327
return result;
288328
} else {
289329
throw err;
@@ -311,7 +351,11 @@ class NotMatch extends Matcher {
311351
const innerResult = matcher.test(actual);
312352
const result = new MatchResult(actual);
313353
if (innerResult.failCount === 0) {
314-
result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`);
354+
result.recordFailure({
355+
matcher: this,
356+
path: [],
357+
message: `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`,
358+
});
315359
}
316360
return result;
317361
}
@@ -325,7 +369,11 @@ class AnyMatch extends Matcher {
325369
public test(actual: any): MatchResult {
326370
const result = new MatchResult(actual);
327371
if (actual == null) {
328-
result.push(this, [], 'Expected a value but found none');
372+
result.recordFailure({
373+
matcher: this,
374+
path: [],
375+
message: 'Expected a value but found none',
376+
});
329377
}
330378
return result;
331379
}

‎packages/@aws-cdk/assertions/lib/matcher.ts

+82-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Capture } from './capture';
2+
13
/**
24
* Represents a matcher that can perform special data matching
35
* capabilities between a given pattern and a target.
@@ -25,6 +27,43 @@ export abstract class Matcher {
2527
public abstract test(actual: any): MatchResult;
2628
}
2729

30+
/**
31+
* Match failure details
32+
*/
33+
export interface MatchFailure {
34+
/**
35+
* The matcher that had the failure
36+
*/
37+
readonly matcher: Matcher;
38+
39+
/**
40+
* The relative path in the target where the failure occurred.
41+
* If the failure occurred at root of the match tree, set the path to an empty list.
42+
* If it occurs in the 5th index of an array nested within the 'foo' key of an object,
43+
* set the path as `['/foo', '[5]']`.
44+
*/
45+
readonly path: string[];
46+
47+
/**
48+
* Failure message
49+
*/
50+
readonly message: string;
51+
}
52+
53+
/**
54+
* Information about a value captured during match
55+
*/
56+
export interface MatchCapture {
57+
/**
58+
* The instance of Capture class to which this capture is associated with.
59+
*/
60+
readonly capture: Capture;
61+
/**
62+
* The value that was captured
63+
*/
64+
readonly value: any;
65+
}
66+
2867
/**
2968
* The result of `Match.test()`.
3069
*/
@@ -34,21 +73,26 @@ export class MatchResult {
3473
*/
3574
public readonly target: any;
3675
private readonly failures: MatchFailure[] = [];
76+
private readonly captures: Map<Capture, any[]> = new Map();
77+
private finalized: boolean = false;
3778

3879
constructor(target: any) {
3980
this.target = target;
4081
}
4182

4283
/**
43-
* Push a new failure into this result at a specific path.
44-
* If the failure occurred at root of the match tree, set the path to an empty list.
45-
* If it occurs in the 5th index of an array nested within the 'foo' key of an object,
46-
* set the path as `['/foo', '[5]']`.
47-
* @param path the path at which the failure occurred.
48-
* @param message the failure
84+
* DEPRECATED
85+
* @deprecated use recordFailure()
4986
*/
5087
public push(matcher: Matcher, path: string[], message: string): this {
51-
this.failures.push({ matcher, path, message });
88+
return this.recordFailure({ matcher, path, message });
89+
}
90+
91+
/**
92+
* Record a new failure into this result at a specific path.
93+
*/
94+
public recordFailure(failure: MatchFailure): this {
95+
this.failures.push(failure);
5296
return this;
5397
}
5498

@@ -67,10 +111,29 @@ export class MatchResult {
67111
* @param id the id of the parent tree.
68112
*/
69113
public compose(id: string, inner: MatchResult): this {
70-
const innerF = (inner as any).failures as MatchFailure[];
114+
const innerF = inner.failures;
71115
this.failures.push(...innerF.map(f => {
72116
return { path: [id, ...f.path], message: f.message, matcher: f.matcher };
73117
}));
118+
inner.captures.forEach((vals, capture) => {
119+
vals.forEach(value => this.recordCapture({ capture, value }));
120+
});
121+
return this;
122+
}
123+
124+
/**
125+
* Prepare the result to be analyzed.
126+
* This API *must* be called prior to analyzing these results.
127+
*/
128+
public finalize(): this {
129+
if (this.finalized) {
130+
return this;
131+
}
132+
133+
if (this.failCount === 0) {
134+
this.captures.forEach((vals, cap) => cap._captured.push(...vals));
135+
}
136+
this.finalized = true;
74137
return this;
75138
}
76139

@@ -83,10 +146,16 @@ export class MatchResult {
83146
return '' + r.message + loc + ` (using ${r.matcher.name} matcher)`;
84147
});
85148
}
86-
}
87149

88-
type MatchFailure = {
89-
matcher: Matcher;
90-
path: string[];
91-
message: string;
150+
/**
151+
* Record a capture against in this match result.
152+
*/
153+
public recordCapture(options: MatchCapture): void {
154+
let values = this.captures.get(options.capture);
155+
if (values === undefined) {
156+
values = [];
157+
}
158+
values.push(options.value);
159+
this.captures.set(options.capture, values);
160+
}
92161
}

‎packages/@aws-cdk/assertions/lib/private/matchers/absent.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ export class AbsentMatch extends Matcher {
88
public test(actual: any): MatchResult {
99
const result = new MatchResult(actual);
1010
if (actual !== undefined) {
11-
result.push(this, [], `Received ${actual}, but key should be absent`);
11+
result.recordFailure({
12+
matcher: this,
13+
path: [],
14+
message: `Received ${actual}, but key should be absent`,
15+
});
1216
}
1317
return result;
1418
}

‎packages/@aws-cdk/assertions/lib/private/section.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function matchSection(section: any, props: any): MatchSuccess | MatchFail
1515

1616
(logicalId, entry) => {
1717
const result = matcher.test(entry);
18+
result.finalize();
1819
if (!result.hasFailed()) {
1920
matching[logicalId] = entry;
2021
} else {

‎packages/@aws-cdk/assertions/test/capture.test.ts

+67-13
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,109 @@ describe('Capture', () => {
1515
expect(result.toHumanStrings()[0]).toMatch(/Can only capture non-nullish values/);
1616
});
1717

18+
test('no captures if not finalized', () => {
19+
const capture = new Capture();
20+
const matcher = Match.objectEquals({ foo: capture });
21+
22+
matcher.test({ foo: 'bar' }); // Not calling finalize()
23+
expect(() => capture.asString()).toThrow(/No value captured/);
24+
});
25+
1826
test('asString()', () => {
1927
const capture = new Capture();
2028
const matcher = Match.objectEquals({ foo: capture });
2129

22-
matcher.test({ foo: 'bar' });
23-
expect(capture.asString()).toEqual('bar');
30+
matcher.test({ foo: 'bar' }).finalize();
31+
matcher.test({ foo: 3 }).finalize();
2432

25-
matcher.test({ foo: 3 });
33+
expect(capture.asString()).toEqual('bar');
34+
expect(capture.next()).toEqual(true);
2635
expect(() => capture.asString()).toThrow(/expected to be string but found number/);
2736
});
2837

2938
test('asNumber()', () => {
3039
const capture = new Capture();
3140
const matcher = Match.objectEquals({ foo: capture });
3241

33-
matcher.test({ foo: 3 });
34-
expect(capture.asNumber()).toEqual(3);
42+
matcher.test({ foo: 3 }).finalize();
43+
matcher.test({ foo: 'bar' }).finalize();
3544

36-
matcher.test({ foo: 'bar' });
45+
expect(capture.asNumber()).toEqual(3);
46+
expect(capture.next()).toEqual(true);
3747
expect(() => capture.asNumber()).toThrow(/expected to be number but found string/);
3848
});
3949

4050
test('asArray()', () => {
4151
const capture = new Capture();
4252
const matcher = Match.objectEquals({ foo: capture });
4353

44-
matcher.test({ foo: ['bar'] });
45-
expect(capture.asArray()).toEqual(['bar']);
54+
matcher.test({ foo: ['bar'] }).finalize();
55+
matcher.test({ foo: 'bar' }).finalize();
4656

47-
matcher.test({ foo: 'bar' });
57+
expect(capture.asArray()).toEqual(['bar']);
58+
expect(capture.next()).toEqual(true);
4859
expect(() => capture.asArray()).toThrow(/expected to be array but found string/);
4960
});
5061

5162
test('asObject()', () => {
5263
const capture = new Capture();
5364
const matcher = Match.objectEquals({ foo: capture });
5465

55-
matcher.test({ foo: { fred: 'waldo' } });
56-
expect(capture.asObject()).toEqual({ fred: 'waldo' });
66+
matcher.test({ foo: { fred: 'waldo' } }).finalize();
67+
matcher.test({ foo: 'bar' }).finalize();
5768

58-
matcher.test({ foo: 'bar' });
69+
expect(capture.asObject()).toEqual({ fred: 'waldo' });
70+
expect(capture.next()).toEqual(true);
5971
expect(() => capture.asObject()).toThrow(/expected to be object but found string/);
6072
});
6173

6274
test('nested within an array', () => {
6375
const capture = new Capture();
6476
const matcher = Match.objectEquals({ foo: ['bar', capture] });
6577

66-
matcher.test({ foo: ['bar', 'baz'] });
78+
matcher.test({ foo: ['bar', 'baz'] }).finalize();
6779
expect(capture.asString()).toEqual('baz');
6880
});
81+
82+
test('multiple captures', () => {
83+
const capture = new Capture();
84+
const matcher = Match.objectEquals({ foo: capture, real: true });
85+
86+
matcher.test({ foo: 3, real: true }).finalize();
87+
matcher.test({ foo: 5, real: true }).finalize();
88+
matcher.test({ foo: 7, real: false }).finalize();
89+
90+
expect(capture.asNumber()).toEqual(3);
91+
expect(capture.next()).toEqual(true);
92+
expect(capture.asNumber()).toEqual(5);
93+
expect(capture.next()).toEqual(false);
94+
});
95+
96+
test('nested pattern match', () => {
97+
const capture = new Capture(Match.objectLike({ bar: 'baz' }));
98+
const matcher = Match.objectLike({ foo: capture });
99+
100+
matcher.test({
101+
foo: {
102+
bar: 'baz',
103+
fred: 'waldo',
104+
},
105+
}).finalize();
106+
107+
expect(capture.asObject()).toEqual({ bar: 'baz', fred: 'waldo' });
108+
expect(capture.next()).toEqual(false);
109+
});
110+
111+
test('nested pattern does not match', () => {
112+
const capture = new Capture(Match.objectLike({ bar: 'baz' }));
113+
const matcher = Match.objectLike({ foo: capture });
114+
115+
matcher.test({
116+
foo: {
117+
fred: 'waldo',
118+
},
119+
}).finalize();
120+
121+
expect(() => capture.asObject()).toThrow(/No value captured/);
122+
});
69123
});

‎packages/@aws-cdk/assertions/test/template.test.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { App, CfnMapping, CfnOutput, CfnResource, NestedStack, Stack } from '@aws-cdk/core';
22
import { Construct } from 'constructs';
3-
import { Match, Template } from '../lib';
3+
import { Capture, Match, Template } from '../lib';
44

55
describe('Template', () => {
66
test('fromString', () => {
@@ -293,6 +293,33 @@ describe('Template', () => {
293293
Properties: Match.objectLike({ baz: 'qux' }),
294294
})).toThrow(/No resource/);
295295
});
296+
297+
test('capture', () => {
298+
const stack = new Stack();
299+
new CfnResource(stack, 'Bar1', {
300+
type: 'Foo::Bar',
301+
properties: { baz: 'qux', real: true },
302+
});
303+
new CfnResource(stack, 'Bar2', {
304+
type: 'Foo::Bar',
305+
properties: { baz: 'waldo', real: true },
306+
});
307+
new CfnResource(stack, 'Bar3', {
308+
type: 'Foo::Bar',
309+
properties: { baz: 'fred', real: false },
310+
});
311+
312+
const capture = new Capture();
313+
const inspect = Template.fromStack(stack);
314+
inspect.hasResource('Foo::Bar', {
315+
Properties: Match.objectLike({ baz: capture, real: true }),
316+
});
317+
318+
expect(capture.asString()).toEqual('qux');
319+
expect(capture.next()).toEqual(true);
320+
expect(capture.asString()).toEqual('waldo');
321+
expect(capture.next()).toEqual(false);
322+
});
296323
});
297324

298325
describe('hasResourceProperties', () => {

‎packages/@aws-cdk/pipelines/test/compliance/assets.test.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ describe('pipeline with single asset publisher', () => {
742742

743743
function THEN_codePipelineExpectation() {
744744
// THEN
745-
const buildSpecName = new Capture();
745+
const buildSpecName = new Capture(stringLike('buildspec-*.yaml'));
746746
Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', {
747747
Stages: Match.arrayWith([{
748748
Name: 'Assets',
@@ -764,7 +764,6 @@ describe('pipeline with single asset publisher', () => {
764764

765765
const actualFileName = buildSpecName.asString();
766766

767-
expect(actualFileName).toMatch(/^buildspec-.*\.yaml$/);
768767
const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, actualFileName), { encoding: 'utf-8' }));
769768
expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`);
770769
expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`);
@@ -804,8 +803,8 @@ describe('pipeline with single asset publisher', () => {
804803

805804
function THEN_codePipelineExpectation(pipelineStack2: Stack) {
806805
// THEN
807-
const buildSpecName1 = new Capture();
808-
const buildSpecName2 = new Capture();
806+
const buildSpecName1 = new Capture(stringLike('buildspec-*.yaml'));
807+
const buildSpecName2 = new Capture(stringLike('buildspec-*.yaml'));
809808
Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', {
810809
Source: {
811810
BuildSpec: buildSpecName1,
@@ -870,7 +869,7 @@ describe('pipeline with custom asset publisher BuildSpec', () => {
870869

871870

872871
function THEN_codePipelineExpectation() {
873-
const buildSpecName = new Capture();
872+
const buildSpecName = new Capture(stringLike('buildspec-*'));
874873

875874
Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', {
876875
Stages: Match.arrayWith([{

0 commit comments

Comments
 (0)
Please sign in to comment.