ava 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api.js CHANGED
@@ -2,19 +2,47 @@
2
2
  var EventEmitter = require('events').EventEmitter;
3
3
  var path = require('path');
4
4
  var util = require('util');
5
- var Promise = require('bluebird');
6
- var objectAssign = require('object-assign');
7
5
  var commonPathPrefix = require('common-path-prefix');
8
- var resolveCwd = require('resolve-cwd');
9
6
  var uniqueTempDir = require('unique-temp-dir');
10
7
  var findCacheDir = require('find-cache-dir');
8
+ var objectAssign = require('object-assign');
9
+ var resolveCwd = require('resolve-cwd');
11
10
  var debounce = require('lodash.debounce');
11
+ var AvaFiles = require('ava-files');
12
+ var autoBind = require('auto-bind');
13
+ var Promise = require('bluebird');
14
+ var getPort = require('get-port');
15
+ var arrify = require('arrify');
12
16
  var ms = require('ms');
13
- var AvaError = require('./lib/ava-error');
14
- var fork = require('./lib/fork');
15
17
  var CachingPrecompiler = require('./lib/caching-precompiler');
16
- var AvaFiles = require('./lib/ava-files');
17
18
  var RunStatus = require('./lib/run-status');
19
+ var AvaError = require('./lib/ava-error');
20
+ var fork = require('./lib/fork');
21
+
22
+ function resolveModules(modules) {
23
+ return arrify(modules).map(function (name) {
24
+ var modulePath = resolveCwd(name);
25
+ if (modulePath === null) {
26
+ throw new Error('Could not resolve required module \'' + name + '\'');
27
+ }
28
+
29
+ return modulePath;
30
+ });
31
+ }
32
+
33
+ function getBlankResults() {
34
+ return {
35
+ stats: {
36
+ knownFailureCount: 0,
37
+ testCount: 0,
38
+ passCount: 0,
39
+ skipCount: 0,
40
+ todoCount: 0,
41
+ failCount: 0
42
+ },
43
+ tests: []
44
+ };
45
+ }
18
46
 
