|
| 1 | +const Exclude = require('test-exclude') |
| 2 | +const fs = require('fs') |
| 3 | +const path = require('path') |
| 4 | +const { fileURLToPath } = require('url') |
| 5 | + |
| 6 | +const { CoverageReport } = require('monocart-coverage-reports') |
| 7 | + |
| 8 | +module.exports = async (argv) => { |
| 9 | + // console.log(argv); |
| 10 | + const exclude = new Exclude({ |
| 11 | + exclude: argv.exclude, |
| 12 | + include: argv.include, |
| 13 | + extension: argv.extension, |
| 14 | + relativePath: !argv.allowExternal, |
| 15 | + excludeNodeModules: argv.excludeNodeModules |
| 16 | + }) |
| 17 | + |
| 18 | + // adapt coverage options |
| 19 | + const coverageOptions = getCoverageOptions(argv, exclude) |
| 20 | + const coverageReport = new CoverageReport(coverageOptions) |
| 21 | + coverageReport.cleanCache() |
| 22 | + |
| 23 | + // read v8 coverage data from tempDirectory |
| 24 | + await addV8Coverage(coverageReport, argv) |
| 25 | + |
| 26 | + // generate report |
| 27 | + await coverageReport.generate() |
| 28 | +} |
| 29 | + |
| 30 | +function getReports (argv) { |
| 31 | + const reports = Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter] |
| 32 | + const reporterOptions = argv.reporterOptions || {} |
| 33 | + |
| 34 | + return reports.map((reportName) => { |
| 35 | + const reportOptions = { |
| 36 | + ...reporterOptions[reportName] |
| 37 | + } |
| 38 | + if (reportName === 'text') { |
| 39 | + reportOptions.skipEmpty = false |
| 40 | + reportOptions.skipFull = argv.skipFull |
| 41 | + reportOptions.maxCols = process.stdout.columns || 100 |
| 42 | + } |
| 43 | + return [reportName, reportOptions] |
| 44 | + }) |
| 45 | +} |
| 46 | + |
| 47 | +// --all: add empty coverage for all files |
| 48 | +function getAllOptions (argv, exclude) { |
| 49 | + if (!argv.all) { |
| 50 | + return |
| 51 | + } |
| 52 | + |
| 53 | + const src = argv.src |
| 54 | + const workingDirs = Array.isArray(src) ? src : (typeof src === 'string' ? [src] : [process.cwd()]) |
| 55 | + return { |
| 56 | + dir: workingDirs, |
| 57 | + filter: (filePath) => { |
| 58 | + return exclude.shouldInstrument(filePath) |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +function getEntryFilter (argv, exclude) { |
| 64 | + if (argv.entryFilter) { |
| 65 | + return argv.entryFilter |
| 66 | + } |
| 67 | + return (entry) => { |
| 68 | + return exclude.shouldInstrument(fileURLToPath(entry.url)) |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +function getSourceFilter (argv, exclude) { |
| 73 | + if (argv.sourceFilter) { |
| 74 | + return argv.sourceFilter |
| 75 | + } |
| 76 | + return (sourcePath) => { |
| 77 | + if (argv.excludeAfterRemap) { |
| 78 | + // console.log(sourcePath) |
| 79 | + return exclude.shouldInstrument(sourcePath) |
| 80 | + } |
| 81 | + return true |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +function getCoverageOptions (argv, exclude) { |
| 86 | + const reports = getReports(argv) |
| 87 | + const allOptions = getAllOptions(argv, exclude) |
| 88 | + |
| 89 | + return { |
| 90 | + logging: argv.logging, |
| 91 | + name: argv.name, |
| 92 | + inline: argv.inline, |
| 93 | + lcov: argv.lcov, |
| 94 | + outputDir: argv.reportsDir, |
| 95 | + clean: argv.clean, |
| 96 | + |
| 97 | + reports, |
| 98 | + all: allOptions, |
| 99 | + |
| 100 | + // use default value for istanbul |
| 101 | + defaultSummarizer: 'pkg', |
| 102 | + |
| 103 | + entryFilter: getEntryFilter(argv, exclude), |
| 104 | + |
| 105 | + sourceFilter: getSourceFilter(argv, exclude), |
| 106 | + |
| 107 | + // sourcePath: (filePath) => { |
| 108 | + // return path.resolve(filePath); |
| 109 | + // }, |
| 110 | + |
| 111 | + onEnd: (coverageResults) => { |
| 112 | + // console.log(`Coverage report generated: ${coverageResults.reportPath}`); |
| 113 | + |
| 114 | + if (!argv.checkCoverage) { |
| 115 | + return |
| 116 | + } |
| 117 | + |
| 118 | + // check thresholds |
| 119 | + const thresholds = {} |
| 120 | + const metrics = ['bytes', 'statements', 'branches', 'functions', 'lines'] |
| 121 | + metrics.forEach((k) => { |
| 122 | + if (argv[k]) { |
| 123 | + thresholds[k] = argv[k] |
| 124 | + } |
| 125 | + }) |
| 126 | + |
| 127 | + const { summary, files } = coverageResults |
| 128 | + |
| 129 | + if (argv.perFile) { |
| 130 | + files.forEach((file) => { |
| 131 | + checkCoverage(file.summary, thresholds, file) |
| 132 | + }) |
| 133 | + } else { |
| 134 | + checkCoverage(summary, thresholds) |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +function checkCoverage (summary, thresholds, file) { |
| 141 | + if (file && file.empty) { |
| 142 | + process.exitCode = 1 |
| 143 | + console.error( |
| 144 | + 'ERROR: Empty coverage (untested file) does not meet threshold for ' + |
| 145 | + path.relative('./', file.sourcePath).replace(/\\/g, '/') |
| 146 | + ) |
| 147 | + return |
| 148 | + } |
| 149 | + Object.keys(thresholds).forEach(key => { |
| 150 | + const coverage = summary[key].pct |
| 151 | + if (typeof coverage !== 'number') { |
| 152 | + return |
| 153 | + } |
| 154 | + if (coverage < thresholds[key]) { |
| 155 | + process.exitCode = 1 |
| 156 | + if (file) { |
| 157 | + console.error( |
| 158 | + 'ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet threshold (' + thresholds[key] + '%) for ' + |
| 159 | + path.relative('./', file.sourcePath).replace(/\\/g, '/') // standardize path for Windows. |
| 160 | + ) |
| 161 | + } else { |
| 162 | + console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet global threshold (' + thresholds[key] + '%)') |
| 163 | + } |
| 164 | + } |
| 165 | + }) |
| 166 | +} |
| 167 | + |
| 168 | +function getFileSource (filePath) { |
| 169 | + if (fs.existsSync(filePath)) { |
| 170 | + return fs.readFileSync(filePath).toString('utf8') |
| 171 | + } |
| 172 | + return '' |
| 173 | +} |
| 174 | + |
| 175 | +const resolveSourceMap = (sourceMap, url) => { |
| 176 | + if (!sourceMap.sourcesContent) { |
| 177 | + sourceMap.sourcesContent = sourceMap.sources.map(fileUrl => { |
| 178 | + return getFileSource(fileURLToPath(fileUrl)) |
| 179 | + }) |
| 180 | + } |
| 181 | + return sourceMap |
| 182 | +} |
| 183 | + |
| 184 | +const resolveEntrySource = (entry, sourceMapCache = {}) => { |
| 185 | + let source |
| 186 | + const filePath = fileURLToPath(entry.url) |
| 187 | + const extname = path.extname(filePath) |
| 188 | + if (fs.existsSync(filePath)) { |
| 189 | + source = fs.readFileSync(filePath).toString('utf8') |
| 190 | + } |
| 191 | + |
| 192 | + // not for typescript |
| 193 | + if (source && !['.ts', '.tsx'].includes(extname)) { |
| 194 | + entry.source = source |
| 195 | + return |
| 196 | + } |
| 197 | + |
| 198 | + const sourcemapData = sourceMapCache[entry.url] |
| 199 | + const lineLengths = sourcemapData && sourcemapData.lineLengths |
| 200 | + |
| 201 | + // for fake source file (can not parse to AST) |
| 202 | + if (lineLengths) { |
| 203 | + // get runtime code with ts-node |
| 204 | + let fakeSource = '' |
| 205 | + sourcemapData.lineLengths.forEach((length) => { |
| 206 | + fakeSource += `${''.padEnd(length, '*')}\n` |
| 207 | + }) |
| 208 | + entry.fake = true |
| 209 | + entry.source = fakeSource |
| 210 | + } |
| 211 | + |
| 212 | + // Note: no runtime code in source map cache |
| 213 | + // This is a problem for typescript |
| 214 | +} |
| 215 | + |
| 216 | +const resolveEntrySourceMap = (entry, sourceMapCache = {}) => { |
| 217 | + // sourcemap data |
| 218 | + const sourcemapData = sourceMapCache[entry.url] |
| 219 | + if (sourcemapData) { |
| 220 | + if (sourcemapData.data) { |
| 221 | + entry.sourceMap = resolveSourceMap(sourcemapData.data, entry.url) |
| 222 | + } |
| 223 | + } |
| 224 | +} |
| 225 | + |
| 226 | +const collectCoverageData = (coverageList, entryFilterHandler, sourceMapCache = {}) => { |
| 227 | + if (!coverageList.length) { |
| 228 | + return |
| 229 | + } |
| 230 | + |
| 231 | + // filter node internal files |
| 232 | + coverageList = coverageList.filter((entry) => entry.url && entry.url.startsWith('file:')) |
| 233 | + coverageList = coverageList.filter(entryFilterHandler) |
| 234 | + |
| 235 | + if (!coverageList.length) { |
| 236 | + return |
| 237 | + } |
| 238 | + |
| 239 | + for (const entry of coverageList) { |
| 240 | + resolveEntrySource(entry, sourceMapCache) |
| 241 | + resolveEntrySourceMap(entry, sourceMapCache) |
| 242 | + } |
| 243 | + |
| 244 | + return coverageList |
| 245 | +} |
| 246 | + |
| 247 | +async function addV8Coverage (coverageReport, argv) { |
| 248 | + const entryFilterHandler = coverageReport.getEntryFilter() |
| 249 | + const dir = argv.tempDirectory |
| 250 | + const files = fs.readdirSync(dir) |
| 251 | + for (const filename of files) { |
| 252 | + const content = fs.readFileSync(path.resolve(dir, filename)).toString('utf-8') |
| 253 | + if (!content) { |
| 254 | + continue |
| 255 | + } |
| 256 | + const json = JSON.parse(content) |
| 257 | + const coverageList = json.result |
| 258 | + const sourceMapCache = json['source-map-cache'] |
| 259 | + const coverageData = await collectCoverageData(coverageList, entryFilterHandler, sourceMapCache) |
| 260 | + if (coverageData) { |
| 261 | + await coverageReport.add(coverageData) |
| 262 | + } |
| 263 | + } |
| 264 | +} |
0 commit comments