Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80629 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 colors = require('./colors');
11
var fs = require('graceful-fs');
12
var path = require('path');
13
var Q = require('q');
14
15
var DEFAULT_CONFIG_VALUES = {
16
cacheDirectory: path.resolve(__dirname, '..', '..', '.haste_cache'),
17
coverageCollector: require.resolve('../IstanbulCollector'),
18
globals: {},
19
moduleFileExtensions: ['js', 'json'],
20
moduleLoader: require.resolve('../HasteModuleLoader/HasteModuleLoader'),
21
preprocessorIgnorePatterns: [],
22
modulePathIgnorePatterns: [],
23
testDirectoryName: '__tests__',
24
testEnvironment: require.resolve('../JSDomEnvironment'),
25
testEnvData: {},
26
testFileExtensions: ['js'],
27
testPathDirs: ['<rootDir>'],
28
testPathIgnorePatterns: ['/node_modules/'],
29
testReporter: require.resolve('../IstanbulTestReporter'),
30
testRunner: require.resolve('../jasmineTestRunner/jasmineTestRunner'),
31
noHighlight: false,
32
};
33
34
function _replaceRootDirTags(rootDir, config) {
35
switch (typeof config) {
36
case 'object':
37
if (config instanceof RegExp) {
38
return config;
39
}
40
41
if (Array.isArray(config)) {
42
return config.map(function(item) {
43
return _replaceRootDirTags(rootDir, item);
44
});
45
}
46
47
if (config !== null) {
48
var newConfig = {};
49
for (var configKey in config) {
50
newConfig[configKey] =
51
configKey === 'rootDir'
52
? config[configKey]
53
: _replaceRootDirTags(rootDir, config[configKey]);
54
}
55
return newConfig;
56
}
57
break;
58
case 'string':
59
if (!/^<rootDir>/.test(config)) {
60
return config;
61
}
62
63
return pathNormalize(path.resolve(
64
rootDir,
65
'./' + path.normalize(config.substr('<rootDir>'.length))
66
));
67
}
68
return config;
69
}
70
71
function escapeStrForRegex(str) {
72
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
73
}
74
75
/**
76
* Given the coverage info for a single file (as output by
77
* CoverageCollector.js), return an array whose entries are bools indicating
78
* whether anything on the line could have been covered and was, or null if the
79
* line wasn't measurable (like empty lines, declaration keywords, etc).
80
*
81
* For example, for the following coverage info:
82
*
83
* COVERED: var a = [];
84
* NO CODE:
85
* COVERED: for (var i = 0; i < a.length; i++)
86
* NOT COVERED: console.log('hai!');
87
*
88
* You'd get an array that looks like this:
89
*
90
* [true, null, true, false]
91
*/
92
function getLineCoverageFromCoverageInfo(coverageInfo) {
93
var coveredLines = {};
94
coverageInfo.coveredSpans.forEach(function(coveredSpan) {
95
var startLine = coveredSpan.start.line;
96
var endLine = coveredSpan.end.line;
97
for (var i = startLine - 1; i < endLine; i++) {
98
coveredLines[i] = true;
99
}
100
});
101
102
var uncoveredLines = {};
103
coverageInfo.uncoveredSpans.forEach(function(uncoveredSpan) {
104
var startLine = uncoveredSpan.start.line;
105
var endLine = uncoveredSpan.end.line;
106
for (var i = startLine - 1; i < endLine; i++) {
107
uncoveredLines[i] = true;
108
}
109
});
110
111
var sourceLines = coverageInfo.sourceText.trim().split('\n');
112
113
return sourceLines.map(function(line, lineIndex) {
114
if (uncoveredLines[lineIndex] === true) {
115
return false;
116
} else if (coveredLines[lineIndex] === true) {
117
return true;
118
} else {
119
return null;
120
}
121
});
122
}
123
124
/**
125
* Given the coverage info for a single file (as output by
126
* CoverageCollector.js), return the decimal percentage of lines in the file
127
* that had any coverage info.
128
*
129
* For example, for the following coverage info:
130
*
131
* COVERED: var a = [];
132
* NO CODE:
133
* COVERED: for (var i = 0; i < a.length; i++)
134
* NOT COVERED: console.log('hai');
135
*
136
* You'd get: 2/3 = 0.666666
137
*/
138
function getLinePercentCoverageFromCoverageInfo(coverageInfo) {
139
var lineCoverage = getLineCoverageFromCoverageInfo(coverageInfo);
140
var numMeasuredLines = 0;
141
var numCoveredLines = lineCoverage.reduce(function(counter, lineIsCovered) {
142
if (lineIsCovered !== null) {
143
numMeasuredLines++;
144
if (lineIsCovered === true) {
145
counter++;
146
}
147
}
148
return counter;
149
}, 0);
150
151
return numCoveredLines / numMeasuredLines;
152
}
153
154
function normalizeConfig(config) {
155
var newConfig = {};
156
157
// Assert that there *is* a rootDir
158
if (!config.hasOwnProperty('rootDir')) {
159
throw new Error('No rootDir config value found!');
160
}
161
162
config.rootDir = pathNormalize(config.rootDir);
163
164
// Normalize user-supplied config options
165
Object.keys(config).reduce(function(newConfig, key) {
166
var value;
167
switch (key) {
168
case 'collectCoverageOnlyFrom':
169
value = Object.keys(config[key]).reduce(function(normObj, filePath) {
170
filePath = pathNormalize(path.resolve(
171
config.rootDir,
172
_replaceRootDirTags(config.rootDir, filePath)
173
));
174
normObj[filePath] = true;
175
return normObj;
176
}, {});
177
break;
178
179
case 'testPathDirs':
180
value = config[key].map(function(scanDir) {
181
return pathNormalize(path.resolve(
182
config.rootDir,
183
_replaceRootDirTags(config.rootDir, scanDir)
184
));
185
});
186
break;
187
188
case 'cacheDirectory':
189
case 'scriptPreprocessor':
190
case 'setupEnvScriptFile':
191
case 'setupTestFrameworkScriptFile':
192
value = pathNormalize(path.resolve(
193
config.rootDir,
194
_replaceRootDirTags(config.rootDir, config[key])
195
));
196
break;
197
198
case 'preprocessorIgnorePatterns':
199
case 'testPathIgnorePatterns':
200
case 'modulePathIgnorePatterns':
201
case 'unmockedModulePathPatterns':
202
// _replaceRootDirTags is specifically well-suited for substituting
203
// <rootDir> in paths (it deals with properly interpreting relative path
204
// separators, etc).
205
//
206
// For patterns, direct global substitution is far more ideal, so we
207
// special case substitutions for patterns here.
208
value = config[key].map(function(pattern) {
209
return pattern.replace(/<rootDir>/g, config.rootDir);
210
});
211
break;
212
213
case 'collectCoverage':
214
case 'coverageCollector':
215
case 'globals':
216
case 'moduleLoader':
217
case 'name':
218
case 'persistModuleRegistryBetweenSpecs':
219
case 'rootDir':
220
case 'setupJSLoaderOptions':
221
case 'setupJSTestLoaderOptions':
222
case 'setupJSMockLoaderOptions':
223
case 'testDirectoryName':
224
case 'testEnvData':
225
case 'testFileExtensions':
226
case 'testReporter':
227
case 'moduleFileExtensions':
228
case 'noHighlight':
229
value = config[key];
230
break;
231
232
default:
233
throw new Error('Unknown config option: ' + key);
234
}
235
newConfig[key] = value;
236
return newConfig;
237
}, newConfig);
238
239
// If any config entries weren't specified but have default values, apply the
240
// default values
241
Object.keys(DEFAULT_CONFIG_VALUES).reduce(function(newConfig, key) {
242
if (!newConfig[key]) {
243
newConfig[key] = DEFAULT_CONFIG_VALUES[key];
244
}
245
return newConfig;
246
}, newConfig);
247
248
// Fill in some default values for node-haste config
249
newConfig.setupJSLoaderOptions = newConfig.setupJSLoaderOptions || {};
250
newConfig.setupJSTestLoaderOptions = newConfig.setupJSTestLoaderOptions || {};
251
newConfig.setupJSMockLoaderOptions = newConfig.setupJSMockLoaderOptions || {};
252
253
if (!newConfig.setupJSTestLoaderOptions.extensions) {
254
newConfig.setupJSTestLoaderOptions.extensions =
255
newConfig.testFileExtensions.map(_addDot);
256
}
257
258
if (!newConfig.setupJSLoaderOptions.extensions) {
259
newConfig.setupJSLoaderOptions.extensions = uniqueStrings(
260
newConfig.moduleFileExtensions.map(_addDot).concat(
261
newConfig.setupJSTestLoaderOptions.extensions
262
)
263
);
264
}
265
266
if (!newConfig.setupJSMockLoaderOptions.extensions) {
267
newConfig.setupJSMockLoaderOptions.extensions =
268
newConfig.setupJSLoaderOptions.extensions;
269
}
270
271
return _replaceRootDirTags(newConfig.rootDir, newConfig);
272
}
273
274
function _addDot(ext) {
275
return '.' + ext;
276
}
277
278
function uniqueStrings(set) {
279
var newSet = [];
280
var has = {};
281
set.forEach(function (item) {
282
if (!has[item]) {
283
has[item] = true;
284
newSet.push(item);
285
}
286
});
287
return newSet;
288
}
289
290
function pathNormalize(dir) {
291
return path.normalize(dir.replace(/\\/g, '/')).replace(/\\/g, '/');
292
}
293
294
function loadConfigFromFile(filePath) {
295
var fileDir = path.dirname(filePath);
296
return Q.nfcall(fs.readFile, filePath, 'utf8').then(function(fileData) {
297
var config = JSON.parse(fileData);
298
if (!config.hasOwnProperty('rootDir')) {
299
config.rootDir = fileDir;
300
} else {
301
config.rootDir = path.resolve(fileDir, config.rootDir);
302
}
303
return normalizeConfig(config);
304
});
305
}
306
307
function loadConfigFromPackageJson(filePath) {
308
var pkgJsonDir = path.dirname(filePath);
309
return Q.nfcall(fs.readFile, filePath, 'utf8').then(function(fileData) {
310
var packageJsonData = JSON.parse(fileData);
311
var config = packageJsonData.jest;
312
config.name = packageJsonData.name;
313
if (!config.hasOwnProperty('rootDir')) {
314
config.rootDir = pkgJsonDir;
315
} else {
316
config.rootDir = path.resolve(pkgJsonDir, config.rootDir);
317
}
318
return normalizeConfig(config);
319
});
320
}
321
322
var _contentCache = {};
323
function readAndPreprocessFileContent(filePath, config) {
324
var cacheRec;
325
var mtime = fs.statSync(filePath).mtime;
326
if (_contentCache.hasOwnProperty(filePath)) {
327
cacheRec = _contentCache[filePath];
328
if (cacheRec.mtime.getTime() === mtime.getTime()) {
329
return cacheRec.content;
330
}
331
}
332
333
var fileData = fs.readFileSync(filePath, 'utf8');
334
335
// If the file data starts with a shebang remove it (but leave the line empty
336
// to keep stack trace line numbers correct)
337
if (fileData.substr(0, 2) === '#!') {
338
fileData = fileData.replace(/^#!.*/, '');
339
}
340
341
if (config.scriptPreprocessor &&
342
!config.preprocessorIgnorePatterns.some(function(pattern) {
343
return pattern.test(filePath);
344
})) {
345
try {
346
fileData = require(config.scriptPreprocessor).process(
347
fileData,
348
filePath,
349
{}, // options
350
[], // excludes
351
config
352
);
353
} catch (e) {
354
e.message = config.scriptPreprocessor + ': ' + e.message;
355
throw e;
356
}
357
}
358
_contentCache[filePath] = cacheRec = {mtime: mtime, content: fileData};
359
return cacheRec.content;
360
}
361
362
function runContentWithLocalBindings(contextRunner, scriptContent, scriptPath,
363
bindings) {
364
var boundIdents = Object.keys(bindings);
365
try {
366
var wrapperFunc = contextRunner(
367
'(function(' + boundIdents.join(',') + '){' +
368
scriptContent +
369
'\n})',
370
scriptPath
371
);
372
} catch (e) {
373
e.message = scriptPath + ': ' + e.message;
374
throw e;
375
}
376
377
var bindingValues = boundIdents.map(function(ident) {
378
return bindings[ident];
379
});
380
381
try {
382
wrapperFunc.apply(null, bindingValues);
383
} catch (e) {
384
e.message = scriptPath + ': ' + e.message;
385
throw e;
386
}
387
}
388
389
/**
390
* Given a test result, return a human readable string representing the
391
* failures.
392
*
393
* @param {Object} testResult
394
* @param {boolean} color true if message should include color flags
395
* @return {String}
396
*/
397
function formatFailureMessage(testResult, color) {
398
var colorize = color ? colors.colorize : function (str) { return str; };
399
var ancestrySeparator = ' \u203A ';
400
var descBullet = colorize('\u25cf ', colors.BOLD);
401
var msgBullet = ' - ';
402
var msgIndent = msgBullet.replace(/./g, ' ');
403
404
return testResult.testResults.filter(function (result) {
405
return result.failureMessages.length !== 0;
406
}).map(function(result) {
407
var failureMessages = result.failureMessages.map(function (errorMsg) {
408
// Filter out q and jasmine entries from the stack trace.
409
// They're super noisy and unhelpful
410
errorMsg = errorMsg.split('\n').filter(function(line) {
411
if (/^\s+at .*?/.test(line)) {
412
// Extract the file path from the trace line
413
var filePath = line.match(/(?:\(|at (?=\/))(.*):[0-9]+:[0-9]+\)?$/);
414
if (filePath
415
&& STACK_TRACE_LINE_IGNORE_RE.test(filePath[1])) {
416
return false;
417
}
418
}
419
return true;
420
}).join('\n');
421
422
return msgBullet + errorMsg.replace(/\n/g, '\n' + msgIndent);
423
}).join('\n');
424
425
var testTitleAncestry = result.ancestorTitles.map(function(title) {
426
return colorize(title, colors.BOLD);
427
}).join(ancestrySeparator) + ancestrySeparator;
428
429
return descBullet + testTitleAncestry + result.title + '\n' +
430
failureMessages;
431
}).join('\n');
432
}
433
434
// A RegExp that matches paths that should not be included in error stack traces
435
// (mostly because these paths represent noisy/unhelpful libs)
436
var STACK_TRACE_LINE_IGNORE_RE = new RegExp('^(?:' + [
437
path.resolve(__dirname, '..', 'node_modules', 'q'),
438
path.resolve(__dirname, '..', 'vendor', 'jasmine')
439
].join('|') + ')');
440
441
442
exports.escapeStrForRegex = escapeStrForRegex;
443
exports.getLineCoverageFromCoverageInfo = getLineCoverageFromCoverageInfo;
444
exports.getLinePercentCoverageFromCoverageInfo =
445
getLinePercentCoverageFromCoverageInfo;
446
exports.loadConfigFromFile = loadConfigFromFile;
447
exports.loadConfigFromPackageJson = loadConfigFromPackageJson;
448
exports.normalizeConfig = normalizeConfig;
449
exports.pathNormalize = pathNormalize;
450
exports.readAndPreprocessFileContent = readAndPreprocessFileContent;
451
exports.runContentWithLocalBindings = runContentWithLocalBindings;
452
exports.formatFailureMessage = formatFailureMessage;
453
454