UNPKG

6.5 kBPlain TextView Raw
1import {
2 assign
3} from 'lodash';
4import * as path from 'path';
5import { spawn, ChildProcess } from 'child_process';
6import { ui, Command, Project, unwrap } from 'denali-cli';
7
8/**
9 * Run your app's test suite
10 *
11 * @package commands
12 */
13export default class TestCommand extends Command {
14
15 /* tslint:disable:completed-docs typedef */
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 // Swap common file extensions out with `.js` so ava will find the actual, built files This
94 // doesn't cover every possible edge case, hence the `isValidJsPattern` below, but it should
95 // cover the common use cases.
96 files = files.map((pattern) => pattern.replace(/\.[A-z0-9]{1,4}$/, '.js'));
97 }
98 // Filter for .js files only
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 // Don't let broccoli rebuild while tests are still running, or else
129 // we'll be removing the test files while in progress leading to cryptic
130 // errors.
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