562 lines
18 KiB
JavaScript
562 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', {
|
|
value: true
|
|
});
|
|
exports.default = void 0;
|
|
function path() {
|
|
const data = _interopRequireWildcard(require('path'));
|
|
path = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _v8Coverage() {
|
|
const data = require('@bcoe/v8-coverage');
|
|
_v8Coverage = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _chalk() {
|
|
const data = _interopRequireDefault(require('chalk'));
|
|
_chalk = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _glob() {
|
|
const data = _interopRequireDefault(require('glob'));
|
|
_glob = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function fs() {
|
|
const data = _interopRequireWildcard(require('graceful-fs'));
|
|
fs = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _istanbulLibCoverage() {
|
|
const data = _interopRequireDefault(require('istanbul-lib-coverage'));
|
|
_istanbulLibCoverage = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _istanbulLibReport() {
|
|
const data = _interopRequireDefault(require('istanbul-lib-report'));
|
|
_istanbulLibReport = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _istanbulLibSourceMaps() {
|
|
const data = _interopRequireDefault(require('istanbul-lib-source-maps'));
|
|
_istanbulLibSourceMaps = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _istanbulReports() {
|
|
const data = _interopRequireDefault(require('istanbul-reports'));
|
|
_istanbulReports = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _v8ToIstanbul() {
|
|
const data = _interopRequireDefault(require('v8-to-istanbul'));
|
|
_v8ToIstanbul = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _jestUtil() {
|
|
const data = require('jest-util');
|
|
_jestUtil = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
function _jestWorker() {
|
|
const data = require('jest-worker');
|
|
_jestWorker = function () {
|
|
return data;
|
|
};
|
|
return data;
|
|
}
|
|
var _BaseReporter = _interopRequireDefault(require('./BaseReporter'));
|
|
var _getWatermarks = _interopRequireDefault(require('./getWatermarks'));
|
|
function _interopRequireDefault(obj) {
|
|
return obj && obj.__esModule ? obj : {default: obj};
|
|
}
|
|
function _getRequireWildcardCache(nodeInterop) {
|
|
if (typeof WeakMap !== 'function') return null;
|
|
var cacheBabelInterop = new WeakMap();
|
|
var cacheNodeInterop = new WeakMap();
|
|
return (_getRequireWildcardCache = function (nodeInterop) {
|
|
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
})(nodeInterop);
|
|
}
|
|
function _interopRequireWildcard(obj, nodeInterop) {
|
|
if (!nodeInterop && obj && obj.__esModule) {
|
|
return obj;
|
|
}
|
|
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
|
return {default: obj};
|
|
}
|
|
var cache = _getRequireWildcardCache(nodeInterop);
|
|
if (cache && cache.has(obj)) {
|
|
return cache.get(obj);
|
|
}
|
|
var newObj = {};
|
|
var hasPropertyDescriptor =
|
|
Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
for (var key in obj) {
|
|
if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
var desc = hasPropertyDescriptor
|
|
? Object.getOwnPropertyDescriptor(obj, key)
|
|
: null;
|
|
if (desc && (desc.get || desc.set)) {
|
|
Object.defineProperty(newObj, key, desc);
|
|
} else {
|
|
newObj[key] = obj[key];
|
|
}
|
|
}
|
|
}
|
|
newObj.default = obj;
|
|
if (cache) {
|
|
cache.set(obj, newObj);
|
|
}
|
|
return newObj;
|
|
}
|
|
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
const FAIL_COLOR = _chalk().default.bold.red;
|
|
const RUNNING_TEST_COLOR = _chalk().default.bold.dim;
|
|
class CoverageReporter extends _BaseReporter.default {
|
|
_context;
|
|
_coverageMap;
|
|
_globalConfig;
|
|
_sourceMapStore;
|
|
_v8CoverageResults;
|
|
static filename = __filename;
|
|
constructor(globalConfig, context) {
|
|
super();
|
|
this._context = context;
|
|
this._coverageMap = _istanbulLibCoverage().default.createCoverageMap({});
|
|
this._globalConfig = globalConfig;
|
|
this._sourceMapStore =
|
|
_istanbulLibSourceMaps().default.createSourceMapStore();
|
|
this._v8CoverageResults = [];
|
|
}
|
|
onTestResult(_test, testResult) {
|
|
if (testResult.v8Coverage) {
|
|
this._v8CoverageResults.push(testResult.v8Coverage);
|
|
return;
|
|
}
|
|
if (testResult.coverage) {
|
|
this._coverageMap.merge(testResult.coverage);
|
|
}
|
|
}
|
|
async onRunComplete(testContexts, aggregatedResults) {
|
|
await this._addUntestedFiles(testContexts);
|
|
const {map, reportContext} = await this._getCoverageResult();
|
|
try {
|
|
const coverageReporters = this._globalConfig.coverageReporters || [];
|
|
if (!this._globalConfig.useStderr && coverageReporters.length < 1) {
|
|
coverageReporters.push('text-summary');
|
|
}
|
|
coverageReporters.forEach(reporter => {
|
|
let additionalOptions = {};
|
|
if (Array.isArray(reporter)) {
|
|
[reporter, additionalOptions] = reporter;
|
|
}
|
|
_istanbulReports()
|
|
.default.create(reporter, {
|
|
maxCols: process.stdout.columns || Infinity,
|
|
...additionalOptions
|
|
})
|
|
.execute(reportContext);
|
|
});
|
|
aggregatedResults.coverageMap = map;
|
|
} catch (e) {
|
|
console.error(
|
|
_chalk().default.red(`
|
|
Failed to write coverage reports:
|
|
ERROR: ${e.toString()}
|
|
STACK: ${e.stack}
|
|
`)
|
|
);
|
|
}
|
|
this._checkThreshold(map);
|
|
}
|
|
async _addUntestedFiles(testContexts) {
|
|
const files = [];
|
|
testContexts.forEach(context => {
|
|
const config = context.config;
|
|
if (
|
|
this._globalConfig.collectCoverageFrom &&
|
|
this._globalConfig.collectCoverageFrom.length
|
|
) {
|
|
context.hasteFS
|
|
.matchFilesWithGlob(
|
|
this._globalConfig.collectCoverageFrom,
|
|
config.rootDir
|
|
)
|
|
.forEach(filePath =>
|
|
files.push({
|
|
config,
|
|
path: filePath
|
|
})
|
|
);
|
|
}
|
|
});
|
|
if (!files.length) {
|
|
return;
|
|
}
|
|
if (_jestUtil().isInteractive) {
|
|
process.stderr.write(
|
|
RUNNING_TEST_COLOR('Running coverage on untested files...')
|
|
);
|
|
}
|
|
let worker;
|
|
if (this._globalConfig.maxWorkers <= 1) {
|
|
worker = require('./CoverageWorker');
|
|
} else {
|
|
worker = new (_jestWorker().Worker)(require.resolve('./CoverageWorker'), {
|
|
enableWorkerThreads: this._globalConfig.workerThreads,
|
|
exposedMethods: ['worker'],
|
|
forkOptions: {
|
|
serialization: 'json'
|
|
},
|
|
maxRetries: 2,
|
|
numWorkers: this._globalConfig.maxWorkers
|
|
});
|
|
}
|
|
const instrumentation = files.map(async fileObj => {
|
|
const filename = fileObj.path;
|
|
const config = fileObj.config;
|
|
const hasCoverageData = this._v8CoverageResults.some(v8Res =>
|
|
v8Res.some(innerRes => innerRes.result.url === filename)
|
|
);
|
|
if (
|
|
!hasCoverageData &&
|
|
!this._coverageMap.data[filename] &&
|
|
'worker' in worker
|
|
) {
|
|
try {
|
|
const result = await worker.worker({
|
|
config,
|
|
context: {
|
|
changedFiles:
|
|
this._context.changedFiles &&
|
|
Array.from(this._context.changedFiles),
|
|
sourcesRelatedToTestsInChangedFiles:
|
|
this._context.sourcesRelatedToTestsInChangedFiles &&
|
|
Array.from(this._context.sourcesRelatedToTestsInChangedFiles)
|
|
},
|
|
globalConfig: this._globalConfig,
|
|
path: filename
|
|
});
|
|
if (result) {
|
|
if (result.kind === 'V8Coverage') {
|
|
this._v8CoverageResults.push([
|
|
{
|
|
codeTransformResult: undefined,
|
|
result: result.result
|
|
}
|
|
]);
|
|
} else {
|
|
this._coverageMap.addFileCoverage(result.coverage);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
_chalk().default.red(
|
|
[
|
|
`Failed to collect coverage from ${filename}`,
|
|
`ERROR: ${error.message}`,
|
|
`STACK: ${error.stack}`
|
|
].join('\n')
|
|
)
|
|
);
|
|
}
|
|
}
|
|
});
|
|
try {
|
|
await Promise.all(instrumentation);
|
|
} catch {
|
|
// Do nothing; errors were reported earlier to the console.
|
|
}
|
|
if (_jestUtil().isInteractive) {
|
|
(0, _jestUtil().clearLine)(process.stderr);
|
|
}
|
|
if (worker && 'end' in worker && typeof worker.end === 'function') {
|
|
await worker.end();
|
|
}
|
|
}
|
|
_checkThreshold(map) {
|
|
const {coverageThreshold} = this._globalConfig;
|
|
if (coverageThreshold) {
|
|
function check(name, thresholds, actuals) {
|
|
return ['statements', 'branches', 'lines', 'functions'].reduce(
|
|
(errors, key) => {
|
|
const actual = actuals[key].pct;
|
|
const actualUncovered = actuals[key].total - actuals[key].covered;
|
|
const threshold = thresholds[key];
|
|
if (threshold !== undefined) {
|
|
if (threshold < 0) {
|
|
if (threshold * -1 < actualUncovered) {
|
|
errors.push(
|
|
`Jest: Uncovered count for ${key} (${actualUncovered}) ` +
|
|
`exceeds ${name} threshold (${-1 * threshold})`
|
|
);
|
|
}
|
|
} else if (actual < threshold) {
|
|
errors.push(
|
|
`Jest: "${name}" coverage threshold for ${key} (${threshold}%) not met: ${actual}%`
|
|
);
|
|
}
|
|
}
|
|
return errors;
|
|
},
|
|
[]
|
|
);
|
|
}
|
|
const THRESHOLD_GROUP_TYPES = {
|
|
GLOB: 'glob',
|
|
GLOBAL: 'global',
|
|
PATH: 'path'
|
|
};
|
|
const coveredFiles = map.files();
|
|
const thresholdGroups = Object.keys(coverageThreshold);
|
|
const groupTypeByThresholdGroup = {};
|
|
const filesByGlob = {};
|
|
const coveredFilesSortedIntoThresholdGroup = coveredFiles.reduce(
|
|
(files, file) => {
|
|
const pathOrGlobMatches = thresholdGroups.reduce(
|
|
(agg, thresholdGroup) => {
|
|
// Preserve trailing slash, but not required if root dir
|
|
// See https://github.com/jestjs/jest/issues/12703
|
|
const resolvedThresholdGroup = path().resolve(thresholdGroup);
|
|
const suffix =
|
|
(thresholdGroup.endsWith(path().sep) ||
|
|
(process.platform === 'win32' &&
|
|
thresholdGroup.endsWith('/'))) &&
|
|
!resolvedThresholdGroup.endsWith(path().sep)
|
|
? path().sep
|
|
: '';
|
|
const absoluteThresholdGroup = `${resolvedThresholdGroup}${suffix}`;
|
|
|
|
// The threshold group might be a path:
|
|
|
|
if (file.indexOf(absoluteThresholdGroup) === 0) {
|
|
groupTypeByThresholdGroup[thresholdGroup] =
|
|
THRESHOLD_GROUP_TYPES.PATH;
|
|
return agg.concat([[file, thresholdGroup]]);
|
|
}
|
|
|
|
// If the threshold group is not a path it might be a glob:
|
|
|
|
// Note: glob.sync is slow. By memoizing the files matching each glob
|
|
// (rather than recalculating it for each covered file) we save a tonne
|
|
// of execution time.
|
|
if (filesByGlob[absoluteThresholdGroup] === undefined) {
|
|
filesByGlob[absoluteThresholdGroup] = _glob()
|
|
.default.sync(absoluteThresholdGroup)
|
|
.map(filePath => path().resolve(filePath));
|
|
}
|
|
if (filesByGlob[absoluteThresholdGroup].indexOf(file) > -1) {
|
|
groupTypeByThresholdGroup[thresholdGroup] =
|
|
THRESHOLD_GROUP_TYPES.GLOB;
|
|
return agg.concat([[file, thresholdGroup]]);
|
|
}
|
|
return agg;
|
|
},
|
|
[]
|
|
);
|
|
if (pathOrGlobMatches.length > 0) {
|
|
return files.concat(pathOrGlobMatches);
|
|
}
|
|
|
|
// Neither a glob or a path? Toss it in global if there's a global threshold:
|
|
if (thresholdGroups.indexOf(THRESHOLD_GROUP_TYPES.GLOBAL) > -1) {
|
|
groupTypeByThresholdGroup[THRESHOLD_GROUP_TYPES.GLOBAL] =
|
|
THRESHOLD_GROUP_TYPES.GLOBAL;
|
|
return files.concat([[file, THRESHOLD_GROUP_TYPES.GLOBAL]]);
|
|
}
|
|
|
|
// A covered file that doesn't have a threshold:
|
|
return files.concat([[file, undefined]]);
|
|
},
|
|
[]
|
|
);
|
|
const getFilesInThresholdGroup = thresholdGroup =>
|
|
coveredFilesSortedIntoThresholdGroup
|
|
.filter(fileAndGroup => fileAndGroup[1] === thresholdGroup)
|
|
.map(fileAndGroup => fileAndGroup[0]);
|
|
function combineCoverage(filePaths) {
|
|
return filePaths
|
|
.map(filePath => map.fileCoverageFor(filePath))
|
|
.reduce((combinedCoverage, nextFileCoverage) => {
|
|
if (combinedCoverage === undefined || combinedCoverage === null) {
|
|
return nextFileCoverage.toSummary();
|
|
}
|
|
return combinedCoverage.merge(nextFileCoverage.toSummary());
|
|
}, undefined);
|
|
}
|
|
let errors = [];
|
|
thresholdGroups.forEach(thresholdGroup => {
|
|
switch (groupTypeByThresholdGroup[thresholdGroup]) {
|
|
case THRESHOLD_GROUP_TYPES.GLOBAL: {
|
|
const coverage = combineCoverage(
|
|
getFilesInThresholdGroup(THRESHOLD_GROUP_TYPES.GLOBAL)
|
|
);
|
|
if (coverage) {
|
|
errors = errors.concat(
|
|
check(
|
|
thresholdGroup,
|
|
coverageThreshold[thresholdGroup],
|
|
coverage
|
|
)
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case THRESHOLD_GROUP_TYPES.PATH: {
|
|
const coverage = combineCoverage(
|
|
getFilesInThresholdGroup(thresholdGroup)
|
|
);
|
|
if (coverage) {
|
|
errors = errors.concat(
|
|
check(
|
|
thresholdGroup,
|
|
coverageThreshold[thresholdGroup],
|
|
coverage
|
|
)
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case THRESHOLD_GROUP_TYPES.GLOB:
|
|
getFilesInThresholdGroup(thresholdGroup).forEach(
|
|
fileMatchingGlob => {
|
|
errors = errors.concat(
|
|
check(
|
|
fileMatchingGlob,
|
|
coverageThreshold[thresholdGroup],
|
|
map.fileCoverageFor(fileMatchingGlob).toSummary()
|
|
)
|
|
);
|
|
}
|
|
);
|
|
break;
|
|
default:
|
|
// If the file specified by path is not found, error is returned.
|
|
if (thresholdGroup !== THRESHOLD_GROUP_TYPES.GLOBAL) {
|
|
errors = errors.concat(
|
|
`Jest: Coverage data for ${thresholdGroup} was not found.`
|
|
);
|
|
}
|
|
// Sometimes all files in the coverage data are matched by
|
|
// PATH and GLOB threshold groups in which case, don't error when
|
|
// the global threshold group doesn't match any files.
|
|
}
|
|
});
|
|
|
|
errors = errors.filter(
|
|
err => err !== undefined && err !== null && err.length > 0
|
|
);
|
|
if (errors.length > 0) {
|
|
this.log(`${FAIL_COLOR(errors.join('\n'))}`);
|
|
this._setError(new Error(errors.join('\n')));
|
|
}
|
|
}
|
|
}
|
|
async _getCoverageResult() {
|
|
if (this._globalConfig.coverageProvider === 'v8') {
|
|
const mergedCoverages = (0, _v8Coverage().mergeProcessCovs)(
|
|
this._v8CoverageResults.map(cov => ({
|
|
result: cov.map(r => r.result)
|
|
}))
|
|
);
|
|
const fileTransforms = new Map();
|
|
this._v8CoverageResults.forEach(res =>
|
|
res.forEach(r => {
|
|
if (r.codeTransformResult && !fileTransforms.has(r.result.url)) {
|
|
fileTransforms.set(r.result.url, r.codeTransformResult);
|
|
}
|
|
})
|
|
);
|
|
const transformedCoverage = await Promise.all(
|
|
mergedCoverages.result.map(async res => {
|
|
const fileTransform = fileTransforms.get(res.url);
|
|
let sourcemapContent = undefined;
|
|
if (
|
|
fileTransform?.sourceMapPath &&
|
|
fs().existsSync(fileTransform.sourceMapPath)
|
|
) {
|
|
sourcemapContent = JSON.parse(
|
|
fs().readFileSync(fileTransform.sourceMapPath, 'utf8')
|
|
);
|
|
}
|
|
const converter = (0, _v8ToIstanbul().default)(
|
|
res.url,
|
|
fileTransform?.wrapperLength ?? 0,
|
|
fileTransform && sourcemapContent
|
|
? {
|
|
originalSource: fileTransform.originalCode,
|
|
source: fileTransform.code,
|
|
sourceMap: {
|
|
sourcemap: {
|
|
file: res.url,
|
|
...sourcemapContent
|
|
}
|
|
}
|
|
}
|
|
: {
|
|
source: fs().readFileSync(res.url, 'utf8')
|
|
}
|
|
);
|
|
await converter.load();
|
|
converter.applyCoverage(res.functions);
|
|
const istanbulData = converter.toIstanbul();
|
|
return istanbulData;
|
|
})
|
|
);
|
|
const map = _istanbulLibCoverage().default.createCoverageMap({});
|
|
transformedCoverage.forEach(res => map.merge(res));
|
|
const reportContext = _istanbulLibReport().default.createContext({
|
|
coverageMap: map,
|
|
dir: this._globalConfig.coverageDirectory,
|
|
watermarks: (0, _getWatermarks.default)(this._globalConfig)
|
|
});
|
|
return {
|
|
map,
|
|
reportContext
|
|
};
|
|
}
|
|
const map = await this._sourceMapStore.transformCoverage(this._coverageMap);
|
|
const reportContext = _istanbulLibReport().default.createContext({
|
|
coverageMap: map,
|
|
dir: this._globalConfig.coverageDirectory,
|
|
sourceFinder: this._sourceMapStore.sourceFinder,
|
|
watermarks: (0, _getWatermarks.default)(this._globalConfig)
|
|
});
|
|
return {
|
|
map,
|
|
reportContext
|
|
};
|
|
}
|
|
}
|
|
exports.default = CoverageReporter;
|