1 | 'use strict';
|
2 | var EventEmitter = require('events').EventEmitter;
|
3 | var path = require('path');
|
4 | var util = require('util');
|
5 | var fs = require('fs');
|
6 | var flatten = require('arr-flatten');
|
7 | var Promise = require('bluebird');
|
8 | var figures = require('figures');
|
9 | var globby = require('globby');
|
10 | var chalk = require('chalk');
|
11 | var objectAssign = require('object-assign');
|
12 | var commonPathPrefix = require('common-path-prefix');
|
13 | var resolveCwd = require('resolve-cwd');
|
14 | var uniqueTempDir = require('unique-temp-dir');
|
15 | var findCacheDir = require('find-cache-dir');
|
16 | var slash = require('slash');
|
17 | var AvaError = require('./lib/ava-error');
|
18 | var fork = require('./lib/fork');
|
19 | var formatter = require('./lib/enhance-assert').formatter();
|
20 | var CachingPrecompiler = require('./lib/caching-precompiler');
|
21 |
|
22 | function 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 |
|
46 | util.inherits(Api, EventEmitter);
|
47 | module.exports = Api;
|
48 |
|
49 | Api.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 |
|
65 | Api.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 |
|
80 | Api.prototype._handleOutput = function (channel, data) {
|
81 | this.emit(channel, data);
|
82 | };
|
83 |
|
84 | Api.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 |
|
95 | Api.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 |
|
104 | Api.prototype._handleTeardown = function (data) {
|
105 | this.emit('dependencies', data.file, data.dependencies);
|
106 | };
|
107 |
|
108 | Api.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 |
|
123 | Api.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 |
|
147 | Api.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 |
|
170 | Api.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 |
|
225 |
|
226 |
|
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 |
|
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 |
|
272 | tests.forEach(function (test) {
|
273 | test.send('teardown');
|
274 | });
|
275 | }
|
276 |
|
277 | return results;
|
278 | });
|
279 | })
|
280 | .then(function (results) {
|
281 |
|
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 |
|
299 | function handlePaths(files, excludePatterns) {
|
300 |
|
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 |
|
309 | pattern = slash(pattern);
|
310 | }
|
311 | return handlePaths([pattern], excludePatterns);
|
312 | }
|
313 |
|
314 |
|
315 |
|
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 |
|
324 | function sum(arr, key) {
|
325 | var result = 0;
|
326 |
|
327 | arr.forEach(function (item) {
|
328 | result += item[key];
|
329 | });
|
330 |
|
331 | return result;
|
332 | }
|