Skip to content

Commit 484cfb2

Browse files
authored
chore(integ-runner): --inspect-failures option (#20399)
When the integ runner fails to validate a snapshot, add an option to retain the tempdir with the "new actual" snapshot so that a human can go look around in it. Also make the `--verbose` flag print out a command that will reproduce the running of the CDK app directly from the command-line, so that it can be debugged more easily. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 88ea829 commit 484cfb2

File tree

7 files changed

+94
-20
lines changed

7 files changed

+94
-20
lines changed

packages/@aws-cdk/integ-runner/lib/cli.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ async function main() {
2727
.options('max-workers', { type: 'number', desc: 'The max number of workerpool workers to use when running integration tests in parallel', default: 16 })
2828
.options('exclude', { type: 'boolean', desc: 'All tests should be run, except for the list of tests provided', default: false })
2929
.options('from-file', { type: 'string', desc: 'Import tests to include or exclude from a file' })
30+
.option('inspect-failures', { type: 'boolean', desc: 'Keep the integ test cloud assembly if a failure occurs for inspection', default: false })
3031
.option('disable-update-workflow', { type: 'boolean', default: false, desc: 'If this is "true" then the stack update workflow will be disabled' })
32+
.strict()
3133
.argv;
3234

3335
const pool = workerpool.pool(path.join(__dirname, '../lib/workers/extract/index.js'), {
@@ -70,7 +72,10 @@ async function main() {
7072
// always run snapshot tests, but if '--force' is passed then
7173
// run integration tests on all failed tests, not just those that
7274
// failed snapshot tests
73-
failedSnapshots = await runSnapshotTests(pool, testsFromArgs);
75+
failedSnapshots = await runSnapshotTests(pool, testsFromArgs, {
76+
retain: argv['inspect-failures'],
77+
verbose: argv.verbose,
78+
});
7479
for (const failure of failedSnapshots) {
7580
destructiveChanges.push(...failure.destructiveChanges ?? []);
7681
}

packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export abstract class IntegRunner {
129129

130130
protected readonly profile?: string;
131131

132+
protected readonly cdkExecutable: string;
133+
132134
protected _destructiveChanges?: DestructiveChange[];
133135
private legacyContext?: Record<string, any>;
134136

@@ -150,8 +152,10 @@ export abstract class IntegRunner {
150152
this.relativeSnapshotDir = `${testName}.integ.snapshot`;
151153
this.sourceFilePath = path.join(this.directory, parsed.base);
152154
this.cdkContextPath = path.join(this.directory, 'cdk.context.json');
155+
156+
this.cdkExecutable = require.resolve('aws-cdk/bin/cdk');
153157
this.cdk = options.cdk ?? new CdkCliWrapper({
154-
cdkExecutable: require.resolve('aws-cdk/bin/cdk'),
158+
cdkExecutable: this.cdkExecutable,
155159
directory: this.directory,
156160
env: {
157161
...options.env,

packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts

+45-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'path';
22
import { Writable, WritableOptions } from 'stream';
33
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
44
import { diffTemplate, formatDifferences, ResourceDifference, ResourceImpact } from '@aws-cdk/cloudformation-diff';
5-
import { Diagnostic, DiagnosticReason, DestructiveChange } from '../workers/common';
5+
import { Diagnostic, DiagnosticReason, DestructiveChange, SnapshotVerificationOptions } from '../workers/common';
66
import { canonicalizeTemplate } from './private/canonicalize-assets';
77
import { AssemblyManifestReader } from './private/cloud-assembly';
88
import { IntegRunnerOptions, IntegRunner, DEFAULT_SYNTH_OPTIONS } from './runner-base';
@@ -22,7 +22,8 @@ export class IntegSnapshotRunner extends IntegRunner {
2222
*
2323
* @returns any diagnostics and any destructive changes
2424
*/
25-
public testSnapshot(): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } {
25+
public testSnapshot(options: SnapshotVerificationOptions = {}): { diagnostics: Diagnostic[], destructiveChanges: DestructiveChange[] } {
26+
let doClean = true;
2627
try {
2728
// read the existing snapshot
2829
const expectedStacks = this.readAssembly(this.snapshotDir);
@@ -39,17 +40,19 @@ export class IntegSnapshotRunner extends IntegRunner {
3940
// the cdkOutDir exists already, but for some reason generateActualSnapshot
4041
// generates an incorrect snapshot and I have no idea why so synth again here
4142
// to produce the "correct" snapshot
43+
const env = {
44+
...DEFAULT_SYNTH_OPTIONS.env,
45+
CDK_CONTEXT_JSON: JSON.stringify(this.getContext()),
46+
};
4247
this.cdk.synthFast({
4348
execCmd: this.cdkApp.split(' '),
44-
env: {
45-
...DEFAULT_SYNTH_OPTIONS.env,
46-
CDK_CONTEXT_JSON: JSON.stringify(this.getContext()),
47-
},
49+
env,
4850
output: this.cdkOutDir,
4951
});
5052

5153
// read the "actual" snapshot
52-
const actualStacks = this.readAssembly(path.join(this.directory, this.cdkOutDir));
54+
const actualDir = path.join(this.directory, this.cdkOutDir);
55+
const actualStacks = this.readAssembly(actualDir);
5356
// only diff stacks that are part of the test case
5457
const actualStacksToDiff: Record<string, any> = {};
5558
for (const [stackName, template] of Object.entries(actualStacks)) {
@@ -60,11 +63,45 @@ export class IntegSnapshotRunner extends IntegRunner {
6063

6164
// diff the existing snapshot (expected) with the integration test (actual)
6265
const diagnostics = this.diffAssembly(expectedStacksToDiff, actualStacksToDiff);
66+
67+
if (diagnostics.diagnostics.length) {
68+
// Attach additional messages to the first diagnostic
69+
const additionalMessages: string[] = [];
70+
71+
if (options.retain) {
72+
additionalMessages.push(
73+
`(Failure retained) Expected: ${path.relative(process.cwd(), this.snapshotDir)}`,
74+
` Actual: ${path.relative(process.cwd(), actualDir)}`,
75+
),
76+
doClean = false;
77+
}
78+
79+
if (options.verbose) {
80+
// Show the command necessary to repro this
81+
const envSet = Object.entries(env)
82+
.filter(([k, _]) => k !== 'CDK_CONTEXT_JSON')
83+
.map(([k, v]) => `${k}='${v}'`);
84+
const envCmd = envSet.length > 0 ? ['env', ...envSet] : [];
85+
86+
additionalMessages.push(
87+
'Repro:',
88+
` ${[...envCmd, 'cdk synth', `-a '${this.cdkApp}'`, `-o '${this.cdkOutDir}'`, ...Object.entries(this.getContext()).flatMap(([k, v]) => typeof v !== 'object' ? [`-c '${k}=${v}'`] : [])].join(' ')}`,
89+
);
90+
}
91+
92+
diagnostics.diagnostics[0] = {
93+
...diagnostics.diagnostics[0],
94+
additionalMessages,
95+
};
96+
}
97+
6398
return diagnostics;
6499
} catch (e) {
65100
throw e;
66101
} finally {
67-
this.cleanup();
102+
if (doClean) {
103+
this.cleanup();
104+
}
68105
}
69106
}
70107

packages/@aws-cdk/integ-runner/lib/workers/common.ts

+24
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@ export interface IntegRunnerMetrics {
8989
readonly profile?: string;
9090
}
9191

92+
export interface SnapshotVerificationOptions {
93+
/**
94+
* Retain failed snapshot comparisons
95+
*
96+
* @default false
97+
*/
98+
readonly retain?: boolean;
99+
100+
/**
101+
* Verbose mode
102+
*
103+
* @default false
104+
*/
105+
readonly verbose?: boolean;
106+
}
107+
92108
/**
93109
* Integration test results
94110
*/
@@ -208,6 +224,11 @@ export interface Diagnostic {
208224
* The reason for the diagnostic
209225
*/
210226
readonly reason: DiagnosticReason;
227+
228+
/**
229+
* Additional messages to print
230+
*/
231+
readonly additionalMessages?: string[];
211232
}
212233

213234
export function printSummary(total: number, failed: number): void {
@@ -251,4 +272,7 @@ export function printResults(diagnostic: Diagnostic): void {
251272
case DiagnosticReason.ASSERTION_FAILED:
252273
logger.error(' %s - Assertions Failed! %s\n%s', diagnostic.testName, chalk.gray(`${diagnostic.duration}s`), diagnostic.message);
253274
}
275+
for (const addl of diagnostic.additionalMessages ?? []) {
276+
logger.print(` ${addl}`);
277+
}
254278
}

packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as workerpool from 'workerpool';
22
import { IntegSnapshotRunner, IntegTestRunner } from '../../runner';
33
import { IntegTestConfig } from '../../runner/integration-tests';
4-
import { DiagnosticReason, IntegTestWorkerConfig, formatAssertionResults } from '../common';
4+
import { DiagnosticReason, IntegTestWorkerConfig, SnapshotVerificationOptions, Diagnostic, formatAssertionResults } from '../common';
55
import { IntegTestBatchRequest } from '../integ-test-worker';
66

77
/**
@@ -84,7 +84,7 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker
8484
* if there is an existing snapshot, and if there is will
8585
* check if there are any changes
8686
*/
87-
export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig[] {
87+
export function snapshotTestWorker(test: IntegTestConfig, options: SnapshotVerificationOptions = {}): IntegTestWorkerConfig[] {
8888
const failedTests = new Array<IntegTestWorkerConfig>();
8989
const runner = new IntegSnapshotRunner({ fileName: test.fileName, directory: test.directory });
9090
const start = Date.now();
@@ -98,12 +98,12 @@ export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig
9898
});
9999
failedTests.push(test);
100100
} else {
101-
const { diagnostics, destructiveChanges } = runner.testSnapshot();
101+
const { diagnostics, destructiveChanges } = runner.testSnapshot(options);
102102
if (diagnostics.length > 0) {
103103
diagnostics.forEach(diagnostic => workerpool.workerEmit({
104104
...diagnostic,
105105
duration: (Date.now() - start) / 1000,
106-
}));
106+
} as Diagnostic));
107107
failedTests.push({
108108
fileName: test.fileName,
109109
directory: test.directory,
@@ -115,7 +115,7 @@ export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig
115115
testName: runner.testName,
116116
message: 'Success',
117117
duration: (Date.now() - start) / 1000,
118-
});
118+
} as Diagnostic);
119119
}
120120
}
121121
} catch (e) {
@@ -125,7 +125,7 @@ export function snapshotTestWorker(test: IntegTestConfig): IntegTestWorkerConfig
125125
testName: runner.testName,
126126
reason: DiagnosticReason.SNAPSHOT_FAILED,
127127
duration: (Date.now() - start) / 1000,
128-
});
128+
} as Diagnostic);
129129
}
130130

131131
return failedTests;

packages/@aws-cdk/integ-runner/lib/workers/integ-snapshot-worker.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ import * as workerpool from 'workerpool';
22
import * as logger from '../logger';
33
import { IntegTestConfig } from '../runner/integration-tests';
44
import { flatten } from '../utils';
5-
import { printSummary, printResults, IntegTestWorkerConfig } from './common';
5+
import { printSummary, printResults, IntegTestWorkerConfig, SnapshotVerificationOptions } from './common';
66

77
/**
88
* Run Snapshot tests
99
* First batch up the tests. By default there will be 3 tests per batch.
1010
* Use a workerpool to run the batches in parallel.
1111
*/
12-
export async function runSnapshotTests(pool: workerpool.WorkerPool, tests: IntegTestConfig[]): Promise<IntegTestWorkerConfig[]> {
12+
export async function runSnapshotTests(
13+
pool: workerpool.WorkerPool,
14+
tests: IntegTestConfig[],
15+
options: SnapshotVerificationOptions,
16+
): Promise<IntegTestWorkerConfig[]> {
1317
logger.highlight('\nVerifying integration test snapshots...\n');
1418

1519
const failedTests: IntegTestWorkerConfig[][] = await Promise.all(
16-
tests.map((test) => pool.exec('snapshotTestWorker', [test], {
20+
tests.map((test) => pool.exec('snapshotTestWorker', [test, options], {
1721
on: printResults,
1822
})),
1923
);

packages/@aws-cdk/integ-runner/test/workers/integ-worker.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describe('test runner', () => {
147147
]),
148148
]));
149149

150-
expect(results.length).toEqual(0);
150+
expect(results).toEqual([]);
151151
});
152152

153153
test('deploy failed', () => {

0 commit comments

Comments
 (0)