-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into huijbers/awscli-upgrade
- Loading branch information
Showing
42 changed files
with
683 additions
and
326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# Migrating to Assertions | ||
|
||
Most of the APIs in the old `assert` module has a corresponding API in `assertions`. | ||
Make the following modifications to your CDK test files to migrate to the | ||
`@aws-cdk/assertions` module. | ||
|
||
For a migration script that handles most common use cases for you, see | ||
[Migration Script](migration-script). | ||
|
||
## Translation Guide | ||
|
||
- Rewrite module imports that use `@aws-cdk/aws-assert` to `@aws-cdk/aws-assertions`. | ||
For example: | ||
|
||
```ts | ||
import '@aws-cdk/assert/jest'; | ||
import { ABSENT, SynthUtils, ResourcePart } from '@aws-cdk/assert'; | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
import { Template } from '@aws-cdk/assertions'; | ||
import { Match, Template } from '@aws-cdk/assertions'; | ||
``` | ||
|
||
- Replace instances of `toHaveResource()` with `hasResourceProperties()` or `hasResource()`. | ||
For example: | ||
|
||
```ts | ||
expect(stack).toHaveResource('FOO::BAR', {/*...*/}); | ||
expect(stack).toHaveResource('FOO::BAR', {/*...*/}, ResourcePart.CompleteDefinition); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
Template.fromStack(stack).hasResourceProperties('FOO::BAR', {/*...*/}); | ||
Template.fromStack(stacK).hasResource('FOO::BAR', {/*...*/}); | ||
``` | ||
|
||
- Replace instances of `toCountResources()` with `resourceCountIs`. For example: | ||
|
||
```ts | ||
expect(stack).toCountResources('FOO::BAR', 1); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
Template.fromStack(stack).resourceCountIs('FOO::BAR', 1); | ||
``` | ||
- Replace instances of `toMatchTemplate()` with `templateMatches()`. For example: | ||
|
||
```ts | ||
expect(stack).toMatchTemplate({/*...*/}); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
Template.fromStack(stack).templateMatches({/*...*/}); | ||
``` | ||
|
||
- Replace `arrayWith()` with `Match.arrayWith()`, `objectLike()` with `Match.objectLike()`, and | ||
`ABSENT` with `Match.absent()`. | ||
|
||
- `not` can be replaced with `Match.not()` _or_ `resourceCountIs()` depending on the use case. | ||
|
||
```ts | ||
// asserting that the stack does not have a particular resource. | ||
expect(stack).not.toHaveResource('FOO::BAR'); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
Template.fromStack(stack).resourceCountIs('FOO::BAR', 0); | ||
``` | ||
|
||
```ts | ||
// asserting that the stack does not have a resource with these properties | ||
expect(stack).not.toHaveResource('FOO::BAR', { | ||
prop: 'does not exist', | ||
}); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
Template.fromStack(stack).hasResourceProperties('FOO::BAR', Match.not({ | ||
prop: 'does not exist', | ||
})); | ||
``` | ||
|
||
- `SynthUtils.synthesize(stack)` can be replaced as well. For example: | ||
|
||
```ts | ||
expect(SynthUtils.synthesize(stack).template).toEqual(/*...*/); | ||
SynthUtils.syntesize(stack); | ||
``` | ||
|
||
...becomes... | ||
|
||
```ts | ||
expect(Template.fromStack(stack).toJSON()).toEqual(/*...*/); | ||
App.of(stack).synth(); | ||
``` | ||
|
||
## Migration Script | ||
|
||
> NOTE: We have some code rewrite rules that will make it easier to migrate from one library | ||
> to the other. This tool will not do a complete rewrite and is not guaranteed to produce | ||
> compilable code! It will just save you the effort of performing a lot of code substitutions | ||
> you would otherwise have to do by hand. | ||
Comby is a tool used to do structured code rewriting. You can install it | ||
[here](https://comby.dev/). Download the [rewrite.toml](rewrite.toml) file from our GitHub | ||
repository, and run the following command in the root directory of your project: | ||
|
||
```bash | ||
comby -config ~/rewrite.toml -f .ts -d test -in-place -timeout 10 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { Resource, Template } from './template'; | ||
|
||
/** | ||
* Check a template for cyclic dependencies | ||
* | ||
* This will make sure that we don't happily validate templates | ||
* in unit tests that wouldn't deploy to CloudFormation anyway. | ||
*/ | ||
export function checkTemplateForCyclicDependencies(template: Template): void { | ||
const logicalIds = new Set(Object.keys(template.Resources ?? {})); | ||
|
||
const dependencies = new Map<string, Set<string>>(); | ||
for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { | ||
dependencies.set(logicalId, intersect(findResourceDependencies(resource), logicalIds)); | ||
} | ||
|
||
// We will now progressively remove entries from the map of 'dependencies' that have | ||
// 0 elements in them. If we can't do that anymore and the map isn't empty, we | ||
// have a cyclic dependency. | ||
while (dependencies.size > 0) { | ||
const free = Array.from(dependencies.entries()).filter(([_, deps]) => deps.size === 0); | ||
if (free.length === 0) { | ||
// Oops! | ||
const cycle = findCycle(dependencies); | ||
|
||
const cycleResources: any = {}; | ||
for (const logicalId of cycle) { | ||
cycleResources[logicalId] = template.Resources?.[logicalId]; | ||
} | ||
|
||
throw new Error(`Template is undeployable, these resources have a dependency cycle: ${cycle.join(' -> ')}:\n\n${JSON.stringify(cycleResources, undefined, 2)}`); | ||
} | ||
|
||
for (const [logicalId, _] of free) { | ||
for (const deps of dependencies.values()) { | ||
deps.delete(logicalId); | ||
} | ||
dependencies.delete(logicalId); | ||
} | ||
} | ||
} | ||
|
||
function findResourceDependencies(res: Resource): Set<string> { | ||
return new Set([ | ||
...toArray(res.DependsOn ?? []), | ||
...findExpressionDependencies(res.Properties), | ||
]); | ||
} | ||
|
||
function toArray<A>(x: A | A[]): A[] { | ||
return Array.isArray(x) ? x : [x]; | ||
} | ||
|
||
function findExpressionDependencies(obj: any): Set<string> { | ||
const ret = new Set<string>(); | ||
recurse(obj); | ||
return ret; | ||
|
||
function recurse(x: any): void { | ||
if (!x) { return; } | ||
if (Array.isArray(x)) { | ||
x.forEach(recurse); | ||
} | ||
if (typeof x === 'object') { | ||
const keys = Object.keys(x); | ||
if (keys.length === 1 && keys[0] === 'Ref') { | ||
ret.add(x[keys[0]]); | ||
} else if (keys.length === 1 && keys[0] === 'Fn::GetAtt') { | ||
ret.add(x[keys[0]][0]); | ||
} else if (keys.length === 1 && keys[0] === 'Fn::Sub') { | ||
const argument = x[keys[0]]; | ||
const pattern = Array.isArray(argument) ? argument[0] : argument; | ||
for (const logId of logicalIdsInSubString(pattern)) { | ||
ret.add(logId); | ||
} | ||
const contextDict = Array.isArray(argument) ? argument[1] : undefined; | ||
if (contextDict) { | ||
Object.values(contextDict).forEach(recurse); | ||
} | ||
} else { | ||
Object.values(x).forEach(recurse); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Return the logical IDs found in a {Fn::Sub} format string | ||
*/ | ||
function logicalIdsInSubString(x: string): string[] { | ||
return analyzeSubPattern(x).flatMap((fragment) => { | ||
switch (fragment.type) { | ||
case 'getatt': | ||
case 'ref': | ||
return [fragment.logicalId]; | ||
case 'literal': | ||
return []; | ||
} | ||
}); | ||
} | ||
|
||
|
||
function analyzeSubPattern(pattern: string): SubFragment[] { | ||
const ret: SubFragment[] = []; | ||
let start = 0; | ||
|
||
let ph0 = pattern.indexOf('${', start); | ||
while (ph0 > -1) { | ||
if (pattern[ph0 + 2] === '!') { | ||
// "${!" means "don't actually substitute" | ||
start = ph0 + 3; | ||
ph0 = pattern.indexOf('${', start); | ||
continue; | ||
} | ||
|
||
const ph1 = pattern.indexOf('}', ph0 + 2); | ||
if (ph1 === -1) { | ||
break; | ||
} | ||
const placeholder = pattern.substring(ph0 + 2, ph1); | ||
|
||
if (ph0 > start) { | ||
ret.push({ type: 'literal', content: pattern.substring(start, ph0) }); | ||
} | ||
if (placeholder.includes('.')) { | ||
const [logicalId, attr] = placeholder.split('.'); | ||
ret.push({ type: 'getatt', logicalId: logicalId!, attr: attr! }); | ||
} else { | ||
ret.push({ type: 'ref', logicalId: placeholder }); | ||
} | ||
|
||
start = ph1 + 1; | ||
ph0 = pattern.indexOf('${', start); | ||
} | ||
|
||
if (start < pattern.length - 1) { | ||
ret.push({ type: 'literal', content: pattern.substr(start) }); | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
type SubFragment = | ||
| { readonly type: 'literal'; readonly content: string } | ||
| { readonly type: 'ref'; readonly logicalId: string } | ||
| { readonly type: 'getatt'; readonly logicalId: string; readonly attr: string }; | ||
|
||
|
||
function intersect<A>(xs: Set<A>, ys: Set<A>): Set<A> { | ||
return new Set<A>(Array.from(xs).filter(x => ys.has(x))); | ||
} | ||
|
||
/** | ||
* Find cycles in a graph | ||
* | ||
* Not the fastest, but effective and should be rare | ||
*/ | ||
function findCycle(deps: ReadonlyMap<string, ReadonlySet<string>>): string[] { | ||
for (const node of deps.keys()) { | ||
const cycle = recurse(node, [node]); | ||
if (cycle) { return cycle; } | ||
} | ||
throw new Error('No cycle found. Assertion failure!'); | ||
|
||
function recurse(node: string, path: string[]): string[] | undefined { | ||
for (const dep of deps.get(node) ?? []) { | ||
if (dep === path[0]) { return [...path, dep]; } | ||
|
||
const cycle = recurse(dep, [...path, dep]); | ||
if (cycle) { return cycle; } | ||
} | ||
|
||
return undefined; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.