19
47
  function Api(options) {
20
48
  if (!(this instanceof Api)) {
@@ -22,27 +50,21 @@ function Api(options) {
22
50
  }
23
51
 
24
52
  EventEmitter.call(this);
53
+ autoBind(this);
25
54
 
26
- this.options = options || {};
27
- this.options.match = this.options.match || [];
28
- this.options.require = (this.options.require || []).map(function (moduleId) {
29
- var ret = resolveCwd(moduleId);
30
- if (ret === null) {
31
- throw new Error('Could not resolve required module \'' + moduleId + '\'');
32
- }
33
-
34
- return ret;
35
- });
55
+ this.options = objectAssign({
56
+ cwd: process.cwd(),
57
+ resolveTestsFrom: process.cwd(),
58
+ match: []
59
+ }, options);
36
60
 
37
- Object.keys(Api.prototype).forEach(function (key) {
38
- this[key] = this[key].bind(this);
39
- }, this);
61
+ this.options.require = resolveModules(this.options.require);
40
62
  }
41
63
 
42
64
  util.inherits(Api, EventEmitter);
43
65
  module.exports = Api;
44
66
 
45
- Api.prototype._runFile = function (file, runStatus) {
67
+ Api.prototype._runFile = function (file, runStatus, execArgv) {
46
68
  var hash = this.precompiler.precompileFile(file);
47
69
  var precompiled = {};
48
70
  precompiled[file] = hash;
@@ -51,103 +73,198 @@ Api.prototype._runFile = function (file, runStatus) {
51
73
  precompiled: precompiled
52
74
  });
53
75
 
54
- var emitter = fork(file, options);
55
-
76
+ var emitter = fork(file, options, execArgv);
56
77
  runStatus.observeFork(emitter);
57
78
 
58
79
  return emitter;
59
80
  };
60
81
 
82
+ Api.prototype.run = function (files, options) {
83
+ var self = this;
84
+
85
+ return new AvaFiles({cwd: this.options.resolveTestsFrom, files: files})
86
+ .findTestFiles()
87
+ .then(function (files) {
88
+ return self._run(files, options);
89
+ });
90
+ };
91
+
61
92
  Api.prototype._onTimeout = function (runStatus) {
62
93
  var timeout = ms(this.options.timeout);
63
- var message = 'Exited because no new tests completed within the last ' + timeout + 'ms of inactivity';
64
-
65
- runStatus.handleExceptions({
66
- exception: new AvaError(message),
67
- file: undefined
68
- });
94
+ var err = new AvaError('Exited because no new tests completed within the last ' + timeout + 'ms of inactivity');
95
+ this._handleError(runStatus, err);
69
96
 
70
97
  runStatus.emit('timeout');
71
98
  };
72
99
 
73
- Api.prototype.run = function (files, options) {
100
+ Api.prototype._setupTimeout = function (runStatus) {
74
101
  var self = this;
102
+ var timeout = ms(this.options.timeout);
75
103
 
76
- return new AvaFiles(files)
77
- .findTestFiles()
78
- .then(function (files) {
79
- return self._run(files, options);
104
+ runStatus._restartTimer = debounce(function () {
105
+ self._onTimeout(runStatus);
106
+ }, timeout);
107
+
108
+ runStatus._restartTimer();
109
+ runStatus.on('test', runStatus._restartTimer);
110
+ };
111
+
112
+ Api.prototype._cancelTimeout = function (runStatus) {
113
+ runStatus._restartTimer.cancel();
114
+ };
115
+
116
+ Api.prototype._setupPrecompiler = function (files) {
117
+ var isCacheEnabled = this.options.cacheEnabled !== false;
118
+ var cacheDir = uniqueTempDir();
119
+
120
+ if (isCacheEnabled) {
121
+ var foundDir = findCacheDir({
122
+ name: 'ava',
123
+ files: files
80
124
  });
125
+ if (foundDir !== null) {
126
+ cacheDir = foundDir;
127
+ }
128
+ }
129
+
130
+ this.options.cacheDir = cacheDir;
131
+
132
+ var isPowerAssertEnabled = this.options.powerAssert !== false;
133
+ this.precompiler = new CachingPrecompiler({
134
+ path: cacheDir,
135
+ babel: this.options.babelConfig,
136
+ powerAssert: isPowerAssertEnabled
137
+ });
81
138
  };
82
139
 
83
- Api.prototype._run = function (files, _options) {
84
- var self = this;
140
+ Api.prototype._run = function (files, options) {
141
+ options = options || {};
142
+
85
143
  var runStatus = new RunStatus({
144
+ runOnlyExclusive: options.runOnlyExclusive,
86
145
  prefixTitles: this.options.explicitTitles || files.length > 1,
87
- runOnlyExclusive: _options && _options.runOnlyExclusive,
88
146
  base: path.relative('.', commonPathPrefix(files)) + path.sep
89
147
  });
90
148
 
91
- if (self.options.timeout) {
92
- var timeout = ms(self.options.timeout);
93
- runStatus._restartTimer = debounce(function () {
94
- self._onTimeout(runStatus);
95
- }, timeout);
96
- runStatus._restartTimer();
97
- runStatus.on('test', runStatus._restartTimer);
98
- }
99
-
100
- self.emit('test-run', runStatus, files);
149
+ this.emit('test-run', runStatus, files);
101
150
 
102
151
  if (files.length === 0) {
103
- runStatus.handleExceptions({
104
- exception: new AvaError('Couldn\'t find any files to test'),
105
- file: undefined
106
- });
152
+ var err = new AvaError('Couldn\'t find any files to test');
153
+ this._handleError(runStatus, err);
107
154
 
108
155
  return Promise.resolve(runStatus);
109
156
  }
110
157
 
111
- var cacheEnabled = self.options.cacheEnabled !== false;
112
- var cacheDir = (cacheEnabled && findCacheDir({name: 'ava', files: files})) ||
113
- uniqueTempDir();
158
+ this._setupPrecompiler(files);
114
159
 
115
- self.options.cacheDir = cacheDir;
116
- self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig);
117
- self.fileCount = files.length;
160
+ if (this.options.timeout) {
161
+ this._setupTimeout(runStatus);
162
+ }
118
163
 
119
164
  var overwatch;
120
165
  if (this.options.concurrency > 0) {
121
- overwatch = this._runLimitedPool(files, runStatus, self.options.serial ? 1 : this.options.concurrency);
166
+ var concurrency = this.options.serial ? 1 : this.options.concurrency;
167
+ overwatch = this._runWithPool(files, runStatus, concurrency);
122
168
  } else {
123
- // _runNoPool exists to preserve legacy behavior, specifically around `.only`
124
- overwatch = this._runNoPool(files, runStatus);
169
+ // _runWithoutPool exists to preserve legacy behavior, specifically around `.only`
170
+ overwatch = this._runWithoutPool(files, runStatus);
125
171
  }
126
172
 
127
173
  return overwatch;
128
174
  };
129
175
 
130
- Api.prototype._runNoPool = function (files, runStatus) {
176
+ Api.prototype._computeForkExecArgs = function (files) {
177
+ var execArgv = this.options.testOnlyExecArgv || process.execArgv;
178
+ var debugArgIndex = -1;
179
+
180
+ // --debug-brk is used in addition to --inspect to break on first line and wait
181
+ execArgv.some(function (arg, index) {
182
+ var isDebugArg = arg === '--inspect' || arg.indexOf('--inspect=') === 0;
183
+ if (isDebugArg) {
184
+ debugArgIndex = index;
185
+ }
186
+
187
+ return isDebugArg;
188
+ });
189
+
190
+ var isInspect = debugArgIndex >= 0;
191
+ if (!isInspect) {
192
+ execArgv.some(function (arg, index) {
193
+ var isDebugArg = arg === '--debug' || arg === '--debug-brk' || arg.indexOf('--debug-brk=') === 0 || arg.indexOf('--debug=') === 0;
194
+ if (isDebugArg) {
195
+ debugArgIndex = index;
196
+ }
197
+
198
+ return isDebugArg;
199
+ });
200
+ }
201
+
202
+ if (debugArgIndex === -1) {
203
+ return Promise.resolve([]);
204
+ }
205
+
206
+ return Promise
207
+ .map(files, getPort)
208
+ .map(function (port) {
209
+ var forkExecArgv = execArgv.slice();
210
+ var flagName = isInspect ? '--inspect' : '--debug';
211
+ var oldValue = forkExecArgv[debugArgIndex];
212
+ if (oldValue.indexOf('brk') > 0) {
213
+ flagName += '-brk';
214
+ }
215
+
216
+ forkExecArgv[debugArgIndex] = flagName + '=' + port;
217
+
218
+ return forkExecArgv;
219
+ });
220
+ };
221
+
222
+ Api.prototype._handleError = function (runStatus, err) {
223
+ runStatus.handleExceptions({
224
+ exception: err,
225
+ file: err.file ? path.relative('.', err.file) : undefined
226
+ });
227
+ };
228
+
229
+ Api.prototype._runWithoutPool = function (files, runStatus) {
131
230
  var self = this;
132
- var tests = new Array(self.fileCount);
133
231
 
134
- // TODO: thid should be cleared at the end of the run
232
+ var tests = [];
233
+ var execArgvList;
234
+
235
+ // TODO: this should be cleared at the end of the run
135
236
  runStatus.on('timeout', function () {
136
237
  tests.forEach(function (fork) {
137
238
  fork.exit();
138
239
  });
139
240
  });
140
241
 
141
- return new Promise(function (resolve) {
142
- function run() {
242
+ return this._computeForkExecArgs(files)
243
+ .then(function (argvList) {
244
+ execArgvList = argvList;
245
+ })
246
+ .return(files)
247
+ .each(function (file, index) {
248
+ return new Promise(function (resolve) {
249
+ var forkArgs = execArgvList[index];
250
+ var test = self._runFile(file, runStatus, forkArgs);
251
+ tests.push(test);
252
+
253
+ test.on('stats', resolve);
254
+ test.catch(resolve);
255
+ }).catch(function (err) {
256
+ err.results = [];
257
+ err.file = file;
258
+ return Promise.reject(err);
259
+ });
260
+ })
261
+ .then(function () {
143
262
  if (self.options.match.length > 0 && !runStatus.hasExclusive) {
144
- runStatus.handleExceptions({
145
- exception: new AvaError('Couldn\'t find any matching tests'),
146
- file: undefined
147
- });
263
+ var err = new AvaError('Couldn\'t find any matching tests');
264
+ err.file = undefined;
265
+ err.results = [];
148
266
 
149
- resolve([]);
150
- return;
267
+ return Promise.reject(err);
151
268
  }
152
269
 
153
270
  var method = self.options.serial ? 'mapSeries' : 'map';
@@ -155,155 +272,94 @@ Api.prototype._runNoPool = function (files, runStatus) {
155
272
  runOnlyExclusive: runStatus.hasExclusive
156
273
  };
157
274
 
158
- resolve(Promise[method](files, function (file, index) {
275
+ return Promise[method](files, function (file, index) {
159
276
  return tests[index].run(options).catch(function (err) {
160
- // The test failed catastrophically. Flag it up as an
161
- // exception, then return an empty result. Other tests may
162
- // continue to run.
163
- runStatus.handleExceptions({
164
- exception: err,
165
- file: path.relative('.', file)
166
- });
277
+ err.file = file;
278
+ self._handleError(runStatus, err);
167
279
 
168
280
  return getBlankResults();
169
281
  });
170
- }));
171
- }
172
-
173
- // receive test count from all files and then run the tests
174
- var unreportedFiles = self.fileCount;
175
- var bailed = false;
176
-
177
- files.every(function (file, index) {
178
- var tried = false;
179
-
180
- function tryRun() {
181
- if (!tried && !bailed) {
182
- tried = true;
183
- unreportedFiles--;
184
-
185
- if (unreportedFiles === 0) {
186
- run();
187
- }
188
- }
189
- }
190
-
191
- try {
192
- var test = tests[index] = self._runFile(file, runStatus);
193
-
194
- test.on('stats', tryRun);
195
- test.catch(tryRun);
196
-
197
- return true;
198
- } catch (err) {
199
- bailed = true;
200
-
201
- runStatus.handleExceptions({
202
- exception: err,
203
- file: path.relative('.', file)
282
+ });
283
+ })
284
+ .catch(function (err) {
285
+ self._handleError(runStatus, err);
286
+
287
+ return err.results;
288
+ })
289
+ .tap(function (results) {
290
+ // if no tests ran, make sure to tear down the child processes
291
+ if (results.length === 0) {
292
+ tests.forEach(function (test) {
293
+ test.send('teardown');
204
294
  });
205
-
206
- resolve([]);
207
-
208
- return false;
209
295
  }
210
- });
211
- }).then(function (results) {
212
- if (results.length === 0) {
213
- // No tests ran, make sure to tear down the child processes.
214
- tests.forEach(function (test) {
215
- test.send('teardown');
216
- });
217
- }
296
+ })
297
+ .then(function (results) {
298
+ // cancel debounced _onTimeout() from firing
299
+ if (self.options.timeout) {
300
+ self._cancelTimeout(runStatus);
301
+ }
218
302
 
219
- return results;
220
- }).then(function (results) {
221
- // cancel debounced _onTimeout() from firing
222
- if (self.options.timeout) {
223
- runStatus._restartTimer.cancel();
224
- }
303
+ runStatus.processResults(results);
225
304
 
226
- runStatus.processResults(results);
227
- return runStatus;
228
- });
305
+ return runStatus;
306
+ });
229
307
  };
230
308
 
231
- function getBlankResults() {
232
- return {
233
- stats: {
234
- testCount: 0,
235
- passCount: 0,
236
- knownFailureCount: 0,
237
- skipCount: 0,
238
- todoCount: 0,
239
- failCount: 0
240
- },
241
- tests: []
242
- };
243
- }
244
-
245
- Api.prototype._runLimitedPool = function (files, runStatus, concurrency) {
309
+ Api.prototype._runWithPool = function (files, runStatus, concurrency) {
246
310
  var self = this;
247
- var tests = {};
311
+
312
+ var tests = [];
313
+ var execArgvList;
248
314
 
249
315
  runStatus.on('timeout', function () {
250
- Object.keys(tests).forEach(function (file) {
251
- var fork = tests[file];
316
+ tests.forEach(function (fork) {
252
317
  fork.exit();
253
318
  });
254
319
  });
255
320
 
256
- return Promise.map(files, function (file) {
257
- var handleException = function (err) {
258
- runStatus.handleExceptions({
259
- exception: err,
260
- file: path.relative('.', file)
261
- });
262
- };
263
-
264
- try {
265
- var test = tests[file] = self._runFile(file, runStatus);
266
-
267
- return new Promise(function (resolve, reject) {
268
- var runner = function () {
269
- var options = {
270
- // If we're looking for matches, run every single test process in exclusive-only mode
271
- runOnlyExclusive: self.options.match.length > 0
272
- };
273
- test.run(options)
274
- .then(resolve)
275
- .catch(reject);
321
+ return this._computeForkExecArgs(files)
322
+ .then(function (argvList) {
323
+ execArgvList = argvList;
324
+ })
325
+ .return(files)
326
+ .map(function (file, index) {
327
+ return new Promise(function (resolve) {
328
+ var forkArgs = execArgvList[index];
329
+ var test = self._runFile(file, runStatus, forkArgs);
330
+ tests.push(test);
331
+
332
+ // If we're looking for matches, run every single test process in exclusive-only mode
333
+ var options = {
334
+ runOnlyExclusive: self.options.match.length > 0
276
335
  };
277
336
 
278
- test.on('stats', runner);
279
- test.on('exit', function () {
280
- delete tests[file];
281
- });
282
- test.catch(runner);
283
- }).catch(handleException);
284
- } catch (err) {
285
- handleException(err);
286
- }
287
- }, {concurrency: concurrency})
337
+ resolve(test.run(options));
338
+ }).catch(function (err) {
339
+ err.file = file;
340
+ self._handleError(runStatus, err);
341
+
342
+ return getBlankResults();
343
+ });
344
+ }, {concurrency: concurrency})
288
345
  .then(function (results) {
289
346
  // Filter out undefined results (usually result of caught exceptions)
290
347
  results = results.filter(Boolean);
291
348
 
292
349
  // cancel debounced _onTimeout() from firing
293
350
  if (self.options.timeout) {
294
- runStatus._restartTimer.cancel();
351
+ self._cancelTimeout(runStatus);
295
352
  }
296
353
 
297
354
  if (self.options.match.length > 0 && !runStatus.hasExclusive) {
298
- // Ensure results are empty
299
355
  results = [];
300
- runStatus.handleExceptions({
301
- exception: new AvaError('Couldn\'t find any matching tests'),
302
- file: undefined
303
- });
356
+
357
+ var err = new AvaError('Couldn\'t find any matching tests');
358
+ self._handleError(runStatus, err);
304
359
  }
305
360
 
306
361
  runStatus.processResults(results);
362
+
307
363
  return runStatus;
308
364
  });
309
365
  };