Skip to content

Commit 22673b2

Browse files
authored
feat(integ-runner): support --language presets for JavaScript, TypeScript, Python and Go (#22058)
It was already possible to run tests in any language by providing `--app` and `--test-regex` directly. This change introduces the concept of language presets that can be selected. By default all supported languages will be detected. Users can run integration tests for multiple languages at the same time, using the default preset configuration. To further customize anything, only a single language can be selected. However it's always possible to call the `integ-runner` multiple times: ```console integ-runner --language typescript integ-runner --language python --app="python3.2" integ-runner --language go --test-regex=".*\.integ\.go" ``` Resolves part of #21169 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 921426e commit 22673b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+5418
-124
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc');
22
baseConfig.parserOptions.project = __dirname + '/tsconfig.json';
3+
baseConfig.ignorePatterns = [...baseConfig.ignorePatterns, "test/language-tests/**/integ.*.ts"];
34
module.exports = baseConfig;

packages/@aws-cdk/integ-runner/README.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,35 @@ to be a self contained CDK app. The runner will execute the following for each f
7070
If this is set to `true` then the [update workflow](#update-workflow) will be disabled
7171
- `--app`
7272
The custom CLI command that will be used to run the test files. You can include {filePath} to specify where in the command the test file path should be inserted. Example: --app="python3.8 {filePath}".
73+
74+
Use together with `--test-regex` to fully customize how tests are run, or use with a single `--language` preset to change the command used for this language.
7375
- `--test-regex`
7476
Detect integration test files matching this JavaScript regex pattern. If used multiple times, all files matching any one of the patterns are detected.
75-
77+
78+
Use together with `--app` to fully customize how tests are run, or use with a single `--language` preset to change which files are detected for this language.
79+
- `--language`
80+
The language presets to use. You can discover and run tests written in multiple languages by passing this flag multiple times (`--language typescript --language python`). Defaults to all supported languages. Currently supported language presets are:
81+
- `javascript`:
82+
- File RegExp: `^integ\..*\.js$`
83+
- App run command: `node {filePath}`
84+
- `typescript`:\
85+
Note that for TypeScript files compiled to JavaScript, the JS tests will take precedence and the TS ones won't be evaluated.
86+
- File RegExp: `^integ\..*(?<!\.d)\.ts$`
87+
- App run command: `node -r ts-node/register {filePath}`
88+
- `python`:
89+
- File RegExp: `^integ_.*\.py$`
90+
- App run command: `python {filePath}`
91+
- `go`:
92+
- File RegExp: `^integ_.*\.go$`
93+
- App run command: `go run {filePath}`
94+
7695
Example:
7796

7897
```bash
79-
integ-runner --update-on-failed --parallel-regions us-east-1 --parallel-regions us-east-2 --parallel-regions us-west-2 --directory ./
98+
integ-runner --update-on-failed --parallel-regions us-east-1 --parallel-regions us-east-2 --parallel-regions us-west-2 --directory ./ --language python
8099
```
81100

82-
This will search for integration tests recursively from the current directory and then execute them in parallel across `us-east-1`, `us-east-2`, & `us-west-2`.
101+
This will search for python integration tests recursively from the current directory and then execute them in parallel across `us-east-1`, `us-east-2`, & `us-west-2`.
83102

84103
If you are providing a list of tests to execute, either as CLI arguments or from a file, the name of the test needs to be relative to the `directory`.
85104
For example, if there is a test `aws-iam/test/integ.policy.js` and the current working directory is `aws-iam` you would provide `integ.policy.js`

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

+28-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function parseCliArgs(args: string[] = []) {
1717
.usage('Usage: integ-runner [TEST...]')
1818
.option('config', {
1919
config: true,
20-
configParser: IntegrationTests.configFromFile,
20+
configParser: configFromFile,
2121
default: 'integ.config.json',
2222
desc: 'Load options from a JSON config file. Options provided as CLI arguments take precedent.',
2323
})
@@ -35,6 +35,13 @@ export function parseCliArgs(args: string[] = []) {
3535
.options('from-file', { type: 'string', desc: 'Read TEST names from a file (one TEST per line)' })
3636
.option('inspect-failures', { type: 'boolean', desc: 'Keep the integ test cloud assembly if a failure occurs for inspection', default: false })
3737
.option('disable-update-workflow', { type: 'boolean', default: false, desc: 'If this is "true" then the stack update workflow will be disabled' })
38+
.option('language', {
39+
alias: 'l',
40+
default: ['javascript', 'typescript', 'python', 'go'],
41+
choices: ['javascript', 'typescript', 'python', 'go'],
42+
type: 'array',
43+
desc: 'Use these presets to run integration tests for the selected languages',
44+
})
3845
.option('app', { type: 'string', default: undefined, desc: 'The custom CLI command that will be used to run the test files. You can include {filePath} to specify where in the command the test file path should be inserted. Example: --app="python3.8 {filePath}".' })
3946
.option('test-regex', { type: 'array', desc: 'Detect integration test files matching this JavaScript regex pattern. If used multiple times, all files matching any one of the patterns are detected.', default: [] })
4047
.strict()
@@ -80,19 +87,15 @@ export function parseCliArgs(args: string[] = []) {
8087
force: argv.force as boolean,
8188
dryRun: argv['dry-run'] as boolean,
8289
disableUpdateWorkflow: argv['disable-update-workflow'] as boolean,
90+
language: arrayFromYargs(argv.language),
8391
};
8492
}
8593

8694

8795
export async function main(args: string[]) {
8896
const options = parseCliArgs(args);
8997

90-
const testsFromArgs = await new IntegrationTests(path.resolve(options.directory)).fromCliArgs({
91-
app: options.app,
92-
testRegex: options.testRegex,
93-
tests: options.tests,
94-
exclude: options.exclude,
95-
});
98+
const testsFromArgs = await new IntegrationTests(path.resolve(options.directory)).fromCliOptions(options);
9699

97100
// List only prints the discoverd tests
98101
if (options.list) {
@@ -227,3 +230,21 @@ export function cli(args: string[] = process.argv.slice(2)) {
227230
process.exitCode = 1;
228231
});
229232
}
233+
234+
/**
235+
* Read CLI options from a config file if provided.
236+
*
237+
* @param fileName
238+
* @returns parsed CLI config options
239+
*/
240+
function configFromFile(fileName?: string): Record<string, any> {
241+
if (!fileName) {
242+
return {};
243+
}
244+
245+
try {
246+
return JSON.parse(fs.readFileSync(fileName, { encoding: 'utf-8' }));
247+
} catch {
248+
return {};
249+
}
250+
}

packages/@aws-cdk/integ-runner/lib/runner/integration-tests.ts

+119-42
Original file line numberDiff line numberDiff line change
@@ -159,42 +159,112 @@ export interface IntegrationTestsDiscoveryOptions {
159159
readonly tests?: string[];
160160

161161
/**
162-
* Detect integration test files matching any of these JavaScript regex patterns.
163-
*
164-
* @default
165-
*/
166-
readonly testRegex?: string[];
167-
168-
/**
169-
* The CLI command used to run this test.
170-
* If it contains {filePath}, the test file names will be substituted at that place in the command for each run.
162+
* A map of of the app commands to run integration tests with,
163+
* and the regex patterns matching the integration test files each app command.
171164
*
172-
* @default - test run command will be `node {filePath}`
165+
* If the app command contains {filePath}, the test file names will be substituted at that place in the command for each run.
173166
*/
174-
readonly app?: string;
167+
readonly testCases: {
168+
[app: string]: string[]
169+
}
175170
}
176171

172+
/**
173+
* Returns the name of the Python executable for the current OS
174+
*/
175+
function pythonExecutable() {
176+
let python = 'python3';
177+
if (process.platform === 'win32') {
178+
python = 'python';
179+
}
180+
return python;
181+
}
177182

178183
/**
179184
* Discover integration tests
180185
*/
181186
export class IntegrationTests {
187+
constructor(private readonly directory: string) {}
188+
182189
/**
183-
* Return configuration options from a file
190+
* Get integration tests discovery options from CLI options
184191
*/
185-
public static configFromFile(fileName?: string): Record<string, any> {
186-
if (!fileName) {
187-
return {};
192+
public async fromCliOptions(options: {
193+
app?: string;
194+
exclude?: boolean,
195+
language?: string[],
196+
testRegex?: string[],
197+
tests?: string[],
198+
}): Promise<IntegTest[]> {
199+
const baseOptions = {
200+
tests: options.tests,
201+
exclude: options.exclude,
202+
};
203+
204+
// Explicitly set both, app and test-regex
205+
if (options.app && options.testRegex) {
206+
return this.discover({
207+
testCases: {
208+
[options.app]: options.testRegex,
209+
},
210+
...baseOptions,
211+
});
212+
}
213+
214+
// Use the selected presets
215+
if (!options.app && !options.testRegex) {
216+
// Only case with multiple languages, i.e. the only time we need to check the special case
217+
const ignoreUncompiledTypeScript = options.language?.includes('javascript') && options.language?.includes('typescript');
218+
219+
return this.discover({
220+
testCases: this.getLanguagePresets(options.language),
221+
...baseOptions,
222+
}, ignoreUncompiledTypeScript);
188223
}
189224

190-
try {
191-
return JSON.parse(fs.readFileSync(fileName, { encoding: 'utf-8' }));
192-
} catch {
193-
return {};
225+
// Only one of app or test-regex is set, with a single preset selected
226+
// => override either app or test-regex
227+
if (options.language?.length === 1) {
228+
const [presetApp, presetTestRegex] = this.getLanguagePreset(options.language[0]);
229+
return this.discover({
230+
testCases: {
231+
[options.app ?? presetApp]: options.testRegex ?? presetTestRegex,
232+
},
233+
...baseOptions,
234+
});
194235
}
236+
237+
// Only one of app or test-regex is set, with multiple presets
238+
// => impossible to resolve
239+
const option = options.app ? '--app' : '--test-regex';
240+
throw new Error(`Only a single "--language" can be used with "${option}". Alternatively provide both "--app" and "--test-regex" to fully customize the configuration.`);
241+
}
242+
243+
/**
244+
* Get the default configuration for a language
245+
*/
246+
private getLanguagePreset(language: string) {
247+
const languagePresets: {
248+
[language: string]: [string, string[]]
249+
} = {
250+
javascript: ['node {filePath}', ['^integ\\..*\\.js$']],
251+
typescript: ['node -r ts-node/register {filePath}', ['^integ\\.(?!.*\\.d\\.ts$).*\\.ts$']],
252+
python: [`${pythonExecutable()} {filePath}`, ['^integ_.*\\.py$']],
253+
go: ['go run {filePath}', ['^integ_.*\\.go$']],
254+
};
255+
256+
return languagePresets[language];
195257
}
196258

197-
constructor(private readonly directory: string) {
259+
/**
260+
* Get the config for all selected languages
261+
*/
262+
private getLanguagePresets(languages: string[] = []) {
263+
return Object.fromEntries(
264+
languages
265+
.map(language => this.getLanguagePreset(language))
266+
.filter(Boolean),
267+
);
198268
}
199269

200270
/**
@@ -209,7 +279,6 @@ export class IntegrationTests {
209279
return discoveredTests;
210280
}
211281

212-
213282
const allTests = discoveredTests.filter(t => {
214283
const matches = requestedTests.some(pattern => t.matches(pattern));
215284
return matches !== !!exclude; // Looks weird but is equal to (matches && !exclude) || (!matches && exclude)
@@ -237,33 +306,41 @@ export class IntegrationTests {
237306
* @param tests Tests to include or exclude, undefined means include all tests.
238307
* @param exclude Whether the 'tests' list is inclusive or exclusive (inclusive by default).
239308
*/
240-
public async fromCliArgs(options: IntegrationTestsDiscoveryOptions = {}): Promise<IntegTest[]> {
241-
return this.discover(options);
242-
}
243-
244-
private async discover(options: IntegrationTestsDiscoveryOptions): Promise<IntegTest[]> {
245-
const patterns = options.testRegex ?? ['^integ\\..*\\.js$'];
246-
309+
private async discover(options: IntegrationTestsDiscoveryOptions, ignoreUncompiledTypeScript: boolean = false): Promise<IntegTest[]> {
247310
const files = await this.readTree();
248-
const integs = files.filter(fileName => patterns.some((p) => {
249-
const regex = new RegExp(p);
250-
return regex.test(fileName) || regex.test(path.basename(fileName));
251-
}));
252-
253-
return this.request(integs, options);
254-
}
255-
256-
private request(files: string[], options: IntegrationTestsDiscoveryOptions): IntegTest[] {
257-
const discoveredTests = files.map(fileName => new IntegTest({
258-
discoveryRoot: this.directory,
259-
fileName,
260-
appCommand: options.app,
261-
}));
262311

312+
const testCases = Object.entries(options.testCases)
313+
.flatMap(([appCommand, patterns]) => files
314+
.filter(fileName => patterns.some((pattern) => {
315+
const regex = new RegExp(pattern);
316+
return regex.test(fileName) || regex.test(path.basename(fileName));
317+
}))
318+
.map(fileName => new IntegTest({
319+
discoveryRoot: this.directory,
320+
fileName,
321+
appCommand,
322+
})),
323+
);
324+
325+
const discoveredTests = ignoreUncompiledTypeScript ? this.filterUncompiledTypeScript(testCases) : testCases;
263326

264327
return this.filterTests(discoveredTests, options.tests, options.exclude);
265328
}
266329

330+
private filterUncompiledTypeScript(testCases: IntegTest[]): IntegTest[] {
331+
const jsTestCases = testCases.filter(t => t.fileName.endsWith('.js'));
332+
333+
return testCases
334+
// Remove all TypeScript test cases (ending in .ts)
335+
// for which a compiled version is present (same name, ending in .js)
336+
.filter((tsCandidate) => {
337+
if (!tsCandidate.fileName.endsWith('.ts')) {
338+
return true;
339+
}
340+
return jsTestCases.findIndex(jsTest => jsTest.testName === tsCandidate.testName) === -1;
341+
});
342+
}
343+
267344
private async readTree(): Promise<string[]> {
268345
const ret = new Array<string>();
269346

packages/@aws-cdk/integ-runner/package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"awslint": "cdk-awslint",
1515
"pkglint": "pkglint -f",
1616
"test": "cdk-test",
17+
"integ": "integ-runner",
1718
"watch": "cdk-watch",
1819
"build+test": "yarn build && yarn test",
1920
"build+test+package": "yarn build+test && yarn package",
@@ -52,15 +53,19 @@
5253
"license": "Apache-2.0",
5354
"devDependencies": {
5455
"@aws-cdk/cdk-build-tools": "0.0.0",
55-
"@types/mock-fs": "^4.13.1",
56-
"mock-fs": "^4.14.0",
56+
"@aws-cdk/core": "0.0.0",
57+
"@aws-cdk/integ-tests": "0.0.0",
5758
"@aws-cdk/pkglint": "0.0.0",
5859
"@types/fs-extra": "^8.1.2",
5960
"@types/jest": "^27.5.2",
61+
"@types/mock-fs": "^4.13.1",
6062
"@types/node": "^14.18.34",
6163
"@types/workerpool": "^6.1.0",
6264
"@types/yargs": "^15.0.14",
63-
"jest": "^27.5.1"
65+
"constructs": "^10.0.0",
66+
"mock-fs": "^4.14.0",
67+
"jest": "^27.5.1",
68+
"ts-node": "^10.9.1"
6469
},
6570
"dependencies": {
6671
"@aws-cdk/cloud-assembly-schema": "0.0.0",

0 commit comments

Comments
 (0)