lib: make coverage work for Node.js

PR-URL: https://github.com/nodejs/node/pull/23941
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yang Guo <yangguo@chromium.org>
This commit is contained in:
Benjamin 2018-11-03 15:42:13 -07:00 committed by Rich Trott
parent 2ea70eae73
commit 616fac9169
7 changed files with 129 additions and 48 deletions

View File

@ -362,6 +362,12 @@
NativeModule._cache[this.id] = this; NativeModule._cache[this.id] = this;
}; };
// coverage must be turned on early, so that we can collect
// it for Node.js' own internal libraries.
if (process.env.NODE_V8_COVERAGE) {
NativeModule.require('internal/process/coverage').setup();
}
// This will be passed to the bootstrapNodeJSCore function in // This will be passed to the bootstrapNodeJSCore function in
// bootstrap/node.js. // bootstrap/node.js.
return loaderExports; return loaderExports;

View File

@ -103,9 +103,7 @@
NativeModule.require('internal/process/write-coverage').setup(); NativeModule.require('internal/process/write-coverage').setup();
if (process.env.NODE_V8_COVERAGE) { if (process.env.NODE_V8_COVERAGE) {
const { resolve } = NativeModule.require('path'); NativeModule.require('internal/process/coverage').setupExitHooks();
process.env.NODE_V8_COVERAGE = resolve(process.env.NODE_V8_COVERAGE);
NativeModule.require('internal/process/coverage').setup();
} }
if (process.config.variables.v8_enable_inspector) { if (process.config.variables.v8_enable_inspector) {

View File

@ -1,23 +1,19 @@
'use strict'; 'use strict';
const path = require('path'); let coverageConnection = null;
const { mkdirSync, writeFileSync } = require('fs'); let coverageDirectory;
const hasInspector = process.config.variables.v8_enable_inspector === 1;
let inspector = null;
if (hasInspector) inspector = require('inspector');
let session;
function writeCoverage() { function writeCoverage() {
if (!session) { if (!coverageConnection && coverageDirectory) {
return; return;
} }
const { join } = require('path');
const { mkdirSync, writeFileSync } = require('fs');
const { threadId } = require('internal/worker'); const { threadId } = require('internal/worker');
const filename = `coverage-${process.pid}-${Date.now()}-${threadId}.json`; const filename = `coverage-${process.pid}-${Date.now()}-${threadId}.json`;
try { try {
// TODO(bcoe): switch to mkdirp once #22302 is addressed. mkdirSync(coverageDirectory, { recursive: true });
mkdirSync(process.env.NODE_V8_COVERAGE);
} catch (err) { } catch (err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
console.error(err); console.error(err);
@ -25,41 +21,73 @@ function writeCoverage() {
} }
} }
const target = path.join(process.env.NODE_V8_COVERAGE, filename); const target = join(coverageDirectory, filename);
try { try {
session.post('Profiler.takePreciseCoverage', (err, coverageInfo) => { disableAllAsyncHooks();
if (err) return console.error(err); let msg;
try { coverageConnection._coverageCallback = function(_msg) {
writeFileSync(target, JSON.stringify(coverageInfo)); msg = _msg;
} catch (err) { };
console.error(err); coverageConnection.dispatch(JSON.stringify({
} id: 3,
}); method: 'Profiler.takePreciseCoverage'
}));
const coverageInfo = JSON.parse(msg).result;
writeFileSync(target, JSON.stringify(coverageInfo));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} finally { } finally {
session.disconnect(); coverageConnection.disconnect();
session = null; coverageConnection = null;
} }
} }
function disableAllAsyncHooks() {
const { getHookArrays } = require('internal/async_hooks');
const [hooks_array] = getHookArrays();
hooks_array.forEach((hook) => { hook.disable(); });
}
exports.writeCoverage = writeCoverage; exports.writeCoverage = writeCoverage;
function setup() { function setup() {
if (!hasInspector) { const { Connection } = process.binding('inspector');
console.warn('coverage currently only supported in main thread'); if (!Connection) {
console.warn('inspector not enabled');
return; return;
} }
session = new inspector.Session(); coverageConnection = new Connection((res) => {
session.connect(); if (coverageConnection._coverageCallback) {
session.post('Profiler.enable'); coverageConnection._coverageCallback(res);
session.post('Profiler.startPreciseCoverage', { callCount: true, }
detailed: true }); });
coverageConnection.dispatch(JSON.stringify({
id: 1,
method: 'Profiler.enable'
}));
coverageConnection.dispatch(JSON.stringify({
id: 2,
method: 'Profiler.startPreciseCoverage',
params: {
callCount: true,
detailed: true
}
}));
try {
const { resolve } = require('path');
coverageDirectory = process.env.NODE_V8_COVERAGE =
resolve(process.env.NODE_V8_COVERAGE);
} catch (err) {
console.error(err);
}
}
exports.setup = setup;
function setupExitHooks() {
const reallyReallyExit = process.reallyExit; const reallyReallyExit = process.reallyExit;
process.reallyExit = function(code) { process.reallyExit = function(code) {
writeCoverage(); writeCoverage();
reallyReallyExit(code); reallyReallyExit(code);
@ -68,4 +96,4 @@ function setup() {
process.on('exit', writeCoverage); process.on('exit', writeCoverage);
} }
exports.setup = setup; exports.setupExitHooks = setupExitHooks;

View File

@ -0,0 +1,11 @@
const async_hooks = require('async_hooks');
const common = require('../../common');
const hook = async_hooks.createHook({
init: common.mustNotCall(),
before: common.mustNotCall(),
after: common.mustNotCall(),
destroy: common.mustNotCall()
});
hook.enable();

View File

@ -7,17 +7,38 @@ common.skipIfInspectorDisabled();
const { validateSnapshotNodes } = require('../common/heap'); const { validateSnapshotNodes } = require('../common/heap');
const inspector = require('inspector'); const inspector = require('inspector');
const session = new inspector.Session(); const snapshotNode = {
validateSnapshotNodes('Node / JSBindingsConnection', []); children: [
session.connect(); { node_name: 'Node / InspectorSession', edge_name: 'session' }
validateSnapshotNodes('Node / JSBindingsConnection', [ ]
{ };
children: [
{ node_name: 'Node / InspectorSession', edge_name: 'session' }, // starts with no JSBindingsConnection (or 1 if coverage enabled).
{ node_name: 'Connection', edge_name: 'wrapped' }, {
(edge) => edge.name === 'callback' && const expected = [];
(edge.to.type === undefined || // embedded graph if (process.env.NODE_V8_COVERAGE) {
edge.to.type === 'closure') // snapshot expected.push(snapshotNode);
]
} }
]); validateSnapshotNodes('Node / JSBindingsConnection', expected);
}
// JSBindingsConnection should be added.
{
const session = new inspector.Session();
session.connect();
const expected = [
{
children: [
{ node_name: 'Node / InspectorSession', edge_name: 'session' },
{ node_name: 'Connection', edge_name: 'wrapped' },
(edge) => edge.name === 'callback' &&
(edge.to.type === undefined || // embedded graph
edge.to.type === 'closure') // snapshot
]
}
];
if (process.env.NODE_V8_COVERAGE) {
expected.push(snapshotNode);
}
validateSnapshotNodes('Node / JSBindingsConnection', expected);
}

View File

@ -108,6 +108,20 @@ function nextdir() {
assert.strictEqual(fixtureCoverage, undefined); assert.strictEqual(fixtureCoverage, undefined);
} }
// disables async hooks before writing coverage.
{
const coverageDirectory = path.join(tmpdir.path, nextdir());
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/v8-coverage/async-hooks')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
assert.strictEqual(output.status, 0);
const fixtureCoverage = getFixtureCoverage('async-hooks.js',
coverageDirectory);
assert.ok(fixtureCoverage);
// first branch executed.
assert.strictEqual(fixtureCoverage.functions[1].ranges[0].count, 1);
}
// extracts the coverage object for a given fixture name. // extracts the coverage object for a given fixture name.
function getFixtureCoverage(fixtureFile, coverageDirectory) { function getFixtureCoverage(fixtureFile, coverageDirectory) {
const coverageFiles = fs.readdirSync(coverageDirectory); const coverageFiles = fs.readdirSync(coverageDirectory);

View File

@ -20,7 +20,10 @@ assert(
`; `;
const args = ['--inspect', '-e', script]; const args = ['--inspect', '-e', script];
const child = spawn(process.execPath, args, { stdio: 'inherit' }); const child = spawn(process.execPath, args, {
stdio: 'inherit',
env: { ...process.env, NODE_V8_COVERAGE: '' }
});
child.on('exit', (code, signal) => { child.on('exit', (code, signal) => {
process.exit(code || signal); process.exit(code || signal);
}); });