UNPKG

12 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 let debug = null;
92 let resetCache = false;
93 const {argv} = yargs
94 .parserConfiguration({
95 'boolean-negation': true,
96 'camel-case-expansion': false,
97 'combine-arrays': false,
98 'dot-notation': false,
99 'duplicate-arguments-array': true,
100 'flatten-duplicate-arrays': true,
101 'negation-prefix': 'no-',
102 'parse-numbers': true,
103 'populate--': true,
104 'set-placeholder-key': false,
105 'short-option-groups': true,
106 'strip-aliased': true,
107 'unknown-options-as-args': false
108 })
109 .usage('$0 [<pattern>...]')
110 .usage('$0 debug [<pattern>...]')
111 .usage('$0 reset-cache')
112 .options({
113 color: {
114 description: 'Force color output',
115 type: 'boolean'
116 },
117 config: {
118 description: 'Specific JavaScript file for AVA to read its config from, instead of using package.json or ava.config.* files'
119 }
120 })
121 .command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
122 array: true,
123 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',
124 type: 'string'
125 }))
126 .command(
127 'debug [<pattern>...]',
128 'Activate Node.js inspector and run a single test file',
129 yargs => yargs.options(FLAGS).options({
130 break: {
131 description: 'Break before the test file is loaded',
132 type: 'boolean'
133 },
134 host: {
135 default: '127.0.0.1',
136 description: 'Address or hostname through which you can connect to the inspector',
137 type: 'string'
138 },
139 port: {
140 default: 9229,
141 description: 'Port on which you can connect to the inspector',
142 type: 'number'
143 }
144 }).positional('pattern', {
145 demand: true,
146 describe: 'Glob patterns to select a single test file to debug. Add a colon and specify line numbers of specific tests to run',
147 type: 'string'
148 }),
149 argv => {
150 debug = {
151 break: argv.break === true,
152 files: argv.pattern,
153 host: argv.host,
154 port: argv.port
155 };
156 })
157 .command(
158 'reset-cache',
159 'Reset AVA’s compilation cache and exit',
160 yargs => yargs,
161 () => {
162 resetCache = true;
163 })
164 .example('$0')
165 .example('$0 test.js')
166 .example('$0 test.js:4,7-9')
167 .help();
168
169 const combined = {...conf};
170 for (const flag of Object.keys(FLAGS)) {
171 if (Reflect.has(argv, flag)) {
172 if (flag === 'fail-fast') {
173 combined.failFast = argv[flag];
174 } else if (flag === 'update-snapshots') {
175 combined.updateSnapshots = argv[flag];
176 } else if (flag !== 'node-arguments') {
177 combined[flag] = argv[flag];
178 }
179 }
180 }
181
182 const chalkOptions = {level: combined.color === false ? 0 : require('chalk').level};
183 const chalk = require('./chalk').set(chalkOptions);
184
185 if (confError) {
186 if (confError.parent) {
187 exit(`${confError.message}\n\n${chalk.gray((confError.parent && confError.parent.stack) || confError.parent)}`);
188 } else {
189 exit(confError.message);
190 }
191 }
192
193 updateNotifier({pkg: require('../package.json')}).notify();
194
195 const {nonSemVerExperiments: experiments, projectDir} = conf;
196 if (resetCache) {
197 const cacheDir = path.join(projectDir, 'node_modules', '.cache', 'ava');
198 try {
199 await del('*', {
200 cwd: cacheDir,
201 nodir: true
202 });
203 console.error(`\n${chalk.green(figures.tick)} Removed AVA cache files in ${cacheDir}`);
204 process.exit(0); // eslint-disable-line unicorn/no-process-exit
205 } catch (error) {
206 exit(`Error removing AVA cache files in ${cacheDir}\n\n${chalk.gray((error && error.stack) || error)}`);
207 }
208
209 return;
210 }
211
212 if (argv.watch) {
213 if (argv.tap && !conf.tap) {
214 exit('The TAP reporter is not available when using watch mode.');
215 }
216
217 if (isCi) {
218 exit('Watch mode is not available in CI, as it prevents AVA from terminating.');
219 }
220
221 if (debug !== null) {
222 exit('Watch mode is not available when debugging.');
223 }
224 }
225
226 if (debug !== null) {
227 if (argv.tap && !conf.tap) {
228 exit('The TAP reporter is not available when debugging.');
229 }
230
231 if (isCi) {
232 exit('Debugging is not available in CI.');
233 }
234
235 if (combined.timeout) {
236 console.log(chalk.magenta(` ${figures.warning} The timeout option has been disabled to help with debugging.`));
237 }
238 }
239
240 if (Reflect.has(combined, 'concurrency') && (!Number.isInteger(combined.concurrency) || combined.concurrency < 0)) {
241 exit('The --concurrency or -c flag must be provided with a nonnegative integer.');
242 }
243
244 if (!combined.tap && Object.keys(experiments).length > 0) {
245 console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`));
246 }
247
248 if (Reflect.has(conf, 'compileEnhancements')) {
249 exit('Enhancement compilation must be configured in AVA’s Babel options.');
250 }
251
252 if (Reflect.has(conf, 'helpers')) {
253 exit('AVA no longer compiles helpers. Add exclusion patterns to the ’files’ configuration and specify ’compileAsTests’ in the Babel options instead.');
254 }
255
256 if (Reflect.has(conf, 'sources')) {
257 exit('’sources’ has been removed. Use ’ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.');
258 }
259
260 const ciParallelVars = require('ci-parallel-vars');
261 const Api = require('./api');
262 const VerboseReporter = require('./reporters/verbose');
263 const MiniReporter = require('./reporters/mini');
264 const TapReporter = require('./reporters/tap');
265 const Watcher = require('./watcher');
266 const normalizeExtensions = require('./extensions');
267 const {normalizeGlobs, normalizePattern} = require('./globs');
268 const normalizeNodeArguments = require('./node-arguments');
269 const validateEnvironmentVariables = require('./environment-variables');
270 const {splitPatternAndLineNumbers} = require('./line-numbers');
271 const providerManager = require('./provider-manager');
272
273 let pkg;
274 try {
275 pkg = readPkg.sync({cwd: projectDir});
276 } catch (error) {
277 if (error.code !== 'ENOENT') {
278 throw error;
279 }
280 }
281
282 const {type: defaultModuleType = 'commonjs'} = pkg || {};
283
284 const moduleTypes = {
285 cjs: 'commonjs',
286 mjs: 'module',
287 js: defaultModuleType
288 };
289
290 const providers = [];
291 if (Reflect.has(conf, 'babel')) {
292 try {
293 const {level, main} = providerManager.babel(projectDir);
294 providers.push({
295 level,
296 main: main({config: conf.babel}),
297 type: 'babel'
298 });
299 } catch (error) {
300 exit(error.message);
301 }
302 }
303
304 if (Reflect.has(conf, 'typescript')) {
305 try {
306 const {level, main} = providerManager.typescript(projectDir);
307 providers.push({
308 level,
309 main: main({config: conf.typescript}),
310 type: 'typescript'
311 });
312 } catch (error) {
313 exit(error.message);
314 }
315 }
316
317 let environmentVariables;
318 try {
319 environmentVariables = validateEnvironmentVariables(conf.environmentVariables);
320 } catch (error) {
321 exit(error.message);
322 }
323
324 let extensions;
325 try {
326 extensions = normalizeExtensions(conf.extensions, providers);
327 } catch (error) {
328 exit(error.message);
329 }
330
331 let globs;
332 try {
333 globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers});
334 } catch (error) {
335 exit(error.message);
336 }
337
338 let nodeArguments;
339 try {
340 nodeArguments = normalizeNodeArguments(conf.nodeArguments, argv['node-arguments']);
341 } catch (error) {
342 exit(error.message);
343 }
344
345 let parallelRuns = null;
346 if (isCi && ciParallelVars) {
347 const {index: currentIndex, total: totalRuns} = ciParallelVars;
348 parallelRuns = {currentIndex, totalRuns};
349 }
350
351 const match = combined.match === '' ? [] : arrify(combined.match);
352
353 const input = debug ? debug.files : (argv.pattern || []);
354 const filter = input
355 .map(pattern => splitPatternAndLineNumbers(pattern))
356 .map(({pattern, ...rest}) => ({
357 pattern: normalizePattern(path.relative(projectDir, path.resolve(process.cwd(), pattern))),
358 ...rest
359 }));
360
361 const api = new Api({
362 cacheEnabled: combined.cache !== false,
363 chalkOptions,
364 concurrency: combined.concurrency || 0,
365 debug,
366 environmentVariables,
367 experiments,
368 extensions,
369 failFast: combined.failFast,
370 failWithoutAssertions: combined.failWithoutAssertions !== false,
371 globs,
372 match,
373 moduleTypes,
374 nodeArguments,
375 parallelRuns,
376 projectDir,
377 providers,
378 ranFromCli: true,
379 require: arrify(combined.require),
380 serial: combined.serial,
381 snapshotDir: combined.snapshotDir ? path.resolve(projectDir, combined.snapshotDir) : null,
382 timeout: combined.timeout || '10s',
383 updateSnapshots: combined.updateSnapshots,
384 workerArgv: argv['--']
385 });
386
387 let reporter;
388 if (combined.tap && !combined.watch && debug === null) {
389 reporter = new TapReporter({
390 projectDir,
391 reportStream: process.stdout,
392 stdStream: process.stderr
393 });
394 } else if (debug !== null || combined.verbose || isCi || !process.stdout.isTTY) {
395 reporter = new VerboseReporter({
396 projectDir,
397 reportStream: process.stdout,
398 stdStream: process.stderr,
399 watching: combined.watch
400 });
401 } else {
402 reporter = new MiniReporter({
403 projectDir,
404 reportStream: process.stdout,
405 stdStream: process.stderr,
406 watching: combined.watch
407 });
408 }
409
410 api.on('run', plan => {
411 reporter.startRun(plan);
412
413 plan.status.on('stateChange', evt => {
414 if (evt.type === 'interrupt') {
415 reporter.endRun();
416 process.exit(1); // eslint-disable-line unicorn/no-process-exit
417 }
418 });
419 });
420
421 if (combined.watch) {
422 const watcher = new Watcher({
423 api,
424 filter,
425 globs,
426 projectDir,
427 providers,
428 reporter
429 });
430 watcher.observeStdin(process.stdin);
431 } else {
432 let debugWithoutSpecificFile = false;
433 api.on('run', plan => {
434 if (plan.debug && plan.files.length !== 1) {
435 debugWithoutSpecificFile = true;
436 }
437 });
438
439 const runStatus = await api.run({filter});
440
441 if (debugWithoutSpecificFile) {
442 exit('Provide the path to the test file you wish to debug');
443 return;
444 }
445
446 process.exitCode = runStatus.suggestExitCode({matching: match.length > 0});
447 reporter.endRun();
448 }
449};