Skip to content

Commit 5a3a32f

Browse files
authored
refactor: make node-bundle tests executable using ts-jest (#32022)
Make `node-bundle` easier to test (in-process instead of using a subcommand that requires `.js` to have been compiled), and fix a bug in the tests that used `--license` instead of `--allowed-license` (configure `yargs` to be `strict`). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ae29bb5 commit 5a3a32f

File tree

7 files changed

+167
-138
lines changed

7 files changed

+167
-138
lines changed

tools/@aws-cdk/node-bundle/package.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/@aws-cdk/node-bundle/src/api/bundle.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ export class Bundle {
327327

328328
if (this.test) {
329329
const command = `${path.join(bundleDir, this.test)}`;
330-
console.log(`Running santiy test: ${command}`);
330+
console.log(`Running sanity test: ${command}`);
331331
shell(command, { cwd: bundleDir });
332332
}
333333

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs-extra';
3+
import * as yargs from 'yargs';
4+
import { Bundle, BundleProps, BundleValidateOptions } from './api';
5+
6+
function versionNumber(): string {
7+
return fs.readJSONSync(path.join(__dirname, '..', 'package.json')).version;
8+
}
9+
10+
export async function cliMain(cliArgs: string[]) {
11+
const argv = await yargs
12+
.usage('Usage: node-bundle COMMAND')
13+
.option('entrypoint', { type: 'array', nargs: 1, desc: 'List of entrypoints to bundle' })
14+
.option('external', { type: 'array', nargs: 1, default: [], desc: 'Packages in this list will be excluded from the bundle and added as dependencies (example: fsevents:optional)' })
15+
.option('allowed-license', { type: 'array', nargs: 1, default: [], desc: 'List of valid licenses' })
16+
.option('resource', { type: 'array', nargs: 1, default: [], desc: 'List of resources that need to be explicitly copied to the bundle (example: node_modules/proxy-agent/contextify.js:bin/contextify.js)' })
17+
.option('dont-attribute', { type: 'string', desc: 'Dependencies matching this regular expressions wont be added to the notice file' })
18+
.option('test', { type: 'string', desc: 'Validation command to sanity test the bundle after its created' })
19+
.command('validate', 'Validate the package is ready for bundling', args => args
20+
.option('fix', { type: 'boolean', default: false, alias: 'f', desc: 'Fix any fixable violations' }),
21+
)
22+
.command('write', 'Write the bundled version of the project to a temp directory')
23+
.command('pack', 'Write the bundle and create the tarball')
24+
.demandCommand() // require a subcommand
25+
.strict() // require a VALID subcommand, and only supported options
26+
.fail((msg, err) => {
27+
// Throw an error in test mode, exit with an error code otherwise
28+
if (err) { throw err; }
29+
if (process.env.NODE_ENV === 'test') {
30+
throw new Error(msg);
31+
}
32+
console.error(msg);
33+
process.exit(1); // exit() not exitCode, we must not return.
34+
})
35+
.help()
36+
.version(versionNumber())
37+
.parse(cliArgs);
38+
39+
const command = argv._[0];
40+
41+
function undefinedIfEmpty(arr?: any[]): string[] | undefined {
42+
if (!arr || arr.length === 0) return undefined;
43+
return arr as string[];
44+
}
45+
46+
const resources: any = {};
47+
for (const resource of (argv.resource as string[])) {
48+
const parts = resource.split(':');
49+
resources[parts[0]] = parts[1];
50+
}
51+
52+
const optionalExternals = [];
53+
const runtimeExternals = [];
54+
55+
for (const external of (argv.external as string[])) {
56+
const parts = external.split(':');
57+
const name = parts[0];
58+
const type = parts[1];
59+
switch (type) {
60+
case 'optional':
61+
optionalExternals.push(name);
62+
break;
63+
case 'runtime':
64+
runtimeExternals.push(name);
65+
break;
66+
default:
67+
throw new Error(`Unsupported dependency type '${type}' for external package '${name}'. Supported types are: ['optional', 'runtime']`);
68+
}
69+
}
70+
71+
const props: BundleProps = {
72+
packageDir: process.cwd(),
73+
entryPoints: undefinedIfEmpty(argv.entrypoint),
74+
externals: { dependencies: runtimeExternals, optionalDependencies: optionalExternals },
75+
allowedLicenses: undefinedIfEmpty(argv['allowed-license']),
76+
resources: resources,
77+
dontAttribute: argv['dont-attribute'],
78+
test: argv.test,
79+
};
80+
81+
const bundle = new Bundle(props);
82+
83+
switch (command) {
84+
case 'validate':
85+
// When using `yargs.command(command, builder [, handler])` without the handler
86+
// as we do here, there is no typing for command-specific options. So force a cast.
87+
const fix = argv.fix as boolean | undefined;
88+
validate(bundle, { fix });
89+
break;
90+
case 'write':
91+
write(bundle);
92+
break;
93+
case 'pack':
94+
pack(bundle);
95+
break;
96+
default:
97+
throw new Error(`Unknown command: ${command}`);
98+
}
99+
}
100+
101+
function write(bundle: Bundle) {
102+
const bundleDir = bundle.write();
103+
console.log(bundleDir);
104+
}
105+
106+
function validate(bundle: Bundle, options: BundleValidateOptions = {}) {
107+
const report = bundle.validate(options);
108+
if (!report.success) {
109+
throw new Error(report.summary);
110+
}
111+
}
112+
113+
function pack(bundle: Bundle) {
114+
bundle.pack();
115+
}

tools/@aws-cdk/node-bundle/src/cli.ts

+2-103
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,6 @@
1-
import * as path from 'path';
2-
import * as fs from 'fs-extra';
3-
import * as yargs from 'yargs';
4-
import { Bundle, BundleProps, BundleValidateOptions } from './api';
1+
import { cliMain } from './cli-main';
52

6-
function versionNumber(): string {
7-
return fs.readJSONSync(path.join(__dirname, '..', 'package.json')).version;
8-
}
9-
10-
async function buildCommands() {
11-
12-
const argv = yargs
13-
.usage('Usage: node-bundle COMMAND')
14-
.option('entrypoint', { type: 'array', nargs: 1, desc: 'List of entrypoints to bundle' })
15-
.option('external', { type: 'array', nargs: 1, default: [], desc: 'Packages in this list will be excluded from the bundle and added as dependencies (example: fsevents:optional)' })
16-
.option('allowed-license', { type: 'array', nargs: 1, default: [], desc: 'List of valid licenses' })
17-
.option('resource', { type: 'array', nargs: 1, default: [], desc: 'List of resources that need to be explicitly copied to the bundle (example: node_modules/proxy-agent/contextify.js:bin/contextify.js)' })
18-
.option('dont-attribute', { type: 'string', desc: 'Dependencies matching this regular expressions wont be added to the notice file' })
19-
.option('test', { type: 'string', desc: 'Validation command to sanity test the bundle after its created' })
20-
.command('validate', 'Validate the package is ready for bundling', args => args
21-
.option('fix', { type: 'boolean', default: false, alias: 'f', desc: 'Fix any fixable violations' }),
22-
)
23-
.command('write', 'Write the bundled version of the project to a temp directory')
24-
.command('pack', 'Write the bundle and create the tarball')
25-
.help()
26-
.version(versionNumber())
27-
.argv;
28-
29-
const command = argv._[0];
30-
31-
function undefinedIfEmpty(arr?: any[]): string[] | undefined {
32-
if (!arr || arr.length === 0) return undefined;
33-
return arr as string[];
34-
}
35-
36-
const resources: any = {};
37-
for (const resource of (argv.resource as string[])) {
38-
const parts = resource.split(':');
39-
resources[parts[0]] = parts[1];
40-
}
41-
42-
const optionalExternals = [];
43-
const runtimeExternals = [];
44-
45-
for (const external of (argv.external as string[])) {
46-
const parts = external.split(':');
47-
const name = parts[0];
48-
const type = parts[1];
49-
switch (type) {
50-
case 'optional':
51-
optionalExternals.push(name);
52-
break;
53-
case 'runtime':
54-
runtimeExternals.push(name);
55-
break;
56-
default:
57-
throw new Error(`Unsupported dependency type '${type}' for external package '${name}'. Supported types are: ['optional', 'runtime']`);
58-
}
59-
}
60-
61-
const props: BundleProps = {
62-
packageDir: process.cwd(),
63-
entryPoints: undefinedIfEmpty(argv.entrypoint),
64-
externals: { dependencies: runtimeExternals, optionalDependencies: optionalExternals },
65-
allowedLicenses: undefinedIfEmpty(argv['allowed-license']),
66-
resources: resources,
67-
dontAttribute: argv['dont-attribute'],
68-
test: argv.test,
69-
};
70-
71-
const bundle = new Bundle(props);
72-
73-
switch (command) {
74-
case 'validate':
75-
validate(bundle, { fix: argv.fix });
76-
break;
77-
case 'write':
78-
write(bundle);
79-
break;
80-
case 'pack':
81-
pack(bundle);
82-
break;
83-
default:
84-
throw new Error(`Unknown command: ${command}`);
85-
}
86-
}
87-
88-
function write(bundle: Bundle) {
89-
const bundleDir = bundle.write();
90-
console.log(bundleDir);
91-
}
92-
93-
function validate(bundle: Bundle, options: BundleValidateOptions = {}) {
94-
const report = bundle.validate(options);
95-
if (!report.success) {
96-
throw new Error(report.summary);
97-
}
98-
}
99-
100-
function pack(bundle: Bundle) {
101-
bundle.pack();
102-
}
103-
104-
buildCommands()
3+
cliMain(process.argv.slice(2))
1054
.catch((err: Error) => {
1065
console.error(`Error: ${err.message}`);
1076
process.exitCode = 1;

tools/@aws-cdk/node-bundle/test/cli.test.ts

+46-33
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as path from 'path';
22
import * as fs from 'fs-extra';
3+
import { cliMain } from '../src/cli-main';
34
import { Package } from './_package';
4-
import { shell } from '../src/api/_shell';
5+
import * as util from 'util';
56

6-
test('validate', () => {
7+
test('validate', async () => {
78

89
const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'], circular: true });
910
const dep1 = pkg.addDependency({ name: 'dep1', licenses: ['INVALID'] });
@@ -14,15 +15,14 @@ test('validate', () => {
1415

1516
try {
1617
const command = [
17-
whereami(),
1818
'--entrypoint', pkg.entrypoint,
1919
'--resource', 'missing:bin/missing',
20-
'--license', 'Apache-2.0',
20+
'--allowed-license', 'Apache-2.0',
2121
'validate',
22-
].join(' ');
23-
shell(command, { cwd: pkg.dir, quiet: true });
22+
];
23+
await runCliMain(pkg.dir, command);
2424
} catch (e: any) {
25-
const violations = new Set(e.stderr.toString().trim().split('\n').filter((l: string) => l.startsWith('-')));
25+
const violations = new Set(e.message.trim().split('\n').filter((l: string) => l.startsWith('-')));
2626
const expected = new Set([
2727
`- invalid-license: Dependency ${dep1.name}@${dep1.version} has an invalid license: UNKNOWN`,
2828
`- multiple-license: Dependency ${dep2.name}@${dep2.version} has multiple licenses: Apache-2.0,MIT`,
@@ -35,7 +35,7 @@ test('validate', () => {
3535

3636
});
3737

38-
test('write', () => {
38+
test('write', async () => {
3939

4040
const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] });
4141
pkg.addDependency({ name: 'dep1', licenses: ['MIT'] });
@@ -45,13 +45,12 @@ test('write', () => {
4545
pkg.install();
4646

4747
const command = [
48-
whereami(),
4948
'--entrypoint', pkg.entrypoint,
50-
'--license', 'Apache-2.0',
51-
'--license', 'MIT',
49+
'--allowed-license', 'Apache-2.0',
50+
'--allowed-license', 'MIT',
5251
'write',
53-
].join(' ');
54-
const bundleDir = shell(command, { cwd: pkg.dir, quiet: true });
52+
];
53+
const bundleDir = await runCliMain(pkg.dir, command);
5554

5655
expect(fs.existsSync(path.join(bundleDir, pkg.entrypoint))).toBeTruthy();
5756
expect(fs.existsSync(path.join(bundleDir, 'package.json'))).toBeTruthy();
@@ -67,7 +66,7 @@ test('write', () => {
6766

6867
});
6968

70-
test('validate and fix', () => {
69+
test('validate and fix', async () => {
7170

7271
const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] });
7372
pkg.addDependency({ name: 'dep1', licenses: ['MIT'] });
@@ -76,33 +75,32 @@ test('validate and fix', () => {
7675
pkg.write();
7776
pkg.install();
7877

79-
const run = (sub: string) => {
78+
const run = (sub: string[]) => {
8079
const command = [
81-
whereami(),
8280
'--entrypoint', pkg.entrypoint,
83-
'--license', 'Apache-2.0',
84-
'--license', 'MIT',
85-
sub,
86-
].join(' ');
87-
shell(command, { cwd: pkg.dir, quiet: true });
81+
'--allowed-license', 'Apache-2.0',
82+
'--allowed-license', 'MIT',
83+
...sub,
84+
];
85+
return runCliMain(pkg.dir, command);
8886
};
8987

9088
try {
91-
run('pack');
89+
await run(['pack']);
9290
throw new Error('Expected packing to fail before fixing');
9391
} catch {
9492
// this should fix the fact we don't generate
9593
// the project with the correct attributions
96-
run('validate --fix');
94+
await run(['validate', '--fix']);
9795
}
9896

99-
run('pack');
97+
await run(['pack']);
10098
const tarball = path.join(pkg.dir, `${pkg.name}-${pkg.version}.tgz`);
10199
expect(fs.existsSync(tarball)).toBeTruthy();
102100

103101
});
104102

105-
test('pack', () => {
103+
test('pack', async () => {
106104

107105
const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] });
108106
const dep1 = pkg.addDependency({ name: 'dep1', licenses: ['MIT'] });
@@ -127,19 +125,34 @@ test('pack', () => {
127125
pkg.install();
128126

129127
const command = [
130-
whereami(),
131128
'--entrypoint', pkg.entrypoint,
132-
'--license', 'Apache-2.0',
133-
'--license', 'MIT',
129+
'--allowed-license', 'Apache-2.0',
130+
'--allowed-license', 'MIT',
134131
'pack',
135-
].join(' ');
136-
shell(command, { cwd: pkg.dir, quiet: true });
132+
];
133+
await runCliMain(pkg.dir, command);
137134

138135
const tarball = path.join(pkg.dir, `${pkg.name}-${pkg.version}.tgz`);
139136
expect(fs.existsSync(tarball)).toBeTruthy();
140137

141138
});
142139

143-
function whereami() {
144-
return path.join(path.join(__dirname, '..', 'bin', 'node-bundle'));
145-
}
140+
async function runCliMain(cwd: string, command: string[]): Promise<string> {
141+
const log: string[] = []
142+
const spy = jest
143+
.spyOn(console, 'log')
144+
.mockImplementation((...args) => {
145+
log.push(util.format(...args));
146+
});
147+
148+
const curdir = process.cwd();
149+
process.chdir(cwd);
150+
try {
151+
await cliMain(command);
152+
153+
return log.join('\n');
154+
} finally {
155+
process.chdir(curdir);
156+
spy.mockRestore();
157+
}
158+
}

0 commit comments

Comments
 (0)