Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 621 lines (544 sloc) 19.605 kB
#!/usr/bin/env node
var path = require('path'),
fs = require('fs'),
util = require('util'),
wildcard = require('../lib/utils/wildcard').wildcard,
events = require('events');
//
// Attempt to load Coffee-Script. If it's not available, continue on our
// merry way, if it is available, start searching for `*.coffee` scripts too.
//
var fileExt, specFileExt;
try {
var coffee = require('coffee-script');
fileExt = /\.(js|coffee)$/;
specFileExt = /[.(-|_)]((t|T)est|(s|S)pec)\.(js|coffee)$/;
} catch (_) {
fileExt = /\.js$/;
specFileExt = /[.(-|_)]((t|T)est|(s|S)pec)\.js$/;
}
var inspect = require('eyes').inspector({
stream: null,
styles: { string: 'grey', regexp: 'grey' }
});
var vows = require('../lib/vows');
var cutils = require('../lib/vows/console');
var stylize = require('../lib/vows/console').stylize;
var _reporter = require('../lib/vows/reporters/dot-matrix'), reporter = {
name: _reporter.name
};
var _coverage;
var nodeMinorVersion = parseInt(process.version.split('.')[1], 10);
var help = [
"usage: vows [FILE, ...] [options]",
"",
"options:",
" -v, --verbose Enable verbose output",
" -w, --watch Watch mode",
" -s, --silent Don't report",
" -i, --isolate Run each test in it's own vows process",
" -m PATTERN Only run tests matching the PATTERN string",
" -r PATTERN Only run tests matching the PATTERN regexp",
" --json Use JSON reporter",
" --spec Use Spec reporter",
" --tap Use TAP reporter",
" --dot-matrix Use Dot-Matrix reporter",
" --xunit Use xUnit reporter",
" --cover-plain Print plain coverage map if detected",
" --cover-html Write coverage map to \"coverage.html\"",
" --cover-json Write unified coverage map to \"coverage.json\"",
" --cover-xml Write coverage map to \"coverage.xml\" in Emma xml",
" --no-color Don't use terminal colors",
" --version Show version",
" -h, --help You're staring at it"
].join('\n');
var options = {
reporter: reporter,
matcher: /.*/,
watch: false,
coverage: false,
isolate: false,
shuffle: false,
nocolor: !process.stdout.isTTY
};
var files = [];
var wildcardFiles = [];
// Get rid of process runner
// ('node' in most cases)
var arg, args = [], argv = process.argv.slice(2);
// Current directory index,
// and path of test folder.
var root, testFolder;
//
// Parse command-line parameters
//
while (arg = argv.shift()) {
if (arg === __filename) { continue }
if (arg[0] !== '-') {
args.push(arg);
} else {
arg = arg.match(/^--?(.+)/)[1];
if (arg[0] === 'r') {
options.matcher = new(RegExp)(argv.shift());
} else if (arg[0] === 'm') {
options.matcher = (function (str) { // Create an escaped RegExp
var specials = '. * + ? | ( ) [ ] { } \\ ^ ? ! = : $'.split(' ').join('|\\'),
regex = new(RegExp)('(\\' + specials + ')', 'g');
return new(RegExp)(str.replace(regex, '\\$1'));
})(argv.shift());
} else if (arg in options) {
options[arg] = true;
} else {
switch (arg) {
case 'json':
_reporter = require('../lib/vows/reporters/json');
break;
case 'spec':
_reporter = require('../lib/vows/reporters/spec');
break;
case 'tap':
_reporter = require('../lib/vows/reporters/tap');
break;
case 'dot-matrix':
_reporter = require('../lib/vows/reporters/dot-matrix');
break;
case 'silent':
case 's':
_reporter = require('../lib/vows/reporters/silent');
break;
case 'xunit':
_reporter = require('../lib/vows/reporters/xunit');
break;
case 'cover-plain':
options.coverage = true;
_coverage = require('../lib/vows/coverage/report-plain');
break;
case 'cover-html':
options.coverage = true;
_coverage = require('../lib/vows/coverage/report-html');
break;
case 'cover-json':
options.coverage = true;
_coverage = require('../lib/vows/coverage/report-json');
break;
case 'cover-xml':
options.coverage = true;
_coverage = require('../lib/vows/coverage/report-xml');
break;
case 'verbose':
case 'v':
options.verbose = true;
break;
case 'watch':
case 'w':
options.watch = true;
break;
case 'supress-stdout':
options.supressStdout = true;
break;
case 'shuffle':
options.shuffle = true;
break;
case 'isolate':
case 'i':
options.isolate = true;
break;
case 'no-color':
options.nocolor = true;
break;
case 'color':
options.nocolor = false;
break;
case 'no-error':
options.error = false;
break;
case 'version':
console.log('vows ' + vows.version);
process.exit(0);
case 'help':
case 'h':
console.log(help);
process.exit(0);
break;
}
}
}
}
if (options.nocolor) {
cutils.nocolor = true;
inspect = require('eyes').inspector({ stream: null, styles: false });
}
if (options.supressStdout) {
_reporter.setStream && _reporter.setStream(process.stdout);
var devNullStream = null;
if(process.platform === 'win32'){
devNullStream = fs.createWriteStream('./NULL');
} else {
devNullStream = fs.createWriteStream('/dev/null');
}
process.__defineGetter__('stdout', function () {
return devNullStream;
});
}
if (options.watch) {
options.reporter = reporter = require('../lib/vows/reporters/watch');
}
msg('bin', 'argv', args);
msg('bin', 'options', { reporter: options.reporter.name, matcher: options.matcher });
if (args.length === 0 || options.watch) {
msg('bin', 'discovering', 'folder structure');
root = fs.readdirSync('.');
if (root.indexOf('test') !== -1) {
testFolder = 'test';
} else if (root.indexOf('spec') !== -1) {
testFolder = 'spec';
} else {
abort("runner", "couldn't find test folder");
}
msg('bin', 'discovered', "./" + testFolder);
if (args.length === 0) {
args = paths(testFolder).filter(function (f) {
return specFileExt.test(f);
});
if (options.watch) {
args = args.concat(paths('lib'), paths('src'));
}
}
}
if (! options.watch) {
reporter.report = function (data, filename) {
switch (data[0]) {
case 'subject':
case 'vow':
case 'context':
case 'error':
_reporter.report(data, filename);
break;
case 'end':
(options.verbose || _reporter.name === 'json') &&
_reporter.report(data);
break;
case 'finish':
options.verbose ?
_reporter.print('\n')
:
_reporter.print(' ');
break;
}
};
reporter.reset = function () { _reporter.reset && _reporter.reset() };
reporter.print = _reporter.print;
// preprocess the list of files for any wildcards. win32 does not handle wildcards before calling vows
// any paths not containing wildcards are simple returned by wildcard()
args.forEach(function(a) {
wildcardFiles = wildcardFiles.concat(wildcard(path.join(process.cwd(), a)));
});
// now set up the file list for vows including all the wildcard files
files = wildcardFiles.map(function (a) {
return (!a.match(/^[\/|c|C]/))
? path.join(process.cwd(), a.replace(fileExt, ''))
: a.replace(fileExt, '');
});
if (options.shuffle) {
var source = files.slice(0);
files.length = 0;
while (source.length) {
files.push(source.splice(Math.floor(Math.random() * source.length), 1)[0]);
}
}
runSuites(importSuites(files), function (results) {
var status = results.errored ? 2 : (results.broken ? 1 : 0);
!options.verbose && _reporter.print('\n');
msg('runner', 'finish');
_reporter.report(['finish', results], {
write: function (str) {
util.print(str.replace(/^\n\n/, '\n'));
}
});
try {
if (options.coverage === true && _$jscoverage !== undefined) {
_coverage.report(_$jscoverage);
}
} catch (err) {
// ignore the undefined jscoverage
}
if (process.stdout.write('')) { // Check if stdout is drained
process.exit(status);
} else {
process.stdout.on('drain', function () {
process.exit(status);
});
}
});
} else {
//
// Watch mode
//
(function () {
var pendulum = [
'. ', '.. ', '... ', ' ...',
' ..', ' .', ' .', ' ..',
'... ', '.. ', '. '
];
var strobe = ['.', ' '];
var status,
cue,
current = 0,
running = 0,
lastRun,
colors = ['32m', '33m', '31m'],
timer = setInterval(tick, 100);
process.on('uncaughtException', exception);
process.on('exit', cleanup);
process.on('SIGINT', function () {
process.exit(0);
});
process.on('SIGQUIT', function () {
changed();
});
cursorHide();
// Run every 100ms
function tick() {
if (running && (cue !== strobe)) {
cue = strobe, current = 0;
} else if (!running && (cue !== pendulum)) {
cue = pendulum, current = 0;
}
eraseLine();
lastRun && !running && esc(colors[status.errored ? 2 : (status.broken ? 1 : 0)]);
print(cue[current]);
if (current == cue.length - 1) { current = -1 }
current ++;
esc('39m');
cursorRestore();
}
//
// Utility functions
//
function print(str) { util.print(str) }
function esc(str) { print("\x1b[" + str) }
function eraseLine() { esc("0K") }
function cursorRestore() { esc("0G") }
function cursorHide() { esc("?25l") }
function cursorShow() { esc("?25h") }
function cleanup() { eraseLine(), cursorShow(), clearInterval(timer), print('\n') }
function exception(err) { print(err.stack || err.message || JSON.stringify(err)), running = 0}
//
// Get a matching test for a given file
//
function getMatchingTest(file, join) {
join || (join = '-');
var testFile;
if (specFileExt.test(file)) {
testFile = path.join(testFolder, file);
}
else {
var root, extension;
_s = file.split('.'), root = _s[0], extension = _s[1];
testFile = path.join(testFolder, root + join + testFolder + "." + extension);
}
try {
fs.statSync(testFile);
} catch (e) {
if (join == '-') {
return getMatchingTest(file, '_');
}
else {
msg('watcher', 'no equivalence found, running all tests.');
testFile = null;
}
}
return testFile;
}
//
// Called when a file has been modified.
// Run the matching tests and change the status.
//
function changed(file) {
status = { honored: 0, broken: 0, errored: 0, pending: 0 };
msg('watcher', 'detected change in', file);
file = getMatchingTest(file);
var files = (specFileExt.test(file) ? [file] : paths(testFolder)).map(function (p) {
return path.join(process.cwd(), p);
}).filter(function (p) {
return specFileExt.test(p);
}).map(function (p) {
var cache = require.main.moduleCache || require.cache;
if (cache[p]) { delete(cache[p]) }
return p;
}).map(function (p) {
return p.replace(fileExt, '');
});
running ++;
runSuites(importSuites(files), function (results) {
delete(results.time);
print(cutils.result(results).join('') + '\n\n');
lastRun = new(Date);
status = results;
running --;
});
}
msg('watcher', 'watching', args);
//
// Watch all relevant files,
// and call `changed()` on change.
//
args.forEach(function (p) {
fs.watchFile(p, function (current, previous) {
if (new(Date)(current.mtime).valueOf() ===
new(Date)(previous.mtime).valueOf()) { return }
else {
changed(p);
}
});
});
})();
}
function runSuites(suites, callback) {
var results = {
honored: 0,
broken: 0,
errored: 0,
pending: 0,
total: 0,
time: 0
};
reporter.reset();
(function run(suites, callback) {
var suite = suites.shift();
if (suite) {
msg('runner', "running", suite.subject + ' ', options.watch ? false : true);
suite.run(options, function (result) {
Object.keys(result).forEach(function (k) {
results[k] += result[k];
});
run(suites, callback);
});
} else {
callback(results);
}
})(suites, callback);
}
function importSuites(files) {
msg(options.watcher ? 'watcher' : 'runner', 'loading', files);
var spawn = require('child_process').spawn;
function cwdname(f) {
return f.replace(process.cwd() + '/', '') + '.js';
}
function wrapSpawn(f) {
f = cwdname(f);
return function (options, callback) {
var args = [process.argv[1], '--json', f],
result;
// --supress-stdout stops the stream too early on win32
if(process.platform !== 'win32') {
args.push( '--supress-stdout');
}
var p = spawn(process.execPath, args);
//
// AvianFlu, you broke the API! OH NOEZ.
// Anyway.
// Since node 0.7.something, semantics of child process events
// changed - `exit` event is emitted when child process exits
// and `close` event is emitted when child's streams stdio streams
// are closed. `exit` event is emitted before `close` event, and
// since we use child's stdio streams, we shouldn't rely on `exit`
// event.
//
p.on(nodeMinorVersion >= 7 ? 'close' : 'exit', function (code) {
callback(
!result ?
{errored: 1, total: 1}
:
result
);
});
var buffer = [];
p.stdout.on('data', function (data) {
data = data.toString().split(/\n/g);
if (data.length == 1) {
buffer.push(data[0]);
} else {
data[0] = buffer.concat(data[0]).join('');
buffer = [data.pop()];
data.forEach(function (data) {
if (data && data !== 'undefined') {
try {
data = JSON.parse(data);
// on win32 we may get back console.log strings which break JSON.parse
if (data && data[0] === 'finish') {
result = data[1];
} else {
reporter.report(data);
}
} catch(e) {
}
}
});
}
});
p.stderr.pipe(process.stderr);
}
}
return files.reduce(options.isolate ? function (suites, f) {
return suites.concat({
run: wrapSpawn(f)
});
} : function (suites, f) {
f = path.relative(__dirname, f);
f = f.replace(/\\/g,'/');
f = f.replace('../C:','C:');
var obj = require(f);
f = f.replace('../','');
return suites.concat(Object.keys(obj).map(function (s) {
obj[s]._filename = cwdname(f);
return obj[s];
}));
}, [])
}
//
// Recursively traverse a hierarchy, returning
// a list of all relevant .js files.
//
function paths(dir) {
var paths = [];
try { fs.statSync(dir) }
catch (e) { return [] }
(function traverse(dir, stack) {
stack.push(dir);
var dirPath = stack.join('/');
fs.readdirSync(stack.join('/')).forEach(function (file) {
//
// Skip dotfiles and `vendor` directory before `fs.stat()`ing them.
// Not doing so causes race conditions with Emacs buffer files
// (`.#filename.js`).
//
if (file[0] == '.' || file === 'vendor') {
return;
}
var path = stack.concat([file]).join('/'),
stat = fs.statSync(path);
if (stat.isFile() && fileExt.test(file)) {
paths.push(path);
} else if (stat.isDirectory()) {
traverse(file, stack);
}
});
stack.pop();
})(dir || '.', []);
return paths;
}
function msg(cmd, subject, str, p) {
if (options.verbose) {
util[p ? 'print' : 'puts']( stylize('vows ', 'green')
+ stylize(cmd, 'bold')
+ ' ' + subject + ' '
+ (str ? (typeof(str) === 'string' ? str : inspect(str)) : '')
);
}
}
function abort(cmd, str) {
console.log(stylize('vows ', 'red') + stylize(cmd, 'bold') + ' ' + str);
console.log(stylize('vows ', 'red') + stylize(cmd, 'bold') + ' exiting');
process.exit(-1);
}
Jump to Line
Something went wrong with that request. Please try again.