Skip to content

Commit

Permalink
Close #502 PR: Implement --watch. Fixes #70
Browse files Browse the repository at this point in the history
  • Loading branch information
novemberborn authored and sindresorhus committed Feb 9, 2016
1 parent 158916c commit f0f4f34
Show file tree
Hide file tree
Showing 13 changed files with 1,204 additions and 106 deletions.
91 changes: 60 additions & 31 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ var figures = require('figures');
var globby = require('globby');
var chalk = require('chalk');
var objectAssign = require('object-assign');
var commondir = require('commondir');
var commonPathPrefix = require('common-path-prefix');
var resolveCwd = require('resolve-cwd');
var uniqueTempDir = require('unique-temp-dir');
var findCacheDir = require('find-cache-dir');
Expand All @@ -27,6 +27,34 @@ function Api(files, options) {

this.options = options || {};
this.options.require = (this.options.require || []).map(resolveCwd);

if (!files || files.length === 0) {
this.files = [
'test.js',
'test-*.js',
'test'
];
} else {
this.files = files;
}

this.excludePatterns = [
'!**/node_modules/**',
'!**/fixtures/**',
'!**/helpers/**'
];

Object.keys(Api.prototype).forEach(function (key) {
this[key] = this[key].bind(this);
}, this);

this._reset();
}

util.inherits(Api, EventEmitter);
module.exports = Api;

Api.prototype._reset = function () {
this.rejectionCount = 0;
this.exceptionCount = 0;
this.passCount = 0;
Expand All @@ -37,16 +65,9 @@ function Api(files, options) {
this.errors = [];
this.stats = [];
this.tests = [];
this.files = files || [];
this.base = '';

Object.keys(Api.prototype).forEach(function (key) {
this[key] = this[key].bind(this);
}, this);
}

util.inherits(Api, EventEmitter);
module.exports = Api;
this.explicitTitles = false;
};

