UNPKG

@tapjs/run

Version:

Command-line interface for the node-tap runner

179 lines 6.75 kB
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') ? 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: make levels configurable, just *default* to 100 // only comment it 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; if (coverage < 100) { if (comment) { t.comment(`ERROR: incomplete ${th} coverage (${coverage}%)`); } success = false; } } if (success === false) { t.debug('run/report exit=1 coverage not 100', summary); if (!config.get('allow-incomplete-coverage')) { process.exitCode = 1; } } }; //# sourceMappingURL=report.js.map