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