1 | import {
|
2 | assign
|
3 | } from 'lodash';
|
4 | import * as path from 'path';
|
5 | import { spawn, ChildProcess } from 'child_process';
|
6 | import { ui, Command, Project, unwrap } from 'denali-cli';
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | export default class TestCommand extends Command {
|
14 |
|
15 |
|
16 | static commandName = 'test';
|
17 | static description = "Run your app's test suite";
|
18 | static longDescription = unwrap`
|
19 | Runs your app's test suite, and can optionally keep re-running it on each file change (--watch).
|
20 | `;
|
21 |
|
22 | static runsInApp = true;
|
23 |
|
24 | static params = '[files...]';
|
25 |
|
26 | static flags = {
|
27 | debug: {
|
28 | description: 'The test file you want to debug. Can only debug one file at a time.',
|
29 | type: <any>'boolean'
|
30 | },
|
31 | watch: {
|
32 | description: 'Re-run the tests when the source files change',
|
33 | default: false,
|
34 | type: <any>'boolean'
|
35 | },
|
36 | match: {
|
37 | description: 'Filter which tests run based on the supplied regex pattern',
|
38 | type: <any>'string'
|
39 | },
|
40 | timeout: {
|
41 | description: 'Set the timeout for all tests, i.e. --timeout 10s, --timeout 2m',
|
42 | type: <any>'string'
|
43 | },
|
44 | skipLint: {
|
45 | description: 'Skip linting the app source files',
|
46 | default: false,
|
47 | type: <any>'boolean'
|
48 | },
|
49 | skipAudit: {
|
50 | description: 'Skip auditing your package.json for vulnerabilites',
|
51 | default: false,
|
52 | type: <any>'boolean'
|
53 | },
|
54 | verbose: {
|
55 | description: 'Print detailed output of the status of your test run',
|
56 | default: process.env.CI,
|
57 | type: <any>'boolean'
|
58 | },
|
59 | printSlowTrees: {
|
60 | description: 'Print out an analysis of the build process, showing the slowest nodes.',
|
61 | default: false,
|
62 | type: <any>'boolean'
|
63 | },
|
64 | failFast: {
|
65 | description: 'Stop tests on the first failure',
|
66 | default: false,
|
67 | type: <any>'boolean'
|
68 | },
|
69 | litter: {
|
70 | description: 'Do not clean up tmp directories created during testing (useful for debugging)',
|
71 | default: false,
|
72 | type: <any>'boolean'
|
73 | },
|
74 | serial: {
|
75 | description: 'Run tests serially',
|
76 | default: false,
|
77 | type: <any>'boolean'
|
78 | },
|
79 | concurrency: {
|
80 | description: 'How many test files should run concurrently?',
|
81 | default: 5,
|
82 | type: <any>'number'
|
83 | }
|
84 | };
|
85 |
|
86 | tests: ChildProcess;
|
87 |
|
88 | async run(argv: any) {
|
89 | let files = <string[]>argv.files;
|
90 | if (files.length === 0) {
|
91 | files.push('test/**/*.js');
|
92 | } else {
|
93 |
|
94 |
|
95 |
|
96 | files = files.map((pattern) => pattern.replace(/\.[A-z0-9]{1,4}$/, '.js'));
|
97 | }
|
98 |
|
99 | files = files.filter((pattern: string) => {
|
100 | let isValidJsPattern = pattern.endsWith('*') || pattern.endsWith('.js');
|
101 | if (!isValidJsPattern) {
|
102 | ui.warn(unwrap`
|
103 | If you want to run specific test files, you must use the .js extension. You supplied
|
104 | ${ pattern }. Denali will build your test files before running them, so you need to use
|
105 | the compiled filename which ends in .js
|
106 | `);
|
107 | }
|
108 | return isValidJsPattern;
|
109 | });
|
110 |
|
111 | let project = new Project({
|
112 | environment: 'test',
|
113 | printSlowTrees: argv.printSlowTrees,
|
114 | audit: !argv.skipAudit,
|
115 | lint: !argv.skipLint,
|
116 | buildDummy: true
|
117 | });
|
118 |
|
119 | let outputDir = path.join('tmp', `${ project.rootBuilder.pkg.name }-test`);
|
120 |
|
121 | process.on('exit', this.cleanExit.bind(this));
|
122 | process.on('SIGINT', this.cleanExit.bind(this));
|
123 | process.on('SIGTERM', this.cleanExit.bind(this));
|
124 |
|
125 | if (argv.watch) {
|
126 | project.watch({
|
127 | outputDir,
|
128 |
|
129 |
|
130 |
|
131 | beforeRebuild: async () => {
|
132 | if (this.tests) {
|
133 | return new Promise<void>((resolve) => {
|
134 | this.tests.removeAllListeners('exit');
|
135 | this.tests.on('exit', () => {
|
136 | delete this.tests;
|
137 | resolve();
|
138 | });
|
139 | this.tests.kill();
|
140 | ui.info('\n\n===> Changes detected, cancelling in-progress tests ...\n\n');
|
141 | });
|
142 | }
|
143 | },
|
144 | onBuild: this.runTests.bind(this, files, project, outputDir, argv)
|
145 | });
|
146 | } else {
|
147 | try {
|
148 | await project.build(outputDir);
|
149 | this.runTests(files, project, outputDir, argv);
|
150 | } catch (error) {
|
151 | process.exitCode = 1;
|
152 | throw error;
|
153 | }
|
154 | }
|
155 | }
|
156 |
|
157 | protected cleanExit() {
|
158 | if (this.tests) {
|
159 | this.tests.kill();
|
160 | }
|
161 | }
|
162 |
|
163 | protected runTests(files: string[], project: Project, outputDir: string, argv: any) {
|
164 | let avaPath = path.join(process.cwd(), 'node_modules', '.bin', 'ava');
|
165 | files = files.map((pattern) => path.join(outputDir, pattern));
|
166 | let args = files.concat([ '--concurrency', argv.concurrency ]);
|
167 | if (argv.debug) {
|
168 | avaPath = process.execPath;
|
169 | args = [ '--inspect', '--inspect-brk', path.join(process.cwd(), 'node_modules', 'ava', 'profile.js'), ...files ];
|
170 | }
|
171 | if (argv.match) {
|
172 | args.push('--match', argv.match);
|
173 | }
|
174 | if (argv.verbose) {
|
175 | args.unshift('--verbose');
|
176 | }
|
177 | if (argv.timeout) {
|
178 | args.unshift('--timeout', argv.timeout);
|
179 | }
|
180 | if (argv.failFast) {
|
181 | args.unshift('--fail-fast');
|
182 | }
|
183 | if (argv.serial) {
|
184 | args.unshift('--serial');
|
185 | }
|
186 | this.tests = spawn(avaPath, args, {
|
187 | stdio: [ 'pipe', process.stdout, process.stderr ],
|
188 | env: assign({}, process.env, {
|
189 | PORT: argv.port,
|
190 | DENALI_LEAVE_TMP: argv.litter,
|
191 | NODE_ENV: project.environment,
|
192 | DEBUG_COLORS: 1,
|
193 | DENALI_TEST_BUILD_DIR: outputDir
|
194 | })
|
195 | });
|
196 | ui.info(`===> Running ${ project.pkg.name } tests ...`);
|
197 | this.tests.on('exit', (code: number | null) => {
|
198 | if (code === 0) {
|
199 | ui.success('\n===> Tests passed 👍');
|
200 | } else {
|
201 | ui.error('\n===> Tests failed 💥');
|
202 | }
|
203 | delete this.tests;
|
204 | if (argv.watch) {
|
205 | ui.info('===> Waiting for changes to re-run ...\n\n');
|
206 | } else {
|
207 | process.exitCode = code == null ? 1 : code;
|
208 | ui.info(`===> exiting with ${ process.exitCode }`);
|
209 | }
|
210 | });
|
211 | }
|
212 | }
|
213 |
|
\ | No newline at end of file |