1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs";
7 // eslint-disable-next-line mozilla/reject-globalThis-modification
8 addDebuggerToGlobal(globalThis);
11 * Records coverage for each test by way of the js debugger.
13 export var CoverageCollector = function (prefix) {
14 this._prefix = prefix;
15 this._dbg = new Debugger();
16 this._dbg.collectCoverageInfo = true;
17 this._dbg.addAllGlobalsAsDebuggees();
18 this._scripts = this._dbg.findScripts();
20 this._dbg.onNewScript = script => {
21 this._scripts.push(script);
24 // Source -> coverage data;
25 this._allCoverage = {};
26 this._encoder = new TextEncoder();
31 CoverageCollector.prototype._getLinesCovered = function () {
32 let coveredLines = {};
33 let currentCoverage = {};
34 this._scripts.forEach(s => {
35 let scriptName = s.url;
36 let cov = s.getOffsetsCoverage();
41 cov.forEach(covered => {
42 let { lineNumber, columnNumber, offset, count } = covered;
47 if (!currentCoverage[scriptName]) {
48 currentCoverage[scriptName] = {};
50 if (!this._allCoverage[scriptName]) {
51 this._allCoverage[scriptName] = {};
54 // NOTE: columnNumber is 1-origin.
55 let key = [lineNumber, columnNumber - 1, offset].join("#");
56 if (!currentCoverage[scriptName][key]) {
57 currentCoverage[scriptName][key] = count;
59 currentCoverage[scriptName][key] += count;
64 // Covered lines are determined by comparing every offset mentioned as of the
65 // the completion of a test to the last time we measured coverage. If an
66 // offset in a line is novel as of this test, or a count has increased for
67 // any offset on a particular line, that line must have been covered.
68 for (let scriptName in currentCoverage) {
69 for (let key in currentCoverage[scriptName]) {
71 !this._allCoverage[scriptName] ||
72 !this._allCoverage[scriptName][key] ||
73 this._allCoverage[scriptName][key] < currentCoverage[scriptName][key]
75 // eslint-disable-next-line no-unused-vars
76 let [lineNumber, colNumber, offset] = key.split("#");
77 if (!coveredLines[scriptName]) {
78 coveredLines[scriptName] = new Set();
80 coveredLines[scriptName].add(parseInt(lineNumber, 10));
81 this._allCoverage[scriptName][key] = currentCoverage[scriptName][key];
89 CoverageCollector.prototype._getUncoveredLines = function () {
90 let uncoveredLines = {};
91 this._scripts.forEach(s => {
92 let scriptName = s.url;
93 let scriptOffsets = s.getAllOffsets();
95 if (!uncoveredLines[scriptName]) {
96 uncoveredLines[scriptName] = new Set();
99 // Get all lines in the script
100 scriptOffsets.forEach(function (element, index) {
104 uncoveredLines[scriptName].add(index);
108 // For all covered lines, delete their entry
109 for (let scriptName in this._allCoverage) {
110 for (let key in this._allCoverage[scriptName]) {
111 // eslint-disable-next-line no-unused-vars
112 let [lineNumber, columnNumber, offset] = key.split("#");
113 uncoveredLines[scriptName].delete(parseInt(lineNumber, 10));
117 return uncoveredLines;
120 CoverageCollector.prototype._getMethodNames = function () {
121 let methodNames = {};
122 this._scripts.forEach(s => {
123 let method = s.displayName;
124 // If the method name is undefined, we return early
129 let scriptName = s.url;
130 let tempMethodCov = [];
131 let scriptOffsets = s.getAllOffsets();
133 if (!methodNames[scriptName]) {
134 methodNames[scriptName] = {};
138 * Get all lines contained within the method and
139 * push a record of the form:
140 * <method name> : <lines covered>
142 scriptOffsets.forEach(function (element, index) {
146 tempMethodCov.push(index);
148 methodNames[scriptName][method] = tempMethodCov;
155 * Records lines covered since the last time coverage was recorded,
156 * associating them with the given test name. The result is written
157 * to a json file in a specified directory.
159 CoverageCollector.prototype.recordTestCoverage = function (testName) {
160 dump("Collecting coverage for: " + testName + "\n");
161 let rawLines = this._getLinesCovered(testName);
162 let methods = this._getMethodNames();
163 let uncoveredLines = this._getUncoveredLines();
165 let versionControlBlock = { version: 1.0 };
166 result.push(versionControlBlock);
168 for (let scriptName in rawLines) {
171 sourceFile: scriptName,
178 typeof methods[scriptName] != "undefined" &&
179 methods[scriptName] != null
181 for (let [methodName, methodLines] of Object.entries(
184 rec.methods[methodName] = methodLines;
188 for (let line of rawLines[scriptName]) {
189 rec.covered.push(line);
192 for (let line of uncoveredLines[scriptName]) {
193 rec.uncovered.push(line);
198 let path = this._prefix + "/jscov_" + Date.now() + ".json";
199 dump("Writing coverage to: " + path + "\n");
200 return IOUtils.writeUTF8(path, JSON.stringify(result, undefined, 2), {
201 tmpPath: `${path}.tmp`,
206 * Tear down the debugger after all tests are complete.
208 CoverageCollector.prototype.finalize = function () {
209 this._dbg.removeAllDebuggees();
210 this._dbg.enabled = false;