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/. */
7 var EXPORTED_SYMBOLS = ["CoverageCollector"];
10 const { addDebuggerToGlobal } = ChromeUtils.import(
11 "resource://gre/modules/jsdebugger.jsm"
13 addDebuggerToGlobal(Cu.getGlobalForObject(this));
16 * Records coverage for each test by way of the js debugger.
18 var CoverageCollector = function(prefix) {
19 this._prefix = prefix;
20 this._dbg = new Debugger();
21 this._dbg.collectCoverageInfo = true;
22 this._dbg.addAllGlobalsAsDebuggees();
23 this._scripts = this._dbg.findScripts();
25 this._dbg.onNewScript = script => {
26 this._scripts.push(script);
29 // Source -> coverage data;
30 this._allCoverage = {};
31 this._encoder = new TextEncoder();
36 CoverageCollector.prototype._getLinesCovered = function() {
37 let coveredLines = {};
38 let currentCoverage = {};
39 this._scripts.forEach(s => {
40 let scriptName = s.url;
41 let cov = s.getOffsetsCoverage();
46 cov.forEach(covered => {
47 let { lineNumber, columnNumber, offset, count } = covered;
52 if (!currentCoverage[scriptName]) {
53 currentCoverage[scriptName] = {};
55 if (!this._allCoverage[scriptName]) {
56 this._allCoverage[scriptName] = {};
59 let key = [lineNumber, columnNumber, offset].join("#");
60 if (!currentCoverage[scriptName][key]) {
61 currentCoverage[scriptName][key] = count;
63 currentCoverage[scriptName][key] += count;
68 // Covered lines are determined by comparing every offset mentioned as of the
69 // the completion of a test to the last time we measured coverage. If an
70 // offset in a line is novel as of this test, or a count has increased for
71 // any offset on a particular line, that line must have been covered.
72 for (let scriptName in currentCoverage) {
73 for (let key in currentCoverage[scriptName]) {
75 !this._allCoverage[scriptName] ||
76 !this._allCoverage[scriptName][key] ||
77 this._allCoverage[scriptName][key] < currentCoverage[scriptName][key]
79 // eslint-disable-next-line no-unused-vars
80 let [lineNumber, colNumber, offset] = key.split("#");
81 if (!coveredLines[scriptName]) {
82 coveredLines[scriptName] = new Set();
84 coveredLines[scriptName].add(parseInt(lineNumber, 10));
85 this._allCoverage[scriptName][key] = currentCoverage[scriptName][key];
93 CoverageCollector.prototype._getUncoveredLines = function() {
94 let uncoveredLines = {};
95 this._scripts.forEach(s => {
96 let scriptName = s.url;
97 let scriptOffsets = s.getAllOffsets();
99 if (!uncoveredLines[scriptName]) {
100 uncoveredLines[scriptName] = new Set();
103 // Get all lines in the script
104 scriptOffsets.forEach(function(element, index) {
108 uncoveredLines[scriptName].add(index);
112 // For all covered lines, delete their entry
113 for (let scriptName in this._allCoverage) {
114 for (let key in this._allCoverage[scriptName]) {
115 // eslint-disable-next-line no-unused-vars
116 let [lineNumber, columnNumber, offset] = key.split("#");
117 uncoveredLines[scriptName].delete(parseInt(lineNumber, 10));
121 return uncoveredLines;
124 CoverageCollector.prototype._getMethodNames = function() {
125 let methodNames = {};
126 this._scripts.forEach(s => {
127 let method = s.displayName;
128 // If the method name is undefined, we return early
133 let scriptName = s.url;
134 let tempMethodCov = [];
135 let scriptOffsets = s.getAllOffsets();
137 if (!methodNames[scriptName]) {
138 methodNames[scriptName] = {};
142 * Get all lines contained within the method and
143 * push a record of the form:
144 * <method name> : <lines covered>
146 scriptOffsets.forEach(function(element, index) {
150 tempMethodCov.push(index);
152 methodNames[scriptName][method] = tempMethodCov;
159 * Records lines covered since the last time coverage was recorded,
160 * associating them with the given test name. The result is written
161 * to a json file in a specified directory.
163 CoverageCollector.prototype.recordTestCoverage = function(testName) {
165 const { OS } = ChromeUtils.import(
166 "resource://gre/modules/osfile.jsm",
170 dump("Collecting coverage for: " + testName + "\n");
171 let rawLines = this._getLinesCovered(testName);
172 let methods = this._getMethodNames();
173 let uncoveredLines = this._getUncoveredLines();
175 let versionControlBlock = { version: 1.0 };
176 result.push(versionControlBlock);
178 for (let scriptName in rawLines) {
181 sourceFile: scriptName,
188 typeof methods[scriptName] != "undefined" &&
189 methods[scriptName] != null
191 for (let [methodName, methodLines] of Object.entries(
194 rec.methods[methodName] = methodLines;
198 for (let line of rawLines[scriptName]) {
199 rec.covered.push(line);
202 for (let line of uncoveredLines[scriptName]) {
203 rec.uncovered.push(line);
208 let arr = this._encoder.encode(JSON.stringify(result, null, 2));
209 let path = this._prefix + "/jscov_" + Date.now() + ".json";
210 dump("Writing coverage to: " + path + "\n");
211 return OS.File.writeAtomic(path, arr, { tmpPath: path + ".tmp" });
215 * Tear down the debugger after all tests are complete.
217 CoverageCollector.prototype.finalize = function() {
218 this._dbg.removeAllDebuggees();
219 this._dbg.enabled = false;