Skip to content

Commit fa1a439

Browse files
authored
feat(integ-runner): support custom --test-regex to match integ test files (#22786)
Follow-up to #22761. To support other languages than JavaScript (see #22521) we need to be able to detect test files with any patterns. With this PR, users can specify a number of custom `--test-regex` patterns that will bed used to discover integration test files. Together with `--app` this can already be used to run integ tests in arbitrary languages. Example usage: `integ-runner --app="python3 {filePath}" --test-regex="^integ_.*\.py$"` Also contains a minor refactor to make `--app` available via `IntegrationTests.fromFile()`. This is in preparation of an upcoming change to reestablish support for an integration test config file. ---- ### 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 * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] 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 0a55e91 commit fa1a439

File tree

10 files changed

+276
-76
lines changed

10 files changed

+276
-76
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ 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+
- `--test-regex`
74+
Detect integration test files matching this JavaScript regex pattern. If used multiple times, all files matching any one of the patterns are detected.
75+
7376
Example:
7477

7578
```bash

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

+14-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { runSnapshotTests, runIntegrationTests, IntegRunnerMetrics, IntegTestWor
1313
const yargs = require('yargs');
1414

1515

16-
async function main() {
16+
export async function main(args: string[]) {
1717
const argv = yargs
1818
.usage('Usage: integ-runner [TEST...]')
1919
.option('list', { type: 'boolean', default: false, desc: 'List tests instead of running them' })
@@ -31,14 +31,16 @@ async function main() {
3131
.option('inspect-failures', { type: 'boolean', desc: 'Keep the integ test cloud assembly if a failure occurs for inspection', default: false })
3232
.option('disable-update-workflow', { type: 'boolean', default: false, desc: 'If this is "true" then the stack update workflow will be disabled' })
3333
.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}".' })
34+
.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: [] })
3435
.strict()
35-
.argv;
36+
.parse(args);
3637

3738
const pool = workerpool.pool(path.join(__dirname, '../lib/workers/extract/index.js'), {
3839
maxWorkers: argv['max-workers'],
3940
});
4041

4142
// list of integration tests that will be executed
43+
const testRegex = arrayFromYargs(argv['test-regex']);
4244
const testsToRun: IntegTestWorkerConfig[] = [];
4345
const destructiveChanges: DestructiveChange[] = [];
4446
const testsFromArgs: IntegTest[] = [];
@@ -48,6 +50,7 @@ async function main() {
4850
const runUpdateOnFailed = argv['update-on-failed'] ?? false;
4951
const fromFile: string | undefined = argv['from-file'];
5052
const exclude: boolean = argv.exclude;
53+
const app: string | undefined = argv.app;
5154

5255
let failedSnapshots: IntegTestWorkerConfig[] = [];
5356
if (argv['max-workers'] < testRegions.length * (profiles ?? [1]).length) {
@@ -57,7 +60,7 @@ async function main() {
5760
let testsSucceeded = false;
5861
try {
5962
if (argv.list) {
60-
const tests = await new IntegrationTests(argv.directory).fromCliArgs();
63+
const tests = await new IntegrationTests(argv.directory).fromCliArgs({ testRegex, app });
6164
process.stdout.write(tests.map(t => t.discoveryRelativeFileName).join('\n') + '\n');
6265
return;
6366
}
@@ -69,15 +72,19 @@ async function main() {
6972
? (await fs.readFile(fromFile, { encoding: 'utf8' })).split('\n').filter(x => x)
7073
: (argv._.length > 0 ? argv._ : undefined); // 'undefined' means no request
7174

72-
testsFromArgs.push(...(await new IntegrationTests(path.resolve(argv.directory)).fromCliArgs(requestedTests, exclude)));
75+
testsFromArgs.push(...(await new IntegrationTests(path.resolve(argv.directory)).fromCliArgs({
76+
app,
77+
testRegex,
78+
tests: requestedTests,
79+
exclude,
80+
})));
7381

7482
// always run snapshot tests, but if '--force' is passed then
7583
// run integration tests on all failed tests, not just those that
7684
// failed snapshot tests
7785
failedSnapshots = await runSnapshotTests(pool, testsFromArgs, {
7886
retain: argv['inspect-failures'],
7987
verbose: Boolean(argv.verbose),
80-
appCommand: argv.app,
8188
});
8289
for (const failure of failedSnapshots) {
8390
destructiveChanges.push(...failure.destructiveChanges ?? []);
@@ -101,7 +108,6 @@ async function main() {
101108
dryRun: argv['dry-run'],
102109
verbosity: argv.verbose,
103110
updateWorkflow: !argv['disable-update-workflow'],
104-
appCommand: argv.app,
105111
});
106112
testsSucceeded = success;
107113

@@ -184,8 +190,8 @@ function mergeTests(testFromArgs: IntegTestInfo[], failedSnapshotTests: IntegTes
184190
return final;
185191
}
186192

187-
export function cli() {
188-
main().then().catch(err => {
193+
export function cli(args: string[] = process.argv.slice(2)) {
194+
main(args).then().catch(err => {
189195
logger.error(err);
190196
process.exitCode = 1;
191197
});

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

+70-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export interface IntegTestInfo {
2323
* Path is relative to the current working directory.
2424
*/
2525
readonly discoveryRoot: string;
26+
27+
/**
28+
* The CLI command used to run this test.
29+
* If it contains {filePath}, the test file names will be substituted at that place in the command for each run.
30+
*
31+
* @default - test run command will be `node {filePath}`
32+
*/
33+
readonly appCommand?: string;
2634
}
2735

2836
/**
@@ -79,7 +87,16 @@ export class IntegTest {
7987
*/
8088
public readonly temporaryOutputDir: string;
8189

90+
/**
91+
* The CLI command used to run this test.
92+
* If it contains {filePath}, the test file names will be substituted at that place in the command for each run.
93+
*
94+
* @default - test run command will be `node {filePath}`
95+
*/
96+
readonly appCommand: string;
97+
8298
constructor(public readonly info: IntegTestInfo) {
99+
this.appCommand = info.appCommand ?? 'node {filePath}';
83100
this.absoluteFileName = path.resolve(info.fileName);
84101
this.fileName = path.relative(process.cwd(), info.fileName);
85102

@@ -123,10 +140,9 @@ export class IntegTest {
123140
}
124141

125142
/**
126-
* The list of tests to run can be provided in a file
127-
* instead of as command line arguments.
143+
* Configuration options how integration test files are discovered
128144
*/
129-
export interface IntegrationTestFileConfig {
145+
export interface IntegrationTestsDiscoveryOptions {
130146
/**
131147
* If this is set to true then the list of tests
132148
* provided will be excluded
@@ -135,6 +151,35 @@ export interface IntegrationTestFileConfig {
135151
*/
136152
readonly exclude?: boolean;
137153

154+
/**
155+
* List of tests to include (or exclude if `exclude=true`)
156+
*
157+
* @default - all matched files
158+
*/
159+
readonly tests?: string[];
160+
161+
/**
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.
171+
*
172+
* @default - test run command will be `node {filePath}`
173+
*/
174+
readonly app?: string;
175+
}
176+
177+
178+
/**
179+
* The list of tests to run can be provided in a file
180+
* instead of as command line arguments.
181+
*/
182+
export interface IntegrationTestFileConfig extends IntegrationTestsDiscoveryOptions {
138183
/**
139184
* List of tests to include (or exclude if `exclude=true`)
140185
*/
@@ -154,11 +199,8 @@ export class IntegrationTests {
154199
*/
155200
public async fromFile(fileName: string): Promise<IntegTest[]> {
156201
const file: IntegrationTestFileConfig = JSON.parse(fs.readFileSync(fileName, { encoding: 'utf-8' }));
157-
const foundTests = await this.discover();
158-
159-
const allTests = this.filterTests(foundTests, file.tests, file.exclude);
160202

161-
return allTests;
203+
return this.discover(file);
162204
}
163205

164206
/**
@@ -201,22 +243,31 @@ export class IntegrationTests {
201243
* @param tests Tests to include or exclude, undefined means include all tests.
202244
* @param exclude Whether the 'tests' list is inclusive or exclusive (inclusive by default).
203245
*/
204-
public async fromCliArgs(tests?: string[], exclude?: boolean): Promise<IntegTest[]> {
205-
const discoveredTests = await this.discover();
206-
207-
const allTests = this.filterTests(discoveredTests, tests, exclude);
208-
209-
return allTests;
246+
public async fromCliArgs(options: IntegrationTestsDiscoveryOptions = {}): Promise<IntegTest[]> {
247+
return this.discover(options);
210248
}
211249

212-
private async discover(): Promise<IntegTest[]> {
250+
private async discover(options: IntegrationTestsDiscoveryOptions): Promise<IntegTest[]> {
251+
const patterns = options.testRegex ?? ['^integ\\..*\\.js$'];
252+
213253
const files = await this.readTree();
214-
const integs = files.filter(fileName => path.basename(fileName).startsWith('integ.') && path.basename(fileName).endsWith('.js'));
215-
return this.request(integs);
254+
const integs = files.filter(fileName => patterns.some((p) => {
255+
const regex = new RegExp(p);
256+
return regex.test(fileName) || regex.test(path.basename(fileName));
257+
}));
258+
259+
return this.request(integs, options);
216260
}
217261

218-
private request(files: string[]): IntegTest[] {
219-
return files.map(fileName => new IntegTest({ discoveryRoot: this.directory, fileName }));
262+
private request(files: string[], options: IntegrationTestsDiscoveryOptions): IntegTest[] {
263+
const discoveredTests = files.map(fileName => new IntegTest({
264+
discoveryRoot: this.directory,
265+
fileName,
266+
appCommand: options.app,
267+
}));
268+
269+
270+
return this.filterTests(discoveredTests, options.tests, options.exclude);
220271
}
221272

222273
private async readTree(): Promise<string[]> {
@@ -228,7 +279,7 @@ export class IntegrationTests {
228279
const fullPath = path.join(dir, file);
229280
const statf = await fs.stat(fullPath);
230281
if (statf.isFile()) { ret.push(fullPath); }
231-
if (statf.isDirectory()) { await recurse(path.join(fullPath)); }
282+
if (statf.isDirectory()) { await recurse(fullPath); }
232283
}
233284
}
234285

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

+1-9
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,6 @@ export interface IntegRunnerOptions {
4949
*/
5050
readonly cdk?: ICdk;
5151

52-
/**
53-
* You can specify a custom run command, and it will be applied to all test files.
54-
* If it contains {filePath}, the test file names will be substituted at that place in the command for each run.
55-
*
56-
* @default - test run command will be `node {filePath}`
57-
*/
58-
readonly appCommand?: string;
59-
6052
/**
6153
* Show output from running integration tests
6254
*
@@ -159,7 +151,7 @@ export abstract class IntegRunner {
159151
});
160152
this.cdkOutDir = options.integOutDir ?? this.test.temporaryOutputDir;
161153

162-
const testRunCommand = options.appCommand ?? 'node {filePath}';
154+
const testRunCommand = this.test.appCommand;
163155
this.cdkApp = testRunCommand.replace('{filePath}', path.relative(this.directory, this.test.fileName));
164156

165157
this.profile = options.profile;

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

-14
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,6 @@ export interface SnapshotVerificationOptions {
103103
* @default false
104104
*/
105105
readonly verbose?: boolean;
106-
107-
/**
108-
* The CLI command used to run the test files.
109-
*
110-
* @default - test run command will be `node {filePath}`
111-
*/
112-
readonly appCommand?: string;
113106
}
114107

115108
/**
@@ -169,13 +162,6 @@ export interface IntegTestOptions {
169162
* @default true
170163
*/
171164
readonly updateWorkflow?: boolean;
172-
173-
/**
174-
* The CLI command used to run the test files.
175-
*
176-
* @default - test run command will be `node {filePath}`
177-
*/
178-
readonly appCommand?: string;
179165
}
180166

181167
/**

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker
2727
env: {
2828
AWS_REGION: request.region,
2929
},
30-
appCommand: request.appCommand,
3130
showOutput: verbosity >= 2,
3231
}, testInfo.destructiveChanges);
3332

@@ -106,7 +105,7 @@ export function snapshotTestWorker(testInfo: IntegTestInfo, options: SnapshotVer
106105
}, 60_000);
107106

108107
try {
109-
const runner = new IntegSnapshotRunner({ test, appCommand: options.appCommand });
108+
const runner = new IntegSnapshotRunner({ test });
110109
if (!runner.hasSnapshot()) {
111110
workerpool.workerEmit({
112111
reason: DiagnosticReason.NO_SNAPSHOT,

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

-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ export async function runIntegrationTestsInParallel(
135135
dryRun: options.dryRun,
136136
verbosity: options.verbosity,
137137
updateWorkflow: options.updateWorkflow,
138-
appCommand: options.appCommand,
139138
}], {
140139
on: printResults,
141140
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as path from 'path';
2+
import { main } from '../lib/cli';
3+
4+
describe('CLI', () => {
5+
const currentCwd = process.cwd();
6+
beforeAll(() => {
7+
process.chdir(path.join(__dirname, '..'));
8+
});
9+
afterAll(() => {
10+
process.chdir(currentCwd);
11+
});
12+
13+
let stdoutMock: jest.SpyInstance;
14+
beforeEach(() => {
15+
stdoutMock = jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; });
16+
});
17+
afterEach(() => {
18+
stdoutMock.mockRestore();
19+
});
20+
21+
test('find by default pattern', async () => {
22+
await main(['--list', '--directory=test/test-data']);
23+
24+
// Expect nothing to be found since this directory doesn't contain files with the default pattern
25+
expect(stdoutMock.mock.calls).toEqual([['\n']]);
26+
});
27+
28+
test('find by custom pattern', async () => {
29+
await main(['--list', '--directory=test/test-data', '--test-regex="^xxxxx\..*\.js$"']);
30+
31+
expect(stdoutMock.mock.calls).toEqual([[
32+
[
33+
'xxxxx.integ-test1.js',
34+
'xxxxx.integ-test2.js',
35+
'xxxxx.test-with-new-assets-diff.js',
36+
'xxxxx.test-with-new-assets.js',
37+
'xxxxx.test-with-snapshot-assets-diff.js',
38+
'xxxxx.test-with-snapshot-assets.js',
39+
'xxxxx.test-with-snapshot.js',
40+
'',
41+
].join('\n'),
42+
]]);
43+
});
44+
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,8 @@ describe('IntegTest runIntegTests', () => {
567567
test: new IntegTest({
568568
fileName: 'test/test-data/xxxxx.test-with-snapshot.js',
569569
discoveryRoot: 'test/test-data',
570+
appCommand: 'node --no-warnings {filePath}',
570571
}),
571-
appCommand: 'node --no-warnings {filePath}',
572572
});
573573
integTest.runIntegTestCase({
574574
testCaseName: 'xxxxx.test-with-snapshot',

0 commit comments

Comments
 (0)