tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };