Skip to content

Commit f0f4f34

Browse files
novemberbornsindresorhus
authored andcommitted
Close #502 PR: Implement --watch. Fixes #70
1 parent 158916c commit f0f4f34

File tree

13 files changed

+1204
-106
lines changed

13 files changed

+1204
-106
lines changed

api.js

+60-31
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var figures = require('figures');
99
var globby = require('globby');
1010
var chalk = require('chalk');
1111
var objectAssign = require('object-assign');
12-
var commondir = require('commondir');
12+
var commonPathPrefix = require('common-path-prefix');
1313
var resolveCwd = require('resolve-cwd');
1414
var uniqueTempDir = require('unique-temp-dir');
1515
var findCacheDir = require('find-cache-dir');
@@ -27,6 +27,34 @@ function Api(files, options) {
2727

2828
this.options = options || {};
2929
this.options.require = (this.options.require || []).map(resolveCwd);
30+
31+
if (!files || files.length === 0) {
32+
this.files = [
33+
'test.js',
34+
'test-*.js',
35+
'test'
36+
];
37+
} else {
38+
this.files = files;
39+
}
40+
41+
this.excludePatterns = [
42+
'!**/node_modules/**',
43+
'!**/fixtures/**',
44+
'!**/helpers/**'
45+
];
46+
47+
Object.keys(Api.prototype).forEach(function (key) {
48+
this[key] = this[key].bind(this);
49+
}, this);
50+
51+
this._reset();
52+
}
53+
54+
util.inherits(Api, EventEmitter);
55+
module.exports = Api;
56+
57+
Api.prototype._reset = function () {
3058
this.rejectionCount = 0;
3159
this.exceptionCount = 0;
3260
this.passCount = 0;
@@ -37,16 +65,9 @@ function Api(files, options) {
3765
this.errors = [];
3866
this.stats = [];
3967
this.tests = [];
40-
this.files = files || [];
4168
this.base = '';
42-
43-
Object.keys(Api.prototype).forEach(function (key) {
44-
this[key] = this[key].bind(this);
45-
}, this);
46-
}
47-
48-
util.inherits(Api, EventEmitter);
49-
module.exports = Api;
69+
this.explicitTitles = false;
70+
};
5071

