Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Istanbul reporting #8

Merged
merged 6 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
node_modules
.nyc_output
coverage
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not like it matters much, but can you use asynchronous I/O if possible?

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
}
})
})
}
}
Loading