1 | 'use strict';
|
2 | const path = require('path');
|
3 | const del = require('del');
|
4 | const updateNotifier = require('update-notifier');
|
5 | const figures = require('figures');
|
6 | const arrify = require('arrify');
|
7 | const yargs = require('yargs');
|
8 | const readPkg = require('read-pkg');
|
9 | const isCi = require('./is-ci');
|
10 | const loadConfig = require('./load-config');
|
11 |
|
12 | function exit(message) {
|
13 | console.error(`\n ${require('./chalk').get().red(figures.cross)} ${message}`);
|
14 | process.exit(1);
|
15 | }
|
16 |
|
17 | const coerceLastValue = value => {
|
18 | return Array.isArray(value) ? value.pop() : value;
|
19 | };
|
20 |
|
21 | const 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 |
|
81 | exports.run = async () => {
|
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 |
|
92 |
|
93 |
|
94 | const activeInspector = require('inspector').url() !== undefined;
|
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);
|
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.17.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);
|
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 | };
|