UNPKG

danger-plugin-coverage

Version:
342 lines (271 loc) 9.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.coverage = void 0; var _path = _interopRequireDefault(require("path")); var _report = require("./report"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const newLine = '\n'; /** * Get flatened file details. */ const getFlatFiles = coverage => { const parentKey = ['project', 'package'].find(key => key in coverage); if (parentKey) { return [].concat(...coverage[parentKey].map(item => getFlatFiles(item))); } return coverage.file || []; }; /** * Get the percentage covered for a given metric. */ const getCoveredPercentage = (covered, total) => { const percentage = covered / total * 100; if (!Number(total)) { return 100; } if (Number.isNaN(percentage)) { return '-'; } return Number(percentage.toFixed(2)); }; /** * Get the percentages for all metrics. */ const getMetricPercentages = ({ statements, coveredstatements, conditionals, coveredconditionals, methods, coveredmethods, lines, coveredlines }) => ({ statements: getCoveredPercentage(coveredstatements, statements), branches: getCoveredPercentage(coveredconditionals, conditionals), functions: getCoveredPercentage(coveredmethods, methods), lines: getCoveredPercentage(coveredlines, lines) }); /** * Get the metrics for a file. */ const getFileMetrics = file => { const { line: lines = [], metrics } = file; const fileMetrics = (metrics === null || metrics === void 0 ? void 0 : metrics[0].$) || {}; const uncoveredLines = lines.filter(line => { var _line$$; return !Number(((_line$$ = line.$) === null || _line$$ === void 0 ? void 0 : _line$$.count) || 0); }); return { ...fileMetrics, lines: lines.length, coveredlines: lines.length - uncoveredLines.length, uncoveredLines }; }; /** * Shorten a path so that it fits in a GitHub comment. */ const getShortPath = (filePath, maxChars) => { const parts = filePath.split('/').reverse().filter(x => x); if (parts.length === 1) { return filePath; } const shortParts = []; let currentChars = 0; parts.forEach((part, index) => { const isLastPart = parts.length - 1 === index; currentChars += part.length + 1; // +1 for the path seperator const prefixLength = !isLastPart ? 3 : 0; if (currentChars + prefixLength < maxChars) { shortParts.push(part); } }); if (shortParts.length < parts.length) { shortParts.push('..'); } return shortParts.reverse().join('/'); }; /** * Insert whitespace into a path so that it wraps within the Markdown table. */ const getWrappedPath = filePath => { const parts = filePath.split('/'); const maxPerLine = 25; let currentChars = 0; return parts.map((pathPart, i) => { var _parts; const isLastPart = parts.length - 1 === i; const newPart = isLastPart ? pathPart : `${pathPart}/`; currentChars += pathPart.length; if (currentChars + (((_parts = parts[i + 1]) === null || _parts === void 0 ? void 0 : _parts.length) || 0) > maxPerLine) { currentChars = 0; return `${newPart}<br>`; } return newPart; }).join(''); }; /** * Check if we have passed the thresholds for the given percentages. */ const hasPassed = (threshold, { statements, branches, functions, lines }) => (Number(statements) >= threshold.statements || statements === '-') && (Number(branches) >= threshold.branches || branches === '-') && (Number(functions) >= threshold.functions || functions === '-') && (Number(lines) >= threshold.lines || lines === '-'); /** * Build a row for the coverage table. */ const buildRow = (file, { threshold, maxChars, maxUncovered, wrapFilenames }) => { var _danger$git, _danger$git$commits; const fileMetrics = getFileMetrics(file); const { sha } = ((_danger$git = danger.git) === null || _danger$git === void 0 ? void 0 : (_danger$git$commits = _danger$git.commits) === null || _danger$git$commits === void 0 ? void 0 : _danger$git$commits[danger.git.commits.length - 1]) || {}; const longPath = _path.default.relative(process.cwd(), file.$.path); const shortPath = getShortPath(longPath, maxChars); const readablePath = wrapFilenames ? getWrappedPath(shortPath) : shortPath; const fileLink = `../blob/${sha}/${longPath}`; const fileCell = sha ? `[${readablePath}](${fileLink})` : readablePath; const percentages = getMetricPercentages(fileMetrics); const noLines = !fileMetrics.lines; let emoji = hasPassed(threshold, percentages) ? ':white_check_mark:' : ':x:'; if (noLines) { emoji = '-'; } let uncoveredCell = fileMetrics.uncoveredLines.slice(0, maxUncovered).map(line => { const lineNumber = line.$.num; const anchor = `#L${lineNumber}`; return sha ? `[${lineNumber}](${fileLink + anchor})` : lineNumber; }).join(', '); if (fileMetrics.uncoveredLines.length > maxUncovered) { uncoveredCell += '...'; } return ['', fileCell, noLines ? '-' : percentages.statements, noLines ? '-' : percentages.branches, noLines ? '-' : percentages.functions, noLines ? '-' : percentages.lines, uncoveredCell, emoji, ''].join('|'); }; /** * Join items in a table row. */ const joinRow = items => `|${items.map(item => item).join('|')}|`; /** * Build the coverage table. */ const buildTable = (files, opts) => { const { maxRows, showAllFiles } = opts; const headings = [`${showAllFiles ? '' : 'Impacted '}Files`, '% Stmts', '% Branch', '% Funcs', '% Lines', 'Uncovered Lines', '']; const headingRow = joinRow(headings); const seperator = joinRow(new Array(headings.length).fill().reduce((acc, _, index) => [...acc, index === 0 ? '---' : ':-:' // Center align all but the first column ], [])); const allFileRows = files.map(file => buildRow(file, opts)); const mainFileRows = allFileRows.slice(0, maxRows); const extraFileRows = allFileRows.slice(maxRows); let table = [headingRow, seperator, ...mainFileRows].join(newLine); if (extraFileRows.length) { table += [newLine, '<details>', '<summary>', `and ${extraFileRows.length} more...`, '</summary>', '', headingRow, seperator, ...extraFileRows, '</details>'].join(newLine); } return table; }; /** * Get a line for the threshold summary. */ const getThresholdSummaryLine = (percentages, key, threshold) => { const wasMet = Number(percentages[key]) >= (threshold[key] || 0); if (wasMet) { return ''; } return `Coverage threshold for ${key} (${threshold[key]}%) not met: ${percentages[key]}%`; }; /** * Build the test summary. */ const buildSummary = (metrics, { successMessage, failureMessage, threshold }) => { const percentages = getMetricPercentages(metrics); const passed = hasPassed(threshold, percentages); const thresholdSummary = [getThresholdSummaryLine(percentages, 'statements', threshold), getThresholdSummaryLine(percentages, 'branches', threshold), getThresholdSummaryLine(percentages, 'functions', threshold), getThresholdSummaryLine(percentages, 'lines', threshold)].filter(x => !!x); // Remove empty strings if (passed) { return `> ${successMessage}`; } return [`> ${failureMessage}`, ...(thresholdSummary.length ? ['', '```', ...thresholdSummary, '```'] : [])].join(newLine); }; /** * Get the combined metrics for the checked files. */ const getCombinedMetrics = files => files.reduce((acc, file) => { const fileMetrics = getFileMetrics(file); Object.keys(fileMetrics).forEach(key => { acc[key] = acc[key] || 0 + Number(fileMetrics[key]); }); return acc; }, {}); /** * Get the relevant files. */ const getRelevantFiles = (coverageXml, { showAllFiles }) => { var _danger$git2, _danger$git3; const files = getFlatFiles(coverageXml); const allFiles = [...(((_danger$git2 = danger.git) === null || _danger$git2 === void 0 ? void 0 : _danger$git2.created_files) || []), ...(((_danger$git3 = danger.git) === null || _danger$git3 === void 0 ? void 0 : _danger$git3.modified_files) || [])]; const relevantFiles = files.filter(file => allFiles.includes(_path.default.relative(process.cwd(), file.$.path))); if (showAllFiles) { return files; } return relevantFiles; }; /** * Report coverage. */ const coverage = async (initialOpts = {}) => { const opts = { successMessage: ':+1: Test coverage is looking good.', failureMessage: 'Test coverage is looking a little low for the files created ' + 'or modified in this PR, perhaps we need to improve this.', cloverReportPath: null, maxRows: 3, maxChars: 100, maxUncovered: 10, wrapFilenames: true, showAllFiles: false, warnOnNoReport: true, threshold: { statements: 80, branches: 80, functions: 80, lines: 80 }, ...initialOpts }; const coverageXml = await (0, _report.getCoverageReport)(opts.cloverReportPath); if (!coverageXml) { if (opts.warnOnNoReport) { warn('No coverage report was detected. ' + 'Please output a report in the `clover.xml` format before running danger'); } return; } const relevantFiles = getRelevantFiles(coverageXml, opts); if (!relevantFiles.length) { return; } const combinedMetrics = getCombinedMetrics(relevantFiles); const table = buildTable(relevantFiles, opts); const summary = buildSummary(combinedMetrics, opts); const report = ['## Coverage Report', summary, table].join(newLine + newLine); markdown(report); }; exports.coverage = coverage;