DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (04da00a61c6d)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
/* 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/. */

"use strict";

var EXPORTED_SYMBOLS = ["CoverageCollector"];

/* globals Debugger */
const { addDebuggerToGlobal } = ChromeUtils.import(
  "resource://gre/modules/jsdebugger.jsm"
);
addDebuggerToGlobal(Cu.getGlobalForObject(this));

/**
 * Records coverage for each test by way of the js debugger.
 */
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] = {};
      }

      let key = [lineNumber, columnNumber, 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) {
  let ccov_scope = {};
  const { OS } = ChromeUtils.import(
    "resource://gre/modules/osfile.jsm",
    ccov_scope
  );

  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 arr = this._encoder.encode(JSON.stringify(result, null, 2));
  let path = this._prefix + "/jscov_" + Date.now() + ".json";
  dump("Writing coverage to: " + path + "\n");
  return OS.File.writeAtomic(path, arr, { tmpPath: path + ".tmp" });
};

/**
 * Tear down the debugger after all tests are complete.
 */
CoverageCollector.prototype.finalize = function() {
  this._dbg.removeAllDebuggees();
  this._dbg.enabled = false;
};