Skip to content

Commit 114be25

Browse files
committed
feat: support monocart report
1 parent bf3073b commit 114be25

File tree

8 files changed

+760
-12
lines changed

8 files changed

+760
-12
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Here is a list of common options. Run `c8 --help` for the full list and document
4343
| `--per-file` | check thresholds per file | `boolean` | `false` |
4444
| `--temp-directory` | directory V8 coverage data is written to and read from | `string` | `process.env.NODE_V8_COVERAGE` |
4545
| `--clean` | should temp files be deleted before script execution | `boolean` | `true` |
46+
| `--experimental-monocart` | see [section below](#using-monocart-coverage-reports-experimental) for more info | `boolean` | `false` |
4647

4748
## Checking for "full" source coverage using `--all`
4849

@@ -119,6 +120,12 @@ The `--100` flag can be set for the `check-coverage` as well:
119120
c8 check-coverage --100
120121
```
121122

123+
## Using Monocart coverage reports (experimental)
124+
[Monocart](https://github.com/cenfun/monocart-coverage-reports) will bring additional support for native V8 coverage reports, for example:
125+
```sh
126+
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js
127+
```
128+
122129
## Ignoring Uncovered Lines, Functions, and Blocks
123130

124131
Sometimes you might find yourself wanting to ignore uncovered portions of your

lib/commands/report.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { checkCoverages } = require('./check-coverage')
22
const Report = require('../report')
3+
const monocartReport = require('../monocart-report')
34

45
exports.command = 'report'
56

@@ -18,6 +19,12 @@ exports.outputReport = async function (argv) {
1819
argv.branches = 100
1920
argv.statements = 100
2021
}
22+
23+
if (argv.experimentalMonocart || process.env.EXPERIMENTAL_MONOCART) {
24+
await monocartReport(argv)
25+
return
26+
}
27+
2128
const report = Report({
2229
include: argv.include,
2330
exclude: argv.exclude,

lib/monocart-report.js

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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+
}

lib/parse-args.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ function buildYargs (withCommands = false) {
158158
describe: 'supplying --merge-async will merge all v8 coverage reports asynchronously and incrementally. ' +
159159
'This is to avoid OOM issues with Node.js runtime.'
160160
})
161+
.option('experimental-monocart', {
162+
default: false,
163+
type: 'boolean',
164+
describe: 'Using Monocart coverage reports'
165+
})
161166
.pkgConf('c8')
162167
.demandCommand(1)
163168
.check((argv) => {

0 commit comments

Comments
 (0)