UNPKG

ava

Version:

Testing can be a drag. AVA helps you get it done.

450 lines (406 loc) 12 kB
'use strict'; const path = require('path'); const del = require('del'); const updateNotifier = require('update-notifier'); const figures = require('figures'); const arrify = require('arrify'); const yargs = require('yargs'); const readPkg = require('read-pkg'); const isCi = require('./is-ci'); const loadConfig = require('./load-config'); function exit(message) { console.error(`\n ${require('./chalk').get().red(figures.cross)} ${message}`); process.exit(1); // eslint-disable-line unicorn/no-process-exit } const coerceLastValue = value => { return Array.isArray(value) ? value.pop() : value; }; const FLAGS = { concurrency: { alias: 'c', coerce: coerceLastValue, description: 'Max number of test files running at the same time (default: CPU cores)', type: 'number' }, 'fail-fast': { coerce: coerceLastValue, description: 'Stop after first test failure', type: 'boolean' }, match: { alias: 'm', description: 'Only run tests with matching title (can be repeated)', type: 'string' }, 'node-arguments': { coerce: coerceLastValue, description: 'Additional Node.js arguments for launching worker processes (specify as a single string)', type: 'string' }, serial: { alias: 's', coerce: coerceLastValue, description: 'Run tests serially', type: 'boolean' }, tap: { alias: 't', coerce: coerceLastValue, description: 'Generate TAP output', type: 'boolean' }, timeout: { alias: 'T', coerce: coerceLastValue, description: 'Set global timeout (milliseconds or human-readable, e.g. 10s, 2m)', type: 'string' }, 'update-snapshots': { alias: 'u', coerce: coerceLastValue, description: 'Update snapshots', type: 'boolean' }, verbose: { alias: 'v', coerce: coerceLastValue, description: 'Enable verbose output', type: 'boolean' }, watch: { alias: 'w', coerce: coerceLastValue, description: 'Re-run tests when files change', type: 'boolean' } }; exports.run = async () => { // eslint-disable-line complexity let conf = {}; let confError = null; try { const {argv: {config: configFile}} = yargs.help(false); conf = loadConfig({configFile}); } catch (error) { confError = error; } let debug = null; let resetCache = false; const {argv} = yargs .parserConfiguration({ 'boolean-negation': true, 'camel-case-expansion': false, 'combine-arrays': false, 'dot-notation': false, 'duplicate-arguments-array': true, 'flatten-duplicate-arrays': true, 'negation-prefix': 'no-', 'parse-numbers': true, 'populate--': true, 'set-placeholder-key': false, 'short-option-groups': true, 'strip-aliased': true, 'unknown-options-as-args': false }) .usage('$0 [<pattern>...]') .usage('$0 debug [<pattern>...]') .usage('$0 reset-cache') .options({ color: { description: 'Force color output', type: 'boolean' }, config: { description: 'Specific JavaScript file for AVA to read its config from, instead of using package.json or ava.config.* files' } }) .command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', { array: true, 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', type: 'string' })) .command( 'debug [<pattern>...]', 'Activate Node.js inspector and run a single test file', yargs => yargs.options(FLAGS).options({ break: { description: 'Break before the test file is loaded', type: 'boolean' }, host: { default: '127.0.0.1', description: 'Address or hostname through which you can connect to the inspector', type: 'string' }, port: { default: 9229, description: 'Port on which you can connect to the inspector', type: 'number' } }).positional('pattern', { demand: true, describe: 'Glob patterns to select a single test file to debug. Add a colon and specify line numbers of specific tests to run', type: 'string' }), argv => { debug = { break: argv.break === true, files: argv.pattern, host: argv.host, port: argv.port }; }) .command( 'reset-cache', 'Reset AVA’s compilation cache and exit', yargs => yargs, () => { resetCache = true; }) .example('$0') .example('$0 test.js') .example('$0 test.js:4,7-9') .help(); const combined = {...conf}; for (const flag of Object.keys(FLAGS)) { if (Reflect.has(argv, flag)) { if (flag === 'fail-fast') { combined.failFast = argv[flag]; } else if (flag === 'update-snapshots') { combined.updateSnapshots = argv[flag]; } else if (flag !== 'node-arguments') { combined[flag] = argv[flag]; } } } const chalkOptions = {level: combined.color === false ? 0 : require('chalk').level}; const chalk = require('./chalk').set(chalkOptions); if (confError) { if (confError.parent) { exit(`${confError.message}\n\n${chalk.gray((confError.parent && confError.parent.stack) || confError.parent)}`); } else { exit(confError.message); } } updateNotifier({pkg: require('../package.json')}).notify(); const {nonSemVerExperiments: experiments, projectDir} = conf; if (resetCache) { const cacheDir = path.join(projectDir, 'node_modules', '.cache', 'ava'); try { await del('*', { cwd: cacheDir, nodir: true }); console.error(`\n${chalk.green(figures.tick)} Removed AVA cache files in ${cacheDir}`); process.exit(0); // eslint-disable-line unicorn/no-process-exit } catch (error) { exit(`Error removing AVA cache files in ${cacheDir}\n\n${chalk.gray((error && error.stack) || error)}`); } return; } if (argv.watch) { if (argv.tap && !conf.tap) { exit('The TAP reporter is not available when using watch mode.'); } if (isCi) { exit('Watch mode is not available in CI, as it prevents AVA from terminating.'); } if (debug !== null) { exit('Watch mode is not available when debugging.'); } } if (debug !== null) { if (argv.tap && !conf.tap) { exit('The TAP reporter is not available when debugging.'); } if (isCi) { exit('Debugging is not available in CI.'); } if (combined.timeout) { console.log(chalk.magenta(` ${figures.warning} The timeout option has been disabled to help with debugging.`)); } } if (Reflect.has(combined, 'concurrency') && (!Number.isInteger(combined.concurrency) || combined.concurrency < 0)) { exit('The --concurrency or -c flag must be provided with a nonnegative integer.'); } if (!combined.tap && Object.keys(experiments).length > 0) { console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`)); } if (Reflect.has(conf, 'compileEnhancements')) { exit('Enhancement compilation must be configured in AVA’s Babel options.'); } if (Reflect.has(conf, 'helpers')) { exit('AVA no longer compiles helpers. Add exclusion patterns to the ’files’ configuration and specify ’compileAsTests’ in the Babel options instead.'); } if (Reflect.has(conf, 'sources')) { exit('’sources’ has been removed. Use ’ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.'); } const ciParallelVars = require('ci-parallel-vars'); const Api = require('./api'); const VerboseReporter = require('./reporters/verbose'); const MiniReporter = require('./reporters/mini'); const TapReporter = require('./reporters/tap'); const Watcher = require('./watcher'); const normalizeExtensions = require('./extensions'); const {normalizeGlobs, normalizePattern} = require('./globs'); const normalizeNodeArguments = require('./node-arguments'); const validateEnvironmentVariables = require('./environment-variables'); const {splitPatternAndLineNumbers} = require('./line-numbers'); const providerManager = require('./provider-manager'); let pkg; try { pkg = readPkg.sync({cwd: projectDir}); } catch (error) { if (error.code !== 'ENOENT') { throw error; } } const {type: defaultModuleType = 'commonjs'} = pkg || {}; const moduleTypes = { cjs: 'commonjs', mjs: 'module', js: defaultModuleType }; const providers = []; if (Reflect.has(conf, 'babel')) { try { const {level, main} = providerManager.babel(projectDir); providers.push({ level, main: main({config: conf.babel}), type: 'babel' }); } catch (error) { exit(error.message); } } if (Reflect.has(conf, 'typescript')) { try { const {level, main} = providerManager.typescript(projectDir); providers.push({ level, main: main({config: conf.typescript}), type: 'typescript' }); } catch (error) { exit(error.message); } } let environmentVariables; try { environmentVariables = validateEnvironmentVariables(conf.environmentVariables); } catch (error) { exit(error.message); } let extensions; try { extensions = normalizeExtensions(conf.extensions, providers); } catch (error) { exit(error.message); } let globs; try { globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers}); } catch (error) { exit(error.message); } let nodeArguments; try { nodeArguments = normalizeNodeArguments(conf.nodeArguments, argv['node-arguments']); } catch (error) { exit(error.message); } let parallelRuns = null; if (isCi && ciParallelVars) { const {index: currentIndex, total: totalRuns} = ciParallelVars; parallelRuns = {currentIndex, totalRuns}; } const match = combined.match === '' ? [] : arrify(combined.match); const input = debug ? debug.files : (argv.pattern || []); const filter = input .map(pattern => splitPatternAndLineNumbers(pattern)) .map(({pattern, ...rest}) => ({ pattern: normalizePattern(path.relative(projectDir, path.resolve(process.cwd(), pattern))), ...rest })); const api = new Api({ cacheEnabled: combined.cache !== false, chalkOptions, concurrency: combined.concurrency || 0, debug, environmentVariables, experiments, extensions, failFast: combined.failFast, failWithoutAssertions: combined.failWithoutAssertions !== false, globs, match, moduleTypes, nodeArguments, parallelRuns, projectDir, providers, ranFromCli: true, require: arrify(combined.require), serial: combined.serial, snapshotDir: combined.snapshotDir ? path.resolve(projectDir, combined.snapshotDir) : null, timeout: combined.timeout || '10s', updateSnapshots: combined.updateSnapshots, workerArgv: argv['--'] }); let reporter; if (combined.tap && !combined.watch && debug === null) { reporter = new TapReporter({ projectDir, reportStream: process.stdout, stdStream: process.stderr }); } else if (debug !== null || combined.verbose || isCi || !process.stdout.isTTY) { reporter = new VerboseReporter({ projectDir, reportStream: process.stdout, stdStream: process.stderr, watching: combined.watch }); } else { reporter = new MiniReporter({ projectDir, reportStream: process.stdout, stdStream: process.stderr, watching: combined.watch }); } api.on('run', plan => { reporter.startRun(plan); plan.status.on('stateChange', evt => { if (evt.type === 'interrupt') { reporter.endRun(); process.exit(1); // eslint-disable-line unicorn/no-process-exit } }); }); if (combined.watch) { const watcher = new Watcher({ api, filter, globs, projectDir, providers, reporter }); watcher.observeStdin(process.stdin); } else { let debugWithoutSpecificFile = false; api.on('run', plan => { if (plan.debug && plan.files.length !== 1) { debugWithoutSpecificFile = true; } }); const runStatus = await api.run({filter}); if (debugWithoutSpecificFile) { exit('Provide the path to the test file you wish to debug'); return; } process.exitCode = runStatus.suggestExitCode({matching: match.length > 0}); reporter.endRun(); } };