CoverageUtils.jsm 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. "use strict";
  5. this.EXPORTED_SYMBOLS = [
  6. "CoverageCollector",
  7. ]
  8. const Cc = Components.classes;
  9. const Ci = Components.interfaces;
  10. const Cu = Components.utils;
  11. const {TextEncoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
  12. const {addDebuggerToGlobal} = Cu.import("resource://gre/modules/jsdebugger.jsm",
  13. {});
  14. addDebuggerToGlobal(this);
  15. /**
  16. * Records coverage for each test by way of the js debugger.
  17. */
  18. this.CoverageCollector = function (prefix) {
  19. this._prefix = prefix;
  20. this._dbg = new Debugger();
  21. this._dbg.collectCoverageInfo = true;
  22. this._dbg.addAllGlobalsAsDebuggees();
  23. this._scripts = this._dbg.findScripts();
  24. this._dbg.onNewScript = (script) => {
  25. this._scripts.push(script);
  26. };
  27. // Source -> coverage data;
  28. this._allCoverage = {};
  29. this._encoder = new TextEncoder();
  30. this._testIndex = 0;
  31. }
  32. CoverageCollector.prototype._getLinesCovered = function () {
  33. let coveredLines = {};
  34. let currentCoverage = {};
  35. this._scripts.forEach(s => {
  36. let scriptName = s.url;
  37. let cov = s.getOffsetsCoverage();
  38. if (!cov) {
  39. return;
  40. }
  41. cov.forEach(covered => {
  42. let {lineNumber, columnNumber, offset, count} = covered;
  43. if (!count) {
  44. return;
  45. }
  46. if (!currentCoverage[scriptName]) {
  47. currentCoverage[scriptName] = {};
  48. }
  49. if (!this._allCoverage[scriptName]) {
  50. this._allCoverage[scriptName] = {};
  51. }
  52. let key = [lineNumber, columnNumber, offset].join('#');
  53. if (!currentCoverage[scriptName][key]) {
  54. currentCoverage[scriptName][key] = count;
  55. } else {
  56. currentCoverage[scriptName][key] += count;
  57. }
  58. });
  59. });
  60. // Covered lines are determined by comparing every offset mentioned as of the
  61. // the completion of a test to the last time we measured coverage. If an
  62. // offset in a line is novel as of this test, or a count has increased for
  63. // any offset on a particular line, that line must have been covered.
  64. for (let scriptName in currentCoverage) {
  65. for (let key in currentCoverage[scriptName]) {
  66. if (!this._allCoverage[scriptName] ||
  67. !this._allCoverage[scriptName][key] ||
  68. (this._allCoverage[scriptName][key] <
  69. currentCoverage[scriptName][key])) {
  70. let [lineNumber, colNumber, offset] = key.split('#');
  71. if (!coveredLines[scriptName]) {
  72. coveredLines[scriptName] = new Set();
  73. }
  74. coveredLines[scriptName].add(parseInt(lineNumber, 10));
  75. this._allCoverage[scriptName][key] = currentCoverage[scriptName][key];
  76. }
  77. }
  78. }
  79. return coveredLines;
  80. }
  81. CoverageCollector.prototype._getUncoveredLines = function() {
  82. let uncoveredLines = {};
  83. this._scripts.forEach(s => {
  84. let scriptName = s.url;
  85. let scriptOffsets = s.getAllOffsets();
  86. if (!uncoveredLines[scriptName]){
  87. uncoveredLines[scriptName] = new Set();
  88. }
  89. // Get all lines in the script
  90. scriptOffsets.forEach( function(element, index) {
  91. if (!element){
  92. return;
  93. }
  94. uncoveredLines[scriptName].add(index);
  95. });
  96. });
  97. // For all covered lines, delete their entry
  98. for (let scriptName in this._allCoverage){
  99. for (let key in this._allCoverage[scriptName]){
  100. let [lineNumber, columnNumber, offset] = key.split('#');
  101. uncoveredLines[scriptName].delete(parseInt(lineNumber, 10));
  102. }
  103. }
  104. return uncoveredLines;
  105. }
  106. CoverageCollector.prototype._getMethodNames = function() {
  107. let methodNames = {};
  108. this._scripts.forEach(s => {
  109. let method = s.displayName;
  110. // If the method name is undefined, we return early
  111. if (!method){
  112. return;
  113. }
  114. let scriptName = s.url;
  115. let tempMethodCov = [];
  116. let scriptOffsets = s.getAllOffsets();
  117. if (!methodNames[scriptName]){
  118. methodNames[scriptName] = {};
  119. }
  120. /**
  121. * Get all lines contained within the method and
  122. * push a record of the form:
  123. * <method name> : <lines covered>
  124. */
  125. scriptOffsets.forEach(function (element, index){
  126. if (!element){
  127. return;
  128. }
  129. tempMethodCov.push(index);
  130. });
  131. methodNames[scriptName][method] = tempMethodCov;
  132. });
  133. return methodNames;
  134. }
  135. /**
  136. * Records lines covered since the last time coverage was recorded,
  137. * associating them with the given test name. The result is written
  138. * to a json file in a specified directory.
  139. */
  140. CoverageCollector.prototype.recordTestCoverage = function (testName) {
  141. dump("Collecting coverage for: " + testName + "\n");
  142. let rawLines = this._getLinesCovered(testName);
  143. let methods = this._getMethodNames();
  144. let uncoveredLines = this._getUncoveredLines();
  145. let result = [];
  146. let versionControlBlock = {version: 1.0};
  147. result.push(versionControlBlock);
  148. for (let scriptName in rawLines) {
  149. let rec = {
  150. testUrl: testName,
  151. sourceFile: scriptName,
  152. methods: {},
  153. covered: [],
  154. uncovered: []
  155. };
  156. for (let methodName in methods[scriptName]){
  157. rec.methods[methodName] = methods[scriptName][methodName];
  158. }
  159. for (let line of rawLines[scriptName]) {
  160. rec.covered.push(line);
  161. }
  162. for (let line of uncoveredLines[scriptName]){
  163. rec.uncovered.push(line);
  164. }
  165. result.push(rec);
  166. }
  167. let arr = this._encoder.encode(JSON.stringify(result, null, 2));
  168. let path = this._prefix + '/' + 'jscov_' + Date.now() + '.json';
  169. dump("Writing coverage to: " + path + "\n");
  170. return OS.File.writeAtomic(path, arr, {tmpPath: path + '.tmp'});
  171. }
  172. /**
  173. * Tear down the debugger after all tests are complete.
  174. */
  175. CoverageCollector.prototype.finalize = function () {
  176. this._dbg.removeAllDebuggees();
  177. this._dbg.enabled = false;
  178. }