Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80600 views
1
/**
2
* Copyright (c) 2014, Facebook, Inc. All rights reserved.
3
*
4
* This source code is licensed under the BSD-style license found in the
5
* LICENSE file in the root directory of this source tree. An additional grant
6
* of patent rights can be found in the PATENTS file in the same directory.
7
*/
8
'use strict';
9
10
var fs = require('graceful-fs');
11
var os = require('os');
12
var path = require('path');
13
var q = require('q');
14
var through = require('through');
15
var utils = require('./lib/utils');
16
var WorkerPool = require('node-worker-pool');
17
var Console = require('./Console');
18
19
var TEST_WORKER_PATH = require.resolve('./TestWorker');
20
21
var DEFAULT_OPTIONS = {
22
23
/**
24
* When true, runs all tests serially in the current process, rather than
25
* creating a worker pool of child processes.
26
*
27
* This can be useful for debugging, or when the environment limits to a
28
* single process.
29
*/
30
runInBand: false,
31
32
/**
33
* The maximum number of workers to run tests concurrently with.
34
*
35
* It's probably good to keep this at something close to the number of cores
36
* on the machine that's running the test.
37
*/
38
maxWorkers: Math.max(os.cpus().length, 1),
39
40
/**
41
* The path to the executable node binary.
42
*
43
* This is used in the process of booting each of the workers.
44
*/
45
nodePath: process.execPath,
46
47
/**
48
* The args to be passed to the node binary executable.
49
*
50
* this is used in the process of booting each of the workers.
51
*/
52
nodeArgv: process.execArgv.filter(function(arg) {
53
// Passing --debug off to child processes can screw with socket connections
54
// of the parent process.
55
return arg !== '--debug';
56
})
57
};
58
59
var HIDDEN_FILE_RE = /\/\.[^\/]*$/;
60
61
/**
62
* A class that takes a project's test config and provides various utilities for
63
* executing its tests.
64
*
65
* @param config The jest configuration
66
* @param options See DEFAULT_OPTIONS for descriptions on the various options
67
* and their defaults.
68
*/
69
function TestRunner(config, options) {
70
this._config = config;
71
this._configDeps = null;
72
this._moduleLoaderResourceMap = null;
73
this._testPathDirsRegExp = new RegExp(
74
config.testPathDirs
75
.map(function(dir) {
76
return utils.escapeStrForRegex(dir);
77
})
78
.join('|')
79
);
80
81
this._nodeHasteTestRegExp = new RegExp(
82
'/' + utils.escapeStrForRegex(config.testDirectoryName) + '/' +
83
'.*\\.(' +
84
config.testFileExtensions.map(function(ext) {
85
return utils.escapeStrForRegex(ext);
86
})
87
.join('|') +
88
')$'
89
);
90
this._opts = Object.create(DEFAULT_OPTIONS);
91
if (options) {
92
for (var key in options) {
93
this._opts[key] = options[key];
94
}
95
}
96
}
97
98
TestRunner.prototype._constructModuleLoader = function(environment, customCfg) {
99
var config = customCfg || this._config;
100
var ModuleLoader = this._loadConfigDependencies().ModuleLoader;
101
return this._getModuleLoaderResourceMap().then(function(resourceMap) {
102
return new ModuleLoader(config, environment, resourceMap);
103
});
104
};
105
106
TestRunner.prototype._getModuleLoaderResourceMap = function() {
107
var ModuleLoader = this._loadConfigDependencies().ModuleLoader;
108
if (this._moduleLoaderResourceMap === null) {
109
if (this._opts.useCachedModuleLoaderResourceMap) {
110
this._moduleLoaderResourceMap =
111
ModuleLoader.loadResourceMapFromCacheFile(this._config);
112
} else {
113
this._moduleLoaderResourceMap =
114
ModuleLoader.loadResourceMap(this._config);
115
}
116
}
117
return this._moduleLoaderResourceMap;
118
};
119
120
TestRunner.prototype._isTestFilePath = function(filePath) {
121
filePath = utils.pathNormalize(filePath);
122
var testPathIgnorePattern =
123
this._config.testPathIgnorePatterns
124
? new RegExp(this._config.testPathIgnorePatterns.join('|'))
125
: null;
126
127
return (
128
this._nodeHasteTestRegExp.test(filePath)
129
&& !HIDDEN_FILE_RE.test(filePath)
130
&& (!testPathIgnorePattern || !testPathIgnorePattern.test(filePath))
131
&& this._testPathDirsRegExp.test(filePath)
132
);
133
};
134
135
TestRunner.prototype._loadConfigDependencies = function() {
136
var config = this._config;
137
if (this._configDeps === null) {
138
this._configDeps = {
139
ModuleLoader: require(config.moduleLoader),
140
testEnvironment: require(config.testEnvironment),
141
testRunner: require(config.testRunner).bind(null)
142
};
143
}
144
return this._configDeps;
145
};
146
147
/**
148
* Given a list of paths to modules or tests, find all tests that are related to
149
* any of those paths. For a test to be considered "related" to a path, the test
150
* must depend on that path (either directly, or indirectly through one of its
151
* direct dependencies).
152
*
153
* @param {Array<String>} paths A list of path strings to find related tests for
154
* @return {Stream<String>} Stream of absolute path strings
155
*/
156
TestRunner.prototype.streamTestPathsRelatedTo = function(paths) {
157
var pathStream = through(
158
function write(data) {
159
if (data.isError) {
160
this.emit('error', data);
161
this.emit('end');
162
} else {
163
this.emit('data', data);
164
}
165
},
166
function end() {
167
this.emit('end');
168
}
169
);
170
171
var testRunner = this;
172
this._constructModuleLoader().done(function(moduleLoader) {
173
var discoveredModules = {};
174
175
// If a path to a test file is given, make sure we consider that test as
176
// related to itself...
177
//
178
// (If any of the supplied paths aren't tests, it's ok because we filter
179
// non-tests out at the end)
180
paths.forEach(function(path) {
181
discoveredModules[path] = true;
182
if (testRunner._isTestFilePath(path) && fs.existsSync(path)) {
183
pathStream.write(path);
184
}
185
});
186
187
var modulesToSearch = [].concat(paths);
188
while (modulesToSearch.length > 0) {
189
var modulePath = modulesToSearch.shift();
190
var depPaths = moduleLoader.getDependentsFromPath(modulePath);
191
192
/* jshint loopfunc:true */
193
depPaths.forEach(function(depPath) {
194
if (!discoveredModules.hasOwnProperty(depPath)) {
195
discoveredModules[depPath] = true;
196
modulesToSearch.push(depPath);
197
if (testRunner._isTestFilePath(depPath) && fs.existsSync(depPath)) {
198
pathStream.write(depPath);
199
}
200
}
201
});
202
}
203
204
pathStream.end();
205
});
206
207
return pathStream;
208
};
209
210
211
/**
212
* Like `streamTestPathsRelatedTo`, but returns a Promise resolving an array of
213
* all paths.
214
*
215
* @param {Array<String>} paths A list of path strings to find related tests for
216
* @return {Promise<Array<String>>} Promise of array of absolute path strings
217
*/
218
TestRunner.prototype.promiseTestPathsRelatedTo = function(paths) {
219
return _pathStreamToPromise(this.streamTestPathsRelatedTo(paths));
220
};
221
222
/**
223
* Given a path pattern, find all absolute paths for all tests that match the
224
* pattern.
225
*
226
* @param {RegExp} pathPattern
227
* @return {Stream<String>} Stream of absolute path strings
228
*/
229
TestRunner.prototype.streamTestPathsMatching = function(pathPattern) {
230
var pathStream = through(
231
function write(data) {
232
if (data.isError) {
233
this.emit('error', data);
234
this.emit('end');
235
} else {
236
this.emit('data', data);
237
}
238
},
239
function end() {
240
this.emit('end');
241
}
242
);
243
244
this._getModuleLoaderResourceMap().then(function(resourceMap) {
245
var resourcePathMap = resourceMap.resourcePathMap;
246
for (var i in resourcePathMap) {
247
// Sometimes the loader finds a path with no resource. This typically
248
// happens if a file is recently deleted.
249
if (!resourcePathMap[i]) {
250
continue;
251
}
252
253
var pathStr = resourcePathMap[i].path;
254
if (
255
this._isTestFilePath(pathStr) &&
256
pathPattern.test(pathStr)
257
) {
258
pathStream.write(pathStr);
259
}
260
}
261
pathStream.end();
262
}.bind(this));
263
264
265
return pathStream;
266
};
267
268
/**
269
* Like `streamTestPathsMatching`, but returns a Promise resolving an array of
270
* all paths
271
*
272
* @param {RegExp} pathPattern
273
* @return {Promise<Array<String>>} Promise of array of absolute path strings
274
*/
275
TestRunner.prototype.promiseTestPathsMatching = function(pathPattern) {
276
return _pathStreamToPromise(this.streamTestPathsMatching(pathPattern));
277
};
278
279
/**
280
* For use by external users of TestRunner as a means of optimization.
281
*
282
* Imagine the following scenario executing in a child worker process:
283
*
284
* var runner = new TestRunner(config, {
285
* moduleLoaderResourceMap: serializedResourceMap
286
* });
287
* someOtherAyncProcess.then(function() {
288
* runner.runTestsParallel();
289
* });
290
*
291
* Here we wouldn't start deserializing the resource map (passed to us from the
292
* parent) until runner.runTestsParallel() is called. At the time of this
293
* writing, resource map deserialization is slow and a bottleneck on running the
294
* first test in a child.
295
*
296
* So this API gives scenarios such as the one above an optimization path to
297
* potentially start deserializing the resource map while we wait on the
298
* someOtherAsyncProcess to resolve (rather that doing it after it's resolved).
299
*/
300
TestRunner.prototype.preloadResourceMap = function() {
301
this._getModuleLoaderResourceMap().done();
302
};
303
304
TestRunner.prototype.preloadConfigDependencies = function() {
305
this._loadConfigDependencies();
306
};
307
308
/**
309
* Run the given single test file path.
310
* This just contains logic for running a single test given it's file path.
311
*
312
* @param {String} testFilePath
313
* @return {Promise<Object>} Results of the test
314
*/
315
TestRunner.prototype.runTest = function(testFilePath) {
316
// Using Object.create() lets us adjust the config object locally without
317
// worrying about the external consequences of changing the config object for
318
// needs that are local to this particular function call
319
var config = Object.create(this._config);
320
var configDeps = this._loadConfigDependencies();
321
322
var env = new configDeps.testEnvironment(config);
323
var testRunner = configDeps.testRunner;
324
325
// Capture and serialize console.{log|warning|error}s so they can be passed
326
// around (such as through some channel back to a parent process)
327
var consoleMessages = [];
328
env.global.console = new Console(consoleMessages);
329
330
return this._constructModuleLoader(env, config).then(function(moduleLoader) {
331
// This is a kind of janky way to ensure that we only collect coverage
332
// information on modules that are immediate dependencies of the test file.
333
//
334
// Collecting coverage info on more than that is often not useful as
335
// *usually*, when one is looking for coverage info, one is only looking
336
// for coverage info on the files under test. Since a test file is just a
337
// regular old module that can depend on whatever other modules it likes,
338
// it's usually pretty hard to tell which of those dependencies is/are the
339
// "module(s)" under test.
340
//
341
// I'm not super happy with having to inject stuff into the config object
342
// mid-stream here, but it gets the job done.
343
if (config.collectCoverage && !config.collectCoverageOnlyFrom) {
344
config.collectCoverageOnlyFrom = {};
345
moduleLoader.getDependenciesFromPath(testFilePath)
346
.filter(function(depPath) {
347
// Skip over built-in and node modules
348
return /^\//.test(depPath);
349
}).forEach(function(depPath) {
350
config.collectCoverageOnlyFrom[depPath] = true;
351
});
352
}
353
354
if (config.setupEnvScriptFile) {
355
utils.runContentWithLocalBindings(
356
env.runSourceText.bind(env),
357
utils.readAndPreprocessFileContent(config.setupEnvScriptFile, config),
358
config.setupEnvScriptFile,
359
{
360
__dirname: path.dirname(config.setupEnvScriptFile),
361
__filename: config.setupEnvScriptFile,
362
global: env.global,
363
require: moduleLoader.constructBoundRequire(
364
config.setupEnvScriptFile
365
),
366
jest: moduleLoader.getJestRuntime(config.setupEnvScriptFile)
367
}
368
);
369
}
370
371
var testExecStats = {start: Date.now()};
372
return testRunner(config, env, moduleLoader, testFilePath)
373
.then(function(results) {
374
testExecStats.end = Date.now();
375
376
results.logMessages = consoleMessages;
377
results.perfStats = testExecStats;
378
results.testFilePath = testFilePath;
379
results.coverage =
380
config.collectCoverage
381
? moduleLoader.getAllCoverageInfo()
382
: {};
383
384
return results;
385
});
386
}).finally(function() {
387
env.dispose();
388
});
389
};
390
391
/**
392
* Run all given test paths.
393
*
394
* @param {Array<String>} testPaths Array of paths to test files
395
* @param {Object} reporter Collection of callbacks called on test events
396
* @return {Promise<Object>} Fulfilled with information about test run:
397
* success: true if all tests passed
398
* runTime: elapsed time in seconds to run all tests
399
* numTotalTests: total number of tests considered
400
* numPassedTests: number of tests run and passed
401
* numFailedTests: number of tests run and failed
402
* testResults: the jest result info for all tests run
403
*/
404
TestRunner.prototype.runTests = function(testPaths, reporter) {
405
var config = this._config;
406
if (!reporter) {
407
var TestReporter = require(config.testReporter);
408
reporter = new TestReporter();
409
}
410
411
var aggregatedResults = {
412
success: null,
413
runTime: null,
414
numTotalTests: testPaths.length,
415
numPassedTests: 0,
416
numFailedTests: 0,
417
testResults: [],
418
};
419
420
reporter.onRunStart && reporter.onRunStart(config, aggregatedResults);
421
422
var onTestResult = function (testPath, testResult) {
423
aggregatedResults.testResults.push(testResult);
424
if (testResult.numFailingTests > 0) {
425
aggregatedResults.numFailedTests++;
426
} else {
427
aggregatedResults.numPassedTests++;
428
}
429
reporter.onTestResult && reporter.onTestResult(
430
config,
431
testResult,
432
aggregatedResults
433
);
434
};
435
436
var onRunFailure = function (testPath, err) {
437
aggregatedResults.numFailedTests++;
438
reporter.onTestResult && reporter.onTestResult(config, {
439
testFilePath: testPath,
440
testExecError: err,
441
suites: {},
442
tests: {},
443
logMessages: []
444
}, aggregatedResults);
445
};
446
447
var testRun = this._createTestRun(testPaths, onTestResult, onRunFailure);
448
449
var startTime = Date.now();
450
451
return testRun.then(function() {
452
aggregatedResults.runTime = (Date.now() - startTime) / 1000;
453
aggregatedResults.success = aggregatedResults.numFailedTests === 0;
454
reporter.onRunComplete && reporter.onRunComplete(config, aggregatedResults);
455
return aggregatedResults;
456
});
457
};
458
459
TestRunner.prototype._createTestRun = function(
460
testPaths, onTestResult, onRunFailure
461
) {
462
if (this._opts.runInBand || testPaths.length <= 1) {
463
return this._createInBandTestRun(testPaths, onTestResult, onRunFailure);
464
} else {
465
return this._createParallelTestRun(testPaths, onTestResult, onRunFailure);
466
}
467
};
468
469
TestRunner.prototype._createInBandTestRun = function(
470
testPaths, onTestResult, onRunFailure
471
) {
472
var testSequence = q();
473
testPaths.forEach(function(testPath) {
474
testSequence = testSequence.then(this.runTest.bind(this, testPath))
475
.then(function(testResult) {
476
onTestResult(testPath, testResult);
477
})
478
.catch(function(err) {
479
onRunFailure(testPath, err);
480
});
481
}, this);
482
return testSequence;
483
};
484
485
TestRunner.prototype._createParallelTestRun = function(
486
testPaths, onTestResult, onRunFailure
487
) {
488
var workerPool = new WorkerPool(
489
this._opts.maxWorkers,
490
this._opts.nodePath,
491
this._opts.nodeArgv.concat([
492
'--harmony',
493
TEST_WORKER_PATH,
494
'--config=' + JSON.stringify(this._config)
495
])
496
);
497
498
return this._getModuleLoaderResourceMap()
499
.then(function() {
500
return q.all(testPaths.map(function(testPath) {
501
return workerPool.sendMessage({testFilePath: testPath})
502
.then(function(testResult) {
503
onTestResult(testPath, testResult);
504
})
505
.catch(function(err) {
506
onRunFailure(testPath, err);
507
508
// Jest uses regular worker messages to initialize workers, so
509
// there's no way for node-worker-pool to understand how to
510
// recover/re-initialize a child worker that needs to be restarted.
511
// (node-worker-pool can't distinguish between initialization
512
// messages and ephemeral "normal" messages in order to replay the
513
// initialization message upon booting the new, replacement worker
514
// process).
515
//
516
// This is mostly a limitation of node-worker-pool's initialization
517
// features, and ideally it would be possible to recover from a
518
// test that causes a worker process to exit unexpectedly. However,
519
// for now Jest will just fail hard if any child process exits
520
// unexpectedly.
521
//
522
// This will likely bite me in the ass as an unbreak now if we hit
523
// this issue again -- but I guess that's a faster failure than
524
// having Jest just hang forever without any indication as to why.
525
if (err.message
526
&& /Worker process exited before /.test(err.message)) {
527
console.error(
528
'A worker process has quit unexpectedly! This is bad news, ' +
529
'shutting down now!'
530
);
531
process.exit(1);
532
}
533
});
534
}));
535
})
536
.then(function() {
537
return workerPool.destroy();
538
});
539
};
540
541
function _pathStreamToPromise(stream) {
542
var defer = q.defer();
543
var paths = [];
544
stream.on('data', function(path) {
545
paths.push(path);
546
});
547
stream.on('error', function(err) {
548
defer.reject(err);
549
});
550
stream.on('end', function() {
551
defer.resolve(paths);
552
});
553
return defer.promise;
554
}
555
556
557
module.exports = TestRunner;
558
559