file.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
  4. */
  5. 'use strict';
  6. function percent(covered, total) {
  7. let tmp;
  8. if (total > 0) {
  9. tmp = (1000 * 100 * covered) / total + 5;
  10. return Math.floor(tmp / 10) / 100;
  11. } else {
  12. return 100.0;
  13. }
  14. }
  15. function blankSummary() {
  16. const empty = function() {
  17. return {
  18. total: 0,
  19. covered: 0,
  20. skipped: 0,
  21. pct: 'Unknown'
  22. };
  23. };
  24. return {
  25. lines: empty(),
  26. statements: empty(),
  27. functions: empty(),
  28. branches: empty()
  29. };
  30. }
  31. // asserts that a data object "looks like" a summary coverage object
  32. function assertValidSummary(obj) {
  33. const valid =
  34. obj && obj.lines && obj.statements && obj.functions && obj.branches;
  35. if (!valid) {
  36. throw new Error(
  37. 'Invalid summary coverage object, missing keys, found:' +
  38. Object.keys(obj).join(',')
  39. );
  40. }
  41. }
  42. /**
  43. * CoverageSummary provides a summary of code coverage . It exposes 4 properties,
  44. * `lines`, `statements`, `branches`, and `functions`. Each of these properties
  45. * is an object that has 4 keys `total`, `covered`, `skipped` and `pct`.
  46. * `pct` is a percentage number (0-100).
  47. * @param {Object|CoverageSummary} [obj=undefined] an optional data object or
  48. * another coverage summary to initialize this object with.
  49. * @constructor
  50. */
  51. function CoverageSummary(obj) {
  52. if (!obj) {
  53. this.data = blankSummary();
  54. } else if (obj instanceof CoverageSummary) {
  55. this.data = obj.data;
  56. } else {
  57. this.data = obj;
  58. }
  59. assertValidSummary(this.data);
  60. }
  61. ['lines', 'statements', 'functions', 'branches'].forEach(p => {
  62. Object.defineProperty(CoverageSummary.prototype, p, {
  63. enumerable: true,
  64. get() {
  65. return this.data[p];
  66. }
  67. });
  68. });
  69. /**
  70. * merges a second summary coverage object into this one
  71. * @param {CoverageSummary} obj - another coverage summary object
  72. */
  73. CoverageSummary.prototype.merge = function(obj) {
  74. const keys = ['lines', 'statements', 'branches', 'functions'];
  75. keys.forEach(key => {
  76. this[key].total += obj[key].total;
  77. this[key].covered += obj[key].covered;
  78. this[key].skipped += obj[key].skipped;
  79. this[key].pct = percent(this[key].covered, this[key].total);
  80. });
  81. return this;
  82. };
  83. /**
  84. * returns a POJO that is JSON serializable. May be used to get the raw
  85. * summary object.
  86. */
  87. CoverageSummary.prototype.toJSON = function() {
  88. return this.data;
  89. };
  90. /**
  91. * return true if summary has no lines of code
  92. */
  93. CoverageSummary.prototype.isEmpty = function() {
  94. return this.lines.total === 0;
  95. };
  96. // returns a data object that represents empty coverage
  97. function emptyCoverage(filePath) {
  98. return {
  99. path: filePath,
  100. statementMap: {},
  101. fnMap: {},
  102. branchMap: {},
  103. s: {},
  104. f: {},
  105. b: {}
  106. };
  107. }
  108. // asserts that a data object "looks like" a coverage object
  109. function assertValidObject(obj) {
  110. const valid =
  111. obj &&
  112. obj.path &&
  113. obj.statementMap &&
  114. obj.fnMap &&
  115. obj.branchMap &&
  116. obj.s &&
  117. obj.f &&
  118. obj.b;
  119. if (!valid) {
  120. throw new Error(
  121. 'Invalid file coverage object, missing keys, found:' +
  122. Object.keys(obj).join(',')
  123. );
  124. }
  125. }
  126. /**
  127. * provides a read-only view of coverage for a single file.
  128. * The deep structure of this object is documented elsewhere. It has the following
  129. * properties:
  130. *
  131. * * `path` - the file path for which coverage is being tracked
  132. * * `statementMap` - map of statement locations keyed by statement index
  133. * * `fnMap` - map of function metadata keyed by function index
  134. * * `branchMap` - map of branch metadata keyed by branch index
  135. * * `s` - hit counts for statements
  136. * * `f` - hit count for functions
  137. * * `b` - hit count for branches
  138. *
  139. * @param {Object|FileCoverage|String} pathOrObj is a string that initializes
  140. * and empty coverage object with the specified file path or a data object that
  141. * has all the required properties for a file coverage object.
  142. * @constructor
  143. */
  144. function FileCoverage(pathOrObj) {
  145. if (!pathOrObj) {
  146. throw new Error(
  147. 'Coverage must be initialized with a path or an object'
  148. );
  149. }
  150. if (typeof pathOrObj === 'string') {
  151. this.data = emptyCoverage(pathOrObj);
  152. } else if (pathOrObj instanceof FileCoverage) {
  153. this.data = pathOrObj.data;
  154. } else if (typeof pathOrObj === 'object') {
  155. this.data = pathOrObj;
  156. } else {
  157. throw new Error('Invalid argument to coverage constructor');
  158. }
  159. assertValidObject(this.data);
  160. }
  161. /**
  162. * returns computed line coverage from statement coverage.
  163. * This is a map of hits keyed by line number in the source.
  164. */
  165. FileCoverage.prototype.getLineCoverage = function() {
  166. const statementMap = this.data.statementMap;
  167. const statements = this.data.s;
  168. const lineMap = Object.create(null);
  169. Object.keys(statements).forEach(st => {
  170. if (!statementMap[st]) {
  171. return;
  172. }
  173. const line = statementMap[st].start.line;
  174. const count = statements[st];
  175. const prevVal = lineMap[line];
  176. if (prevVal === undefined || prevVal < count) {
  177. lineMap[line] = count;
  178. }
  179. });
  180. return lineMap;
  181. };
  182. /**
  183. * returns an array of uncovered line numbers.
  184. * @returns {Array} an array of line numbers for which no hits have been
  185. * collected.
  186. */
  187. FileCoverage.prototype.getUncoveredLines = function() {
  188. const lc = this.getLineCoverage();
  189. const ret = [];
  190. Object.keys(lc).forEach(l => {
  191. const hits = lc[l];
  192. if (hits === 0) {
  193. ret.push(l);
  194. }
  195. });
  196. return ret;
  197. };
  198. /**
  199. * returns a map of branch coverage by source line number.
  200. * @returns {Object} an object keyed by line number. Each object
  201. * has a `covered`, `total` and `coverage` (percentage) property.
  202. */
  203. FileCoverage.prototype.getBranchCoverageByLine = function() {
  204. const branchMap = this.branchMap;
  205. const branches = this.b;
  206. const ret = {};
  207. Object.keys(branchMap).forEach(k => {
  208. const line = branchMap[k].line || branchMap[k].loc.start.line;
  209. const branchData = branches[k];
  210. ret[line] = ret[line] || [];
  211. ret[line].push(...branchData);
  212. });
  213. Object.keys(ret).forEach(k => {
  214. const dataArray = ret[k];
  215. const covered = dataArray.filter(item => item > 0);
  216. const coverage = (covered.length / dataArray.length) * 100;
  217. ret[k] = {
  218. covered: covered.length,
  219. total: dataArray.length,
  220. coverage
  221. };
  222. });
  223. return ret;
  224. };
  225. // expose coverage data attributes
  226. ['path', 'statementMap', 'fnMap', 'branchMap', 's', 'f', 'b'].forEach(p => {
  227. Object.defineProperty(FileCoverage.prototype, p, {
  228. enumerable: true,
  229. get() {
  230. return this.data[p];
  231. }
  232. });
  233. });
  234. /**
  235. * return a JSON-serializable POJO for this file coverage object
  236. */
  237. FileCoverage.prototype.toJSON = function() {
  238. return this.data;
  239. };
  240. /**
  241. * merges a second coverage object into this one, updating hit counts
  242. * @param {FileCoverage} other - the coverage object to be merged into this one.
  243. * Note that the other object should have the same structure as this one (same file).
  244. */
  245. FileCoverage.prototype.merge = function(other) {
  246. Object.keys(other.s).forEach(k => {
  247. this.data.s[k] += other.s[k];
  248. });
  249. Object.keys(other.f).forEach(k => {
  250. this.data.f[k] += other.f[k];
  251. });
  252. Object.keys(other.b).forEach(k => {
  253. let i;
  254. const retArray = this.data.b[k];
  255. const secondArray = other.b[k];
  256. if (!retArray) {
  257. this.data.b[k] = secondArray;
  258. return;
  259. }
  260. for (i = 0; i < retArray.length; i += 1) {
  261. retArray[i] += secondArray[i];
  262. }
  263. });
  264. };
  265. FileCoverage.prototype.computeSimpleTotals = function(property) {
  266. let stats = this[property];
  267. const ret = { total: 0, covered: 0, skipped: 0 };
  268. if (typeof stats === 'function') {
  269. stats = stats.call(this);
  270. }
  271. Object.keys(stats).forEach(key => {
  272. const covered = !!stats[key];
  273. ret.total += 1;
  274. if (covered) {
  275. ret.covered += 1;
  276. }
  277. });
  278. ret.pct = percent(ret.covered, ret.total);
  279. return ret;
  280. };
  281. FileCoverage.prototype.computeBranchTotals = function() {
  282. const stats = this.b;
  283. const ret = { total: 0, covered: 0, skipped: 0 };
  284. Object.keys(stats).forEach(key => {
  285. const branches = stats[key];
  286. let covered;
  287. branches.forEach(branchHits => {
  288. covered = branchHits > 0;
  289. if (covered) {
  290. ret.covered += 1;
  291. }
  292. });
  293. ret.total += branches.length;
  294. });
  295. ret.pct = percent(ret.covered, ret.total);
  296. return ret;
  297. };
  298. /**
  299. * resets hit counts for all statements, functions and branches
  300. * in this coverage object resulting in zero coverage.
  301. */
  302. FileCoverage.prototype.resetHits = function() {
  303. const statements = this.s;
  304. const functions = this.f;
  305. const branches = this.b;
  306. Object.keys(statements).forEach(s => {
  307. statements[s] = 0;
  308. });
  309. Object.keys(functions).forEach(f => {
  310. functions[f] = 0;
  311. });
  312. Object.keys(branches).forEach(b => {
  313. const hits = branches[b];
  314. branches[b] = hits.map(() => 0);
  315. });
  316. };
  317. /**
  318. * returns a CoverageSummary for this file coverage object
  319. * @returns {CoverageSummary}
  320. */
  321. FileCoverage.prototype.toSummary = function() {
  322. const ret = {};
  323. ret.lines = this.computeSimpleTotals('getLineCoverage');
  324. ret.functions = this.computeSimpleTotals('f', 'fnMap');
  325. ret.statements = this.computeSimpleTotals('s', 'statementMap');
  326. ret.branches = this.computeBranchTotals();
  327. return new CoverageSummary(ret);
  328. };
  329. module.exports = {
  330. CoverageSummary,
  331. FileCoverage
  332. };