1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/test/merge-coverage.js

250 lines
8.1 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const libCoverage = require('istanbul-lib-coverage');
const libReport = require('istanbul-lib-report');
const reports = require('istanbul-reports');
const glob = require('fast-glob');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const yaml = require('js-yaml');
const codecovTargets = require('../coverage-targets');
const codecovConfig = yaml.load(fs.readFileSync('codecov.yml', 'utf8'));
const COVERAGE_DIR = './coverage/';
const COVERAGE_THRESHOLD_FOR_BUMP = 1;
/**
* Load .json file at path and parse it into a javascript object
*
* @param {string} filePath - path to the file to load
* @returns {object} the JavaScript object parsed from the file
*/
function loadData(filePath) {
const json = fs.readFileSync(filePath);
return JSON.parse(json);
}
/**
* Loads an array of json coverage files and merges them into a final coverage
* report.
*
* @param {string[]} files - array of strings that are paths to files
* @returns {libCoverage.CoverageMap} CoverageMap
*/
function mergeCoverageMaps(files) {
const coverageMap = libCoverage.createCoverageMap({});
files.forEach((covergeFinalFile) => {
coverageMap.merge(loadData(covergeFinalFile));
});
return coverageMap;
}
/**
* Given a target directory and a coverageMap generates a finalized coverage
* summary report and saves it to the directory.
*
* @param {string} dir - target directory
* @param {libCoverage.CoverageMap} coverageMap - CoverageMap to report on
* @param reportType
* @param reportOptions
*/
function generateSummaryReport(dir, coverageMap, reportType, reportOptions) {
const context = libReport.createContext({
dir,
coverageMap,
});
reports.create(reportType, reportOptions ?? {}).execute(context);
}
/**
* Generates a multiline string with coverage data
*
* @param {CoverageTarget} target - Target coverage threshold
* @param {import('istanbul-lib-coverage').CoverageSummaryData} actual -
* istanbul coverage summary detailing actual summary
* @returns {string} multiline report of coverage
*/
function generateConsoleReport(target, actual) {
const { lines, branches, functions, statements } = actual.data;
const breakdown =
`Lines: ${lines.covered}/${lines.total} (${lines.pct}%). Target: ${target.lines}%\n` +
`Branches: ${branches.covered}/${branches.total} (${branches.pct}%). Target: ${target.branches}%\n` +
`Statements: ${statements.covered}/${statements.total} (${statements.pct}%). Target: ${target.statements}%\n` +
`Functions: ${functions.covered}/${functions.total} (${functions.pct}%). Target: ${target.functions}%`;
return breakdown;
}
/**
* @typedef {object} CoverageTarget
* @property {number} lines - percentage of lines that must be covered
* @property {number} statements - percentage of statements that must be covered
* @property {number} branches - percentage of branches that must be covered
* @property {number} functions - percentage of functions that must be covered
*/
/**
* Checks if the coverage meets target
*
* @param {CoverageTarget} target
* @param {import('istanbul-lib-coverage').CoverageSummaryData} actual
* @returns {boolean}
*/
function isCoverageInsufficient(target, actual) {
const lineCoverageNotMet = actual.lines.pct < target.lines;
const branchCoverageNotMet = actual.branches.pct < target.branches;
const functionCoverageNotMet = actual.functions.pct < target.functions;
const statementCoverageNotMet = actual.statements.pct < target.statements;
return (
lineCoverageNotMet ||
branchCoverageNotMet ||
functionCoverageNotMet ||
statementCoverageNotMet
);
}
/**
* Checks if the coverage should be bumped up
*
* @param {CoverageTarget} target
* @param {import('istanbul-lib-coverage').CoverageSummaryData} actual
* @returns {boolean}
*/
function shouldCoverageBeBumped(target, actual) {
const lineCoverageNeedsBumped =
actual.lines.pct > target.lines + COVERAGE_THRESHOLD_FOR_BUMP;
const branchCoverageNeedsBumped =
actual.branches.pct > target.branches + COVERAGE_THRESHOLD_FOR_BUMP;
const functionCoverageNeedsBumped =
actual.functions.pct > target.functions + COVERAGE_THRESHOLD_FOR_BUMP;
const statementCoverageNeedsBumped =
actual.statements.pct > target.statements + COVERAGE_THRESHOLD_FOR_BUMP;
return (
lineCoverageNeedsBumped ||
branchCoverageNeedsBumped ||
functionCoverageNeedsBumped ||
statementCoverageNeedsBumped
);
}
/**
* Creates and returns a combined coverage summary report of every file in the
* provided array.
*
* @param {string[]} files - array of files generated by fast-glob
* @param {libCoverage.CoverageMap} coverageMap
* @returns {import('istanbul-lib-coverage').CoverageSummaryData}
*/
function getFileCoverage(files, coverageMap) {
const subCoverageMap = libCoverage.createCoverageMap({});
files.forEach((file) => {
try {
subCoverageMap.merge(
coverageMap.fileCoverageFor(`${process.cwd()}/${file}`),
);
} catch {
// If the coverage doesn't exist, it means that it was excluded from
// coverage or had no coverage to report, which is fine. Glob is a lot
// wider of a net then what the test file runners match against.
}
});
const summary = subCoverageMap.getCoverageSummary();
return summary;
}
/**
* Checks coverage and reports to console
* Throws an error if coverage isn't met
*
* @param {string} name - The target's name from coverageThresholds in jest
* config
* @param {CoverageTarget} target - the target coverage threshold
* @param {import('istanbul-lib-coverage').CoverageSummaryData} actual -
* istanbul coverage summary representing actual coverage
*/
function checkCoverage(name, target, actual) {
const breakdown = generateConsoleReport(target, actual);
if (isCoverageInsufficient(target, actual)) {
const errorMsg = `Coverage thresholds for ${name} NOT met\n${breakdown}`;
throw new Error(errorMsg);
} else if (shouldCoverageBeBumped(target, actual)) {
const errorMsg = `Coverage EXCEEDS threshold for ${name} and must be bumped\n${breakdown}`;
throw new Error(errorMsg);
}
console.log(`Coverage thresholds for ${name} met\n${breakdown}\n\n`);
}
/**
* Primary script function
*/
async function start() {
const {
argv: { html },
} = yargs(hideBin(process.argv)).usage(
'$0 [options]',
'Run unit tests on the application code.',
(yargsInstance) =>
yargsInstance
.option('html', {
alias: ['h'],
default: false,
description: 'Generate HTML report',
type: 'boolean',
})
.strict(),
);
// First get all of the files matching the pattern coverage-final-${n}.json
// from the coverage directory
const files = fs.readdirSync(COVERAGE_DIR);
const filePaths = files
.filter(
(file) =>
path.basename(file).startsWith('coverage-final') &&
path.extname(file) === '.json',
)
.map((file) => path.join(COVERAGE_DIR, file));
// Next, generate a coverageMap
const coverageMap = mergeCoverageMaps(filePaths, true);
// Persist this to file, which may eventually be used in more steps
generateSummaryReport(COVERAGE_DIR, coverageMap, 'json-summary');
if (html) {
generateSummaryReport(COVERAGE_DIR, coverageMap, 'html');
}
// Use the keys in coverageThreshold in jest config to determine targets
const coverageTargets = Object.keys(codecovConfig.coverage.status.project);
// Check coverage totals for each target
coverageTargets.forEach((target) => {
const summary =
target === 'global'
? coverageMap.getCoverageSummary()
: getFileCoverage(
glob.sync([
...codecovConfig.coverage.status.project[target].paths,
// checking test file coverage is redundant.
'!**/*.test.js',
'!**/__mocks__/**/*.js',
'!**/*.stories.*',
]),
coverageMap,
);
// Check and validate the coverage
checkCoverage(target, codecovTargets[target], summary);
});
}
start().catch((error) => {
// Report the errored coverage check
console.error(error);
process.exit(1);
});