Skip to content

Commit b838f95

Browse files
author
Niranjan Jayakar
authored
feat(assertions): 'not' matcher (#16240)
Supply a 'not' matcher that can be used to invert the matching pattern relates #15868 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent fe81be7 commit b838f95

File tree

3 files changed

+155
-14
lines changed

3 files changed

+155
-14
lines changed

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

+29
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,35 @@ target array. Out of order will be recorded as a match failure.
238238
Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is
239239
exactly equal to the pattern array.
240240
241+
### Not Matcher
242+
243+
The not matcher inverts the search pattern and matches all patterns in the path that does
244+
not match the pattern specified.
245+
246+
```ts
247+
// Given a template -
248+
// {
249+
// "Resources": {
250+
// "MyBar": {
251+
// "Type": "Foo::Bar",
252+
// "Properties": {
253+
// "Fred": ["Flob", "Cat"]
254+
// }
255+
// }
256+
// }
257+
// }
258+
259+
// The following will NOT throw an assertion error
260+
assert.hasResourceProperties('Foo::Bar', {
261+
Fred: Match.not(['Flob']),
262+
});
263+
264+
// The following will throw an assertion error
265+
assert.hasResourceProperties('Foo::Bar', Match.objectLike({
266+
Fred: Match.not(['Flob', 'Cat']);
267+
}});
268+
```
269+
241270
## Strongly typed languages
242271
243272
Some of the APIs documented above, such as `templateMatches()` and

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

+28-11
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export abstract class Match {
5555
public static objectEquals(pattern: {[key: string]: any}): Matcher {
5656
return new ObjectMatch('objectEquals', pattern, { partial: false });
5757
}
58+
59+
/**
60+
* Matches any target which does NOT follow the specified pattern.
61+
* @param pattern the pattern to NOT match
62+
*/
63+
public static not(pattern: any): Matcher {
64+
return new NotMatch('not', pattern);
65+
}
5866
}
5967

6068
/**
@@ -82,7 +90,6 @@ class LiteralMatch extends Matcher {
8290

8391
super();
8492
this.partialObjects = options.partialObjects ?? false;
85-
this.name = 'exact';
8693

8794
if (Matcher.isMatcher(this.pattern)) {
8895
throw new Error('LiteralMatch cannot directly contain another matcher. ' +
@@ -143,11 +150,6 @@ class ArrayMatch extends Matcher {
143150

144151
super();
145152
this.partial = options.subsequence ?? true;
146-
if (this.partial) {
147-
this.name = 'arrayWith';
148-
} else {
149-
this.name = 'arrayEquals';
150-
}
151153
}
152154

153155
public test(actual: any): MatchResult {
@@ -211,11 +213,6 @@ class ObjectMatch extends Matcher {
211213

212214
super();
213215
this.partial = options.partial ?? true;
214-
if (this.partial) {
215-
this.name = 'objectLike';
216-
} else {
217-
this.name = 'objectEquals';
218-
}
219216
}
220217

221218
public test(actual: any): MatchResult {
@@ -254,6 +251,26 @@ class ObjectMatch extends Matcher {
254251
}
255252
}
256253

254+
class NotMatch extends Matcher {
255+
constructor(
256+
public readonly name: string,
257+
private readonly pattern: {[key: string]: any}) {
258+
259+
super();
260+
}
261+
262+
public test(actual: any): MatchResult {
263+
const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern);
264+
265+
const innerResult = matcher.test(actual);
266+
const result = new MatchResult(actual);
267+
if (innerResult.failCount === 0) {
268+
result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`);
269+
}
270+
return result;
271+
}
272+
}
273+
257274
function getType(obj: any): string {
258275
return Array.isArray(obj) ? 'array' : typeof obj;
259276
}

packages/@aws-cdk/assertions/test/match.test.ts

+98-3
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,112 @@ describe('Matchers', () => {
194194
expectFailure(matcher, { foo: 'bar', baz: 'qux' }, [/Unexpected key at \/baz/]);
195195
});
196196
});
197+
198+
describe('not()', () => {
199+
let matcher: Matcher;
200+
201+
test('literal', () => {
202+
matcher = Match.not('foo');
203+
expectPass(matcher, 'bar');
204+
expectPass(matcher, 3);
205+
206+
expectFailure(matcher, 'foo', ['Found unexpected match: "foo"']);
207+
});
208+
209+
test('object', () => {
210+
matcher = Match.not({ foo: 'bar' });
211+
expectPass(matcher, 'bar');
212+
expectPass(matcher, 3);
213+
expectPass(matcher, { foo: 'baz' });
214+
expectPass(matcher, { bar: 'foo' });
215+
216+
const msg = [
217+
'Found unexpected match: {',
218+
' "foo": "bar"',
219+
'}',
220+
].join('\n');
221+
expectFailure(matcher, { foo: 'bar' }, [msg]);
222+
});
223+
224+
test('array', () => {
225+
matcher = Match.not(['foo', 'bar']);
226+
expectPass(matcher, 'foo');
227+
expectPass(matcher, []);
228+
expectPass(matcher, ['bar']);
229+
expectPass(matcher, ['foo', 3]);
230+
231+
const msg = [
232+
'Found unexpected match: [',
233+
' "foo",',
234+
' "bar"',
235+
']',
236+
].join('\n');
237+
expectFailure(matcher, ['foo', 'bar'], [msg]);
238+
});
239+
240+
test('as a nested matcher', () => {
241+
matcher = Match.exact({
242+
foo: { bar: Match.not([1, 2]) },
243+
});
244+
245+
expectPass(matcher, {
246+
foo: { bar: [1] },
247+
});
248+
expectPass(matcher, {
249+
foo: { bar: ['baz'] },
250+
});
251+
252+
const msg = [
253+
'Found unexpected match: [',
254+
' 1,',
255+
' 2',
256+
'] at /foo/bar',
257+
].join('\n');
258+
expectFailure(matcher, {
259+
foo: { bar: [1, 2] },
260+
}, [msg]);
261+
});
262+
263+
test('with nested matcher', () => {
264+
matcher = Match.not({
265+
foo: { bar: Match.arrayWith([1]) },
266+
});
267+
268+
expectPass(matcher, {
269+
foo: { bar: [2] },
270+
});
271+
expectPass(matcher, 'foo');
272+
273+
const msg = [
274+
'Found unexpected match: {',
275+
' "foo": {',
276+
' "bar": [',
277+
' 1,',
278+
' 2',
279+
' ]',
280+
' }',
281+
'}',
282+
].join('\n');
283+
expectFailure(matcher, {
284+
foo: { bar: [1, 2] },
285+
}, [msg]);
286+
});
287+
});
197288
});
198289

199290
function expectPass(matcher: Matcher, target: any): void {
200291
expect(matcher.test(target).hasFailed()).toEqual(false);
201292
}
202293

203-
function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[]): void {
204-
const actual = matcher.test(target).toHumanStrings();
294+
function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void {
295+
const result = matcher.test(target);
296+
expect(result.failCount).toBeGreaterThan(0);
297+
const actual = result.toHumanStrings();
298+
if (expected.length > 0) {
299+
expect(actual.length).toEqual(expected.length);
300+
}
205301
for (let i = 0; i < expected.length; i++) {
206302
const e = expected[i];
207303
expect(actual[i]).toMatch(e);
208304
}
209-
expect(expected.length).toEqual(actual.length);
210305
}

0 commit comments

Comments
 (0)