UNPKG

18.3 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true
5});
6exports.default = void 0;
7function path() {
8 const data = _interopRequireWildcard(require('path'));
9 path = function () {
10 return data;
11 };
12 return data;
13}
14function _v8Coverage() {
15 const data = require('@bcoe/v8-coverage');
16 _v8Coverage = function () {
17 return data;
18 };
19 return data;
20}
21function _chalk() {
22 const data = _interopRequireDefault(require('chalk'));
23 _chalk = function () {
24 return data;
25 };
26 return data;
27}
28function _glob() {
29 const data = _interopRequireDefault(require('glob'));
30 _glob = function () {
31 return data;
32 };
33 return data;
34}
35function fs() {
36 const data = _interopRequireWildcard(require('graceful-fs'));
37 fs = function () {
38 return data;
39 };
40 return data;
41}
42function _istanbulLibCoverage() {
43 const data = _interopRequireDefault(require('istanbul-lib-coverage'));
44 _istanbulLibCoverage = function () {
45 return data;
46 };
47 return data;
48}
49function _istanbulLibReport() {
50 const data = _interopRequireDefault(require('istanbul-lib-report'));
51 _istanbulLibReport = function () {
52 return data;
53 };
54 return data;
55}
56function _istanbulLibSourceMaps() {
57 const data = _interopRequireDefault(require('istanbul-lib-source-maps'));
58 _istanbulLibSourceMaps = function () {
59 return data;
60 };
61 return data;
62}
63function _istanbulReports() {
64 const data = _interopRequireDefault(require('istanbul-reports'));
65 _istanbulReports = function () {
66 return data;
67 };
68 return data;
69}
70function _v8ToIstanbul() {
71 const data = _interopRequireDefault(require('v8-to-istanbul'));
72 _v8ToIstanbul = function () {
73 return data;
74 };
75 return data;
76}
77function _jestUtil() {
78 const data = require('jest-util');
79 _jestUtil = function () {
80 return data;
81 };
82 return data;
83}
84function _jestWorker() {
85 const data = require('jest-worker');
86 _jestWorker = function () {
87 return data;
88 };
89 return data;
90}
91var _BaseReporter = _interopRequireDefault(require('./BaseReporter'));
92var _getWatermarks = _interopRequireDefault(require('./getWatermarks'));
93function _interopRequireDefault(obj) {
94 return obj && obj.__esModule ? obj : {default: obj};
95}
96function _getRequireWildcardCache(nodeInterop) {
97 if (typeof WeakMap !== 'function') return null;
98 var cacheBabelInterop = new WeakMap();
99 var cacheNodeInterop = new WeakMap();
100 return (_getRequireWildcardCache = function (nodeInterop) {
101 return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
102 })(nodeInterop);
103}
104function _interopRequireWildcard(obj, nodeInterop) {
105 if (!nodeInterop && obj && obj.__esModule) {
106 return obj;
107 }
108 if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
109 return {default: obj};
110 }
111 var cache = _getRequireWildcardCache(nodeInterop);
112 if (cache && cache.has(obj)) {
113 return cache.get(obj);
114 }
115 var newObj = {};
116 var hasPropertyDescriptor =
117 Object.defineProperty && Object.getOwnPropertyDescriptor;
118 for (var key in obj) {
119 if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
120 var desc = hasPropertyDescriptor
121 ? Object.getOwnPropertyDescriptor(obj, key)
122 : null;
123 if (desc && (desc.get || desc.set)) {
124 Object.defineProperty(newObj, key, desc);
125 } else {
126 newObj[key] = obj[key];
127 }
128 }
129 }
130 newObj.default = obj;
131 if (cache) {
132 cache.set(obj, newObj);
133 }
134 return newObj;
135}
136/**
137 * Copyright (c) Meta Platforms, Inc. and affiliates.
138 *
139 * This source code is licensed under the MIT license found in the
140 * LICENSE file in the root directory of this source tree.
141 */
142
143const FAIL_COLOR = _chalk().default.bold.red;
144const RUNNING_TEST_COLOR = _chalk().default.bold.dim;
145class CoverageReporter extends _BaseReporter.default {
146 _context;
147 _coverageMap;
148 _globalConfig;
149 _sourceMapStore;
150 _v8CoverageResults;
151 static filename = __filename;
152 constructor(globalConfig, context) {
153 super();
154 this._context = context;
155 this._coverageMap = _istanbulLibCoverage().default.createCoverageMap({});
156 this._globalConfig = globalConfig;
157 this._sourceMapStore =
158 _istanbulLibSourceMaps().default.createSourceMapStore();
159 this._v8CoverageResults = [];
160 }
161 onTestResult(_test, testResult) {
162 if (testResult.v8Coverage) {
163 this._v8CoverageResults.push(testResult.v8Coverage);
164 return;
165 }
166 if (testResult.coverage) {
167 this._coverageMap.merge(testResult.coverage);
168 }
169 }
170 async onRunComplete(testContexts, aggregatedResults) {
171 await this._addUntestedFiles(testContexts);
172 const {map, reportContext} = await this._getCoverageResult();
173 try {
174 const coverageReporters = this._globalConfig.coverageReporters || [];
175 if (!this._globalConfig.useStderr && coverageReporters.length < 1) {
176 coverageReporters.push('text-summary');
177 }
178 coverageReporters.forEach(reporter => {
179 let additionalOptions = {};
180 if (Array.isArray(reporter)) {
181 [reporter, additionalOptions] = reporter;
182 }
183 _istanbulReports()
184 .default.create(reporter, {
185 maxCols: process.stdout.columns || Infinity,
186 ...additionalOptions
187 })
188 .execute(reportContext);
189 });
190 aggregatedResults.coverageMap = map;
191 } catch (e) {
192 console.error(
193 _chalk().default.red(`
194 Failed to write coverage reports:
195 ERROR: ${e.toString()}
196 STACK: ${e.stack}
197 `)
198 );
199 }
200 this._checkThreshold(map);
201 }
202 async _addUntestedFiles(testContexts) {
203 const files = [];
204 testContexts.forEach(context => {
205 const config = context.config;
206 if (
207 this._globalConfig.collectCoverageFrom &&
208 this._globalConfig.collectCoverageFrom.length
209 ) {
210 context.hasteFS
211 .matchFilesWithGlob(
212 this._globalConfig.collectCoverageFrom,
213 config.rootDir
214 )
215 .forEach(filePath =>
216 files.push({
217 config,
218 path: filePath
219 })
220 );
221 }
222 });
223 if (!files.length) {
224 return;
225 }
226 if (_jestUtil().isInteractive) {
227 process.stderr.write(
228 RUNNING_TEST_COLOR('Running coverage on untested files...')
229 );
230 }
231 let worker;
232 if (this._globalConfig.maxWorkers <= 1) {
233 worker = require('./CoverageWorker');
234 } else {
235 worker = new (_jestWorker().Worker)(require.resolve('./CoverageWorker'), {
236 enableWorkerThreads: this._globalConfig.workerThreads,
237 exposedMethods: ['worker'],
238 forkOptions: {
239 serialization: 'json'
240 },
241 maxRetries: 2,
242 numWorkers: this._globalConfig.maxWorkers
243 });
244 }
245 const instrumentation = files.map(async fileObj => {
246 const filename = fileObj.path;
247 const config = fileObj.config;
248 const hasCoverageData = this._v8CoverageResults.some(v8Res =>
249 v8Res.some(innerRes => innerRes.result.url === filename)
250 );
251 if (
252 !hasCoverageData &&
253 !this._coverageMap.data[filename] &&
254 'worker' in worker
255 ) {
256 try {
257 const result = await worker.worker({
258 config,
259 context: {
260 changedFiles:
261 this._context.changedFiles &&
262 Array.from(this._context.changedFiles),
263 sourcesRelatedToTestsInChangedFiles:
264 this._context.sourcesRelatedToTestsInChangedFiles &&
265 Array.from(this._context.sourcesRelatedToTestsInChangedFiles)
266 },
267 globalConfig: this._globalConfig,
268 path: filename
269 });
270 if (result) {
271 if (result.kind === 'V8Coverage') {
272 this._v8CoverageResults.push([
273 {
274 codeTransformResult: undefined,
275 result: result.result
276 }
277 ]);
278 } else {
279 this._coverageMap.addFileCoverage(result.coverage);
280 }
281 }
282 } catch (error) {
283 console.error(
284 _chalk().default.red(
285 [
286 `Failed to collect coverage from ${filename}`,
287 `ERROR: ${error.message}`,
288 `STACK: ${error.stack}`
289 ].join('\n')
290 )
291 );
292 }
293 }
294 });
295 try {
296 await Promise.all(instrumentation);
297 } catch {
298 // Do nothing; errors were reported earlier to the console.
299 }
300 if (_jestUtil().isInteractive) {
301 (0, _jestUtil().clearLine)(process.stderr);
302 }
303 if (worker && 'end' in worker && typeof worker.end === 'function') {
304 await worker.end();
305 }
306 }
307 _checkThreshold(map) {
308 const {coverageThreshold} = this._globalConfig;
309 if (coverageThreshold) {
310 function check(name, thresholds, actuals) {
311 return ['statements', 'branches', 'lines', 'functions'].reduce(
312 (errors, key) => {
313 const actual = actuals[key].pct;
314 const actualUncovered = actuals[key].total - actuals[key].covered;
315 const threshold = thresholds[key];
316 if (threshold !== undefined) {
317 if (threshold < 0) {
318 if (threshold * -1 < actualUncovered) {
319 errors.push(
320 `Jest: Uncovered count for ${key} (${actualUncovered}) ` +
321 `exceeds ${name} threshold (${-1 * threshold})`
322 );
323 }
324 } else if (actual < threshold) {
325 errors.push(
326 `Jest: "${name}" coverage threshold for ${key} (${threshold}%) not met: ${actual}%`
327 );
328 }
329 }
330 return errors;
331 },
332 []
333 );
334 }
335 const THRESHOLD_GROUP_TYPES = {
336 GLOB: 'glob',
337 GLOBAL: 'global',
338 PATH: 'path'
339 };
340 const coveredFiles = map.files();
341 const thresholdGroups = Object.keys(coverageThreshold);
342 const groupTypeByThresholdGroup = {};
343 const filesByGlob = {};
344 const coveredFilesSortedIntoThresholdGroup = coveredFiles.reduce(
345 (files, file) => {
346 const pathOrGlobMatches = thresholdGroups.reduce(
347 (agg, thresholdGroup) => {
348 // Preserve trailing slash, but not required if root dir
349 // See https://github.com/facebook/jest/issues/12703
350 const resolvedThresholdGroup = path().resolve(thresholdGroup);
351 const suffix =
352 (thresholdGroup.endsWith(path().sep) ||
353 (process.platform === 'win32' &&
354 thresholdGroup.endsWith('/'))) &&
355 !resolvedThresholdGroup.endsWith(path().sep)
356 ? path().sep
357 : '';
358 const absoluteThresholdGroup = `${resolvedThresholdGroup}${suffix}`;
359
360 // The threshold group might be a path:
361
362 if (file.indexOf(absoluteThresholdGroup) === 0) {
363 groupTypeByThresholdGroup[thresholdGroup] =
364 THRESHOLD_GROUP_TYPES.PATH;
365 return agg.concat([[file, thresholdGroup]]);
366 }
367
368 // If the threshold group is not a path it might be a glob:
369
370 // Note: glob.sync is slow. By memoizing the files matching each glob
371 // (rather than recalculating it for each covered file) we save a tonne
372 // of execution time.
373 if (filesByGlob[absoluteThresholdGroup] === undefined) {
374 filesByGlob[absoluteThresholdGroup] = _glob()
375 .default.sync(absoluteThresholdGroup)
376 .map(filePath => path().resolve(filePath));
377 }
378 if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) {
379 groupTypeByThresholdGroup[thresholdGroup] =
380 THRESHOLD_GROUP_TYPES.GLOB;
381 return agg.concat([[file, thresholdGroup]]);
382 }
383 return agg;
384 },
385 []
386 );
387 if (pathOrGlobMatches.length > 0) {
388 return files.concat(pathOrGlobMatches);
389 }
390
391 // Neither a glob or a path? Toss it in global if there's a global threshold:
392 if (thresholdGroups.indexOf(THRESHOLD_GROUP_TYPES.GLOBAL) > -1) {
393 groupTypeByThresholdGroup[THRESHOLD_GROUP_TYPES.GLOBAL] =
394 THRESHOLD_GROUP_TYPES.GLOBAL;
395 return files.concat([[file, THRESHOLD_GROUP_TYPES.GLOBAL]]);
396 }
397
398 // A covered file that doesn't have a threshold:
399 return files.concat([[file, undefined]]);
400 },
401 []
402 );
403 const getFilesInThresholdGroup = thresholdGroup =>
404 coveredFilesSortedIntoThresholdGroup
405 .filter(fileAndGroup => fileAndGroup[1] === thresholdGroup)
406 .map(fileAndGroup => fileAndGroup[0]);
407 function combineCoverage(filePaths) {
408 return filePaths
409 .map(filePath => map.fileCoverageFor(filePath))
410 .reduce((combinedCoverage, nextFileCoverage) => {
411 if (combinedCoverage === undefined || combinedCoverage === null) {
412 return nextFileCoverage.toSummary();
413 }
414 return combinedCoverage.merge(nextFileCoverage.toSummary());
415 }, undefined);
416 }
417 let errors = [];
418 thresholdGroups.forEach(thresholdGroup => {
419 switch (groupTypeByThresholdGroup[thresholdGroup]) {
420 case THRESHOLD_GROUP_TYPES.GLOBAL: {
421 const coverage = combineCoverage(
422 getFilesInThresholdGroup(THRESHOLD_GROUP_TYPES.GLOBAL)
423 );
424 if (coverage) {
425 errors = errors.concat(
426 check(
427 thresholdGroup,
428 coverageThreshold[thresholdGroup],
429 coverage
430 )
431 );
432 }
433 break;
434 }
435 case THRESHOLD_GROUP_TYPES.PATH: {
436 const coverage = combineCoverage(
437 getFilesInThresholdGroup(thresholdGroup)
438 );
439 if (coverage) {
440 errors = errors.concat(
441 check(
442 thresholdGroup,
443 coverageThreshold[thresholdGroup],
444 coverage
445 )
446 );
447 }
448 break;
449 }
450 case THRESHOLD_GROUP_TYPES.GLOB:
451 getFilesInThresholdGroup(thresholdGroup).forEach(
452 fileMatchingGlob => {
453 errors = errors.concat(
454 check(
455 fileMatchingGlob,
456 coverageThreshold[thresholdGroup],
457 map.fileCoverageFor(fileMatchingGlob).toSummary()
458 )
459 );
460 }
461 );
462 break;
463 default:
464 // If the file specified by path is not found, error is returned.
465 if (thresholdGroup !== THRESHOLD_GROUP_TYPES.GLOBAL) {
466 errors = errors.concat(
467 `Jest: Coverage data for ${thresholdGroup} was not found.`
468 );
469 }
470 // Sometimes all files in the coverage data are matched by
471 // PATH and GLOB threshold groups in which case, don't error when
472 // the global threshold group doesn't match any files.
473 }
474 });
475
476 errors = errors.filter(
477 err => err !== undefined && err !== null && err.length > 0
478 );
479 if (errors.length > 0) {
480 this.log(`${FAIL_COLOR(errors.join('\n'))}`);
481 this._setError(new Error(errors.join('\n')));
482 }
483 }
484 }
485 async _getCoverageResult() {
486 if (this._globalConfig.coverageProvider === 'v8') {
487 const mergedCoverages = (0, _v8Coverage().mergeProcessCovs)(
488 this._v8CoverageResults.map(cov => ({
489 result: cov.map(r => r.result)
490 }))
491 );
492 const fileTransforms = new Map();
493 this._v8CoverageResults.forEach(res =>
494 res.forEach(r => {
495 if (r.codeTransformResult && !fileTransforms.has(r.result.url)) {
496 fileTransforms.set(r.result.url, r.codeTransformResult);
497 }
498 })
499 );
500 const transformedCoverage = await Promise.all(
501 mergedCoverages.result.map(async res => {
502 const fileTransform = fileTransforms.get(res.url);
503 let sourcemapContent = undefined;
504 if (
505 fileTransform?.sourceMapPath &&
506 fs().existsSync(fileTransform.sourceMapPath)
507 ) {
508 sourcemapContent = JSON.parse(
509 fs().readFileSync(fileTransform.sourceMapPath, 'utf8')
510 );
511 }
512 const converter = (0, _v8ToIstanbul().default)(
513 res.url,
514 fileTransform?.wrapperLength ?? 0,
515 fileTransform && sourcemapContent
516 ? {
517 originalSource: fileTransform.originalCode,
518 source: fileTransform.code,
519 sourceMap: {
520 sourcemap: {
521 file: res.url,
522 ...sourcemapContent
523 }
524 }
525 }
526 : {
527 source: fs().readFileSync(res.url, 'utf8')
528 }
529 );
530 await converter.load();
531 converter.applyCoverage(res.functions);
532 const istanbulData = converter.toIstanbul();
533 return istanbulData;
534 })
535 );
536 const map = _istanbulLibCoverage().default.createCoverageMap({});
537 transformedCoverage.forEach(res => map.merge(res));
538 const reportContext = _istanbulLibReport().default.createContext({
539 coverageMap: map,
540 dir: this._globalConfig.coverageDirectory,
541 watermarks: (0, _getWatermarks.default)(this._globalConfig)
542 });
543 return {
544 map,
545 reportContext
546 };
547 }
548 const map = await this._sourceMapStore.transformCoverage(this._coverageMap);
549 const reportContext = _istanbulLibReport().default.createContext({
550 coverageMap: map,
551 dir: this._globalConfig.coverageDirectory,
552 sourceFinder: this._sourceMapStore.sourceFinder,
553 watermarks: (0, _getWatermarks.default)(this._globalConfig)
554 });
555 return {
556 map,
557 reportContext
558 };
559 }
560}
561exports.default = CoverageReporter;