5172
Api.prototype._runFile = function (file) {
5273
var options = objectAssign({}, this.options, {
@@ -119,7 +140,7 @@ Api.prototype._handleTest = function (test) {
119140
};
120141

121142
Api.prototype._prefixTitle = function (file) {
122-
if (this.fileCount === 1) {
143+
if (this.fileCount === 1 && !this.explicitTitles) {
123144
return '';
124145
}
125146

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

144-
Api.prototype.run = function () {
165+
Api.prototype.run = function (files) {
145166
var self = this;
146167

147-
return handlePaths(this.files)
168+
this._reset();
169+
this.explicitTitles = Boolean(files);
170+
return handlePaths(files || this.files, this.excludePatterns)
148171
.map(function (file) {
149172
return path.resolve(file);
150173
})
151174
.then(function (files) {
152175
if (files.length === 0) {
153-
return Promise.reject(new AvaError('Couldn\'t find any files to test'));
176+
self._handleExceptions({
177+
exception: new AvaError('Couldn\'t find any files to test'),
178+
file: undefined
179+
});
180+
181+
return [];
154182
}
155183

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

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

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

184212
resolve(Promise[method](files, function (file, index) {
185-
return tests[index].run();
213+
return tests[index].run().catch(function (err) {
214+
// The test failed catastrophically. Flag it up as an
215+
// exception, then return an empty result. Other tests may
216+
// continue to run.
217+
self._handleExceptions({
218+
exception: err,
219+
file: file
220+
});
221+
222+
return {
223+
stats: {passCount: 0, skipCount: 0, failCount: 0},
224+
tests: []
225+
};
226+
});
186227
}));
187228
}
188229
}
@@ -210,26 +251,14 @@ Api.prototype.run = function () {
210251
});
211252
};
212253

213-
function handlePaths(files) {
214-
if (files.length === 0) {
215-
files = [
216-
'test.js',
217-
'test-*.js',
218-
'test'
219-
];
220-
}
221-
222-
files.push('!**/node_modules/**');
223-
files.push('!**/fixtures/**');
224-
files.push('!**/helpers/**');
225-
254+
function handlePaths(files, excludePatterns) {
226255
// convert pinkie-promise to Bluebird promise
227-
files = Promise.resolve(globby(files));
256+
files = Promise.resolve(globby(files.concat(excludePatterns)));
228257

229258
return files
230259
.map(function (file) {
231260
if (fs.statSync(file).isDirectory()) {
232-
return handlePaths([path.join(file, '**', '*.js')]);
261+
return handlePaths([path.join(file, '**', '*.js')], excludePatterns);
233262
}
234263

235264
return file;

cli.js

+33-13
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var verboseReporter = require('./lib/reporters/verbose');
2929
var miniReporter = require('./lib/reporters/mini');
3030
var tapReporter = require('./lib/reporters/tap');
3131
var Logger = require('./lib/logger');
32+
var watcher = require('./lib/watcher');
3233
var Api = require('./api');
3334

3435
// Bluebird specific
@@ -48,6 +49,9 @@ var cli = meow([
4849
' --tap, -t Generate TAP output',
4950
' --verbose, -v Enable verbose output',
5051
' --no-cache Disable the transpiler cache',
52+
// Leave --watch and --sources undocumented until they're stable enough
53+
// ' --watch, -w Re-run tests when tests and source files change',
54+
// ' --source Pattern to match source files so tests can be re-run (Can be repeated)',
5155
'',
5256
'Examples',
5357
' ava',
@@ -62,20 +66,23 @@ var cli = meow([
6266
], {
6367
string: [
6468
'_',
65-
'require'
69+
'require',
70+
'source'
6671
],
6772
boolean: [
6873
'fail-fast',
6974
'verbose',
7075
'serial',
71-
'tap'
76+
'tap',
77+
'watch'
7278
],
7379
default: conf,
7480
alias: {
7581
t: 'tap',
7682
v: 'verbose',
7783
r: 'require',
78-
s: 'serial'
84+
s: 'serial',
85+
w: 'watch'
7986
}
8087
});
8188

@@ -112,17 +119,30 @@ api.on('error', logger.unhandledError);
112119
api.on('stdout', logger.stdout);
113120
api.on('stderr', logger.stderr);
114121

115-
api.run()
116-
.then(function () {
117-
logger.finish();
118-
logger.exit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0);
119-
})
120-
.catch(function (err) {
122+
if (cli.flags.watch) {
123+
try {
124+
watcher.start(logger, api, arrify(cli.flags.source), process.stdin);
125+
} catch (err) {
121126
if (err.name === 'AvaError') {
127+
// An AvaError may be thrown if chokidar is not installed. Log it nicely.
122128
console.log(' ' + colors.error(figures.cross) + ' ' + err.message);
129+
logger.exit(1);
123130
} else {
124-
console.error(colors.stack(err.stack));
131+
// Rethrow so it becomes an uncaught exception.
132+
throw err;
125133
}
126-
127-
logger.exit(1);
128-
});
134+
}
135+
} else {
136+
api.run()
137+
.then(function () {
138+
logger.finish();
139+
logger.exit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0);
140+
})
141+
.catch(function (err) {
142+
// Don't swallow exceptions. Note that any expected error should already
143+
// have been logged.
144+
setImmediate(function () {
145+
throw err;
146+
});
147+
});
148+
}

lib/logger.js

+8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ Logger.prototype.start = function () {
2525
this.write(this.reporter.start());
2626
};
2727

28+
Logger.prototype.reset = function () {
29+
if (!this.reporter.reset) {
30+
return;
31+
}
32+
33+
this.write(this.reporter.reset());
34+
};
35+
2836
Logger.prototype.test = function (test) {
2937
this.write(this.reporter.test(test));
3038
};

lib/reporters/mini.js

+20-12
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,7 @@ function MiniReporter() {
1010
return new MiniReporter();
1111
}
1212

13-
this.passCount = 0;
14-
this.failCount = 0;
15-
this.skipCount = 0;
16-
this.rejectionCount = 0;
17-
this.exceptionCount = 0;
18-
this.currentStatus = '';
19-
this.statusLineCount = 0;
20-
this.lastLineTracker = lastLineTracker();
13+
this.reset();
2114
this.stream = process.stderr;
2215
this.stringDecoder = new StringDecoder();
2316
}
@@ -28,6 +21,17 @@ MiniReporter.prototype.start = function () {
2821
return '';
2922
};
3023

24+
MiniReporter.prototype.reset = function () {
25+
this.passCount = 0;
26+
this.failCount = 0;
27+
this.skipCount = 0;
28+
this.rejectionCount = 0;
29+
this.exceptionCount = 0;
30+
this.currentStatus = '';
31+
this.statusLineCount = 0;
32+
this.lastLineTracker = lastLineTracker();
33+
};
34+
3135
MiniReporter.prototype.test = function (test) {
3236
var status = '';
3337
var title;
@@ -120,11 +124,15 @@ MiniReporter.prototype.finish = function () {
120124

121125
i++;
122126

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

126-
status += '\n\n ' + colors.error(i + '.', title) + '\n';
127-
status += ' ' + colors.stack(description);
133+
status += '\n\n ' + colors.error(i + '.', title) + '\n';
134+
status += ' ' + colors.stack(description);
135+
}
128136
});
129137
}
130138

lib/reporters/tap.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,17 @@ TapReporter.prototype.test = function (test) {
5151
TapReporter.prototype.unhandledError = function (err) {
5252
var output = [
5353
'# ' + err.message,
54-
format('not ok %d - %s', ++this.i, err.message),
55-
' ---',
56-
' name: ' + err.name,
57-
' at: ' + getSourceFromStack(err.stack, 1),
58-
' ...'
54+
format('not ok %d - %s', ++this.i, err.message)
5955
];
56+
// AvaErrors don't have stack traces.
57+
if (err.type !== 'exception' || err.name !== 'AvaError') {
58+
output.push(
59+
' ---',
60+
' name: ' + err.name,
61+
' at: ' + getSourceFromStack(err.stack, 1),
62+
' ...'
63+
);
64+
}
6065

6166
return output.join('\n');
6267
};

lib/reporters/verbose.js

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ VerboseReporter.prototype.test = function (test) {
3737
};
3838

3939
VerboseReporter.prototype.unhandledError = function (err) {
40+
if (err.type === 'exception' && err.name === 'AvaError') {
41+
return ' ' + colors.error(figures.cross) + ' ' + err.message;
42+
}
43+
4044
var types = {
4145
rejection: 'Unhandled Rejection',
4246
exception: 'Uncaught Exception'

0 commit comments

Comments
 (0)