Skip to content

Commit 2e5e297

Browse files
authored
feat: add experimental monocart reports (#521)
1 parent dc38051 commit 2e5e297

File tree

9 files changed

+714
-18
lines changed

9 files changed

+714
-18
lines changed

Diff for: README.md

+18
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,23 @@ 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 is an alternate library for outputting [v8 code coverage](https://v8.dev/blog/javascript-code-coverage) data as Istanbul reports.
125+
126+
Monocart also provides reporters based directly on v8's byte-offset-based output. Such as, `console-details` and `v8`. This removes a complex transformation step and may be less bug prone for some environments.
127+
128+
**Example usage:**
129+
130+
```sh
131+
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js
132+
```
133+
134+
NOTE: Monocart requires additional `monocart-coverage-reports` to be installed:
135+
136+
```sh
137+
npm i monocart-coverage-reports --save-dev
138+
```
139+
122140
## Ignoring Uncovered Lines, Functions, and Blocks
123141

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

Diff for: lib/commands/report.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ exports.outputReport = async function (argv) {
3636
src: argv.src,
3737
skipFull: argv.skipFull,
3838
excludeNodeModules: argv.excludeNodeModules,
39-
mergeAsync: argv.mergeAsync
39+
mergeAsync: argv.mergeAsync,
40+
monocartArgv: (argv.experimentalMonocart || process.env.EXPERIMENTAL_MONOCART) ? argv : null
4041
})
4142
await report.run()
4243
if (argv.checkCoverage) await checkCoverages(argv, report)

Diff for: lib/parse-args.js

+5
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: 'Use Monocart coverage reports'
165+
})
161166
.pkgConf('c8')
162167
.demandCommand(1)
163168
.check((argv) => {

Diff for: lib/report.js

+141-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class Report {
3636
allowExternal = false,
3737
skipFull,
3838
excludeNodeModules,
39-
mergeAsync
39+
mergeAsync,
40+
monocartArgv
4041
}) {
4142
this.reporter = reporter
4243
this.reporterOptions = reporterOptions || {}
@@ -60,6 +61,7 @@ class Report {
6061
this.src = this._getSrc(src)
6162
this.skipFull = skipFull
6263
this.mergeAsync = mergeAsync
64+
this.monocartArgv = monocartArgv
6365
}
6466

6567
_getSrc (src) {
@@ -73,6 +75,9 @@ class Report {
7375
}
7476

7577
async run () {
78+
if (this.monocartArgv) {
79+
return this.runMonocart()
80+
}
7681
const context = libReport.createContext({
7782
dir: this.reportsDirectory,
7883
watermarks: this.watermarks,
@@ -89,6 +94,141 @@ class Report {
8994
}
9095
}
9196

97+
async importMonocart () {
98+
return import('monocart-coverage-reports')
99+
}
100+
101+
async getMonocart () {
102+
let MCR
103+
try {
104+
MCR = await this.importMonocart()
105+
} catch (e) {
106+
console.error('--experimental-monocart requires the plugin monocart-coverage-reports. Run: "npm i monocart-coverage-reports --save-dev"')
107+
process.exit(1)
108+
}
109+
return MCR
110+
}
111+
112+
async runMonocart () {
113+
const MCR = await this.getMonocart()
114+
if (!MCR) {
115+
return
116+
}
117+
118+
const argv = this.monocartArgv
119+
const exclude = this.exclude
120+
121+
function getEntryFilter () {
122+
return argv.entryFilter || argv.filter || function (entry) {
123+
return exclude.shouldInstrument(fileURLToPath(entry.url))
124+
}
125+
}
126+
127+
function getSourceFilter () {
128+
return argv.sourceFilter || argv.filter || function (sourcePath) {
129+
if (argv.excludeAfterRemap) {
130+
// console.log(sourcePath)
131+
return exclude.shouldInstrument(sourcePath)
132+
}
133+
return true
134+
}
135+
}
136+
137+
function getReports () {
138+
const reports = Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter]
139+
const reporterOptions = argv.reporterOptions || {}
140+
141+
return reports.map((reportName) => {
142+
const reportOptions = {
143+
...reporterOptions[reportName]
144+
}
145+
if (reportName === 'text') {
146+
reportOptions.skipEmpty = false
147+
reportOptions.skipFull = argv.skipFull
148+
reportOptions.maxCols = process.stdout.columns || 100
149+
}
150+
return [reportName, reportOptions]
151+
})
152+
}
153+
154+
// --all: add empty coverage for all files
155+
function getAllOptions () {
156+
if (!argv.all) {
157+
return
158+
}
159+
160+
const src = argv.src
161+
const workingDirs = Array.isArray(src) ? src : (typeof src === 'string' ? [src] : [process.cwd()])
162+
return {
163+
dir: workingDirs,
164+
filter: (filePath) => {
165+
return exclude.shouldInstrument(filePath)
166+
}
167+
}
168+
}
169+
170+
function initPct (summary) {
171+
Object.keys(summary).forEach(k => {
172+
if (summary[k].pct === '') {
173+
summary[k].pct = 100
174+
}
175+
})
176+
return summary
177+
}
178+
179+
// adapt coverage options
180+
const coverageOptions = {
181+
logging: argv.logging,
182+
name: argv.name,
183+
184+
reports: getReports(),
185+
186+
outputDir: argv.reportsDir,
187+
baseDir: argv.baseDir,
188+
189+
entryFilter: getEntryFilter(),
190+
sourceFilter: getSourceFilter(),
191+
192+
inline: argv.inline,
193+
lcov: argv.lcov,
194+
195+
all: getAllOptions(),
196+
197+
clean: argv.clean,
198+
199+
// use default value for istanbul
200+
defaultSummarizer: 'pkg',
201+
202+
onEnd: (coverageResults) => {
203+
// for check coverage
204+
this._allCoverageFiles = {
205+
files: () => {
206+
return coverageResults.files.map(it => it.sourcePath)
207+
},
208+
fileCoverageFor: (file) => {
209+
const fileCoverage = coverageResults.files.find(it => it.sourcePath === file)
210+
return {
211+
toSummary: () => {
212+
return initPct(fileCoverage.summary)
213+
}
214+
}
215+
},
216+
getCoverageSummary: () => {
217+
return initPct(coverageResults.summary)
218+
}
219+
}
220+
}
221+
}
222+
const coverageReport = new MCR.CoverageReport(coverageOptions)
223+
coverageReport.cleanCache()
224+
225+
// read v8 coverage data from tempDirectory
226+
await coverageReport.addFromDir(argv.tempDirectory)
227+
228+
// generate report
229+
await coverageReport.generate()
230+
}
231+
92232
async getCoverageMapFromAllCoverageFiles () {
93233
// the merge process can be very expensive, and it's often the case that
94234
// check-coverage is called immediately after a report. We memoize the

0 commit comments

Comments
 (0)