/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs"; // eslint-disable-next-line mozilla/reject-globalThis-modification addDebuggerToGlobal(globalThis); /** * Records coverage for each test by way of the js debugger. */ export var CoverageCollector = function (prefix) { this._prefix = prefix; this._dbg = new Debugger(); this._dbg.collectCoverageInfo = true; this._dbg.addAllGlobalsAsDebuggees(); this._scripts = this._dbg.findScripts(); this._dbg.onNewScript = script => { this._scripts.push(script); }; // Source -> coverage data; this._allCoverage = {}; this._encoder = new TextEncoder(); this._testIndex = 0; }; CoverageCollector.prototype._getLinesCovered = function () { let coveredLines = {}; let currentCoverage = {}; this._scripts.forEach(s => { let scriptName = s.url; let cov = s.getOffsetsCoverage(); if (!cov) { return; } cov.forEach(covered => { let { lineNumber, columnNumber, offset, count } = covered; if (!count) { return; } if (!currentCoverage[scriptName]) { currentCoverage[scriptName] = {}; } if (!this._allCoverage[scriptName]) { this._allCoverage[scriptName] = {}; } // NOTE: columnNumber is 1-origin. let key = [lineNumber, columnNumber - 1, offset].join("#"); if (!currentCoverage[scriptName][key]) { currentCoverage[scriptName][key] = count; } else { currentCoverage[scriptName][key] += count; } }); }); // Covered lines are determined by comparing every offset mentioned as of the // the completion of a test to the last time we measured coverage. If an // offset in a line is novel as of this test, or a count has increased for // any offset on a particular line, that line must have been covered. for (let scriptName in currentCoverage) { for (let key in currentCoverage[scriptName]) { if ( !this._allCoverage[scriptName] || !this._allCoverage[scriptName][key] || this._allCoverage[scriptName][key] < currentCoverage[scriptName][key] ) { // eslint-disable-next-line no-unused-vars let [lineNumber, colNumber, offset] = key.split("#"); if (!coveredLines[scriptName]) { coveredLines[scriptName] = new Set(); } coveredLines[scriptName].add(parseInt(lineNumber, 10)); this._allCoverage[scriptName][key] = currentCoverage[scriptName][key]; } } } return coveredLines; }; CoverageCollector.prototype._getUncoveredLines = function () { let uncoveredLines = {}; this._scripts.forEach(s => { let scriptName = s.url; let scriptOffsets = s.getAllOffsets(); if (!uncoveredLines[scriptName]) { uncoveredLines[scriptName] = new Set(); } // Get all lines in the script scriptOffsets.forEach(function (element, index) { if (!element) { return; } uncoveredLines[scriptName].add(index); }); }); // For all covered lines, delete their entry for (let scriptName in this._allCoverage) { for (let key in this._allCoverage[scriptName]) { // eslint-disable-next-line no-unused-vars let [lineNumber, columnNumber, offset] = key.split("#"); uncoveredLines[scriptName].delete(parseInt(lineNumber, 10)); } } return uncoveredLines; }; CoverageCollector.prototype._getMethodNames = function () { let methodNames = {}; this._scripts.forEach(s => { let method = s.displayName; // If the method name is undefined, we return early if (!method) { return; } let scriptName = s.url; let tempMethodCov = []; let scriptOffsets = s.getAllOffsets(); if (!methodNames[scriptName]) { methodNames[scriptName] = {}; } /** * Get all lines contained within the method and * push a record of the form: * : */ scriptOffsets.forEach(function (element, index) { if (!element) { return; } tempMethodCov.push(index); }); methodNames[scriptName][method] = tempMethodCov; }); return methodNames; }; /** * Records lines covered since the last time coverage was recorded, * associating them with the given test name. The result is written * to a json file in a specified directory. */ CoverageCollector.prototype.recordTestCoverage = function (testName) { dump("Collecting coverage for: " + testName + "\n"); let rawLines = this._getLinesCovered(testName); let methods = this._getMethodNames(); let uncoveredLines = this._getUncoveredLines(); let result = []; let versionControlBlock = { version: 1.0 }; result.push(versionControlBlock); for (let scriptName in rawLines) { let rec = { testUrl: testName, sourceFile: scriptName, methods: {}, covered: [], uncovered: [], }; if ( typeof methods[scriptName] != "undefined" && methods[scriptName] != null ) { for (let [methodName, methodLines] of Object.entries( methods[scriptName] )) { rec.methods[methodName] = methodLines; } } for (let line of rawLines[scriptName]) { rec.covered.push(line); } for (let line of uncoveredLines[scriptName]) { rec.uncovered.push(line); } result.push(rec); } let path = this._prefix + "/jscov_" + Date.now() + ".json"; dump("Writing coverage to: " + path + "\n"); return IOUtils.writeUTF8(path, JSON.stringify(result, undefined, 2), { tmpPath: `${path}.tmp`, }); }; /** * Tear down the debugger after all tests are complete. */ CoverageCollector.prototype.finalize = function () { this._dbg.removeAllDebuggees(); this._dbg.enabled = false; };