Skip to content

Commit

Permalink
chore: make tests independent of asset hashes (#8969)
Browse files Browse the repository at this point in the history
Introduce a `canonicalizeTemplate` function in `@aws-cdk/assert` which
translates templates to a common form w.r.t. asset hashes.

Use this in some tests and in `cdk-integ-assert` to make the tests
succeed if all that is different is the specific asset hash. It is switched on
by putting `/// !cdk-integ pragma:ignore-assets` at the top of the integ test file.

Currently only supports legacy assets, should be updated for new-style
assets when we switch to those.

This change is necessary to unblock other build improvements/changes
such as #8946.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
rix0rrr authored Jul 9, 2020
1 parent d80efae commit 0a3ae47
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 27 deletions.
71 changes: 71 additions & 0 deletions packages/@aws-cdk/assert/lib/canonicalize-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Reduce template to a normal form where asset references have been normalized
*
* This makes it possible to compare templates if all that's different between
* them is the hashes of the asset values.
*
* Currently only handles parameterized assets, but can (and should)
* be adapted to handle convention-mode assets as well when we start using
* more of those.
*/
export function canonicalizeTemplate(template: any): any {
// For the weird case where we have an array of templates...
if (Array.isArray(template)) {
return template.map(canonicalizeTemplate);
}

// Find assets via parameters
const stringSubstitutions = new Array<[RegExp, string]>();
const paramRe = /^AssetParameters([a-zA-Z0-9]{64})(S3Bucket|S3VersionKey|ArtifactHash)([a-zA-Z0-9]{8})$/;

const assetsSeen = new Set<string>();
for (const paramName of Object.keys(template?.Parameters || {})) {
const m = paramRe.exec(paramName);
if (!m) { continue; }
if (assetsSeen.has(m[1])) { continue; }

assetsSeen.add(m[1]);
const ix = assetsSeen.size;

// Full parameter reference
stringSubstitutions.push([
new RegExp(`AssetParameters${m[1]}(S3Bucket|S3VersionKey|ArtifactHash)([a-zA-Z0-9]{8})`),
`Asset${ix}$1`,
]);
// Substring asset hash reference
stringSubstitutions.push([
new RegExp(`${m[1]}`),
`Asset${ix}Hash`,
]);
}

// Substitute them out
return substitute(template);

function substitute(what: any): any {
if (Array.isArray(what)) {
return what.map(substitute);
}

if (typeof what === 'object' && what !== null) {
const ret: any = {};
for (const [k, v] of Object.entries(what)) {
ret[stringSub(k)] = substitute(v);
}
return ret;
}

if (typeof what === 'string') {
return stringSub(what);
}

return what;
}

function stringSub(x: string) {
for (const [re, replacement] of stringSubstitutions) {
x = x.replace(re, replacement);
}
return x;
}
}
6 changes: 4 additions & 2 deletions packages/@aws-cdk/assert/lib/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import * as api from '@aws-cdk/cx-api';
import { StackInspector } from './inspector';
import { SynthUtils } from './synth-utils';

export function expect(stack: api.CloudFormationStackArtifact | cdk.Stack, skipValidation = false): StackInspector {
export function expect(stack: api.CloudFormationStackArtifact | cdk.Stack | Record<string, any>, skipValidation = false): StackInspector {
// if this is already a synthesized stack, then just inspect it.
const artifact = stack instanceof api.CloudFormationStackArtifact ? stack : SynthUtils._synthesizeWithNested(stack, { skipValidation });
const artifact = stack instanceof api.CloudFormationStackArtifact ? stack
: cdk.Stack.isStack(stack) ? SynthUtils._synthesizeWithNested(stack, { skipValidation })
: stack; // This is a template already
return new StackInspector(artifact);
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/assert/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './assertion';
export * from './canonicalize-assets';
export * from './expect';
export * from './inspector';
export * from './synth-utils';
Expand Down
135 changes: 135 additions & 0 deletions packages/@aws-cdk/assert/test/canonicalize-assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { canonicalizeTemplate } from '../lib';

test('Canonicalize asset parameters and references to them', () => {
const template = {
Resources: {
AResource: {
Type: 'Some::Resource',
Properties: {
SomeValue: { Ref: 'AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907' },
},
},
},
Parameters: {
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907: {
Type: 'String',
Description: 'S3 bucket for asset \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3VersionKey6841F1F8: {
Type: 'String',
Description: 'S3 key for asset version \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161ArtifactHash67B22EF2: {
Type: 'String',
Description: 'Artifact hash for asset \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
},
};

const expected = {
Resources: {
AResource: {
Type: 'Some::Resource',
Properties: {
SomeValue: { Ref: 'Asset1S3Bucket' },
},
},
},
Parameters: {
Asset1S3Bucket: {
Type: 'String',
Description: 'S3 bucket for asset \'Asset1Hash\'',
},
Asset1S3VersionKey: {
Type: 'String',
Description: 'S3 key for asset version \'Asset1Hash\'',
},
Asset1ArtifactHash: {
Type: 'String',
Description: 'Artifact hash for asset \'Asset1Hash\'',
},
},
};

expect(canonicalizeTemplate(template)).toEqual(expected);
});

test('Distinguished 2 different assets', () => {
const template = {
Resources: {
AResource: {
Type: 'Some::Resource',
Properties: {
SomeValue: { Ref: 'AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907' },
OtherValue: { Ref: 'AssetParameters1111111111111111111111111111111111122222222222222222222222222222ArtifactHash67B22EF2' },
},
},
},
Parameters: {
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3Bucket0C424907: {
Type: 'String',
Description: 'S3 bucket for asset \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161S3VersionKey6841F1F8: {
Type: 'String',
Description: 'S3 key for asset version \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
AssetParametersea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161ArtifactHash67B22EF2: {
Type: 'String',
Description: 'Artifact hash for asset \'ea46702e1c05b2735e48e826d630f7bf6acdf7e55d6fa8d9fa8df858d5542161\'',
},
AssetParameters1111111111111111111111111111111111122222222222222222222222222222S3Bucket0C424907: {
Type: 'String',
Description: 'S3 bucket for asset \'1111111111111111111111111111111111122222222222222222222222222222\'',
},
AssetParameters1111111111111111111111111111111111122222222222222222222222222222S3VersionKey6841F1F8: {
Type: 'String',
Description: 'S3 key for asset version \'1111111111111111111111111111111111122222222222222222222222222222\'',
},
AssetParameters1111111111111111111111111111111111122222222222222222222222222222ArtifactHash67B22EF2: {
Type: 'String',
Description: 'Artifact hash for asset \'1111111111111111111111111111111111122222222222222222222222222222\'',
},
},
};

const expected = {
Resources: {
AResource: {
Type: 'Some::Resource',
Properties: {
SomeValue: { Ref: 'Asset1S3Bucket' },
OtherValue: { Ref: 'Asset2ArtifactHash' },
},
},
},
Parameters: {
Asset1S3Bucket: {
Type: 'String',
Description: 'S3 bucket for asset \'Asset1Hash\'',
},
Asset1S3VersionKey: {
Type: 'String',
Description: 'S3 key for asset version \'Asset1Hash\'',
},
Asset1ArtifactHash: {
Type: 'String',
Description: 'Artifact hash for asset \'Asset1Hash\'',
},
Asset2S3Bucket: {
Type: 'String',
Description: 'S3 bucket for asset \'Asset2Hash\'',
},
Asset2S3VersionKey: {
Type: 'String',
Description: 'S3 key for asset version \'Asset2Hash\'',
},
Asset2ArtifactHash: {
Type: 'String',
Description: 'Artifact hash for asset \'Asset2Hash\'',
},
},
};

expect(canonicalizeTemplate(template)).toEqual(expected);
});
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-lambda/test/test.layers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
import { canonicalizeTemplate, expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
Expand Down Expand Up @@ -85,9 +85,9 @@ export = testCase({
});

// THEN
expect(stack).to(haveResource('AWS::Lambda::LayerVersion', {
expect(canonicalizeTemplate(SynthUtils.toCloudFormation(stack))).to(haveResource('AWS::Lambda::LayerVersion', {
Metadata: {
'aws:asset:path': 'asset.8811a2632ac5564a08fd269e159298f7e497f259578b0dc5e927a1f48ab24d34',
'aws:asset:path': 'asset.Asset1Hash',
'aws:asset:property': 'Content',
},
}, ResourcePart.CompleteDefinition));
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/core/lib/private/runtime-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function collectRuntimeInformation(): cxschema.RuntimeInfo {
function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined {
const mod = require.cache[fileName];

if (!mod.paths) {
if (!mod?.paths) {
// sometimes this can be undefined. for example when querying for .json modules
// inside a jest runtime environment.
// see https://github.com/aws/aws-cdk/issues/7657
Expand All @@ -74,7 +74,7 @@ function findNpmPackage(fileName: string): { name: string, version: string, priv
}

// For any path in ``mod.paths`` that is a node_modules folder, use its parent directory instead.
const paths = mod.paths.map((path: string) => basename(path) === 'node_modules' ? dirname(path) : path);
const paths = mod?.paths.map((path: string) => basename(path) === 'node_modules' ? dirname(path) : path);

try {
const packagePath = require.resolve(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import { Test } from 'nodeunit';
import * as path from 'path';
import { CustomResourceProvider, CustomResourceProviderRuntime, Duration, Size, Stack } from '../../lib';
import { AssetStaging, CustomResourceProvider, CustomResourceProviderRuntime, Duration, Size, Stack } from '../../lib';
import { toCloudFormation } from '../util';

const TEST_HANDLER = `${__dirname}/mock-provider`;
Expand All @@ -20,6 +20,16 @@ export = {
// THEN
test.ok(fs.existsSync(path.join(TEST_HANDLER, '__entrypoint__.js')), 'expecting entrypoint to be copied to the handler directory');
const cfn = toCloudFormation(stack);

// The asset hash constantly changes, so in order to not have to chase it, just look
// it up from the output.
const staging = stack.node.tryFindChild('Custom:MyResourceTypeCustomResourceProvider')?.node.tryFindChild('Staging') as AssetStaging;
const assetHash = staging.sourceHash;
const paramNames = Object.keys(cfn.Parameters);
const bucketParam = paramNames[0];
const keyParam = paramNames[1];
const hashParam = paramNames[2];

test.deepEqual(cfn, {
Resources: {
CustomMyResourceTypeCustomResourceProviderRoleBD5E655F: {
Expand Down Expand Up @@ -48,9 +58,7 @@ export = {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: {
Ref: 'AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226S3Bucket8B4D0E9E',
},
S3Bucket: { Ref: bucketParam },
S3Key: {
'Fn::Join': [
'',
Expand All @@ -61,9 +69,7 @@ export = {
{
'Fn::Split': [
'||',
{
Ref: 'AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226S3VersionKeyDECB34FE',
},
{ Ref: keyParam },
],
},
],
Expand All @@ -74,9 +80,7 @@ export = {
{
'Fn::Split': [
'||',
{
Ref: 'AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226S3VersionKeyDECB34FE',
},
{ Ref: keyParam },
],
},
],
Expand All @@ -102,17 +106,17 @@ export = {
},
},
Parameters: {
AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226S3Bucket8B4D0E9E: {
[bucketParam]: {
Type: 'String',
Description: 'S3 bucket for asset "925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226"',
Description: `S3 bucket for asset "${assetHash}"`,
},
AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226S3VersionKeyDECB34FE: {
[keyParam]: {
Type: 'String',
Description: 'S3 key for asset version "925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226"',
Description: `S3 key for asset version "${assetHash}"`,
},
AssetParameters925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226ArtifactHashEEC400F2: {
[hashParam]: {
Type: 'String',
Description: 'Artifact hash for asset "925e7fbbec7bdbf0136ef5a07b8a0fbe0b1f1bb4ea50ae2154163df78aa9f226"',
Description: `Artifact hash for asset "${assetHash}"`,
},
},
});
Expand Down
11 changes: 9 additions & 2 deletions tools/cdk-integ-tools/bin/cdk-integ-assert.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#!/usr/bin/env node
// Verify that all integration tests still match their expected output
import { canonicalizeTemplate } from '@aws-cdk/assert';
import { diffTemplate, formatDifferences } from '@aws-cdk/cloudformation-diff';
import { DEFAULT_SYNTH_OPTIONS, IntegrationTests } from '../lib/integ-helpers';

// tslint:disable:no-console

const IGNORE_ASSETS_PRAGMA = 'pragma:ignore-assets';

async function main() {
const tests = await new IntegrationTests('test').fromCliArgs(); // always assert all tests
const failures: string[] = [];
Expand All @@ -16,9 +19,13 @@ async function main() {
throw new Error(`No such file: ${test.expectedFileName}. Run 'npm run integ'.`);
}

const expected = await test.readExpected();
let expected = await test.readExpected();
let actual = await test.cdkSynthFast(DEFAULT_SYNTH_OPTIONS);

const actual = await test.cdkSynthFast(DEFAULT_SYNTH_OPTIONS);
if ((await test.pragmas()).includes(IGNORE_ASSETS_PRAGMA)) {
expected = canonicalizeTemplate(expected);
actual = canonicalizeTemplate(actual);
}

const diff = diffTemplate(expected, actual);

Expand Down
Loading

0 comments on commit 0a3ae47

Please sign in to comment.