CoverageUtils.sys.mjs (5973B)
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/. */ 4 5 import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs"; 6 7 // eslint-disable-next-line mozilla/reject-globalThis-modification 8 addDebuggerToGlobal(globalThis); 9 10 /** 11 * Records coverage for each test by way of the js debugger. 12 */ 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(); 19 20 this._dbg.onNewScript = script => { 21 this._scripts.push(script); 22 }; 23 24 // Source -> coverage data; 25 this._allCoverage = {}; 26 this._encoder = new TextEncoder(); 27 28 this._testIndex = 0; 29 }; 30 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(); 37 if (!cov) { 38 return; 39 } 40 41 cov.forEach(covered => { 42 let { lineNumber, columnNumber, offset, count } = covered; 43 if (!count) { 44 return; 45 } 46 47 if (!currentCoverage[scriptName]) { 48 currentCoverage[scriptName] = {}; 49 } 50 if (!this._allCoverage[scriptName]) { 51 this._allCoverage[scriptName] = {}; 52 } 53 54 // NOTE: columnNumber is 1-origin. 55 let key = [lineNumber, columnNumber - 1, offset].join("#"); 56 if (!currentCoverage[scriptName][key]) { 57 currentCoverage[scriptName][key] = count; 58 } else { 59 currentCoverage[scriptName][key] += count; 60 } 61 }); 62 }); 63 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]) { 70 if ( 71 !this._allCoverage[scriptName] || 72 !this._allCoverage[scriptName][key] || 73 this._allCoverage[scriptName][key] < currentCoverage[scriptName][key] 74 ) { 75 // eslint-disable-next-line no-unused-vars 76 let [lineNumber, colNumber, offset] = key.split("#"); 77 if (!coveredLines[scriptName]) { 78 coveredLines[scriptName] = new Set(); 79 } 80 coveredLines[scriptName].add(parseInt(lineNumber, 10)); 81 this._allCoverage[scriptName][key] = currentCoverage[scriptName][key]; 82 } 83 } 84 } 85 86 return coveredLines; 87 }; 88 89 CoverageCollector.prototype._getUncoveredLines = function () { 90 let uncoveredLines = {}; 91 this._scripts.forEach(s => { 92 let scriptName = s.url; 93 let scriptOffsets = s.getAllOffsets(); 94 95 if (!uncoveredLines[scriptName]) { 96 uncoveredLines[scriptName] = new Set(); 97 } 98 99 // Get all lines in the script 100 scriptOffsets.forEach(function (element, index) { 101 if (!element) { 102 return; 103 } 104 uncoveredLines[scriptName].add(index); 105 }); 106 }); 107 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)); 114 } 115 } 116 117 return uncoveredLines; 118 }; 119 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 125 if (!method) { 126 return; 127 } 128 129 let scriptName = s.url; 130 let tempMethodCov = []; 131 let scriptOffsets = s.getAllOffsets(); 132 133 if (!methodNames[scriptName]) { 134 methodNames[scriptName] = {}; 135 } 136 137 /** 138 * Get all lines contained within the method and 139 * push a record of the form: 140 * <method name> : <lines covered> 141 */ 142 scriptOffsets.forEach(function (element, index) { 143 if (!element) { 144 return; 145 } 146 tempMethodCov.push(index); 147 }); 148 methodNames[scriptName][method] = tempMethodCov; 149 }); 150 151 return methodNames; 152 }; 153 154 /** 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. 158 */ 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(); 164 let result = []; 165 let versionControlBlock = { version: 1.0 }; 166 result.push(versionControlBlock); 167 168 for (let scriptName in rawLines) { 169 let rec = { 170 testUrl: testName, 171 sourceFile: scriptName, 172 methods: {}, 173 covered: [], 174 uncovered: [], 175 }; 176 177 if ( 178 typeof methods[scriptName] != "undefined" && 179 methods[scriptName] != null 180 ) { 181 for (let [methodName, methodLines] of Object.entries( 182 methods[scriptName] 183 )) { 184 rec.methods[methodName] = methodLines; 185 } 186 } 187 188 for (let line of rawLines[scriptName]) { 189 rec.covered.push(line); 190 } 191 192 for (let line of uncoveredLines[scriptName]) { 193 rec.uncovered.push(line); 194 } 195 196 result.push(rec); 197 } 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`, 202 }); 203 }; 204 205 /** 206 * Tear down the debugger after all tests are complete. 207 */ 208 CoverageCollector.prototype.finalize = function () { 209 this._dbg.removeAllDebuggees(); 210 this._dbg.enabled = false; 211 };