Skip to content

Commit

Permalink
chore: add support for subprocess spawning (bcoe#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe authored Jan 1, 2018
1 parent 836cc21 commit f14508e
Show file tree
Hide file tree
Showing 10 changed files with 2,440 additions and 408 deletions.
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,18 @@ c8 node foo.js

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

## remaining work
## Disclaimer

- [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
c8 uses bleeding edge v8 features (_it's an ongoing experiment, testing
what will eventually be possible in the realm of test coverage in Node.js_).

- [ ] talk to node.js project about silencing messages:
For the best experience, try running with [a canary build of Node.js](https://github.com/v8/node).

> `Debugger listening on ws://127.0.0.1:56399/e850110a-c5df-41d8-8ef2-400f6829617f`.
## How it Works

- [x] figure out why `detailed` mode does not appear to be working.
* this is fixed in v8, as long as you start with `--inspect-brk` you
can collect coverage in detailed mode.
- [x] figure out a better way to determine that all processes in event loop
have terminated (except the inspector session).
- [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.
- [x] figure out why instrumentation of .mjs files does not work:
* see: https://github.com/nodejs/node/issues/17336
Before running your application c8 creates [an inspector session](https://nodejs.org/api/inspector.html) in v8 and enables v8's
[built in coverage reporting](https://v8project.blogspot.com/2017/12/javascript-code-coverage.html).

Just before your application exits, c8 fetches the coverage information from
v8 and writes it to disk in a format compatible with
[Istanbul's reporters](https://istanbul.js.org/).
98 changes: 15 additions & 83 deletions bin/c8.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,34 @@
#!/usr/bin/env node
'use strict'

const CRI = require('chrome-remote-interface')
const Exclude = require('test-exclude')
const {isAbsolute} = require('path')
const foreground = require('foreground-child')
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 sw = require('spawn-wrap')
const {
hideInstrumenteeArgs,
hideInstrumenterArgs,
yargs
} = require('../lib/parse-args')

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

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

;(async function executeWithCoverage (instrumenteeArgv) {
try {
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) => {
client.once('Debugger.paused', resolve)
})

const mainContextInfo = new Promise((resolve) => {
client.once('Runtime.executionContextCreated', (message) => {
resolve(message.context)
})
})

const executionComplete = new Promise((resolve) => {
client.on('Runtime.executionContextDestroyed', async (message) => {
if (message.executionContextId === (await mainContextInfo).id) {
resolve(message)
}
})
})

const {Debugger, Runtime, Profiler} = client
await Promise.all([
Profiler.enable(),
Runtime.enable(),
Debugger.enable(),
Profiler.startPreciseCoverage({callCount: true, detailed: true}),
Runtime.runIfWaitingForDebugger(),
initialPause
])
await Debugger.resume()
const argv = yargs.parse(instrumenterArgs)

await executionComplete
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))
const tmpDirctory = resolve(argv.coverageDirectory, './tmp')
rimraf.sync(tmpDirctory)
mkdirp.sync(tmpDirctory)

async function collectV8Coverage (Profiler) {
let {result} = await Profiler.takePreciseCoverage()
result = result.filter(({url}) => {
url = url.replace('file://', '')
return isAbsolute(url) && exclude.shouldInstrument(url)
})
return result
}
sw([require.resolve('../lib/wrap')], {
C8_ARGV: JSON.stringify(argv)
})

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'
)
foreground(hideInstrumenterArgs(argv), (out) => {
report({
reporter: Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter],
coverageDirectory: argv.coverageDirectory,
watermarks: argv.watermarks
})
}
})
35 changes: 0 additions & 35 deletions lib/spawn.js

This file was deleted.

81 changes: 81 additions & 0 deletions lib/wrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
'use strict'

const Exclude = require('test-exclude')
const inspector = require('inspector')
const {isAbsolute} = require('path')
const onExit = require('signal-exit')
const {resolve} = require('path')
const sw = require('spawn-wrap')
const uuid = require('uuid')
const v8ToIstanbul = require('v8-to-istanbul')
const {writeFileSync} = require('fs')

const argv = JSON.parse(process.env.C8_ARGV)

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

;(async function runInstrumented () {
try {
// bootstrap the inspector before kicking
// off the user's code.
inspector.open(0, true)
const session = new inspector.Session()
session.connect()

session.post('Profiler.enable')
session.post('Runtime.enable')
session.post(
'Profiler.startPreciseCoverage',
{callCount: true, detailed: true}
)

// hook process.exit() and common exit signals, e.g., SIGTERM,
// and output coverage report when these occur.
onExit(() => {
session.post('Profiler.takePreciseCoverage', (err, res) => {
if (err) console.warn(err.message)
else {
try {
const result = filterResult(res.result)
writeIstanbulFormatCoverage(result)
} catch (err) {
console.warn(err.message)
}
}
})
}, {alwaysLast: true})

// run the user's actual application.
sw.runMain()
} catch (err) {
console.error(err)
process.exit(1)
}
})()

function filterResult (result) {
result = result.filter(({url}) => {
url = url.replace('file://', '')
return isAbsolute(url) &&
exclude.shouldInstrument(url) &&
url !== __filename
})
return result
}

function writeIstanbulFormatCoverage (allV8Coverage) {
const tmpDirctory = resolve(argv.coverageDirectory, './tmp')
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'
)
})
}
Loading

0 comments on commit f14508e

Please sign in to comment.