diff options
Diffstat (limited to 'testing/modules/CoverageUtils.sys.mjs')
-rw-r--r-- | testing/modules/CoverageUtils.sys.mjs | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/testing/modules/CoverageUtils.sys.mjs b/testing/modules/CoverageUtils.sys.mjs new file mode 100644 index 0000000000..a19432305a --- /dev/null +++ b/testing/modules/CoverageUtils.sys.mjs @@ -0,0 +1,211 @@ +/* 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: + * <method name> : <lines covered> + */ + 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; +}; |