Skip to content

Commit

Permalink
feat: add experimental monocart reports (#521)
Browse files Browse the repository at this point in the history
  • Loading branch information
cenfun committed Jun 11, 2024
1 parent dc38051 commit 2e5e297
Show file tree
Hide file tree
Showing 9 changed files with 714 additions and 18 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Here is a list of common options. Run `c8 --help` for the full list and document
| `--per-file` | check thresholds per file | `boolean` | `false` |
| `--temp-directory` | directory V8 coverage data is written to and read from | `string` | `process.env.NODE_V8_COVERAGE` |
| `--clean` | should temp files be deleted before script execution | `boolean` | `true` |
| `--experimental-monocart` | see [section below](#using-monocart-coverage-reports-experimental) for more info | `boolean` | `false` |

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

Expand Down Expand Up @@ -119,6 +120,23 @@ The `--100` flag can be set for the `check-coverage` as well:
c8 check-coverage --100
```

## Using Monocart coverage reports (experimental)
Monocart is an alternate library for outputting [v8 code coverage](https://v8.dev/blog/javascript-code-coverage) data as Istanbul reports.

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.

**Example usage:**

```sh
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js
```

NOTE: Monocart requires additional `monocart-coverage-reports` to be installed:

```sh
npm i monocart-coverage-reports --save-dev
```

## Ignoring Uncovered Lines, Functions, and Blocks

Sometimes you might find yourself wanting to ignore uncovered portions of your
Expand Down
3 changes: 2 additions & 1 deletion lib/commands/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ exports.outputReport = async function (argv) {
src: argv.src,
skipFull: argv.skipFull,
excludeNodeModules: argv.excludeNodeModules,
mergeAsync: argv.mergeAsync
mergeAsync: argv.mergeAsync,
monocartArgv: (argv.experimentalMonocart || process.env.EXPERIMENTAL_MONOCART) ? argv : null
})
await report.run()
if (argv.checkCoverage) await checkCoverages(argv, report)
Expand Down
5 changes: 5 additions & 0 deletions lib/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ function buildYargs (withCommands = false) {
describe: 'supplying --merge-async will merge all v8 coverage reports asynchronously and incrementally. ' +
'This is to avoid OOM issues with Node.js runtime.'
})
.option('experimental-monocart', {
default: false,
type: 'boolean',
describe: 'Use Monocart coverage reports'
})
.pkgConf('c8')
.demandCommand(1)
.check((argv) => {
Expand Down
142 changes: 141 additions & 1 deletion lib/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class Report {
allowExternal = false,
skipFull,
excludeNodeModules,
mergeAsync
mergeAsync,
monocartArgv
}) {
this.reporter = reporter
this.reporterOptions = reporterOptions || {}
Expand All @@ -60,6 +61,7 @@ class Report {
this.src = this._getSrc(src)
this.skipFull = skipFull
this.mergeAsync = mergeAsync
this.monocartArgv = monocartArgv
}

_getSrc (src) {
Expand All @@ -73,6 +75,9 @@ class Report {
}

async run () {
if (this.monocartArgv) {
return this.runMonocart()
}
const context = libReport.createContext({
dir: this.reportsDirectory,
watermarks: this.watermarks,
Expand All @@ -89,6 +94,141 @@ class Report {
}
}

async importMonocart () {
return import('monocart-coverage-reports')
}

async getMonocart () {
let MCR
try {
MCR = await this.importMonocart()
} catch (e) {
console.error('--experimental-monocart requires the plugin monocart-coverage-reports. Run: "npm i monocart-coverage-reports --save-dev"')
process.exit(1)
}
return MCR
}

async runMonocart () {
const MCR = await this.getMonocart()
if (!MCR) {
return
}

const argv = this.monocartArgv
const exclude = this.exclude

function getEntryFilter () {
return argv.entryFilter || argv.filter || function (entry) {
return exclude.shouldInstrument(fileURLToPath(entry.url))
}
}

function getSourceFilter () {
return argv.sourceFilter || argv.filter || function (sourcePath) {
if (argv.excludeAfterRemap) {
// console.log(sourcePath)
return exclude.shouldInstrument(sourcePath)
}
return true
}
}

function getReports () {
const reports = Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter]
const reporterOptions = argv.reporterOptions || {}

return reports.map((reportName) => {
const reportOptions = {
...reporterOptions[reportName]
}
if (reportName === 'text') {
reportOptions.skipEmpty = false
reportOptions.skipFull = argv.skipFull
reportOptions.maxCols = process.stdout.columns || 100
}
return [reportName, reportOptions]
})
}

// --all: add empty coverage for all files
function getAllOptions () {
if (!argv.all) {
return
}

const src = argv.src
const workingDirs = Array.isArray(src) ? src : (typeof src === 'string' ? [src] : [process.cwd()])
return {
dir: workingDirs,
filter: (filePath) => {
return exclude.shouldInstrument(filePath)
}
}
}

function initPct (summary) {
Object.keys(summary).forEach(k => {
if (summary[k].pct === '') {
summary[k].pct = 100
}
})
return summary
}

// adapt coverage options
const coverageOptions = {
logging: argv.logging,
name: argv.name,

reports: getReports(),

outputDir: argv.reportsDir,
baseDir: argv.baseDir,

entryFilter: getEntryFilter(),
sourceFilter: getSourceFilter(),

inline: argv.inline,
lcov: argv.lcov,

all: getAllOptions(),

clean: argv.clean,

// use default value for istanbul
defaultSummarizer: 'pkg',

onEnd: (coverageResults) => {
// for check coverage
this._allCoverageFiles = {
files: () => {
return coverageResults.files.map(it => it.sourcePath)
},
fileCoverageFor: (file) => {
const fileCoverage = coverageResults.files.find(it => it.sourcePath === file)
return {
toSummary: () => {
return initPct(fileCoverage.summary)
}
}
},
getCoverageSummary: () => {
return initPct(coverageResults.summary)
}
}
}
}
const coverageReport = new MCR.CoverageReport(coverageOptions)
coverageReport.cleanCache()

// read v8 coverage data from tempDirectory
await coverageReport.addFromDir(argv.tempDirectory)

// generate report
await coverageReport.generate()
}

async getCoverageMapFromAllCoverageFiles () {
// the merge process can be very expensive, and it's often the case that
// check-coverage is called immediately after a report. We memoize the
Expand Down
Loading

0 comments on commit 2e5e297

Please sign in to comment.