index.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE
  4. file for terms.
  5. */
  6. 'use strict';
  7. const { ReportBase } = require('istanbul-lib-report');
  8. const NAME_COL = 4;
  9. const PCT_COLS = 7;
  10. const MISSING_COL = 17;
  11. const TAB_SIZE = 1;
  12. const DELIM = ' | ';
  13. function padding(num, ch) {
  14. let str = '';
  15. let i;
  16. ch = ch || ' ';
  17. for (i = 0; i < num; i += 1) {
  18. str += ch;
  19. }
  20. return str;
  21. }
  22. function fill(str, width, right, tabs) {
  23. tabs = tabs || 0;
  24. str = String(str);
  25. const leadingSpaces = tabs * TAB_SIZE;
  26. const remaining = width - leadingSpaces;
  27. const leader = padding(leadingSpaces);
  28. let fmtStr = '';
  29. if (remaining > 0) {
  30. const strlen = str.length;
  31. let fillStr;
  32. if (remaining >= strlen) {
  33. fillStr = padding(remaining - strlen);
  34. } else {
  35. fillStr = '...';
  36. const length = remaining - fillStr.length;
  37. str = str.substring(strlen - length);
  38. right = true;
  39. }
  40. fmtStr = right ? fillStr + str : str + fillStr;
  41. }
  42. return leader + fmtStr;
  43. }
  44. function formatName(name, maxCols, level) {
  45. return fill(name, maxCols, false, level);
  46. }
  47. function formatPct(pct, width) {
  48. return fill(pct, width || PCT_COLS, true, 0);
  49. }
  50. function nodeMissing(node) {
  51. if (node.isSummary()) {
  52. return '';
  53. }
  54. const metrics = node.getCoverageSummary();
  55. const isEmpty = metrics.isEmpty();
  56. const lines = isEmpty ? 0 : metrics.lines.pct;
  57. let coveredLines;
  58. const fileCoverage = node.getFileCoverage();
  59. if (lines === 100) {
  60. const branches = fileCoverage.getBranchCoverageByLine();
  61. coveredLines = Object.entries(branches).map(([key, { coverage }]) => [
  62. key,
  63. coverage === 100
  64. ]);
  65. } else {
  66. coveredLines = Object.entries(fileCoverage.getLineCoverage());
  67. }
  68. let newRange = true;
  69. const ranges = coveredLines
  70. .reduce((acum, [line, hit]) => {
  71. if (hit) newRange = true;
  72. else {
  73. line = parseInt(line);
  74. if (newRange) {
  75. acum.push([line]);
  76. newRange = false;
  77. } else acum[acum.length - 1][1] = line;
  78. }
  79. return acum;
  80. }, [])
  81. .map(range => {
  82. const { length } = range;
  83. if (length === 1) return range[0];
  84. return `${range[0]}-${range[1]}`;
  85. });
  86. return [].concat(...ranges).join(',');
  87. }
  88. function nodeName(node) {
  89. return node.getRelativeName() || 'All files';
  90. }
  91. function depthFor(node) {
  92. let ret = 0;
  93. node = node.getParent();
  94. while (node) {
  95. ret += 1;
  96. node = node.getParent();
  97. }
  98. return ret;
  99. }
  100. function nullDepthFor() {
  101. return 0;
  102. }
  103. function findWidth(node, context, nodeExtractor, depthFor = nullDepthFor) {
  104. let last = 0;
  105. function compareWidth(node) {
  106. last = Math.max(
  107. last,
  108. TAB_SIZE * depthFor(node) + nodeExtractor(node).length
  109. );
  110. }
  111. const visitor = {
  112. onSummary: compareWidth,
  113. onDetail: compareWidth
  114. };
  115. node.visit(context.getVisitor(visitor));
  116. return last;
  117. }
  118. function makeLine(nameWidth, missingWidth) {
  119. const name = padding(nameWidth, '-');
  120. const pct = padding(PCT_COLS, '-');
  121. const elements = [];
  122. elements.push(name);
  123. elements.push(pct);
  124. elements.push(padding(PCT_COLS + 1, '-'));
  125. elements.push(pct);
  126. elements.push(pct);
  127. elements.push(padding(missingWidth, '-'));
  128. return elements.join(DELIM.replace(/ /g, '-')) + '-';
  129. }
  130. function tableHeader(maxNameCols, missingWidth) {
  131. const elements = [];
  132. elements.push(formatName('File', maxNameCols, 0));
  133. elements.push(formatPct('% Stmts'));
  134. elements.push(formatPct('% Branch', PCT_COLS + 1));
  135. elements.push(formatPct('% Funcs'));
  136. elements.push(formatPct('% Lines'));
  137. elements.push(formatName('Uncovered Line #s', missingWidth));
  138. return elements.join(DELIM) + ' ';
  139. }
  140. function isFull(metrics) {
  141. return (
  142. metrics.statements.pct === 100 &&
  143. metrics.branches.pct === 100 &&
  144. metrics.functions.pct === 100 &&
  145. metrics.lines.pct === 100
  146. );
  147. }
  148. function tableRow(
  149. node,
  150. context,
  151. colorizer,
  152. maxNameCols,
  153. level,
  154. skipEmpty,
  155. skipFull,
  156. missingWidth
  157. ) {
  158. const name = nodeName(node);
  159. const metrics = node.getCoverageSummary();
  160. const isEmpty = metrics.isEmpty();
  161. if (skipEmpty && isEmpty) {
  162. return '';
  163. }
  164. if (skipFull && isFull(metrics)) {
  165. return '';
  166. }
  167. const mm = {
  168. statements: isEmpty ? 0 : metrics.statements.pct,
  169. branches: isEmpty ? 0 : metrics.branches.pct,
  170. functions: isEmpty ? 0 : metrics.functions.pct,
  171. lines: isEmpty ? 0 : metrics.lines.pct
  172. };
  173. const colorize = isEmpty
  174. ? function(str) {
  175. return str;
  176. }
  177. : function(str, key) {
  178. return colorizer(str, context.classForPercent(key, mm[key]));
  179. };
  180. const elements = [];
  181. elements.push(colorize(formatName(name, maxNameCols, level), 'statements'));
  182. elements.push(colorize(formatPct(mm.statements), 'statements'));
  183. elements.push(colorize(formatPct(mm.branches, PCT_COLS + 1), 'branches'));
  184. elements.push(colorize(formatPct(mm.functions), 'functions'));
  185. elements.push(colorize(formatPct(mm.lines), 'lines'));
  186. elements.push(
  187. colorizer(
  188. formatName(nodeMissing(node), missingWidth),
  189. mm.lines === 100 ? 'medium' : 'low'
  190. )
  191. );
  192. return elements.join(DELIM) + ' ';
  193. }
  194. class TextReport extends ReportBase {
  195. constructor(opts) {
  196. super(opts);
  197. opts = opts || {};
  198. const { maxCols } = opts;
  199. this.file = opts.file || null;
  200. this.maxCols = maxCols != null ? maxCols : process.stdout.columns || 80;
  201. this.cw = null;
  202. this.skipEmpty = opts.skipEmpty;
  203. this.skipFull = opts.skipFull;
  204. }
  205. onStart(root, context) {
  206. this.cw = context.writer.writeFile(this.file);
  207. this.nameWidth = Math.max(
  208. NAME_COL,
  209. findWidth(root, context, nodeName, depthFor)
  210. );
  211. this.missingWidth = Math.max(
  212. MISSING_COL,
  213. findWidth(root, context, nodeMissing)
  214. );
  215. if (this.maxCols > 0) {
  216. const pct_cols = DELIM.length + 4 * (PCT_COLS + DELIM.length) + 2;
  217. const maxRemaining = this.maxCols - (pct_cols + MISSING_COL);
  218. if (this.nameWidth > maxRemaining) {
  219. this.nameWidth = maxRemaining;
  220. this.missingWidth = MISSING_COL;
  221. } else if (this.nameWidth < maxRemaining) {
  222. const maxRemaining = this.maxCols - (this.nameWidth + pct_cols);
  223. if (this.missingWidth > maxRemaining) {
  224. this.missingWidth = maxRemaining;
  225. }
  226. }
  227. }
  228. const line = makeLine(this.nameWidth, this.missingWidth);
  229. this.cw.println(line);
  230. this.cw.println(tableHeader(this.nameWidth, this.missingWidth));
  231. this.cw.println(line);
  232. }
  233. onSummary(node, context) {
  234. const nodeDepth = depthFor(node);
  235. const row = tableRow(
  236. node,
  237. context,
  238. this.cw.colorize.bind(this.cw),
  239. this.nameWidth,
  240. nodeDepth,
  241. this.skipEmpty,
  242. this.skipFull,
  243. this.missingWidth
  244. );
  245. if (row) {
  246. this.cw.println(row);
  247. }
  248. }
  249. onDetail(node, context) {
  250. return this.onSummary(node, context);
  251. }
  252. onEnd() {
  253. this.cw.println(makeLine(this.nameWidth, this.missingWidth));
  254. this.cw.close();
  255. }
  256. }
  257. module.exports = TextReport;