Api.prototype._runFile = function (file) {
var options = objectAssign({}, this.options, {
Expand Down Expand Up @@ -119,7 +140,7 @@ Api.prototype._handleTest = function (test) {
};

Api.prototype._prefixTitle = function (file) {
if (this.fileCount === 1) {
if (this.fileCount === 1 && !this.explicitTitles) {
return '';
}

Expand All @@ -141,16 +162,23 @@ Api.prototype._prefixTitle = function (file) {
return prefix;
};

Api.prototype.run = function () {
Api.prototype.run = function (files) {
var self = this;

return handlePaths(this.files)
this._reset();
this.explicitTitles = Boolean(files);
return handlePaths(files || this.files, this.excludePatterns)
.map(function (file) {
return path.resolve(file);
})
.then(function (files) {
if (files.length === 0) {
return Promise.reject(new AvaError('Couldn\'t find any files to test'));
self._handleExceptions({
exception: new AvaError('Couldn\'t find any files to test'),
file: undefined
});

return [];
}

var cacheEnabled = self.options.cacheEnabled !== false;
Expand All @@ -160,7 +188,7 @@ Api.prototype.run = function () {
self.options.cacheDir = cacheDir;
self.precompiler = new CachingPrecompiler(cacheDir);
self.fileCount = files.length;
self.base = path.relative('.', commondir('.', files)) + path.sep;
self.base = path.relative('.', commonPathPrefix(files)) + path.sep;

var tests = files.map(self._runFile);

Expand All @@ -182,7 +210,20 @@ Api.prototype.run = function () {
var method = self.options.serial ? 'mapSeries' : 'map';

resolve(Promise[method](files, function (file, index) {
return tests[index].run();
return tests[index].run().catch(function (err) {
// The test failed catastrophically. Flag it up as an
// exception, then return an empty result. Other tests may
// continue to run.
self._handleExceptions({
exception: err,
file: file
});

return {
stats: {passCount: 0, skipCount: 0, failCount: 0},
tests: []
};
});
}));
}
}
Expand Down Expand Up @@ -210,26 +251,14 @@ Api.prototype.run = function () {
});
};

function handlePaths(files) {
if (files.length === 0) {
files = [
'test.js',
'test-*.js',
'test'
];
}

files.push('!**/node_modules/**');
files.push('!**/fixtures/**');
files.push('!**/helpers/**');

function handlePaths(files, excludePatterns) {
// convert pinkie-promise to Bluebird promise
files = Promise.resolve(globby(files));
files = Promise.resolve(globby(files.concat(excludePatterns)));

return files
.map(function (file) {
if (fs.statSync(file).isDirectory()) {
return handlePaths([path.join(file, '**', '*.js')]);
return handlePaths([path.join(file, '**', '*.js')], excludePatterns);
}

return file;
Expand Down
46 changes: 33 additions & 13 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var verboseReporter = require('./lib/reporters/verbose');
var miniReporter = require('./lib/reporters/mini');
var tapReporter = require('./lib/reporters/tap');
var Logger = require('./lib/logger');
var watcher = require('./lib/watcher');
var Api = require('./api');

// Bluebird specific
Expand All @@ -48,6 +49,9 @@ var cli = meow([
' --tap, -t Generate TAP output',
' --verbose, -v Enable verbose output',
' --no-cache Disable the transpiler cache',
// Leave --watch and --sources undocumented until they're stable enough
// ' --watch, -w Re-run tests when tests and source files change',
// ' --source Pattern to match source files so tests can be re-run (Can be repeated)',
'',
'Examples',
' ava',
Expand All @@ -62,20 +66,23 @@ var cli = meow([
], {
string: [
'_',
'require'
'require',
'source'
],
boolean: [
'fail-fast',
'verbose',
'serial',
'tap'
'tap',
'watch'
],
default: conf,
alias: {
t: 'tap',
v: 'verbose',
r: 'require',
s: 'serial'
s: 'serial',
w: 'watch'
}
});

Expand Down Expand Up @@ -112,17 +119,30 @@ api.on('error', logger.unhandledError);
api.on('stdout', logger.stdout);
api.on('stderr', logger.stderr);

api.run()
.then(function () {
logger.finish();
logger.exit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0);
})
.catch(function (err) {
if (cli.flags.watch) {
try {
watcher.start(logger, api, arrify(cli.flags.source), process.stdin);
} catch (err) {
if (err.name === 'AvaError') {
// An AvaError may be thrown if chokidar is not installed. Log it nicely.
console.log(' ' + colors.error(figures.cross) + ' ' + err.message);
logger.exit(1);
} else {
console.error(colors.stack(err.stack));
// Rethrow so it becomes an uncaught exception.
throw err;
}

logger.exit(1);
});
}
} else {
api.run()
.then(function () {
logger.finish();
logger.exit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0);
})
.catch(function (err) {
// Don't swallow exceptions. Note that any expected error should already
// have been logged.
setImmediate(function () {
throw err;
});
});
}
8 changes: 8 additions & 0 deletions lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ Logger.prototype.start = function () {
this.write(this.reporter.start());
};

Logger.prototype.reset = function () {
if (!this.reporter.reset) {
return;
}

this.write(this.reporter.reset());
};

Logger.prototype.test = function (test) {
this.write(this.reporter.test(test));
};
Expand Down
32 changes: 20 additions & 12 deletions lib/reporters/mini.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ function MiniReporter() {
return new MiniReporter();
}

this.passCount = 0;
this.failCount = 0;
this.skipCount = 0;
this.rejectionCount = 0;
this.exceptionCount = 0;
this.currentStatus = '';
this.statusLineCount = 0;
this.lastLineTracker = lastLineTracker();
this.reset();
this.stream = process.stderr;
this.stringDecoder = new StringDecoder();
}
Expand All @@ -28,6 +21,17 @@ MiniReporter.prototype.start = function () {
return '';
};

MiniReporter.prototype.reset = function () {
this.passCount = 0;
this.failCount = 0;
this.skipCount = 0;
this.rejectionCount = 0;
this.exceptionCount = 0;
this.currentStatus = '';
this.statusLineCount = 0;
this.lastLineTracker = lastLineTracker();
};

MiniReporter.prototype.test = function (test) {
var status = '';
var title;
Expand Down Expand Up @@ -120,11 +124,15 @@ MiniReporter.prototype.finish = function () {

i++;

var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
var description = err.stack ? err.stack : JSON.stringify(err);
if (err.type === 'exception' && err.name === 'AvaError') {
status += '\n\n ' + colors.error(i + '. ' + err.message) + '\n';
} else {
var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
var description = err.stack ? err.stack : JSON.stringify(err);

status += '\n\n ' + colors.error(i + '.', title) + '\n';
status += ' ' + colors.stack(description);
status += '\n\n ' + colors.error(i + '.', title) + '\n';
status += ' ' + colors.stack(description);
}
});
}

Expand Down
15 changes: 10 additions & 5 deletions lib/reporters/tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,17 @@ TapReporter.prototype.test = function (test) {
TapReporter.prototype.unhandledError = function (err) {
var output = [
'# ' + err.message,
format('not ok %d - %s', ++this.i, err.message),
' ---',
' name: ' + err.name,
' at: ' + getSourceFromStack(err.stack, 1),
' ...'
format('not ok %d - %s', ++this.i, err.message)
];
// AvaErrors don't have stack traces.
if (err.type !== 'exception' || err.name !== 'AvaError') {
output.push(
' ---',
' name: ' + err.name,
' at: ' + getSourceFromStack(err.stack, 1),
' ...'
);
}

return output.join('\n');
};
Expand Down
4 changes: 4 additions & 0 deletions lib/reporters/verbose.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ VerboseReporter.prototype.test = function (test) {
};

VerboseReporter.prototype.unhandledError = function (err) {
if (err.type === 'exception' && err.name === 'AvaError') {
return ' ' + colors.error(figures.cross) + ' ' + err.message;
}

var types = {
rejection: 'Unhandled Rejection',
exception: 'Uncaught Exception'
Expand Down
Loading

0 comments on commit f0f4f34

Please sign in to comment.