@tapjs/run
Version:
Command-line interface for the node-tap runner
177 lines • 6.8 kB
JavaScript
import { tap } from '@tapjs/core';
import { Report } from 'c8';
import { readdir } from 'fs/promises';
import opener from 'opener';
import { resolve } from 'path';
import { mainCommand } from './main-config.js';
const reporterFiles = {
clover: 'clover.xml',
cobertura: 'cobertura-coverage.xml',
json: 'coverage-final.json',
'json-summary': 'coverage-summary.json',
lcov: 'lcov.info',
lcovonly: 'lcov.info',
};
export const report = async (args, config) => {
const rconf = config.get('coverage-report');
// if there are args passed in, use that. The config is used if
// calling this at the end of `tap run`
const reporter = args.length ? args
: rconf && rconf.length ? rconf
: ['text'];
// verify that we actually have coverage, otherwise don't even try
const tempDirectory = resolve(config.projectRoot, '.tap/coverage');
const ok = await readdir(tempDirectory).then(entries => !!entries.length, () => false);
if (!ok) {
tap().comment('No coverage generated');
tap().debug('run/report exit=1 no coverage generated');
if (!config.get('allow-empty-coverage')) {
process.exitCode = 1;
}
return;
}
// C8 report only works when run from the cwd where the files are
const cwd = process.cwd();
/* c8 ignore start */
try {
process.chdir(config.projectRoot);
}
catch { }
/* c8 ignore stop */
let showFullCoverage = config.get('show-full-coverage');
let r = new Report({
skipFull: !showFullCoverage,
// no need to include/exclude, we already did that when we captured
reporter,
reportsDirectory: resolve(config.projectRoot, '.tap/report'),
tempDirectory: resolve(config.projectRoot, '.tap/coverage'),
excludeNodeModules: true,
});
// See if we need to generate a text report, or if we should skip it
// because --show-full-coverage is false and we have full coverage,
// or alternatively, there's NO coverage, and we'll print the warning
// anyway.
if (reporter.includes('text') && !showFullCoverage) {
const summary = await getSummary(r);
if (isFullCoverage(summary) || isEmptyCoverage(summary)) {
if (mainCommand === 'report' && showFullCoverage !== false) {
r = new Report({
skipFull: false,
reporter,
reportsDirectory: resolve(config.projectRoot, '.tap/report'),
tempDirectory: resolve(config.projectRoot, '.tap/coverage'),
excludeNodeModules: true,
});
}
else {
// don't show the text report, it's just noise.
// but make `tap report html --no-show-full-coverage` still
// output a summary, at least, so it doesn't seem broken
const txt = reporter.indexOf('text');
if (txt !== -1) {
const none = args.includes('text') ? 'text-summary' : 'none';
reporter.splice(txt, 1);
if (reporter.length === 0) {
reporter.push(none);
}
}
}
}
}
// XXX: istanbul-reports just dumps to process.stdout, which collides
// with our ink-based reporters. Hijack it and write with console.log
// so that patch-console can put it in the intended place (or ignore it).
// TODO: make istanbul-reports more configurable.
const { write } = process.stdout;
const stdout = [];
process.stdout.write = c => {
stdout.push(String(c));
return true;
};
await r.run();
process.stdout.write = write;
const s = stdout.join('').trimEnd();
if (s)
console.log(s);
for (const [style, filename] of Object.entries(reporterFiles)) {
if (reporter.includes(style)) {
const f = resolve(config.projectRoot, '.tap/report', filename);
console.log(`${style} report: ${f}`);
}
}
const browser = config.get('browser');
const htmlReport = reporter.includes('html') || reporter.includes('html-spa') ?
resolve(config.projectRoot, '.tap/report/index.html')
: reporter.includes('lcov') ?
resolve(config.projectRoot, '.tap/report/lcov-report/index.html')
: undefined;
if (htmlReport) {
if (browser)
opener(htmlReport);
else
console.log(`html report: ${htmlReport}`);
}
await checkCoverage(r, config);
/* c8 ignore start */
try {
process.chdir(cwd);
}
catch { }
/* c8 ignore stop */
};
const summaries = new Map();
const getSummary = async (report) => {
const c = summaries.get(report);
if (c)
return c;
const r = report;
const map = await r.getCoverageMapFromAllCoverageFiles();
const summary = map.getCoverageSummary();
summaries.set(report, summary);
return summary;
};
const thresholds = [
'statements',
'branches',
'functions',
'lines',
];
const isEmptyCoverage = (summary) => Math.max(...thresholds.map(th => Number(summary[th].pct) || 0)) === 0;
const isFullCoverage = (summary) => Math.min(...thresholds.map(th => Number(summary[th].pct) || 0)) === 100;
const checkCoverage = async (report, config) => {
const cr = config.get('coverage-report');
const comment = cr && !cr.includes('text');
const summary = await getSummary(report);
let success = true;
const t = tap();
// if we didn't get anything, that means that even though it wrote
// some coverage files, it didn't actually cover anything, which can
// happen, for example if the test crashes before actually loading.
if (isEmptyCoverage(summary)) {
t.comment('No coverage generated');
t.debug('run/report exit=1 coverage is empty', summary);
if (!config.get('allow-empty-coverage')) {
process.exitCode = 1;
}
return;
}
// TODO: Only comment lack of coverage if not using the text reporter,
// because that makes it pretty obvious where the shortcomings are already.
for (const th of thresholds) {
const coverage = Number(summary[th].pct) || 0;
const minCoverage = Number(config.get(th));
if (coverage < minCoverage) {
if (comment) {
t.comment(`ERROR: incomplete ${th} coverage (${coverage}%)`);
}
success = false;
}
}
if (success === false) {
t.debug(`run/report exit=1 coverage is incomplete`, summary);
if (!config.get('allow-incomplete-coverage')) {
process.exitCode = 1;
}
}
};
//# sourceMappingURL=report.js.map