Skip to content

Commit 211d146

Browse files
authored
Merge branch 'master' into rds-versions
2 parents 6ddb5c6 + fed30fc commit 211d146

29 files changed

Lines changed: 1905 additions & 870 deletions

packages/@aws-cdk/assertions/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,47 @@ assert.hasResourceProperties('Foo::Bar', Match.objectLike({
321321
}});
322322
```
323323
324+
### Serialized JSON
325+
326+
Often, we find that some CloudFormation Resource types declare properties as a string,
327+
but actually expect JSON serialized as a string.
328+
For example, the [`BuildSpec` property of `AWS::CodeBuild::Project`][Pipeline BuildSpec],
329+
the [`Definition` property of `AWS::StepFunctions::StateMachine`][StateMachine Definition],
330+
to name a couple.
331+
332+
The `Match.serializedJson()` matcher allows deep matching within a stringified JSON.
333+
334+
```ts
335+
// Given a template -
336+
// {
337+
// "Resources": {
338+
// "MyBar": {
339+
// "Type": "Foo::Bar",
340+
// "Properties": {
341+
// "Baz": "{ \"Fred\": [\"Waldo\", \"Willow\"] }"
342+
// }
343+
// }
344+
// }
345+
// }
346+
347+
// The following will NOT throw an assertion error
348+
assert.hasResourceProperties('Foo::Bar', {
349+
Baz: Match.serializedJson({
350+
Fred: Match.arrayWith(["Waldo"]),
351+
}),
352+
});
353+
354+
// The following will throw an assertion error
355+
assert.hasResourceProperties('Foo::Bar', {
356+
Baz: Match.serializedJson({
357+
Fred: ["Waldo", "Johnny"],
358+
}),
359+
});
360+
```
361+
362+
[Pipeline BuildSpec]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-buildspec
363+
[StateMachine Definition]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definition
364+
324365
## Capturing Values
325366
326367
This matcher APIs documented above allow capturing values in the matching entry

packages/@aws-cdk/assertions/lib/match.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export abstract class Match {
6565
return new NotMatch('not', pattern);
6666
}
6767

68+
/**
69+
* Matches any string-encoded JSON and applies the specified pattern after parsing it.
70+
* @param pattern the pattern to match after parsing the encoded JSON.
71+
*/
72+
public static serializedJson(pattern: any): Matcher {
73+
return new SerializedJson('serializedJson', pattern);
74+
}
75+
6876
/**
6977
* Matches any non-null value at the target.
7078
*/
@@ -265,6 +273,39 @@ class ObjectMatch extends Matcher {
265273
}
266274
}
267275

276+
class SerializedJson extends Matcher {
277+
constructor(
278+
public readonly name: string,
279+
private readonly pattern: any,
280+
) {
281+
super();
282+
};
283+
284+
public test(actual: any): MatchResult {
285+
const result = new MatchResult(actual);
286+
if (getType(actual) !== 'string') {
287+
result.push(this, [], `Expected JSON as a string but found ${getType(actual)}`);
288+
return result;
289+
}
290+
let parsed;
291+
try {
292+
parsed = JSON.parse(actual);
293+
} catch (err) {
294+
if (err instanceof SyntaxError) {
295+
result.push(this, [], `Invalid JSON string: ${actual}`);
296+
return result;
297+
} else {
298+
throw err;
299+
}
300+
}
301+
302+
const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern);
303+
const innerResult = matcher.test(parsed);
304+
result.compose(`(${this.name})`, innerResult);
305+
return result;
306+
}
307+
}
308+
268309
class NotMatch extends Matcher {
269310
constructor(
270311
public readonly name: string,

packages/@aws-cdk/assertions/test/match.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,18 +323,63 @@ describe('Matchers', () => {
323323
expectFailure(matcher, {}, ['Missing key at /foo']);
324324
});
325325
});
326+
327+
describe('serializedJson()', () => {
328+
let matcher: Matcher;
329+
330+
test('all types', () => {
331+
matcher = Match.serializedJson({ Foo: 'Bar', Baz: 3, Boo: true, Fred: [1, 2] });
332+
expectPass(matcher, '{ "Foo": "Bar", "Baz": 3, "Boo": true, "Fred": [1, 2] }');
333+
});
334+
335+
test('simple match', () => {
336+
matcher = Match.serializedJson({ Foo: 'Bar' });
337+
expectPass(matcher, '{ "Foo": "Bar" }');
338+
339+
expectFailure(matcher, '{ "Foo": "Baz" }', ['Expected Bar but received Baz at (serializedJson)/Foo']);
340+
expectFailure(matcher, '{ "Foo": 4 }', ['Expected type string but received number at (serializedJson)/Foo']);
341+
expectFailure(matcher, '{ "Bar": "Baz" }', [
342+
'Unexpected key at (serializedJson)/Bar',
343+
'Missing key at (serializedJson)/Foo',
344+
]);
345+
});
346+
347+
test('nested matcher', () => {
348+
matcher = Match.serializedJson(Match.objectLike({
349+
Foo: Match.arrayWith(['Bar']),
350+
}));
351+
352+
expectPass(matcher, '{ "Foo": ["Bar"] }');
353+
expectPass(matcher, '{ "Foo": ["Bar", "Baz"] }');
354+
expectPass(matcher, '{ "Foo": ["Bar", "Baz"], "Fred": "Waldo" }');
355+
356+
expectFailure(matcher, '{ "Foo": ["Baz"] }', ['Missing element [Bar] at pattern index 0 at (serializedJson)/Foo']);
357+
expectFailure(matcher, '{ "Bar": ["Baz"] }', ['Missing key at (serializedJson)/Foo']);
358+
});
359+
360+
test('invalid json string', () => {
361+
matcher = Match.serializedJson({ Foo: 'Bar' });
362+
363+
expectFailure(matcher, '{ "Foo"', [/invalid JSON string/i]);
364+
});
365+
});
326366
});
327367

328368
function expectPass(matcher: Matcher, target: any): void {
329-
expect(matcher.test(target).hasFailed()).toEqual(false);
369+
const result = matcher.test(target);
370+
if (result.hasFailed()) {
371+
fail(result.toHumanStrings()); // eslint-disable-line jest/no-jasmine-globals
372+
}
330373
}
331374

332375
function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void {
333376
const result = matcher.test(target);
334377
expect(result.failCount).toBeGreaterThan(0);
335378
const actual = result.toHumanStrings();
336-
if (expected.length > 0) {
337-
expect(actual.length).toEqual(expected.length);
379+
if (expected.length > 0 && actual.length !== expected.length) {
380+
// only do this if the lengths are different, so as to display a nice failure message.
381+
// otherwise need to use `toMatch()` to support RegExp
382+
expect(actual).toEqual(expected);
338383
}
339384
for (let i = 0; i < expected.length; i++) {
340385
const e = expected[i];

packages/@aws-cdk/aws-batch/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ For more information on **AWS Batch** visit the [AWS Docs for Batch](https://doc
3737

3838
## Compute Environment
3939

40-
At the core of AWS Batch is the compute environment. All batch jobs are processed within a compute environment, which uses resource like OnDemand or Spot EC2 instances.
40+
At the core of AWS Batch is the compute environment. All batch jobs are processed within a compute environment, which uses resource like OnDemand/Spot EC2 instances or Fargate.
4141

4242
In **MANAGED** mode, AWS will handle the provisioning of compute resources to accommodate the demand. Otherwise, in **UNMANAGED** mode, you will need to manage the provisioning of those resources.
4343

@@ -74,6 +74,21 @@ const spotEnvironment = new batch.ComputeEnvironment(stack, 'MySpotEnvironment',
7474
});
7575
```
7676

77+
### Fargate Compute Environment
78+
79+
It is possible to have AWS Batch submit jobs to be run on Fargate compute resources. Below is an example of how this can be done:
80+
81+
```ts
82+
const vpc = new ec2.Vpc(this, 'VPC');
83+
84+
const fargateSpotEnvironment = new batch.ComputeEnvironment(stack, 'MyFargateEnvironment', {
85+
computeResources: {
86+
type: batch.ComputeResourceType.FARGATE_SPOT,
87+
vpc,
88+
},
89+
});
90+
```
91+
7792
### Understanding Progressive Allocation Strategies
7893

7994
AWS Batch uses an [allocation strategy](https://docs.aws.amazon.com/batch/latest/userguide/allocation-strategies.html) to determine what compute resource will efficiently handle incoming job requests. By default, **BEST_FIT** will pick an available compute instance based on vCPU requirements. If none exist, the job will wait until resources become available. However, with this strategy, you may have jobs waiting in the queue unnecessarily despite having more powerful instances available. Below is an example of how that situation might look like:

0 commit comments

Comments
 (0)