#!/usr/bin/env node 'use strict'; /* eslint no-unused-vars: off */ /** * Module dependencies. */ const program = require('commander'); const path = require('path'); const fs = require('fs'); const minimatch = require('minimatch'); const resolve = path.resolve; const exists = fs.existsSync; const Mocha = require('../'); const utils = Mocha.utils; const interfaceNames = Object.keys(Mocha.interfaces); const join = path.join; const cwd = process.cwd(); const getOptions = require('./options'); const mocha = new Mocha(); /** * Save timer references to avoid Sinon interfering (see GH-237). */ const Date = global.Date; const setTimeout = global.setTimeout; const setInterval = global.setInterval; const clearTimeout = global.clearTimeout; const clearInterval = global.clearInterval; /** * Exits Mocha when tests + code under test has finished execution (default) * @param {number} code - Exit code; typically # of failures */ const exitLater = code => { process.on('exit', () => { process.exit(Math.min(code, 255)); }); }; /** * Exits Mocha when Mocha itself has finished execution, regardless of * what the tests or code under test is doing. * @param {number} code - Exit code; typically # of failures */ const exit = code => { const clampedCode = Math.min(code, 255); let draining = 0; // Eagerly set the process's exit code in case stream.write doesn't // execute its callback before the process terminates. process.exitCode = clampedCode; // flush output for Node.js Windows pipe bug // https://github.com/joyent/node/issues/6247 is just one bug example // https://github.com/visionmedia/mocha/issues/333 has a good discussion const done = () => { if (!(draining--)) { process.exit(clampedCode); } }; const streams = [process.stdout, process.stderr]; streams.forEach(stream => { // submit empty write request and wait for completion draining += 1; stream.write('', done); }); done(); }; /** * Parse list. */ const list = str => str.split(/ *, */); /** * Parse multiple flag. */ const collect = (val, memo) => memo.concat(val); /** * Hide the cursor. */ const hideCursor = () => { process.stdout.write('\u001b[?25l'); }; /** * Show the cursor. */ const showCursor = () => { process.stdout.write('\u001b[?25h'); }; /** * Stop play()ing. */ const stop = () => { process.stdout.write('\u001b[2K'); clearInterval(play.timer); }; /** * Play the given array of strings. */ const play = (arr, interval) => { const len = arr.length; interval = interval || 100; let i = 0; play.timer = setInterval(() => { const str = arr[i++ % len]; process.stdout.write(`\u001b[0G${str}`); }, interval); }; /** * Files. */ let files = []; /** * Globals. */ let globals = []; /** * Requires. */ const requires = []; /** * Images. */ const images = { fail: path.join(__dirname, '..', 'images', 'error.png'), pass: path.join(__dirname, '..', 'images', 'ok.png') }; // options program .version(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version) .usage('[debug] [options] [files]') .option('-A, --async-only', 'force all tests to take a callback (async) or return a promise') .option('-c, --colors', 'force enabling of colors') .option('-C, --no-colors', 'force disabling of colors') .option('-G, --growl', 'enable growl notification support') .option('-O, --reporter-options ', 'reporter-specific options') .option('-R, --reporter ', 'specify the reporter to use', 'spec') .option('-S, --sort', 'sort test files') .option('-b, --bail', 'bail after first test failure') .option('-d, --debug', "enable node's debugger, synonym for node --debug") .option('-g, --grep ', 'only run tests matching ') .option('-f, --fgrep ', 'only run tests containing ') .option('-gc, --expose-gc', 'expose gc extension') .option('-i, --invert', 'inverts --grep and --fgrep matches') .option('-r, --require ', 'require the given module') .option('-s, --slow ', '"slow" test threshold in milliseconds [75]') .option('-t, --timeout ', 'set test-case timeout in milliseconds [2000]') .option('-u, --ui ', `specify user-interface (${interfaceNames.join('|')})`, 'bdd') .option('-w, --watch', 'watch files for changes') .option('--check-leaks', 'check for global variable leaks') .option('--full-trace', 'display the full stack trace') .option('--compilers :,...', 'use the given module(s) to compile files', list, []) .option('--debug-brk', "enable node's debugger breaking on the first line") .option('--globals ', 'allow the given comma-delimited global [names]', list, []) .option('--es_staging', 'enable all staged features') .option('--harmony<_classes,_generators,...>', 'all node --harmony* flags are available') .option('--preserve-symlinks', 'Instructs the module loader to preserve symbolic links when resolving and caching modules') .option('--icu-data-dir', 'include ICU data') .option('--inline-diffs', 'display actual/expected differences inline within each string') .option('--no-diff', 'do not show a diff on failure') .option('--inspect', 'activate devtools in chrome') .option('--inspect-brk', 'activate devtools in chrome and break on the first line') .option('--interfaces', 'display available interfaces') .option('--no-deprecation', 'silence deprecation warnings') .option('--exit', 'force shutdown of the event loop after test run: mocha will call process.exit') .option('--no-timeouts', 'disables timeouts, given implicitly with --debug') .option('--no-warnings', 'silence all node process warnings') .option('--opts ', 'specify opts path', 'test/mocha.opts') .option('--perf-basic-prof', 'enable perf linux profiler (basic support)') .option('--napi-modules', 'enable experimental NAPI modules') .option('--prof', 'log statistical profiling information') .option('--log-timer-events', 'Time events including external callbacks') .option('--recursive', 'include sub directories') .option('--reporters', 'display available reporters') .option('--retries ', 'set numbers of time to retry a failed test case') .option('--throw-deprecation', 'throw an exception anytime a deprecated function is used') .option('--trace', 'trace function calls') .option('--trace-deprecation', 'show stack traces on deprecations') .option('--trace-warnings', 'show stack traces on node process warnings') .option('--use_strict', 'enforce strict mode') .option('--watch-extensions ,...', 'specify extensions to monitor with --watch', list, []) .option('--delay', 'wait for async suite definition') .option('--allow-uncaught', 'enable uncaught errors to propagate') .option('--forbid-only', 'causes test marked with only to fail the suite') .option('--forbid-pending', 'causes pending tests and test marked with skip to fail the suite') .option('--file ', 'include a file to be ran during the suite', collect, []) .option('--exclude ', 'a file or glob pattern to ignore', collect, []); program._name = 'mocha'; // init command program .command('init ') .description('initialize a client-side mocha setup at ') .action(path => { const mkdir = require('mkdirp'); mkdir.sync(path); const css = fs.readFileSync(join(__dirname, '..', 'mocha.css')); const js = fs.readFileSync(join(__dirname, '..', 'mocha.js')); const tmpl = fs.readFileSync(join(__dirname, '..', 'lib/template.html')); fs.writeFileSync(join(path, 'mocha.css'), css); fs.writeFileSync(join(path, 'mocha.js'), js); fs.writeFileSync(join(path, 'tests.js'), ''); fs.writeFileSync(join(path, 'index.html'), tmpl); process.exit(0); }); // --globals program.on('option:globals', val => { globals = globals.concat(list(val)); }); // --reporters program.on('option:reporters', () => { console.log(); console.log(' dot - dot matrix'); console.log(' doc - html documentation'); console.log(' spec - hierarchical spec list'); console.log(' json - single json object'); console.log(' progress - progress bar'); console.log(' list - spec-style listing'); console.log(' tap - test-anything-protocol'); console.log(' landing - unicode landing strip'); console.log(' xunit - xunit reporter'); console.log(' min - minimal reporter (great with --watch)'); console.log(' json-stream - newline delimited json events'); console.log(' markdown - markdown documentation (github flavour)'); console.log(' nyan - nyan cat!'); console.log(); process.exit(); }); // --interfaces program.on('option:interfaces', () => { console.log(''); interfaceNames.forEach(interfaceName => { console.log(` ${interfaceName}`); }); console.log(''); process.exit(); }); // -r, --require module.paths.push(cwd, join(cwd, 'node_modules')); program.on('option:require', mod => { const abs = exists(mod) || exists(`${mod}.js`); if (abs) { mod = resolve(mod); } requires.push(mod); }); // If not already done, load mocha.opts if (!process.env.LOADED_MOCHA_OPTS) { getOptions(); } // parse args program.parse(process.argv); // infinite stack traces Error.stackTraceLimit = Infinity; // TODO: config // reporter options const reporterOptions = {}; if (program.reporterOptions !== undefined) { program.reporterOptions.split(',').forEach(opt => { const L = opt.split('='); if (L.length > 2 || L.length === 0) { throw new Error(`invalid reporter option '${opt}'`); } else if (L.length === 2) { reporterOptions[L[0]] = L[1]; } else { reporterOptions[L[0]] = true; } }); } // reporter mocha.reporter(program.reporter, reporterOptions); // load reporter let Reporter = null; try { Reporter = require(`../lib/reporters/${program.reporter}`); } catch (err) { try { Reporter = require(program.reporter); } catch (err2) { throw new Error(`reporter "${program.reporter}" does not exist`); } } // --no-colors if (!program.colors) { mocha.useColors(false); } // --colors if (~process.argv.indexOf('--colors') || ~process.argv.indexOf('-c')) { mocha.useColors(true); } // --inline-diffs if (program.inlineDiffs) { mocha.useInlineDiffs(true); } // --no-diff if (process.argv.indexOf('--no-diff') !== -1) { mocha.hideDiff(true); } // --slow if (program.slow) { mocha.suite.slow(program.slow); } // --no-timeouts if (!program.timeouts) { mocha.enableTimeouts(false); } // --timeout if (program.timeout) { mocha.suite.timeout(program.timeout); } // --bail mocha.suite.bail(program.bail); // --grep if (program.grep) { mocha.grep(program.grep); } // --fgrep if (program.fgrep) { mocha.fgrep(program.fgrep); } // --invert if (program.invert) { mocha.invert(); } // --check-leaks if (program.checkLeaks) { mocha.checkLeaks(); } // --stack-trace if (program.fullTrace) { mocha.fullTrace(); } // --growl if (program.growl) { mocha.growl(); } // --async-only if (program.asyncOnly) { mocha.asyncOnly(); } // --delay if (program.delay) { mocha.delay(); } // --allow-uncaught if (program.allowUncaught) { mocha.allowUncaught(); } // --globals mocha.globals(globals); // --retries if (program.retries) { mocha.suite.retries(program.retries); } // --forbid-only if (program.forbidOnly) mocha.forbidOnly(); // --forbid-pending if (program.forbidPending) mocha.forbidPending(); // custom compiler support if (program.compilers.length > 0) { require('util').deprecate(() => {}, '"--compilers" will be removed in a future version of Mocha; see https://git.io/vdcSr for more info')(); } const extensions = ['js']; program.compilers.forEach(c => { const idx = c.indexOf(':'); const ext = c.slice(0, idx); let mod = c.slice(idx + 1); if (mod[0] === '.') { mod = join(process.cwd(), mod); } require(mod); extensions.push(ext); program.watchExtensions.push(ext); }); // requires requires.forEach(mod => { require(mod); }); // interface mocha.ui(program.ui); // args const args = program.args; // default files to test/*.{js,coffee} if (!args.length) { args.push('test'); } args.forEach(arg => { let newFiles; try { newFiles = utils.lookupFiles(arg, extensions, program.recursive); } catch (err) { if (err.message.indexOf('cannot resolve path') === 0) { console.error(`Warning: Could not find any test files matching pattern: ${arg}`); return; } throw err; } if (typeof newFiles !== 'undefined') { if (typeof newFiles === 'string') { newFiles = [newFiles]; } newFiles = newFiles.filter(fileName => program.exclude.every(pattern => !minimatch(fileName, pattern))); } files = files.concat(newFiles); }); if (!files.length) { console.error('No test files found'); process.exit(1); } // resolve let fileArgs = program.file.map(path => resolve(path)); files = files.map(path => resolve(path)); if (program.sort) { files.sort(); } // add files given through --file to be ran first files = fileArgs.concat(files); // --watch let runner; let loadAndRun; let purge; let rerun; if (program.watch) { console.log(); hideCursor(); process.on('SIGINT', () => { showCursor(); console.log('\n'); process.exit(130); }); const watchFiles = utils.files(cwd, program.watchExtensions); let runAgain = false; loadAndRun = () => { try { mocha.files = files; runAgain = false; runner = mocha.run(() => { runner = null; if (runAgain) { rerun(); } }); } catch (e) { console.log(e.stack); } }; purge = () => { watchFiles.forEach(file => { delete require.cache[file]; }); }; loadAndRun(); rerun = () => { purge(); stop(); if (!program.grep) { mocha.grep(null); } mocha.suite = mocha.suite.clone(); mocha.suite.ctx = new Mocha.Context(); mocha.ui(program.ui); loadAndRun(); }; utils.watch(watchFiles, () => { runAgain = true; if (runner) { runner.abort(); } else { rerun(); } }); } else { // load mocha.files = files; runner = mocha.run(program.exit ? exit : exitLater); } process.on('SIGINT', () => { runner.abort(); // This is a hack: // Instead of `process.exit(130)`, set runner.failures to 130 (exit code for SIGINT) // The amount of failures will be emitted as error code later runner.failures = 130; });