packages/@alicloud/ros-cdk-assert/lib/assertions/match-template.ts (188 lines of code) (raw):
import * as rosDiff from '@alicloud/ros-cdk-template-diff';
import {Assertion} from '../assertion';
import {StackInspector} from '../inspector';
import * as colors from 'colors/safe';
export enum MatchStyle {
/** Requires an exact match */
EXACT = 'exactly',
/** Allows any change that does not cause a resource replacement */
NO_REPLACES = 'no replaces',
/** Allows additions, but no updates */
SUPERSET = 'superset',
/** Allows regular expressions to match strings */
REGEXP = 'regexp',
}
export function exactlyMatchTemplate(template: { [key: string]: any }) {
return matchTemplate(template, MatchStyle.EXACT);
}
export function beASupersetOfTemplate(template: { [key: string]: any }) {
return matchTemplate(template, MatchStyle.SUPERSET);
}
export function matchTemplate(
template: { [key: string]: any },
matchStyle: MatchStyle = MatchStyle.EXACT,
): Assertion<StackInspector> {
return new StackMatchesTemplateAssertion(template, matchStyle);
}
class StackMatchesTemplateAssertion extends Assertion<StackInspector> {
public diff?: rosDiff.TemplateDiff;
constructor(private readonly template: { [key: string]: any }, private readonly matchStyle: MatchStyle) {
super();
}
public assertOrThrow(inspector: StackInspector) {
if (!this.assertUsing(inspector)) {
// The details have already been printed, so don't generate a huge error message
throw new Error('Template comparison produced unacceptable match');
}
}
public assertUsing(inspector: StackInspector): boolean {
this.diff = rosDiff.diffTemplate(this.template, inspector.value);
const acceptable = this.isDiffAcceptable();
if (!acceptable) {
// Print the diff
rosDiff.formatDifferences(process.stderr, this.diff);
// Print the actual template
process.stderr.write(colors.rainbow('--------------------------------------------------------------------------------------\n'));
process.stderr.write(colors.white((JSON.stringify(inspector.value, undefined, 2) + '\n')));
}
return acceptable;
}
private isDiffAcceptable(): boolean {
switch (this.matchStyle) {
case MatchStyle.EXACT:
return this.diff!.differenceCount === 0;
case MatchStyle.NO_REPLACES:
for (const change of Object.values(this.diff!.resources.changes)) {
if (change.changeImpact === rosDiff.ResourceImpact.MAY_REPLACE) {
return false;
}
if (change.changeImpact === rosDiff.ResourceImpact.WILL_REPLACE) {
return false;
}
}
for (const change of Object.values(this.diff!.parameters.changes)) {
if (change.isUpdate) {
return false;
}
}
for (const change of Object.values(this.diff!.outputs.changes)) {
if (change.isUpdate) {
return false;
}
}
return true;
case MatchStyle.SUPERSET:
for (const change of Object.values(this.diff!.resources.changes)) {
if (change.changeImpact !== rosDiff.ResourceImpact.WILL_CREATE) {
return false;
}
}
for (const change of Object.values(this.diff!.parameters.changes)) {
if (change.isAddition) {
return false;
}
}
for (const change of Object.values(this.diff!.outputs.changes)) {
if (change.isAddition || change.isUpdate) {
return false;
}
}
return true;
case MatchStyle.REGEXP:
let diffMap = this.diff!.toMap();
let keysToDelete: string[] = [];
for (const key in diffMap) {
const value = diffMap[key];
if (value === undefined || value === null || (typeof value === 'object' && Object.keys(value).length === 0)) {
keysToDelete.push(key);
continue;
}
if (rosDiff.isDifferenceInstance(value)) {
if (compareVariablesWithRegExp(value.oldValue, value.newValue)) {
keysToDelete.push(key);
}
} else if (typeof value === 'object' && !Array.isArray(value)) {
for (const subKey in value) {
if (`${key}.${subKey}` in keysToDelete) {
continue;
}
if (compareVariablesWithRegExp(value[subKey].oldValue, value[subKey].newValue)) {
keysToDelete.push(key + '.' + subKey);
} else {
for (const otherSubKey in value) {
if (`${key}.${otherSubKey}` in keysToDelete) {
continue;
}
const regex = new RegExp(subKey);
if (otherSubKey === subKey || !regex.test(otherSubKey)) {
continue;
}
if (compareVariablesWithRegExp(value[subKey].oldValue, value[otherSubKey].newValue)
|| compareVariablesWithRegExp(value[otherSubKey].oldValue, value[subKey].newValue)) {
keysToDelete.push(key + '.' + subKey);
keysToDelete.push(key + '.' + otherSubKey);
break;
}
}
}
}
}
}
for (const key of keysToDelete) {
const keys = key.split('.');
if (keys.length == 1) {
delete diffMap[key];
} else {
delete diffMap[keys[0]][keys[1]];
if (Object.keys(diffMap[keys[0]]).length === 0) {
delete diffMap[keys[0]];
}
}
}
this.diff = rosDiff.TemplateDiff.fromMap(diffMap);
return this.diff!.differenceCount === 0;
}
throw new Error(`Unsupported match style: ${this.matchStyle}`);
}
public get description(): string {
return `template (${this.matchStyle}): ${JSON.stringify(this.template, null, 2)}`;
}
}
function compareVariablesWithRegExp(var1: any, var2: any): boolean {
if (var1 === undefined || var2 === undefined) {
return false;
}
if (typeof var1 !== typeof var2) {
return false;
}
if (typeof var1 === 'string' && typeof var2 === 'string') {
if (var1 !== var2) {
const regex = new RegExp(var1);
return regex.test(var2);
} else {
return true;
}
}
if (typeof var1 !== 'object' && typeof var2 !== 'object') {
return var1 === var2;
}
if (typeof var1 === 'object' && typeof var2 === 'object') {
if (Array.isArray(var1) && Array.isArray(var2)) {
if (var1.length !== var2.length) {
return false;
}
for (let i = 0; i < var1.length; i++) {
if (!compareVariablesWithRegExp(var1[i], var2[i])) {
return false;
}
}
return true;
} else {
const keys1 = Object.keys(var1);
const keys2 = Object.keys(var2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!compareVariablesWithRegExp(var1[key], var2[key])) {
return false;
}
}
return true;
}
}
return false;
}