Skip to content

Commit 7ee0cea

Browse files
Benjamintargos
Benjamin
authored andcommitted
lib: make coverage work for Node.js
PR-URL: #23941 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yang Guo <yangguo@chromium.org>
1 parent fdba226 commit 7ee0cea

File tree

7 files changed

+129
-48
lines changed

7 files changed

+129
-48
lines changed

lib/internal/bootstrap/loaders.js

+6
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@
362362
NativeModule._cache[this.id] = this;
363363
};
364364

365+
// coverage must be turned on early, so that we can collect
366+
// it for Node.js' own internal libraries.
367+
if (process.env.NODE_V8_COVERAGE) {
368+
NativeModule.require('internal/process/coverage').setup();
369+
}
370+
365371
// This will be passed to the bootstrapNodeJSCore function in
366372
// bootstrap/node.js.
367373
return loaderExports;

lib/internal/bootstrap/node.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,7 @@
103103
NativeModule.require('internal/process/write-coverage').setup();
104104

105105
if (process.env.NODE_V8_COVERAGE) {
106-
const { resolve } = NativeModule.require('path');
107-
process.env.NODE_V8_COVERAGE = resolve(process.env.NODE_V8_COVERAGE);
108-
NativeModule.require('internal/process/coverage').setup();
106+
NativeModule.require('internal/process/coverage').setupExitHooks();
109107
}
110108

