Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --watch #502

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a new Api instance each time instead of resetting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the files parameter just be required? Move the handlePaths logic outside the Api entirely.

The Api is not documented or to be considered stable, so there's no worry about a breaking change here.

.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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, why not just a new instance for a new run?

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