Skip to content

Commit ded7ae1

Browse files
authored
fix(integ-runner): fail with correct error when integ manifest exists but cannot be loaded (#692)
Previously if an integ manifest exists, but could not be loaded for some reason we would be falling back to the deprecated legacy style tests. This code path will than always fail with a misleading error: `"integ-runner" can only operate on apps with a single stack.` The error is because the modern integ tests always have include stacks, but legacy tests only handle a single stack or a pragma list. This change improves the error handling to distinguish between "no integ manifest" and "failed to load manifest". For the former, we still go down the legacy test route. For the latter we can now fail the test with an accurate error message. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent e89f10f commit ded7ae1

File tree

25 files changed

+697
-15
lines changed

25 files changed

+697
-15
lines changed

packages/@aws-cdk/cloud-assembly-schema/lib/integ-tests/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface IntegManifest {
1111
/**
1212
* Enable lookups for this test. If lookups are enabled
1313
* then `stackUpdateWorkflow` must be set to false.
14-
* Lookups should only be enabled when you are explicitely testing
14+
* Lookups should only be enabled when you are explicitly testing
1515
* lookups.
1616
*
1717
* @default false

packages/@aws-cdk/cloud-assembly-schema/projenrc/versioning.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import * as path from 'path';
44
import { SCHEMA_DIR } from './schema-definition';
55

66
export function maybeBumpVersion(schemas: Record<string, any>) {
7+
const $comment = "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.";
78
const serializedSchema = JSON.stringify(sortJson(schemas), null, 2);
89

910
const versionFile = path.join(SCHEMA_DIR, 'version.json');
1011
let current: SchemaVersionFile = JSON.parse(fs.readFileSync(versionFile, 'utf8'));
1112
const schemaHash = sha256(serializedSchema);
1213

1314
if (current.schemaHash !== schemaHash) {
14-
current = { schemaHash, revision: current.revision + 1 };
15+
current = { schemaHash, $comment, revision: current.revision + 1 };
1516
console.log(`Schemas changed, bumping version to ${current.revision}`);
1617
}
1718

@@ -26,6 +27,7 @@ function sha256(x: string) {
2627

2728
interface SchemaVersionFile {
2829
revision: number;
30+
$comment?: string;
2931
schemaHash: string;
3032
}
3133

packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"type": "string"
1111
},
1212
"enableLookups": {
13-
"description": "Enable lookups for this test. If lookups are enabled\nthen `stackUpdateWorkflow` must be set to false.\nLookups should only be enabled when you are explicitely testing\nlookups.",
13+
"description": "Enable lookups for this test. If lookups are enabled\nthen `stackUpdateWorkflow` must be set to false.\nLookups should only be enabled when you are explicitly testing\nlookups.",
1414
"default": false,
1515
"type": "boolean"
1616
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"schemaHash": "4709e40bc5dd6ad7d2bb6c6bd0315c622b7f4aa07ee3af66ff07f9bc77fdf783",
2+
"schemaHash": "0807b42f8220bdafddeb4dad246a696721ed86e99ac6b13aa83359bab834d60d",
33
"$comment": "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.",
44
"revision": 45
55
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export const error = logger(stderr, [chalk.red]);
1818
export const warning = logger(stderr, [chalk.yellow]);
1919
export const success = logger(stderr, [chalk.green]);
2020
export const highlight = logger(stderr, [chalk.bold]);
21+
export const trace = logger(stderr, [chalk.gray]);

packages/@aws-cdk/integ-runner/lib/runner/private/integ-manifest.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,44 @@ import type { IntegManifest, TestCase } from '@aws-cdk/cloud-assembly-schema';
33
import { Manifest } from '@aws-cdk/cloud-assembly-schema';
44
import * as fs from 'fs-extra';
55

6+
/**
7+
* An error indicating that the manifest file does not exists.
8+
*
9+
* This can signal to treat the integ test case as a legacy test.
10+
*/
11+
export class NoManifestError extends Error {
12+
public readonly manifestPath: string;
13+
public readonly cause: Error;
14+
15+
constructor(manifestPath: string, cause: Error) {
16+
super(`Cannot read integ manifest '${manifestPath}': ${cause.message}`);
17+
this.name = 'NoManifestError';
18+
this.manifestPath = manifestPath;
19+
this.cause = cause;
20+
21+
Object.setPrototypeOf(this, NoManifestError.prototype);
22+
}
23+
}
24+
25+
/**
26+
* An error indicating that the manifest failed to load.
27+
*
28+
* The error implies the the manifest file exists, but is invalid.
29+
*/
30+
export class ManifestLoadError extends Error {
31+
public readonly manifestPath: string;
32+
public readonly cause: Error;
33+
34+
constructor(manifestPath: string, cause: Error) {
35+
super(`Failed to load integ manifest '${manifestPath}': ${cause.message}`);
36+
this.name = 'ManifestLoadingError';
37+
this.manifestPath = manifestPath;
38+
this.cause = cause;
39+
40+
Object.setPrototypeOf(this, ManifestLoadError.prototype);
41+
}
42+
}
43+
644
/**
745
* Test case configuration read from the integ manifest
846
*/
@@ -39,11 +77,17 @@ export class IntegManifestReader {
3977
* Reads an integration test manifest from the specified file
4078
*/
4179
public static fromFile(fileName: string): IntegManifestReader {
80+
try {
81+
fs.statSync(fileName);
82+
} catch (e: any) {
83+
throw new NoManifestError(fileName, e);
84+
}
85+
4286
try {
4387
const obj = Manifest.loadIntegManifest(fileName);
4488
return new IntegManifestReader(path.dirname(fileName), obj);
45-
} catch (e: any) {
46-
throw new Error(`Cannot read integ manifest '${fileName}': ${e.message}`);
89+
} catch (loadErr: any) {
90+
throw new ManifestLoadError(fileName, loadErr);
4791
}
4892
}
4993

@@ -57,7 +101,7 @@ export class IntegManifestReader {
57101
try {
58102
st = fs.statSync(filePath);
59103
} catch (e: any) {
60-
throw new Error(`Cannot read integ manifest at '${filePath}': ${e.message}`);
104+
throw new NoManifestError(filePath, e);
61105
}
62106
if (st.isDirectory()) {
63107
return IntegManifestReader.fromFile(path.join(filePath, IntegManifestReader.DEFAULT_FILENAME));

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import type { IntegTest } from './integration-tests';
99
import * as recommendedFlagsFile from '../recommended-feature-flags.json';
1010
import { flatten } from '../utils';
1111
import { makeEngine, type EngineOptions } from './engine';
12+
import * as logger from '../logger';
1213
import type { ManifestTrace } from './private/cloud-assembly';
1314
import { AssemblyManifestReader } from './private/cloud-assembly';
1415
import type { DestructiveChange } from '../workers/common';
16+
import { NoManifestError } from './private/integ-manifest';
1517

1618
const DESTRUCTIVE_CHANGES = '!!DESTRUCTIVE_CHANGES:';
1719

@@ -128,6 +130,11 @@ export abstract class IntegRunner {
128130
*/
129131
protected readonly profile?: string;
130132

133+
/**
134+
* Show output from the integ test run.
135+
*/
136+
protected readonly showOutput: boolean;
137+
131138
protected _destructiveChanges?: DestructiveChange[];
132139
private legacyContext?: Record<string, any>;
133140
private _expectedTestSuite?: IntegTestSuite | LegacyIntegTestSuite;
@@ -139,14 +146,14 @@ export abstract class IntegRunner {
139146
this.testName = this.test.testName;
140147
this.snapshotDir = this.test.snapshotDir;
141148
this.cdkContextPath = path.join(this.directory, 'cdk.context.json');
149+
this.profile = options.profile;
150+
this.showOutput = options.showOutput ?? false;
142151

143152
this.cdk = options.cdk ?? makeEngine(options);
144153
this.cdkOutDir = options.integOutDir ?? this.test.temporaryOutputDir;
145154

146155
const testRunCommand = this.test.appCommand;
147156
this.cdkApp = testRunCommand.replace('{filePath}', path.relative(this.directory, this.test.fileName));
148-
149-
this.profile = options.profile;
150157
}
151158

152159
/**
@@ -219,10 +226,25 @@ export abstract class IntegRunner {
219226
* "legacy mode" and create a manifest from pragma
220227
*/
221228
protected async loadManifest(dir?: string): Promise<IntegTestSuite | LegacyIntegTestSuite> {
229+
const manifest = dir ?? this.snapshotDir;
222230
try {
223-
const testSuite = IntegTestSuite.fromPath(dir ?? this.snapshotDir);
231+
const testSuite = IntegTestSuite.fromPath(manifest);
224232
return testSuite;
225-
} catch {
233+
} catch (modernError: any) {
234+
// Only attempt legacy test case if the integ test manifest was not found
235+
// For any other errors, e.g. when parsing the manifest fails, we abort.
236+
if (!(modernError instanceof NoManifestError)) {
237+
throw modernError;
238+
}
239+
240+
if (this.showOutput) {
241+
logger.trace(
242+
"Failed to load integ test manifest for '%s'. Attempting as deprecated legacy test instead. Error was: %s",
243+
manifest,
244+
modernError.message ?? String(modernError),
245+
);
246+
}
247+
226248
const testCases = await LegacyIntegTestSuite.fromLegacy({
227249
cdk: this.cdk,
228250
testName: this.test.normalizedTestName,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export async function snapshotTestWorker(testInfo: IntegTestInfo, options: Snaps
143143
const runner = new IntegSnapshotRunner({
144144
engine: options.engine,
145145
test,
146+
showOutput: options.verbose ?? false,
146147
});
147148
if (!runner.hasSnapshot()) {
148149
workerpool.workerEmit({

packages/@aws-cdk/integ-runner/test/helpers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { ChildProcess } from 'child_process';
2-
import { Readable, Writable } from 'stream';
1+
import * as path from 'path';
32
import type { CdkCliWrapperOptions, DeployOptions, ICdk, ListOptions, SynthFastOptions, SynthOptions } from '@aws-cdk/cdk-cli-wrapper';
43
import type { DestroyOptions } from '@aws-cdk/cloud-assembly-schema/lib/integ-tests';
54
import { IntegSnapshotRunner, IntegTest } from '../lib/runner';
@@ -106,3 +105,10 @@ export class MockCdkProvider {
106105
return results;
107106
}
108107
}
108+
109+
/**
110+
* Get the absolute path to a data located the test-data directory
111+
*/
112+
export function testDataPath(...location: string[]): string {
113+
return path.join(__dirname, 'test-data', ...location);
114+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { IntegTestSuite } from '../../../lib/runner/integ-test-suite';
2+
import { IntegManifestReader, ManifestLoadError } from '../../../lib/runner/private/integ-manifest';
3+
import { testDataPath } from '../../helpers';
4+
5+
describe('Invalid Manifest Handling', () => {
6+
test('throws ManifestLoadError when loading an invalid JSON manifest', () => {
7+
// GIVEN
8+
const invalidManifestPath = testDataPath('invalid-integ-manifest', 'integ.json');
9+
10+
// WHEN / THEN
11+
expect(() => IntegManifestReader.fromFile(invalidManifestPath)).toThrow(ManifestLoadError);
12+
});
13+
14+
test('IntegTestSuite.fromPath propagates ManifestLoadError', () => {
15+
// GIVEN
16+
const invalidManifestDir = testDataPath('invalid-integ-manifest');
17+
18+
// WHEN / THEN
19+
expect(() => IntegTestSuite.fromPath(invalidManifestDir)).toThrow(ManifestLoadError);
20+
});
21+
});

0 commit comments

Comments
 (0)