file-coverage.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. const percent = require('./percent');
  7. const dataProperties = require('./data-properties');
  8. const { CoverageSummary } = require('./coverage-summary');
  9. // returns a data object that represents empty coverage
  10. function emptyCoverage(filePath) {
  11. return {
  12. path: filePath,
  13. statementMap: {},
  14. fnMap: {},
  15. branchMap: {},
  16. s: {},
  17. f: {},
  18. b: {}
  19. };
  20. }
  21. // asserts that a data object "looks like" a coverage object
  22. function assertValidObject(obj) {
  23. const valid =
  24. obj &&
  25. obj.path &&
  26. obj.statementMap &&
  27. obj.fnMap &&
  28. obj.branchMap &&
  29. obj.s &&
  30. obj.f &&
  31. obj.b;
  32. if (!valid) {
  33. throw new Error(
  34. 'Invalid file coverage object, missing keys, found:' +
  35. Object.keys(obj).join(',')
  36. );
  37. }
  38. }
  39. /**
  40. * provides a read-only view of coverage for a single file.
  41. * The deep structure of this object is documented elsewhere. It has the following
  42. * properties:
  43. *
  44. * * `path` - the file path for which coverage is being tracked
  45. * * `statementMap` - map of statement locations keyed by statement index
  46. * * `fnMap` - map of function metadata keyed by function index
  47. * * `branchMap` - map of branch metadata keyed by branch index
  48. * * `s` - hit counts for statements
  49. * * `f` - hit count for functions
  50. * * `b` - hit count for branches
  51. */
  52. class FileCoverage {
  53. /**
  54. * @constructor
  55. * @param {Object|FileCoverage|String} pathOrObj is a string that initializes
  56. * and empty coverage object with the specified file path or a data object that
  57. * has all the required properties for a file coverage object.
  58. */
  59. constructor(pathOrObj) {
  60. if (!pathOrObj) {
  61. throw new Error(
  62. 'Coverage must be initialized with a path or an object'
  63. );
  64. }
  65. if (typeof pathOrObj === 'string') {
  66. this.data = emptyCoverage(pathOrObj);
  67. } else if (pathOrObj instanceof FileCoverage) {
  68. this.data = pathOrObj.data;
  69. } else if (typeof pathOrObj === 'object') {
  70. this.data = pathOrObj;
  71. } else {
  72. throw new Error('Invalid argument to coverage constructor');
  73. }
  74. assertValidObject(this.data);
  75. }
  76. /**
  77. * returns computed line coverage from statement coverage.
  78. * This is a map of hits keyed by line number in the source.
  79. */
  80. getLineCoverage() {
  81. const statementMap = this.data.statementMap;
  82. const statements = this.data.s;
  83. const lineMap = Object.create(null);
  84. Object.entries(statements).forEach(([st, count]) => {
  85. /* istanbul ignore if: is this even possible? */
  86. if (!statementMap[st]) {
  87. return;
  88. }
  89. const { line } = statementMap[st].start;
  90. const prevVal = lineMap[line];
  91. if (prevVal === undefined || prevVal < count) {
  92. lineMap[line] = count;
  93. }
  94. });
  95. return lineMap;
  96. }
  97. /**
  98. * returns an array of uncovered line numbers.
  99. * @returns {Array} an array of line numbers for which no hits have been
  100. * collected.
  101. */
  102. getUncoveredLines() {
  103. const lc = this.getLineCoverage();
  104. const ret = [];
  105. Object.entries(lc).forEach(([l, hits]) => {
  106. if (hits === 0) {
  107. ret.push(l);
  108. }
  109. });
  110. return ret;
  111. }
  112. /**
  113. * returns a map of branch coverage by source line number.
  114. * @returns {Object} an object keyed by line number. Each object
  115. * has a `covered`, `total` and `coverage` (percentage) property.
  116. */
  117. getBranchCoverageByLine() {
  118. const branchMap = this.branchMap;
  119. const branches = this.b;
  120. const ret = {};
  121. Object.entries(branchMap).forEach(([k, map]) => {
  122. const line = map.line || map.loc.start.line;
  123. const branchData = branches[k];
  124. ret[line] = ret[line] || [];
  125. ret[line].push(...branchData);
  126. });
  127. Object.entries(ret).forEach(([k, dataArray]) => {
  128. const covered = dataArray.filter(item => item > 0);
  129. const coverage = (covered.length / dataArray.length) * 100;
  130. ret[k] = {
  131. covered: covered.length,
  132. total: dataArray.length,
  133. coverage
  134. };
  135. });
  136. return ret;
  137. }
  138. /**
  139. * return a JSON-serializable POJO for this file coverage object
  140. */
  141. toJSON() {
  142. return this.data;
  143. }
  144. /**
  145. * merges a second coverage object into this one, updating hit counts
  146. * @param {FileCoverage} other - the coverage object to be merged into this one.
  147. * Note that the other object should have the same structure as this one (same file).
  148. */
  149. merge(other) {
  150. if (other.all === true) {
  151. return;
  152. }
  153. if (this.all === true) {
  154. this.data = other.data;
  155. return;
  156. }
  157. Object.entries(other.s).forEach(([k, v]) => {
  158. this.data.s[k] += v;
  159. });
  160. Object.entries(other.f).forEach(([k, v]) => {
  161. this.data.f[k] += v;
  162. });
  163. Object.entries(other.b).forEach(([k, v]) => {
  164. let i;
  165. const retArray = this.data.b[k];
  166. /* istanbul ignore if: is this even possible? */
  167. if (!retArray) {
  168. this.data.b[k] = v;
  169. return;
  170. }
  171. for (i = 0; i < retArray.length; i += 1) {
  172. retArray[i] += v[i];
  173. }
  174. });
  175. }
  176. computeSimpleTotals(property) {
  177. let stats = this[property];
  178. if (typeof stats === 'function') {
  179. stats = stats.call(this);
  180. }
  181. const ret = {
  182. total: Object.keys(stats).length,
  183. covered: Object.values(stats).filter(v => !!v).length,
  184. skipped: 0
  185. };
  186. ret.pct = percent(ret.covered, ret.total);
  187. return ret;
  188. }
  189. computeBranchTotals() {
  190. const stats = this.b;
  191. const ret = { total: 0, covered: 0, skipped: 0 };
  192. Object.values(stats).forEach(branches => {
  193. ret.covered += branches.filter(hits => hits > 0).length;
  194. ret.total += branches.length;
  195. });
  196. ret.pct = percent(ret.covered, ret.total);
  197. return ret;
  198. }
  199. /**
  200. * resets hit counts for all statements, functions and branches
  201. * in this coverage object resulting in zero coverage.
  202. */
  203. resetHits() {
  204. const statements = this.s;
  205. const functions = this.f;
  206. const branches = this.b;
  207. Object.keys(statements).forEach(s => {
  208. statements[s] = 0;
  209. });
  210. Object.keys(functions).forEach(f => {
  211. functions[f] = 0;
  212. });
  213. Object.keys(branches).forEach(b => {
  214. branches[b].fill(0);
  215. });
  216. }
  217. /**
  218. * returns a CoverageSummary for this file coverage object
  219. * @returns {CoverageSummary}
  220. */
  221. toSummary() {
  222. const ret = {};
  223. ret.lines = this.computeSimpleTotals('getLineCoverage');
  224. ret.functions = this.computeSimpleTotals('f', 'fnMap');
  225. ret.statements = this.computeSimpleTotals('s', 'statementMap');
  226. ret.branches = this.computeBranchTotals();
  227. return new CoverageSummary(ret);
  228. }
  229. }
  230. // expose coverage data attributes
  231. dataProperties(FileCoverage, [
  232. 'path',
  233. 'statementMap',
  234. 'fnMap',
  235. 'branchMap',
  236. 's',
  237. 'f',
  238. 'b',
  239. 'all'
  240. ]);
  241. module.exports = {
  242. FileCoverage
  243. };