1 | 'use strict';
|
2 | const EventEmitter = require('events');
|
3 | const path = require('path');
|
4 | const fs = require('fs');
|
5 | const os = require('os');
|
6 | const commonPathPrefix = require('common-path-prefix');
|
7 | const uniqueTempDir = require('unique-temp-dir');
|
8 | const findCacheDir = require('find-cache-dir');
|
9 | const isCi = require('is-ci');
|
10 | const resolveCwd = require('resolve-cwd');
|
11 | const debounce = require('lodash.debounce');
|
12 | const autoBind = require('auto-bind');
|
13 | const Promise = require('bluebird');
|
14 | const getPort = require('get-port');
|
15 | const arrify = require('arrify');
|
16 | const ms = require('ms');
|
17 | const babelConfigHelper = require('./lib/babel-config');
|
18 | const CachingPrecompiler = require('./lib/caching-precompiler');
|
19 | const RunStatus = require('./lib/run-status');
|
20 | const AvaError = require('./lib/ava-error');
|
21 | const AvaFiles = require('./lib/ava-files');
|
22 | const fork = require('./lib/fork');
|
23 |
|
24 | function 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 |
|
36 | function 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 |
|
50 | class 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 |
|
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 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | return new AvaFiles({cwd: this.options.resolveTestsFrom})
|
140 | .findTestHelpers()
|
141 | .map(file => {
|
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 |
|
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 |
|
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 |
|
275 | results = results.filter(Boolean);
|
276 |
|
277 |
|
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 |
|
296 | module.exports = Api;
|