Skip to content

Commit

Permalink
partway through messaging
Browse files Browse the repository at this point in the history
  • Loading branch information
Niranjan Jayakar committed Jun 21, 2021
1 parent 58a837a commit cc29908
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 183 deletions.
106 changes: 73 additions & 33 deletions packages/@aws-cdk/assertions/lib/match.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ABSENT } from './vendored/assert';

export interface MatchFailure {
readonly path: string[];
readonly message: string;
}

/**
* Partial and special matching during template assertions
*/
Expand All @@ -23,7 +28,7 @@ export abstract class Match {
return x && x instanceof Match;
}

public abstract test(actual: any): boolean;
public abstract test(actual: any): MatchFailure[];
}

export class ExactMatch extends Match {
Expand All @@ -35,47 +40,48 @@ export class ExactMatch extends Match {
}
}

public test(actual: any): boolean {
if (Array.isArray(actual) !== Array.isArray(this.pattern)) {
return false;
}
public test(actual: any): MatchFailure[] {
if (Array.isArray(this.pattern)) {
if (!Array.isArray(actual)) {
return [{ path: [], message: `Expected type array but received ${typeof actual}` }];
}

if (Array.isArray(actual)) {
if (this.pattern.length !== actual.length) {
return false;
return [{ path: [], message: `Expected array of length ${this.pattern.length} but received ${actual.length}` }];
}

const failures: MatchFailure[] = [];
for (let i = 0; i < this.pattern.length; i++) {
const p = this.pattern[i];
const matcher = Match.isMatcher(p) ? p : new ExactMatch(p);
if (!matcher.test(actual[i])) return false;
const innerFailures = matcher.test(actual[i]);
failures.push(...composeFailures(`[${i}]`, innerFailures));
}

return true;
}

if ((typeof actual === 'object') !== (typeof this.pattern === 'object')) {
return false;
return failures;
}

if (typeof this.pattern === 'object') {
const patternKeys = Object.keys(this.pattern).sort();
const actualKeys = Object.keys(actual).sort();
if (Array.isArray(actual)) {
return [{ path: [], message: 'Expected type object but received array' }];
}

const sameKeys = new ExactMatch(patternKeys).test(actualKeys);
if (!sameKeys) return false;
if (typeof actual !== 'object') {
return [{ path: [], message: `Expected type object but received ${typeof actual}` }];
}

const objectMatch = new ObjectLikeMatch(this.pattern).test(actual);
if (!objectMatch) return false;
return new ObjectLikeMatch(this.pattern, { exact: true }).test(actual);
}

return true;
if (typeof this.pattern !== typeof actual) {
return [{ path: [], message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` }];
}

if (actual !== this.pattern) {
return false;
return [{ path: [], message: `Expected ${this.pattern} but received ${actual}` }];
}

return true;
return [];
}
}

Expand All @@ -84,9 +90,9 @@ export class ArrayWithMatch extends Match {
super();
}

public test(actual: any): boolean {
if (!Array.isArray(actual)) return false;
if (this.pattern.length > actual.length) return false;
public test(actual: any): MatchFailure[] {
if (!Array.isArray(actual)) return [{ path: [], message: 'FIXME' }];
if (this.pattern.length > actual.length) return [{ path: [], message: 'FIXME' }];

let patternIdx = 0;
let actualIdx = 0;
Expand All @@ -103,26 +109,60 @@ export class ArrayWithMatch extends Match {
}

if (patternIdx === this.pattern.length) {
return true;
return [];
}
return false;
return [{ path: [], message: 'FIXME' }];
}
}

export interface ObjectLikeMatchOptions {
readonly exact?: boolean;
}

export class ObjectLikeMatch extends Match {
constructor(private readonly pattern: {[key: string]: any}) {
private readonly exact: boolean;
constructor(
private readonly pattern: {[key: string]: any},
options: ObjectLikeMatchOptions = {}) {

super();
this.exact = options.exact ?? false;
}

public test(actual: any): boolean {
if (typeof actual !== 'object') return false;
public test(actual: any): MatchFailure[] {
if (typeof actual !== 'object') {
return [{ path: [], message: `Expected type object but received ${getType(actual)}` }];
}

const failures: MatchFailure[] = [];
if (this.exact) {
for (const a of Object.keys(actual)) {
if (!(a in this.pattern)) {
failures.push({ path: [], message: `Unexpected key '${a}'` });
}
}
}

for (const [patternKey, patternVal] of Object.entries(this.pattern)) {
if (!(patternKey in actual)) return false;
if (!(patternKey in actual)) {
failures.push({ path: [], message: `Missing key '${patternKey}'` });
continue;
}
const matcher = Match.isMatcher(patternVal) ? patternVal : new ExactMatch(patternVal);
if (!matcher.test(actual[patternKey])) return false;
const innerFailures = matcher.test(actual[patternKey]);
failures.push(...composeFailures(`/${patternKey}`, innerFailures));
}

return true;
return failures;
}
}

function getType(obj: any): string {
return Array.isArray(obj) ? 'array' : typeof obj;
}

function composeFailures(relativePath: string, inner: MatchFailure[]): MatchFailure[] {
return inner.map(f => {
return { path: [relativePath, ...f.path], message: f.message };
});
}
121 changes: 61 additions & 60 deletions packages/@aws-cdk/assertions/test/assertions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable jest/no-commented-out-tests */
import { CfnResource, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Match, TemplateAssertions } from '../lib';
import { TemplateAssertions } from '../lib';

describe('StackAssertions', () => {
describe('fromString', () => {
Expand Down Expand Up @@ -136,63 +137,63 @@ describe('StackAssertions', () => {
});
});

describe('hasResource', () => {
test('exact match', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = TemplateAssertions.fromStack(stack);
inspect.hasResource('Foo::Bar', {
Properties: { baz: 'qux' },
});

expect(() => inspect.hasResource('Foo::Bar', {
Properties: { baz: 'waldo' },
})).toThrow('MYERROR');

expect(() => inspect.hasResource('Foo::Bar', {
Properties: { baz: 'qux', fred: 'waldo' },
})).toThrow('MYERROR');
});

test('arrayWith', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: ['qux', 'quy'] },
});

const inspect = TemplateAssertions.fromStack(stack);
inspect.hasResource('Foo::Bar', {
Properties: { baz: Match.arrayWith(['qux']) },
});

expect(() => inspect.hasResource('Foo::Bar', {
Properties: { baz: Match.arrayWith(['waldo']) },
})).toThrow('MYERROR');
});

test('objectLike', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux', fred: 'waldo' },
});

const inspect = TemplateAssertions.fromStack(stack);
inspect.hasResource('Foo::Bar', {
Properties: Match.objectLike({ baz: 'qux' }),
});
inspect.hasResource('Foo::Bar', {
Properties: Match.objectLike({ fred: 'waldo' }),
});

expect(() => inspect.hasResource('Foo::Bar', {
Properties: Match.objectLike({ baz: 'waldo' }),
})).toThrow('MYERROR');
});
});
// describe('hasResource', () => {
// test('exact match', () => {
// const stack = new Stack();
// new CfnResource(stack, 'Foo', {
// type: 'Foo::Bar',
// properties: { baz: 'qux' },
// });

// const inspect = TemplateAssertions.fromStack(stack);
// inspect.hasResource('Foo::Bar', {
// Properties: { baz: 'qux' },
// });

// expect(() => inspect.hasResource('Foo::Bar', {
// Properties: { baz: 'waldo' },
// })).toThrow('MYERROR');

// expect(() => inspect.hasResource('Foo::Bar', {
// Properties: { baz: 'qux', fred: 'waldo' },
// })).toThrow('MYERROR');
// });

// test('arrayWith', () => {
// const stack = new Stack();
// new CfnResource(stack, 'Foo', {
// type: 'Foo::Bar',
// properties: { baz: ['qux', 'quy'] },
// });

// const inspect = TemplateAssertions.fromStack(stack);
// inspect.hasResource('Foo::Bar', {
// Properties: { baz: Match.arrayWith(['qux']) },
// });

// expect(() => inspect.hasResource('Foo::Bar', {
// Properties: { baz: Match.arrayWith(['waldo']) },
// })).toThrow('MYERROR');
// });

// test('objectLike', () => {
// const stack = new Stack();
// new CfnResource(stack, 'Foo', {
// type: 'Foo::Bar',
// properties: { baz: 'qux', fred: 'waldo' },
// });

// const inspect = TemplateAssertions.fromStack(stack);
// inspect.hasResource('Foo::Bar', {
// Properties: Match.objectLike({ baz: 'qux' }),
// });
// inspect.hasResource('Foo::Bar', {
// Properties: Match.objectLike({ fred: 'waldo' }),
// });

// expect(() => inspect.hasResource('Foo::Bar', {
// Properties: Match.objectLike({ baz: 'waldo' }),
// })).toThrow('MYERROR');
// });
// });
});
Loading

0 comments on commit cc29908

Please sign in to comment.