Skip to content

Commit

Permalink
feat(assertions): 'not' matcher (#16240)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
Niranjan Jayakar authored Aug 27, 2021
1 parent fe81be7 commit b838f95
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 14 deletions.
29 changes: 29 additions & 0 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,35 @@ target array. Out of order will be recorded as a match failure.
Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is
exactly equal to the pattern array.
### Not Matcher
The not matcher inverts the search pattern and matches all patterns in the path that does
not match the pattern specified.
```ts
// Given a template -
// {
// "Resources": {
// "MyBar": {
// "Type": "Foo::Bar",
// "Properties": {
// "Fred": ["Flob", "Cat"]
// }
// }
// }
// }

// The following will NOT throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Fred: Match.not(['Flob']),
});

// The following will throw an assertion error
assert.hasResourceProperties('Foo::Bar', Match.objectLike({
Fred: Match.not(['Flob', 'Cat']);
}});
```
## Strongly typed languages
Some of the APIs documented above, such as `templateMatches()` and
Expand Down
39 changes: 28 additions & 11 deletions packages/@aws-cdk/assertions/lib/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ export abstract class Match {
public static objectEquals(pattern: {[key: string]: any}): Matcher {
return new ObjectMatch('objectEquals', pattern, { partial: false });
}

/**
* Matches any target which does NOT follow the specified pattern.
* @param pattern the pattern to NOT match
*/
public static not(pattern: any): Matcher {
return new NotMatch('not', pattern);
}
}

/**
Expand Down Expand Up @@ -82,7 +90,6 @@ class LiteralMatch extends Matcher {

super();
this.partialObjects = options.partialObjects ?? false;
this.name = 'exact';

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

super();
this.partial = options.subsequence ?? true;
if (this.partial) {
this.name = 'arrayWith';
} else {
this.name = 'arrayEquals';
}
}

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

super();
this.partial = options.partial ?? true;
if (this.partial) {
this.name = 'objectLike';
} else {
this.name = 'objectEquals';
}
}

public test(actual: any): MatchResult {
Expand Down Expand Up @@ -254,6 +251,26 @@ class ObjectMatch extends Matcher {
}
}

class NotMatch extends Matcher {
constructor(
public readonly name: string,
private readonly pattern: {[key: string]: any}) {

super();
}

public test(actual: any): MatchResult {
const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern);

const innerResult = matcher.test(actual);
const result = new MatchResult(actual);
if (innerResult.failCount === 0) {
result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`);
}
return result;
}
}

function getType(obj: any): string {
return Array.isArray(obj) ? 'array' : typeof obj;
}
101 changes: 98 additions & 3 deletions packages/@aws-cdk/assertions/test/match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,112 @@ describe('Matchers', () => {
expectFailure(matcher, { foo: 'bar', baz: 'qux' }, [/Unexpected key at \/baz/]);
});
});

describe('not()', () => {
let matcher: Matcher;

test('literal', () => {
matcher = Match.not('foo');
expectPass(matcher, 'bar');
expectPass(matcher, 3);

expectFailure(matcher, 'foo', ['Found unexpected match: "foo"']);
});

test('object', () => {
matcher = Match.not({ foo: 'bar' });
expectPass(matcher, 'bar');
expectPass(matcher, 3);
expectPass(matcher, { foo: 'baz' });
expectPass(matcher, { bar: 'foo' });

const msg = [
'Found unexpected match: {',
' "foo": "bar"',
'}',
].join('\n');
expectFailure(matcher, { foo: 'bar' }, [msg]);
});

test('array', () => {
matcher = Match.not(['foo', 'bar']);
expectPass(matcher, 'foo');
expectPass(matcher, []);
expectPass(matcher, ['bar']);
expectPass(matcher, ['foo', 3]);

const msg = [
'Found unexpected match: [',
' "foo",',
' "bar"',
']',
].join('\n');
expectFailure(matcher, ['foo', 'bar'], [msg]);
});

test('as a nested matcher', () => {
matcher = Match.exact({
foo: { bar: Match.not([1, 2]) },
});

expectPass(matcher, {
foo: { bar: [1] },
});
expectPass(matcher, {
foo: { bar: ['baz'] },
});

const msg = [
'Found unexpected match: [',
' 1,',
' 2',
'] at /foo/bar',
].join('\n');
expectFailure(matcher, {
foo: { bar: [1, 2] },
}, [msg]);
});

test('with nested matcher', () => {
matcher = Match.not({
foo: { bar: Match.arrayWith([1]) },
});

expectPass(matcher, {
foo: { bar: [2] },
});
expectPass(matcher, 'foo');

const msg = [
'Found unexpected match: {',
' "foo": {',
' "bar": [',
' 1,',
' 2',
' ]',
' }',
'}',
].join('\n');
expectFailure(matcher, {
foo: { bar: [1, 2] },
}, [msg]);
});
});
});

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

function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[]): void {
const actual = matcher.test(target).toHumanStrings();
function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void {
const result = matcher.test(target);
expect(result.failCount).toBeGreaterThan(0);
const actual = result.toHumanStrings();
if (expected.length > 0) {
expect(actual.length).toEqual(expected.length);
}
for (let i = 0; i < expected.length; i++) {
const e = expected[i];
expect(actual[i]).toMatch(e);
}
expect(expected.length).toEqual(actual.length);
}

0 comments on commit b838f95

Please sign in to comment.