I am using the Inspector API to get precision coverage for JavaScript. I have implemented a target function in target.js
(line annotations in target.js are added to reference them in the explanation) and a test runner in main.js
that is supposed to collect precise coverage information. You can test the setup for yourself by running node main.js
.
const inspector = require("node:inspector");
const fs = require("fs");
const target = require("./target.js").target;
async function run(session, data) {
await new Promise((resolve, reject) => {
session.post("Profiler.startPreciseCoverage", { callCount: true, detailed: true }, (err) => {
err ? reject(err) : resolve();
});
});
console.log("Input: ", data, " - Output: ", target(data));
return await new Promise((resolve, reject) => {
session.post("Profiler.takePreciseCoverage", (err, params) => {
err ? reject(err) : resolve(params);
});
});
}
function printCoverage(result) {
const content = fs.readFileSync("./target.js", { encoding: "utf-8" });
for (let scriptCoverage of result) {
if (!scriptCoverage.url.endsWith("target.js")) {
continue;
}
for (let func of scriptCoverage.functions) {
for (let range of func.ranges) {
console.log(
func.functionName,
range.startOffset,
range.endOffset,
range.count,
content.substring(range.startOffset, range.endOffset).replaceAll("\n", "").replaceAll("\t", " ").replaceAll(" ", " ")
);
}
}
}
}
async function main() {
inspector.open(9696);
const session = new inspector.Session();
session.connect();
await new Promise((resolve, reject) => {
session.post("Profiler.enable", (err) => {
if (err) {
reject(err);
}
resolve();
});
});
const param1 = await run(session, "a");
printCoverage(param1.result);
const param2 = await run(session, "ab");
printCoverage(param2.result);
const param3 = await run(session, "ac");
printCoverage(param3.result);
}
main();
// main.js
/* 1: */ function target(data) {
/* 2: */ if (data.length === 1) {
/* 3: */ return 1;
/* 4: */ }
/* 5: */
/* 6: */ if (data.length == 2) {
/* 7: */ if (data[0] === "a" && data[1] === "b") {
/* 8: */ return 2;
/* 9: */ }
/* a: */
/* b: */ if (data[0] === "a") {
/* c: */ return 3;
/* d: */ }
/* e: */ }
/* f: */ }
module.exports = {
target: target
};
// target.js
Unfortunately, the output below looks quite unintuitive to me. I would have expected the coverage information to be:
Instead, the coverage output simply seems to tell me that the target()-function is covered and certain ranges in target() are not covered. Am I using the API wrong? Or is my expectation for block coverage wrong? How do I get 'true' basic block coverage in JavaScript?
Input: a - Output: 1
target 0 259 1 function target(data) { if (data.length === 1) { return 1; } if (data.length == 2) { if (data[0] === "a" && data[1] === "b") { return 2; } if (data[0] === "a") { return 3; } }}
target 76 257 0 if (data.length == 2) { if (data[0] === "a" && data[1] === "b") { return 2; } if (data[0] === "a") { return 3; } }
Input: ab - Output: 2
target 0 259 1 function target(data) { if (data.length === 1) { return 1; } if (data.length == 2) { if (data[0] === "a" && data[1] === "b") { return 2; } if (data[0] === "a") { return 3; } }}
target 51 76 0 { return 1; }
target 187 251 0 if (data[0] === "a") { return 3; }
Input: ac - Output: 3
target 0 259 1 function target(data) { if (data.length === 1) { return 1; } if (data.length == 2) { if (data[0] === "a" && data[1] === "b") { return 2; } if (data[0] === "a") { return 3; } }}
target 51 76 0 { return 1; }
target 154 187 0 { return 2; }
Edit 1:
I think the picture below is better suited to explain my expectations. There are 9 basic blocks in the source code (if you include the implicit return undefined) and I want to retrieve which basic blocks have been covered.
For example, the input "ab" would cover the five blocks A,B,E,G,I. However, from the Inspector API, I only get the information that the entire function is covered and that certain bytes from this function are not covered. From this I can (as far as I can tell) neither derive the number of basic blocks in the function nor the blocks executed.
I found the function in the v8 source code responsible for merging basic blocks. As a quick fix, I removed the calls after RewritePositionSingletonsToRanges
and it seems to work:
input: ab
function: target
Start | End | Count
================================================
49 | 324 | 1 |
100 | 125 | 0 |
125 | 154 | 1 |
154 | 307 | 1 |
184 | 202 | 0 |
204 | 237 | 0 |
237 | 268 | 1 |
268 | 301 | 0 |
307 | 323 | 1 |
Note, that a function has to be called at least once since profiling started to appear in the list. For example, if we have the calls:
startCoverage();
foo()
takeCoverage();
rareFoo();
takeCoverage();
foo();
takeCoverage();
the function rareFoo
(and its basic blocks) will only appear in the second and third coverage information.