111109
if (process.config.variables.v8_enable_inspector) {

lib/internal/process/coverage.js

+59-31
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,93 @@
11
'use strict';
2-
const path = require('path');
3-
const { mkdirSync, writeFileSync } = require('fs');
4-
const hasInspector = process.config.variables.v8_enable_inspector === 1;
5-
let inspector = null;
6-
if (hasInspector) inspector = require('inspector');
7-
8-
let session;
2+
let coverageConnection = null;
3+
let coverageDirectory;
94

105
function writeCoverage() {
11-
if (!session) {
6+
if (!coverageConnection && coverageDirectory) {
127
return;
138
}
149

10+
const { join } = require('path');
11+
const { mkdirSync, writeFileSync } = require('fs');
1512
const { threadId } = require('internal/worker');
1613

1714
const filename = `coverage-${process.pid}-${Date.now()}-${threadId}.json`;
1815
try {
19-
// TODO(bcoe): switch to mkdirp once #22302 is addressed.
20-
mkdirSync(process.env.NODE_V8_COVERAGE);
16+
mkdirSync(coverageDirectory, { recursive: true });
2117
} catch (err) {
2218
if (err.code !== 'EEXIST') {
2319
console.error(err);
2420
return;
2521
}
2622
}
2723

28-
const target = path.join(process.env.NODE_V8_COVERAGE, filename);
29-
24+
const target = join(coverageDirectory, filename);
3025
try {
31-
session.post('Profiler.takePreciseCoverage', (err, coverageInfo) => {
32-
if (err) return console.error(err);
33-
try {
34-
writeFileSync(target, JSON.stringify(coverageInfo));
35-
} catch (err) {
36-
console.error(err);
37-
}
38-
});
26+
disableAllAsyncHooks();
27+
let msg;
28+
coverageConnection._coverageCallback = function(_msg) {
29+
msg = _msg;
30+
};
31+
coverageConnection.dispatch(JSON.stringify({
32+
id: 3,
33+
method: 'Profiler.takePreciseCoverage'
34+
}));
35+
const coverageInfo = JSON.parse(msg).result;
36+
writeFileSync(target, JSON.stringify(coverageInfo));
3937
} catch (err) {
4038
console.error(err);
4139
} finally {
42-
session.disconnect();
43-
session = null;
40+
coverageConnection.disconnect();
41+
coverageConnection = null;
4442
}
4543
}
4644

45+
function disableAllAsyncHooks() {
46+
const { getHookArrays } = require('internal/async_hooks');
47+
const [hooks_array] = getHookArrays();
48+
hooks_array.forEach((hook) => { hook.disable(); });
49+
}
50+
4751
exports.writeCoverage = writeCoverage;
4852

4953
function setup() {
50-
if (!hasInspector) {
51-
console.warn('coverage currently only supported in main thread');
54+
const { Connection } = process.binding('inspector');
55+
if (!Connection) {
56+
console.warn('inspector not enabled');
5257
return;
5358
}
5459

55-
session = new inspector.Session();
56-
session.connect();
57-
session.post('Profiler.enable');
58-
session.post('Profiler.startPreciseCoverage', { callCount: true,
59-
detailed: true });
60+
coverageConnection = new Connection((res) => {
61+
if (coverageConnection._coverageCallback) {
62+
coverageConnection._coverageCallback(res);
63+
}
64+
});
65+
coverageConnection.dispatch(JSON.stringify({
66+
id: 1,
67+
method: 'Profiler.enable'
68+
}));
69+
coverageConnection.dispatch(JSON.stringify({
70+
id: 2,
71+
method: 'Profiler.startPreciseCoverage',
72+
params: {
73+
callCount: true,
74+
detailed: true
75+
}
76+
}));
6077

61-
const reallyReallyExit = process.reallyExit;
78+
try {
79+
const { resolve } = require('path');
80+
coverageDirectory = process.env.NODE_V8_COVERAGE =
81+
resolve(process.env.NODE_V8_COVERAGE);
82+
} catch (err) {
83+
console.error(err);
84+
}
85+
}
6286

87+
exports.setup = setup;
88+
89+
function setupExitHooks() {
90+
const reallyReallyExit = process.reallyExit;
6391
process.reallyExit = function(code) {
6492
writeCoverage();
6593
reallyReallyExit(code);
@@ -68,4 +96,4 @@ function setup() {
6896
process.on('exit', writeCoverage);
6997
}
7098

71-
exports.setup = setup;
99+
exports.setupExitHooks = setupExitHooks;
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const async_hooks = require('async_hooks');
2+
const common = require('../../common');
3+
4+
const hook = async_hooks.createHook({
5+
init: common.mustNotCall(),
6+
before: common.mustNotCall(),
7+
after: common.mustNotCall(),
8+
destroy: common.mustNotCall()
9+
});
10+
11+
hook.enable();

test/parallel/test-heapdump-inspector.js

+34-13
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,38 @@ common.skipIfInspectorDisabled();
77
const { validateSnapshotNodes } = require('../common/heap');
88
const inspector = require('inspector');
99

10-
const session = new inspector.Session();
11-
validateSnapshotNodes('Node / JSBindingsConnection', []);
12-
session.connect();
13-
validateSnapshotNodes('Node / JSBindingsConnection', [
14-
{
15-
children: [
16-
{ node_name: 'Node / InspectorSession', edge_name: 'session' },
17-
{ node_name: 'Connection', edge_name: 'wrapped' },
18-
(edge) => edge.name === 'callback' &&
19-
(edge.to.type === undefined || // embedded graph
20-
edge.to.type === 'closure') // snapshot
21-
]
10+
const snapshotNode = {
11+
children: [
12+
{ node_name: 'Node / InspectorSession', edge_name: 'session' }
13+
]
14+
};
15+
16+
// starts with no JSBindingsConnection (or 1 if coverage enabled).
17+
{
18+
const expected = [];
19+
if (process.env.NODE_V8_COVERAGE) {
20+
expected.push(snapshotNode);
21+
}
22+
validateSnapshotNodes('Node / JSBindingsConnection', expected);
23+
}
24+
25+
// JSBindingsConnection should be added.
26+
{
27+
const session = new inspector.Session();
28+
session.connect();
29+
const expected = [
30+
{
31+
children: [
32+
{ node_name: 'Node / InspectorSession', edge_name: 'session' },
33+
{ node_name: 'Connection', edge_name: 'wrapped' },
34+
(edge) => edge.name === 'callback' &&
35+
(edge.to.type === undefined || // embedded graph
36+
edge.to.type === 'closure') // snapshot
37+
]
38+
}
39+
];
40+
if (process.env.NODE_V8_COVERAGE) {
41+
expected.push(snapshotNode);
2242
}
23-
]);
43+
validateSnapshotNodes('Node / JSBindingsConnection', expected);
44+
}

test/parallel/test-v8-coverage.js

+14
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ function nextdir() {
108108
assert.strictEqual(fixtureCoverage, undefined);
109109
}
110110

111+
// disables async hooks before writing coverage.
112+
{
113+
const coverageDirectory = path.join(tmpdir.path, nextdir());
114+
const output = spawnSync(process.execPath, [
115+
require.resolve('../fixtures/v8-coverage/async-hooks')
116+
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
117+
assert.strictEqual(output.status, 0);
118+
const fixtureCoverage = getFixtureCoverage('async-hooks.js',
119+
coverageDirectory);
120+
assert.ok(fixtureCoverage);
121+
// first branch executed.
122+
assert.strictEqual(fixtureCoverage.functions[1].ranges[0].count, 1);
123+
}
124+
111125
// extracts the coverage object for a given fixture name.
112126
function getFixtureCoverage(fixtureFile, coverageDirectory) {
113127
const coverageFiles = fs.readdirSync(coverageDirectory);

test/sequential/test-inspector-enabled.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ assert(
2020
`;
2121

2222
const args = ['--inspect', '-e', script];
23-
const child = spawn(process.execPath, args, { stdio: 'inherit' });
23+
const child = spawn(process.execPath, args, {
24+
stdio: 'inherit',
25+
env: { ...process.env, NODE_V8_COVERAGE: '' }
26+
});
2427
child.on('exit', (code, signal) => {
2528
process.exit(code || signal);
2629
});

0 commit comments

Comments
 (0)