UNPKG

8.34 kBJavaScriptView Raw
1'use strict';
2var EventEmitter = require('events').EventEmitter;
3var path = require('path');
4var util = require('util');
5var fs = require('fs');
6var flatten = require('arr-flatten');
7var Promise = require('bluebird');
8var figures = require('figures');
9var globby = require('globby');
10var chalk = require('chalk');
11var objectAssign = require('object-assign');
12var commonPathPrefix = require('common-path-prefix');
13var resolveCwd = require('resolve-cwd');
14var uniqueTempDir = require('unique-temp-dir');
15var findCacheDir = require('find-cache-dir');
16var slash = require('slash');
17var AvaError = require('./lib/ava-error');
18var fork = require('./lib/fork');
19var formatter = require('./lib/enhance-assert').formatter();
20var CachingPrecompiler = require('./lib/caching-precompiler');
21
22function Api(options) {
23 if (!(this instanceof Api)) {
24 throw new TypeError('Class constructor Api cannot be invoked without \'new\'');
25 }
26
27 EventEmitter.call(this);
28
29 this.options = options || {};
30 this.options.require = (this.options.require || []).map(resolveCwd);
31 this.options.match = this.options.match || [];
32
33 this.excludePatterns = [
34 '!**/node_modules/**',
35 '!**/fixtures/**',
36 '!**/helpers/**'
37 ];
38
39 Object.keys(Api.prototype).forEach(function (key) {
40 this[key] = this[key].bind(this);
41 }, this);
42
43 this._reset();
44}
45
46util.inherits(Api, EventEmitter);
47module.exports = Api;
48
49Api.prototype._reset = function () {
50 this.rejectionCount = 0;
51 this.exceptionCount = 0;
52 this.passCount = 0;
53 this.skipCount = 0;
54 this.todoCount = 0;
55 this.failCount = 0;
56 this.fileCount = 0;
57 this.testCount = 0;
58 this.hasExclusive = false;
59 this.errors = [];
60 this.stats = [];
61 this.tests = [];
62 this.base = '';
63};
64
65Api.prototype._runFile = function (file) {
66 var options = objectAssign({}, this.options, {
67 precompiled: this.precompiler.generateHashForFile(file)
68 });
69
70 return fork(file, options)
71 .on('teardown', this._handleTeardown)
72 .on('stats', this._handleStats)
73 .on('test', this._handleTest)
74 .on('unhandledRejections', this._handleRejections)
75 .on('uncaughtException', this._handleExceptions)
76 .on('stdout', this._handleOutput.bind(this, 'stdout'))
77 .on('stderr', this._handleOutput.bind(this, 'stderr'));
78};
79
80Api.prototype._handleOutput = function (channel, data) {
81 this.emit(channel, data);
82};
83
84Api.prototype._handleRejections = function (data) {
85 this.rejectionCount += data.rejections.length;
86
87 data.rejections.forEach(function (err) {
88 err.type = 'rejection';
89 err.file = data.file;
90 this.emit('error', err);
91 this.errors.push(err);
92 }, this);
93};
94
95Api.prototype._handleExceptions = function (data) {
96 this.exceptionCount++;
97 var err = data.exception;
98 err.type = 'exception';
99 err.file = data.file;
100 this.emit('error', err);
101 this.errors.push(err);
102};
103
104Api.prototype._handleTeardown = function (data) {
105 this.emit('dependencies', data.file, data.dependencies);
106};
107
108Api.prototype._handleStats = function (stats) {
109 this.emit('stats', stats);
110
111 if (this.hasExclusive && !stats.hasExclusive) {
112 return;
113 }
114
115 if (!this.hasExclusive && stats.hasExclusive) {
116 this.hasExclusive = true;
117 this.testCount = 0;
118 }
119
120 this.testCount += stats.testCount;
121};
122
123Api.prototype._handleTest = function (test) {
124 test.title = this._prefixTitle(test.file) + test.title;
125
126 if (test.error) {
127 if (test.error.powerAssertContext) {
128 var message = formatter(test.error.powerAssertContext);
129
130 if (test.error.originalMessage) {
131 message = test.error.originalMessage + ' ' + message;
132 }
133
134 test.error.message = message;
135 }
136
137 if (test.error.name !== 'AssertionError') {
138 test.error.message = 'failed with "' + test.error.message + '"';
139 }
140
141 this.errors.push(test);
142 }
143
144 this.emit('test', test);
145};
146
147Api.prototype._prefixTitle = function (file) {
148 if (this.fileCount === 1 && !this.options.explicitTitles) {
149 return '';
150 }
151
152 var separator = ' ' + chalk.gray.dim(figures.pointerSmall) + ' ';
153
154 var prefix = path.relative('.', file)
155 .replace(this.base, '')
156 .replace(/\.spec/, '')
157 .replace(/\.test/, '')
158 .replace(/test\-/g, '')
159 .replace(/\.js$/, '')
160 .split(path.sep)
161 .join(separator);
162
163 if (prefix.length > 0) {
164 prefix += separator;
165 }
166
167 return prefix;
168};
169
170Api.prototype.run = function (files, options) {
171 var self = this;
172
173 this._reset();
174
175 if (options && options.runOnlyExclusive) {
176 this.hasExclusive = true;
177 }
178
179 return handlePaths(files, this.excludePatterns)
180 .map(function (file) {
181 return path.resolve(file);
182 })
183 .then(function (files) {
184 if (files.length === 0) {
185 self._handleExceptions({
186 exception: new AvaError('Couldn\'t find any files to test'),
187 file: undefined
188 });
189
190 return [];
191 }
192
193 var cacheEnabled = self.options.cacheEnabled !== false;
194 var cacheDir = (cacheEnabled && findCacheDir({name: 'ava', files: files})) ||
195 uniqueTempDir();
196
197 self.options.cacheDir = cacheDir;
198 self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig);
199 self.fileCount = files.length;
200 self.base = path.relative('.', commonPathPrefix(files)) + path.sep;
201
202 var tests = new Array(self.fileCount);
203 return new Promise(function (resolve) {
204 function run() {
205 if (self.options.match.length > 0 && !self.hasExclusive) {
206 self._handleExceptions({
207 exception: new AvaError('Couldn\'t find any matching tests'),
208 file: undefined
209 });
210
211 resolve([]);
212 return;
213 }
214
215 self.emit('ready');
216
217 var method = self.options.serial ? 'mapSeries' : 'map';
218 var options = {
219 runOnlyExclusive: self.hasExclusive
220 };
221
222 resolve(Promise[method](files, function (file, index) {
223 return tests[index].run(options).catch(function (err) {
224 // The test failed catastrophically. Flag it up as an
225 // exception, then return an empty result. Other tests may
226 // continue to run.
227 self._handleExceptions({
228 exception: err,
229 file: file
230 });
231
232 return {
233 stats: {passCount: 0, skipCount: 0, todoCount: 0, failCount: 0},
234 tests: []
235 };
236 });
237 }));
238 }
239
240 // receive test count from all files and then run the tests
241 var unreportedFiles = self.fileCount;
242 var bailed = false;
243 files.every(function (file, index) {
244 var tried = false;
245 function tryRun() {
246 if (!tried && !bailed) {
247 unreportedFiles--;
248 if (unreportedFiles === 0) {
249 run();
250 }
251 }
252 }
253
254 try {
255 var test = tests[index] = self._runFile(file);
256 test.on('stats', tryRun);
257 test.catch(tryRun);
258 return true;
259 } catch (err) {
260 bailed = true;
261 self._handleExceptions({
262 exception: err,
263 file: file
264 });
265 resolve([]);
266 return false;
267 }
268 });
269 }).then(function (results) {
270 if (results.length === 0) {
271 // No tests ran, make sure to tear down the child processes.
272 tests.forEach(function (test) {
273 test.send('teardown');
274 });
275 }
276
277 return results;
278 });
279 })
280 .then(function (results) {
281 // assemble stats from all tests
282 self.stats = results.map(function (result) {
283 return result.stats;
284 });
285
286 self.tests = results.map(function (result) {
287 return result.tests;
288 });
289
290 self.tests = flatten(self.tests);
291
292 self.passCount = sum(self.stats, 'passCount');
293 self.skipCount = sum(self.stats, 'skipCount');
294 self.todoCount = sum(self.stats, 'todoCount');
295 self.failCount = sum(self.stats, 'failCount');
296 });
297};
298
299function handlePaths(files, excludePatterns) {
300 // convert pinkie-promise to Bluebird promise
301 files = Promise.resolve(globby(files.concat(excludePatterns)));
302
303 return files
304 .map(function (file) {
305 if (fs.statSync(file).isDirectory()) {
306 var pattern = path.join(file, '**', '*.js');
307 if (process.platform === 'win32') {
308 // Always use / in patterns, harmonizing matching across platforms.
309 pattern = slash(pattern);
310 }
311 return handlePaths([pattern], excludePatterns);
312 }
313
314 // globby returns slashes even on Windows. Normalize here so the file
315 // paths are consistently platform-accurate as tests are run.
316 return path.normalize(file);
317 })
318 .then(flatten)
319 .filter(function (file) {
320 return path.extname(file) === '.js' && path.basename(file)[0] !== '_';
321 });
322}
323
324function sum(arr, key) {
325 var result = 0;
326
327 arr.forEach(function (item) {
328 result += item[key];
329 });
330
331 return result;
332}