UNPKG

13.2 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const del = require('del');
4const updateNotifier = require('update-notifier');
5const figures = require('figures');
6const arrify = require('arrify');
7const yargs = require('yargs');
8const readPkg = require('read-pkg');
9const isCi = require('./is-ci');
10const loadConfig = require('./load-config');
11
12function exit(message) {
13 console.error(`\n ${require('./chalk').get().red(figures.cross)} ${message}`);
14 process.exit(1); // eslint-disable-line unicorn/no-process-exit
15}
16
17const coerceLastValue = value => {
18 return Array.isArray(value) ? value.pop() : value;
19};
20
21const FLAGS = {
22 concurrency: {
23 alias: 'c',
24 coerce: coerceLastValue,
25 description: 'Max number of test files running at the same time (default: CPU cores)',
26 type: 'number'
27 },
28 'fail-fast': {
29 coerce: coerceLastValue,
30 description: 'Stop after first test failure',
31 type: 'boolean'
32 },
33 match: {
34 alias: 'm',
35 description: 'Only run tests with matching title (can be repeated)',
36 type: 'string'
37 },
38 'node-arguments': {
39 coerce: coerceLastValue,
40 description: 'Additional Node.js arguments for launching worker processes (specify as a single string)',
41 type: 'string'
42 },
43 serial: {
44 alias: 's',
45 coerce: coerceLastValue,
46 description: 'Run tests serially',
47 type: 'boolean'
48 },
49 tap: {
50 alias: 't',
51 coerce: coerceLastValue,
52 description: 'Generate TAP output',
53 type: 'boolean'
54 },
55 timeout: {
56 alias: 'T',
57 coerce: coerceLastValue,
58 description: 'Set global timeout (milliseconds or human-readable, e.g. 10s, 2m)',
59 type: 'string'
60 },
61 'update-snapshots': {
62 alias: 'u',
63 coerce: coerceLastValue,
64 description: 'Update snapshots',
65 type: 'boolean'
66 },
67 verbose: {
68 alias: 'v',
69 coerce: coerceLastValue,
70 description: 'Enable verbose output',
71 type: 'boolean'
72 },
73 watch: {
74 alias: 'w',
75 coerce: coerceLastValue,
76 description: 'Re-run tests when files change',
77 type: 'boolean'
78 }
79};
80
81exports.run = async () => { // eslint-disable-line complexity
82 let conf = {};
83 let confError = null;
84 try {
85 const {argv: {config: configFile}} = yargs.help(false);
86 conf = loadConfig({configFile});
87 } catch (error) {
88 confError = error;
89 }
90
91 // Enter debug mode if the main process is being inspected. This assumes the
92 // worker processes are automatically inspected, too. It is not necessary to
93 // run AVA with the debug command, though it's allowed.
94 const activeInspector = require('inspector').url() !== undefined; // eslint-disable-line node/no-unsupported-features/node-builtins
95 let debug = activeInspector ?
96 {
97 active: true,
98 break: false,
99 files: [],
100 host: undefined,
101 port: undefined
102 } : null;
103
104 let resetCache = false;
105 const {argv} = yargs
106 .parserConfiguration({
107 'boolean-negation': true,
108 'camel-case-expansion': false,
109 'combine-arrays': false,
110 'dot-notation': false,
111 'duplicate-arguments-array': true,
112 'flatten-duplicate-arrays': true,
113 'negation-prefix': 'no-',
114 'parse-numbers': true,
115 'populate--': true,
116 'set-placeholder-key': false,
117 'short-option-groups': true,
118 'strip-aliased': true,
119 'unknown-options-as-args': false
120 })
121 .usage('$0 [<pattern>...]')
122 .usage('$0 debug [<pattern>...]')
123 .usage('$0 reset-cache')
124 .options({
125 color: {
126 description: 'Force color output',
127 type: 'boolean'
128 },
129 config: {
130 description: 'Specific JavaScript file for AVA to read its config from, instead of using package.json or ava.config.* files'
131 }
132 })
133 .command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
134 array: true,
135 describe: 'Glob patterns to select what test files to run. Leave empty if you want AVA to run all test files instead. Add a colon and specify line numbers of specific tests to run',
136 type: 'string'
137 }), argv => {
138 if (activeInspector) {
139 debug.files = argv.pattern || [];
140 }
141 })
142 .command(
143 'debug [<pattern>...]',
144 'Activate Node.js inspector and run a single test file',
145 yargs => yargs.options(FLAGS).options({
146 break: {
147 description: 'Break before the test file is loaded',
148 type: 'boolean'
149 },
150 host: {
151 default: '127.0.0.1',
152 description: 'Address or hostname through which you can connect to the inspector',
153 type: 'string'
154 },
155 port: {
156 default: 9229,
157 description: 'Port on which you can connect to the inspector',
158 type: 'number'
159 }
160 }).positional('pattern', {
161 demand: true,
162 describe: 'Glob patterns to select a single test file to debug. Add a colon and specify line numbers of specific tests to run',
163 type: 'string'
164 }),
165 argv => {
166 debug = {
167 active: activeInspector,
168 break: argv.break === true,
169 files: argv.pattern,
170 host: argv.host,
171 port: argv.port
172 };
173 })
174 .command(
175 'reset-cache',
176 'Reset AVA’s compilation cache and exit',
177 yargs => yargs,
178 () => {
179 resetCache = true;
180 })
181 .example('$0')
182 .example('$0 test.js')
183 .example('$0 test.js:4,7-9')
184 .help();
185
186 const combined = {...conf};
187 for (const flag of Object.keys(FLAGS)) {
188 if (Reflect.has(argv, flag)) {
189 if (flag === 'fail-fast') {
190 combined.failFast = argv[flag];
191 } else if (flag === 'update-snapshots') {
192 combined.updateSnapshots = argv[flag];
193 } else if (flag !== 'node-arguments') {
194 combined[flag] = argv[flag];
195 }
196 }
197 }
198
199 const chalkOptions = {level: combined.color === false ? 0 : require('chalk').level};
200 const chalk = require('./chalk').set(chalkOptions);
201
202 if (combined.updateSnapshots && combined.match) {
203 exit('Snapshots cannot be updated when matching specific tests.');
204 }
205
206 if (confError) {
207 if (confError.parent) {
208 exit(`${confError.message}\n\n${chalk.gray((confError.parent && confError.parent.stack) || confError.parent)}`);
209 } else {
210 exit(confError.message);
211 }
212 }
213
214 updateNotifier({pkg: require('../package.json')}).notify();
215
216 const {nonSemVerExperiments: experiments, projectDir} = conf;
217 if (resetCache) {
218 const cacheDir = path.join(projectDir, 'node_modules', '.cache', 'ava');
219 try {
220 await del('*', {
221 cwd: cacheDir,
222 nodir: true
223 });
224 console.error(`\n${chalk.green(figures.tick)} Removed AVA cache files in ${cacheDir}`);
225 process.exit(0); // eslint-disable-line unicorn/no-process-exit
226 } catch (error) {
227 exit(`Error removing AVA cache files in ${cacheDir}\n\n${chalk.gray((error && error.stack) || error)}`);
228 }
229
230 return;
231 }
232
233 if (argv.watch) {
234 if (argv.tap && !conf.tap) {
235 exit('The TAP reporter is not available when using watch mode.');
236 }
237
238 if (isCi) {
239 exit('Watch mode is not available in CI, as it prevents AVA from terminating.');
240 }
241
242 if (debug !== null) {
243 exit('Watch mode is not available when debugging.');
244 }
245 }
246
247 if (debug !== null) {
248 if (argv.tap && !conf.tap) {
249 exit('The TAP reporter is not available when debugging.');
250 }
251
252 if (isCi) {
253 exit('Debugging is not available in CI.');
254 }
255
256 if (combined.timeout) {
257 console.log(chalk.magenta(` ${figures.warning} The timeout option has been disabled to help with debugging.`));
258 }
259 }
260
261 if (Reflect.has(combined, 'concurrency') && (!Number.isInteger(combined.concurrency) || combined.concurrency < 0)) {
262 exit('The --concurrency or -c flag must be provided with a nonnegative integer.');
263 }
264
265 if (!combined.tap && Object.keys(experiments).length > 0) {
266 console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`));
267 }
268
269 if (Reflect.has(conf, 'compileEnhancements')) {
270 exit('Enhancement compilation must be configured in AVA’s Babel options.');
271 }
272
273 if (Reflect.has(conf, 'helpers')) {
274 exit('AVA no longer compiles helpers. Add exclusion patterns to the ’files’ configuration and specify ’compileAsTests’ in the Babel options instead.');
275 }
276
277 if (Reflect.has(conf, 'sources')) {
278 exit('’sources’ has been removed. Use ’ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.');
279 }
280
281 const ciParallelVars = require('ci-parallel-vars');
282 const Api = require('./api');
283 const DefaultReporter = require('./reporters/default');
284 const TapReporter = require('./reporters/tap');
285 const Watcher = require('./watcher');
286 const normalizeExtensions = require('./extensions');
287 const normalizeModuleTypes = require('./module-types');
288 const {normalizeGlobs, normalizePattern} = require('./globs');
289 const normalizeNodeArguments = require('./node-arguments');
290 const validateEnvironmentVariables = require('./environment-variables');
291 const {splitPatternAndLineNumbers} = require('./line-numbers');
292 const providerManager = require('./provider-manager');
293
294 let pkg;
295 try {
296 pkg = readPkg.sync({cwd: projectDir});
297 } catch (error) {
298 if (error.code !== 'ENOENT') {
299 throw error;
300 }
301 }
302
303 const {type: defaultModuleType = 'commonjs'} = pkg || {};
304
305 const providers = [];
306 if (Reflect.has(conf, 'babel')) {
307 try {
308 const {level, main} = providerManager.babel(projectDir);
309 providers.push({
310 level,
311 main: main({config: conf.babel}),
312 type: 'babel'
313 });
314 } catch (error) {
315 exit(error.message);
316 }
317 }
318
319 if (Reflect.has(conf, 'typescript')) {
320 try {
321 const {level, main} = providerManager.typescript(projectDir);
322 providers.push({
323 level,
324 main: main({config: conf.typescript}),
325 type: 'typescript'
326 });
327 } catch (error) {
328 exit(error.message);
329 }
330 }
331
332 let environmentVariables;
333 try {
334 environmentVariables = validateEnvironmentVariables(conf.environmentVariables);
335 } catch (error) {
336 exit(error.message);
337 }
338
339 let extensions;
340 try {
341 extensions = normalizeExtensions(conf.extensions, providers);
342 } catch (error) {
343 exit(error.message);
344 }
345
346 let moduleTypes;
347 try {
348 moduleTypes = normalizeModuleTypes(conf.extensions, defaultModuleType, experiments);
349 } catch (error) {
350 exit(error.message);
351 }
352
353 let globs;
354 try {
355 globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers});
356 } catch (error) {
357 exit(error.message);
358 }
359
360 let nodeArguments;
361 try {
362 nodeArguments = normalizeNodeArguments(conf.nodeArguments, argv['node-arguments']);
363 } catch (error) {
364 exit(error.message);
365 }
366
367 let parallelRuns = null;
368 if (isCi && ciParallelVars) {
369 const {index: currentIndex, total: totalRuns} = ciParallelVars;
370 parallelRuns = {currentIndex, totalRuns};
371 }
372
373 const match = combined.match === '' ? [] : arrify(combined.match);
374
375 const input = debug ? debug.files : (argv.pattern || []);
376 const filter = input
377 .map(pattern => splitPatternAndLineNumbers(pattern))
378 .map(({pattern, ...rest}) => ({
379 pattern: normalizePattern(path.relative(projectDir, path.resolve(process.cwd(), pattern))),
380 ...rest
381 }));
382 if (combined.updateSnapshots && filter.some(condition => condition.lineNumbers !== null)) {
383 exit('Snapshots cannot be updated when selecting specific tests by their line number.');
384 }
385
386 const api = new Api({
387 cacheEnabled: combined.cache !== false,
388 chalkOptions,
389 concurrency: combined.concurrency || 0,
390 debug,
391 environmentVariables,
392 experiments,
393 extensions,
394 failFast: combined.failFast,
395 failWithoutAssertions: combined.failWithoutAssertions !== false,
396 globs,
397 match,
398 moduleTypes,
399 nodeArguments,
400 parallelRuns,
401 projectDir,
402 providers,
403 ranFromCli: true,
404 require: arrify(combined.require),
405 serial: combined.serial,
406 snapshotDir: combined.snapshotDir ? path.resolve(projectDir, combined.snapshotDir) : null,
407 timeout: combined.timeout || '10s',
408 updateSnapshots: combined.updateSnapshots,
409 workerArgv: argv['--']
410 });
411
412 let reporter;
413 if (combined.tap && !combined.watch && debug === null) {
414 reporter = new TapReporter({
415 projectDir,
416 reportStream: process.stdout,
417 stdStream: process.stderr
418 });
419 } else {
420 reporter = new DefaultReporter({
421 projectDir,
422 reportStream: process.stdout,
423 stdStream: process.stderr,
424 watching: combined.watch,
425 verbose: debug !== null || combined.verbose || isCi || !process.stdout.isTTY
426 });
427 }
428
429 api.on('run', plan => {
430 reporter.startRun(plan);
431
432 if (process.env.AVA_EMIT_RUN_STATUS_OVER_IPC === 'I\'ll find a payphone baby / Take some time to talk to you') {
433 if (process.versions.node >= '12.16.0') {
434 plan.status.on('stateChange', evt => {
435 process.send(evt);
436 });
437 } else {
438 const v8 = require('v8');
439 plan.status.on('stateChange', evt => {
440 process.send([...v8.serialize(evt)]);
441 });
442 }
443 }
444
445 plan.status.on('stateChange', evt => {
446 if (evt.type === 'interrupt') {
447 reporter.endRun();
448 process.exit(1); // eslint-disable-line unicorn/no-process-exit
449 }
450 });
451 });
452
453 if (combined.watch) {
454 const watcher = new Watcher({
455 api,
456 filter,
457 globs,
458 projectDir,
459 providers,
460 reporter
461 });
462 watcher.observeStdin(process.stdin);
463 } else {
464 let debugWithoutSpecificFile = false;
465 api.on('run', plan => {
466 if (debug !== null && plan.files.length !== 1) {
467 debugWithoutSpecificFile = true;
468 }
469 });
470
471 const runStatus = await api.run({filter});
472
473 if (debugWithoutSpecificFile && !debug.active) {
474 exit('Provide the path to the test file you wish to debug');
475 return;
476 }
477
478 process.exitCode = runStatus.suggestExitCode({matching: match.length > 0});
479 reporter.endRun();
480 }
481};