UNPKG

7.88 kBJavaScriptView Raw
1'use strict';
2const EventEmitter = require('events');
3const path = require('path');
4const fs = require('fs');
5const os = require('os');
6const commonPathPrefix = require('common-path-prefix');
7const uniqueTempDir = require('unique-temp-dir');
8const findCacheDir = require('find-cache-dir');
9const isCi = require('is-ci');
10const resolveCwd = require('resolve-cwd');
11const debounce = require('lodash.debounce');
12const autoBind = require('auto-bind');
13const Promise = require('bluebird');
14const getPort = require('get-port');
15const arrify = require('arrify');
16const ms = require('ms');
17const babelConfigHelper = require('./lib/babel-config');
18const CachingPrecompiler = require('./lib/caching-precompiler');
19const RunStatus = require('./lib/run-status');
20const AvaError = require('./lib/ava-error');
21const AvaFiles = require('./lib/ava-files');
22const fork = require('./lib/fork');
23
24function resolveModules(modules) {
25 return arrify(modules).map(name => {
26 const modulePath = resolveCwd.silent(name);
27
28 if (modulePath === null) {
29 throw new Error(`Could not resolve required module '${name}'`);
30 }
31
32 return modulePath;
33 });
34}
35
36function getBlankResults() {
37 return {
38 stats: {
39 knownFailureCount: 0,
40 testCount: 0,
41 passCount: 0,
42 skipCount: 0,
43 todoCount: 0,
44 failCount: 0
45 },
46 tests: []
47 };
48}
49
50class Api extends EventEmitter {
51 constructor(options) {
52 super();
53 autoBind(this);
54
55 this.options = Object.assign({match: []}, options);
56 this.options.require = resolveModules(this.options.require);
57 }
58
59 _runFile(file, runStatus, execArgv) {
60 const hash = this.precompiler.precompileFile(file);
61 const precompiled = Object.assign({}, this._precompiledHelpers);
62 const resolvedfpath = fs.realpathSync(file);
63 precompiled[resolvedfpath] = hash;
64
65 const options = Object.assign({}, this.options, {precompiled});
66 if (runStatus.updateSnapshots) {
67 // Don't use in Object.assign() since it'll override options.updateSnapshots even when false.
68 options.updateSnapshots = true;
69 }
70 const emitter = fork(file, options, execArgv);
71 runStatus.observeFork(emitter);
72
73 return emitter;
74 }
75
76 run(files, options) {
77 return new AvaFiles({cwd: this.options.resolveTestsFrom, files})
78 .findTestFiles()
79 .then(files => this._run(files, options));
80 }
81
82 _onTimeout(runStatus) {
83 const timeout = ms(this.options.timeout);
84 const err = new AvaError(`Exited because no new tests completed within the last ${timeout}ms of inactivity`);
85 this._handleError(runStatus, err);
86 runStatus.emit('timeout');
87 }
88
89 _setupTimeout(runStatus) {
90 const timeout = ms(this.options.timeout);
91
92 runStatus._restartTimer = debounce(() => {
93 this._onTimeout(runStatus);
94 }, timeout);
95
96 runStatus._restartTimer();
97 runStatus.on('test', runStatus._restartTimer);
98 }
99
100 _cancelTimeout(runStatus) {
101 runStatus._restartTimer.cancel();
102 }
103
104 _setupPrecompiler(files) {
105 const isCacheEnabled = this.options.cacheEnabled !== false;
106 let cacheDir = uniqueTempDir();
107
108 if (isCacheEnabled) {
109 const foundDir = findCacheDir({
110 name: 'ava',
111 files
112 });
113 if (foundDir !== null) {
114 cacheDir = foundDir;
115 }
116 }
117
118 this.options.cacheDir = cacheDir;
119
120 const isPowerAssertEnabled = this.options.powerAssert !== false;
121 return babelConfigHelper.build(this.options.projectDir, cacheDir, this.options.babelConfig, isPowerAssertEnabled)
122 .then(result => {
123 this.precompiler = new CachingPrecompiler({
124 path: cacheDir,
125 getBabelOptions: result.getOptions,
126 babelCacheKeys: result.cacheKeys
127 });
128 });
129 }
130
131 _precompileHelpers() {
132 this._precompiledHelpers = {};
133
134 // Assumes the tests only load helpers from within the `resolveTestsFrom`
135 // directory. Without arguments this is the `projectDir`, else it's
136 // `process.cwd()` which may be nested too deeply. This will be solved
137 // as we implement RFC 001 and move helper compilation into the worker
138 // processes, avoiding the need for precompilation.
139 return new AvaFiles({cwd: this.options.resolveTestsFrom})
140 .findTestHelpers()
141 .map(file => { // eslint-disable-line array-callback-return
142 const hash = this.precompiler.precompileFile(file);
143 this._precompiledHelpers[file] = hash;
144 });
145 }
146
147 _run(files, options) {
148 options = options || {};
149
150 const runStatus = new RunStatus({
151 runOnlyExclusive: options.runOnlyExclusive,
152 prefixTitles: this.options.explicitTitles || files.length > 1,
153 base: path.relative(process.cwd(), commonPathPrefix(files)) + path.sep,
154 failFast: this.options.failFast,
155 updateSnapshots: options.updateSnapshots
156 });
157
158 this.emit('test-run', runStatus, files);
159
160 if (files.length === 0) {
161 const err = new AvaError('Couldn\'t find any files to test');
162 this._handleError(runStatus, err);
163 return Promise.resolve(runStatus);
164 }
165
166 return this._setupPrecompiler(files)
167 .then(() => this._precompileHelpers())
168 .then(() => {
169 if (this.options.timeout) {
170 this._setupTimeout(runStatus);
171 }
172
173 let concurrency = Math.min(os.cpus().length, isCi ? 2 : Infinity);
174
175 if (this.options.concurrency > 0) {
176 concurrency = this.options.concurrency;
177 }
178
179 if (this.options.serial) {
180 concurrency = 1;
181 }
182
183 return this._runWithPool(files, runStatus, concurrency);
184 });
185 }
186
187 _computeForkExecArgs(files) {
188 const execArgv = this.options.testOnlyExecArgv || process.execArgv;
189 let debugArgIndex = -1;
190
191 // --inspect-brk is used in addition to --inspect to break on first line and wait
192 execArgv.some((arg, index) => {
193 const isDebugArg = /^--inspect(-brk)?($|=)/.test(arg);
194 if (isDebugArg) {
195 debugArgIndex = index;
196 }
197
198 return isDebugArg;
199 });
200
201 const isInspect = debugArgIndex >= 0;
202 if (!isInspect) {
203 execArgv.some((arg, index) => {
204 const isDebugArg = /^--debug(-brk)?($|=)/.test(arg);
205 if (isDebugArg) {
206 debugArgIndex = index;
207 }
208
209 return isDebugArg;
210 });
211 }
212
213 if (debugArgIndex === -1) {
214 return Promise.resolve([]);
215 }
216
217 return Promise
218 .map(files, () => getPort())
219 .map(port => {
220 const forkExecArgv = execArgv.slice();
221 let flagName = isInspect ? '--inspect' : '--debug';
222 const oldValue = forkExecArgv[debugArgIndex];
223 if (oldValue.indexOf('brk') > 0) {
224 flagName += '-brk';
225 }
226
227 forkExecArgv[debugArgIndex] = `${flagName}=${port}`;
228
229 return forkExecArgv;
230 });
231 }
232
233 _handleError(runStatus, err) {
234 runStatus.handleExceptions({
235 exception: err,
236 file: err.file ? path.relative(process.cwd(), err.file) : undefined
237 });
238 }
239
240 _runWithPool(files, runStatus, concurrency) {
241 const tests = [];
242 let execArgvList;
243
244 runStatus.on('timeout', () => {
245 tests.forEach(fork => {
246 fork.exit();
247 });
248 });
249
250 return this._computeForkExecArgs(files)
251 .then(argvList => {
252 execArgvList = argvList;
253 })
254 .return(files)
255 .map((file, index) => {
256 return new Promise(resolve => {
257 const forkArgs = execArgvList[index];
258 const test = this._runFile(file, runStatus, forkArgs);
259 tests.push(test);
260
261 // If we're looking for matches, run every single test process in exclusive-only mode
262 const options = {
263 runOnlyExclusive: this.options.match.length > 0
264 };
265
266 resolve(test.run(options));
267 }).catch(err => {
268 err.file = file;
269 this._handleError(runStatus, err);
270 return getBlankResults();
271 });
272 }, {concurrency})
273 .then(results => {
274 // Filter out undefined results (usually result of caught exceptions)
275 results = results.filter(Boolean);
276
277 // Cancel debounced _onTimeout() from firing
278 if (this.options.timeout) {
279 this._cancelTimeout(runStatus);
280 }
281
282 if (this.options.match.length > 0 && !runStatus.hasExclusive) {
283 results = [];
284
285 const err = new AvaError('Couldn\'t find any matching tests');
286 this._handleError(runStatus, err);
287 }
288
289 runStatus.processResults(results);
290
291 return runStatus;
292 });
293 }
294}
295
296module.exports = Api;