Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 648 lines (524 sloc) 14.3 KB
#!/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, '..', 'assets', 'growl', 'error.png'),
pass: path.join(__dirname, '..', 'assets', 'growl', '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 <k=v,k2=v2,...>', 'reporter-specific options')
.option('-R, --reporter <name>', '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 <pattern>', 'only run tests matching <pattern>')
.option('-f, --fgrep <string>', 'only run tests containing <string>')
.option('-gc, --expose-gc', 'expose gc extension')
.option('-i, --invert', 'inverts --grep and --fgrep matches')
.option('-r, --require <name>', 'require the given module')
.option('-s, --slow <ms>', '"slow" test threshold in milliseconds [75]')
.option('-t, --timeout <ms>', 'set test-case timeout in milliseconds [2000]')
.option(
'-u, --ui <name>',
`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 <ext>:<module>,...',
'use the given module(s) to compile files',
list,
[]
)
.option('--debug-brk', "enable node's debugger breaking on the first line")
.option(
'--globals <names>',
'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 <path>', '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 <times>',
'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 <ext>,...',
'specify extensions to monitor with --watch',
list,
['js']
)
.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 <file>',
'include a file to be ran during the suite',
collect,
[]
)
.option('--exclude <file>', 'a file or glob pattern to ignore', collect, []);
program._name = 'mocha';
// init command
program
.command('init <path>')
.description('initialize a client-side mocha setup at <path>')
.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);
// --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 <ms>
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;
});