ava 0.16.0 → 0.18.2

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