Skip to content

Commit

Permalink
feat: implement Istanbul reporting (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe committed Dec 5, 2017
1 parent 01f654e commit 8e430bf
Show file tree
Hide file tree
Showing 11 changed files with 2,156 additions and 225 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
.DS_Store
node_modules
.nyc_output
coverage
10 changes: 5 additions & 5 deletions README.md
Expand Up @@ -3,7 +3,7 @@
Code-coverage using [v8's Inspector](https://nodejs.org/dist/latest-v8.x/docs/api/inspector.html)
that's compatible with [Istanbul's reporters](https://istanbul.js.org/docs/advanced/alternative-reporters/).

Like [nyc](https://github.com/istanbuljs/nyc), c8 just magically works, simply:
Like [nyc](https://github.com/istanbuljs/nyc), c8 just magically works:

```bash
npm i c8 -g
Expand All @@ -12,12 +12,12 @@ c8 node foo.js

The above example will collect coverage for `foo.js` using v8's inspector.

TODO:
## remaining work

- [x] write logic for converting v8 coverage output to [Istanbul Coverage.json format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md).
* https://github.com/bcoe/v8-to-istanbul

- [ ] talk to Node.js project about silencing messages:
- [ ] talk to node.js project about silencing messages:

> `Debugger listening on ws://127.0.0.1:56399/e850110a-c5df-41d8-8ef2-400f6829617f`.
Expand All @@ -29,5 +29,5 @@ TODO:
- [x] process.exit() can't perform an async operation; how can we track coverage
for scripts that exit?
* we can now listen for the `Runtime.executionContextDestroyed` event.
- [ ] figure out why instrumentation of .mjs files does not work:
* see: https://github.com/nodejs/node/issues/17336
- [x] figure out why instrumentation of .mjs files does not work:
* see: https://github.com/nodejs/node/issues/17336
71 changes: 54 additions & 17 deletions bin/c8.js
@@ -1,15 +1,36 @@
#!/usr/bin/env node
'use strict'

const {isAbsolute} = require('path')
const argv = require('yargs').parse()
const CRI = require('chrome-remote-interface')
const Exclude = require('test-exclude')
const {isAbsolute} = require('path')
const mkdirp = require('mkdirp')
const report = require('../lib/report')
const {resolve} = require('path')
const rimraf = require('rimraf')
const spawn = require('../lib/spawn')
const uuid = require('uuid')
const v8ToIstanbul = require('v8-to-istanbul')
const {writeFileSync} = require('fs')
const {
hideInstrumenteeArgs,
hideInstrumenterArgs,
yargs
} = require('../lib/parse-args')

const instrumenterArgs = hideInstrumenteeArgs()
const argv = yargs.parse(instrumenterArgs)

;(async () => {
const exclude = Exclude({
include: argv.include,
exclude: argv.exclude
})

;(async function executeWithCoverage (instrumenteeArgv) {
try {
const info = await spawn(process.execPath,
[`--inspect-brk=0`].concat(process.argv.slice(2)))
const bin = instrumenteeArgv.shift()
const info = await spawn(bin,
[`--inspect-brk=0`].concat(instrumenteeArgv))
const client = await CRI({port: info.port})

const initialPause = new Promise((resolve) => {
Expand Down Expand Up @@ -42,24 +63,40 @@ const spawn = require('../lib/spawn')
await Debugger.resume()

await executionComplete
await outputCoverage(Profiler)
client.close()

const allV8Coverage = await collectV8Coverage(Profiler)
writeIstanbulFormatCoverage(allV8Coverage)
await client.close()
report({
reporter: Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter],
coverageDirectory: argv.coverageDirectory,
watermarks: argv.watermarks
})
} catch (err) {
console.error(err)
process.exit(1)
}
})()
})(hideInstrumenterArgs(argv))

async function outputCoverage (Profiler) {
const IGNORED_PATHS = [
/\/bin\/wrap.js/,
/\/node_modules\//,
/node-spawn-wrap/
]
async function collectV8Coverage (Profiler) {
let {result} = await Profiler.takePreciseCoverage()
result = result.filter(({url}) => {
return isAbsolute(url) && IGNORED_PATHS.every(ignored => !ignored.test(url))
url = url.replace('file://', '')
return isAbsolute(url) && exclude.shouldInstrument(url)
})
return result
}

function writeIstanbulFormatCoverage (allV8Coverage) {
const tmpDirctory = resolve(argv.coverageDirectory, './tmp')
rimraf.sync(tmpDirctory)
mkdirp.sync(tmpDirctory)
allV8Coverage.forEach((v8) => {
const script = v8ToIstanbul(v8.url)
script.applyCoverage(v8.functions)
writeFileSync(
resolve(tmpDirctory, `./${uuid.v4()}.json`),
JSON.stringify(script.toIstanbul(), null, 2),
'utf8'
)
})
console.log(JSON.stringify(result, null, 2))
}
63 changes: 63 additions & 0 deletions lib/parse-args.js
@@ -0,0 +1,63 @@
const Exclude = require('test-exclude')
const findUp = require('find-up')
const {readFileSync} = require('fs')
const yargs = require('yargs')
const parser = require('yargs-parser')

const configPath = findUp.sync(['.c8rc', '.c8rc.json'])
const config = configPath ? JSON.parse(readFileSync(configPath)) : {}

yargs
.usage('$0 [opts] [script] [opts]')
.option('reporter', {
alias: 'r',
describe: 'coverage reporter(s) to use',
default: 'text'
})
.option('exclude', {
alias: 'x',
default: Exclude.defaultExclude,
describe: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported.'
})
.option('include', {
alias: 'n',
default: [],
describe: 'a list of specific files that should be covered, glob patterns are supported'
})
.option('coverage-directory', {
default: './coverage',
describe: 'directory to output coverage JSON and reports'
})
.pkgConf('c8')
.config(config)
.demandCommand(1)
.epilog('visit https://git.io/vHysA for list of available reporters')

function hideInstrumenterArgs (yargv) {
var argv = process.argv.slice(1)
argv = argv.slice(argv.indexOf(yargv._[0]))
if (argv[0][0] === '-') {
argv.unshift(process.execPath)
}
return argv
}

function hideInstrumenteeArgs () {
let argv = process.argv.slice(2)
const yargv = parser(argv)

if (!yargv._.length) return argv

// drop all the arguments after the bin being
// instrumented by c8.
argv = argv.slice(0, argv.indexOf(yargv._[0]))
argv.push(yargv._[0])

return argv
}

module.exports = {
yargs,
hideInstrumenterArgs,
hideInstrumenteeArgs
}
51 changes: 51 additions & 0 deletions lib/report.js
@@ -0,0 +1,51 @@
const libCoverage = require('istanbul-lib-coverage')
const libReport = require('istanbul-lib-report')
const reports = require('istanbul-reports')
const {readdirSync, readFileSync} = require('fs')
const {resolve} = require('path')

class Report {
constructor ({reporter, coverageDirectory, watermarks}) {
this.reporter = reporter
this.coverageDirectory = coverageDirectory
this.watermarks = watermarks
}
run () {
const map = this._getCoverageMapFromAllCoverageFiles()
var context = libReport.createContext({
dir: './coverage',
watermarks: this.watermarks
})

const tree = libReport.summarizers.pkg(map)

this.reporter.forEach(function (_reporter) {
tree.visit(reports.create(_reporter), context)
})
}
_getCoverageMapFromAllCoverageFiles () {
const map = libCoverage.createCoverageMap({})

this._loadReports().forEach(function (report) {
map.merge(report)
})

return map
}
_loadReports () {
const tmpDirctory = resolve(this.coverageDirectory, './tmp')
const files = readdirSync(tmpDirctory)

return files.map((f) => {
return JSON.parse(readFileSync(
resolve(tmpDirctory, f),
'utf8'
))
})
}
}

module.exports = function (opts) {
const report = new Report(opts)
report.run()
}
10 changes: 6 additions & 4 deletions lib/spawn.js
Expand Up @@ -2,7 +2,7 @@ const {spawn} = require('child_process')

const debuggerRe = /Debugger listening on ws:\/\/[^:]*:([^/]*)/

module.exports = function (execPath, args=[]) {
module.exports = function (execPath, args = []) {
const info = {
port: -1
}
Expand All @@ -11,7 +11,7 @@ module.exports = function (execPath, args=[]) {
stdio: [process.stdin, process.stdout, 'pipe'],
env: process.env,
cwd: process.cwd()
});
})

proc.stderr.on('data', (outBuffer) => {
const outString = outBuffer.toString('utf8')
Expand All @@ -23,11 +23,13 @@ module.exports = function (execPath, args=[]) {
console.error(outString)
}
})

proc.on('close', (code) => {
if (info.port === -1) {
return reject(Error('could not connect to inspector'))
} else {
process.exitCode = code
}
})
})
}
}

0 comments on commit 8e430bf

Please sign in to comment.