monocart-coverage-reports 2.8.6 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/generate.js CHANGED
@@ -16,6 +16,7 @@ const { resolveSourceMap } = require('./converter/collect-source-maps.js');
16
16
 
17
17
  const { minimatch } = require('./packages/monocart-coverage-vendor.js');
18
18
  const { getGroupedRows } = require('./utils/snapshot.js');
19
+ const generateMarkdownGrid = require('./utils/markdown.js');
19
20
 
20
21
  // ========================================================================================================
21
22
 
@@ -90,8 +91,11 @@ const getReportGroup = (reports, lcov, dataType) => {
90
91
 
91
92
  // both
92
93
  'codecov': 'both',
94
+ 'codacy': 'both',
93
95
  'console-details': 'both',
94
96
  'console-summary': 'both',
97
+ 'markdown-details': 'both',
98
+ 'markdown-summary': 'both',
95
99
  'raw': 'both'
96
100
  };
97
101
 
@@ -128,13 +132,6 @@ const getReportGroup = (reports, lcov, dataType) => {
128
132
 
129
133
  // ========================================================================================================
130
134
 
131
- const pFormatter = (v, row, column) => {
132
- if (typeof v === 'number') {
133
- return Util.getColorStrByStatus(Util.PSF(v, 100, 2), row.status);
134
- }
135
- return v;
136
- };
137
-
138
135
  const nFormatter = (v) => {
139
136
  if (typeof v === 'number') {
140
137
  return Util.NF(v);
@@ -142,7 +139,7 @@ const nFormatter = (v) => {
142
139
  return v;
143
140
  };
144
141
 
145
- const getRowData = (rowName, summary, metrics) => {
142
+ const getRowData = (rowName, summary, metrics, color) => {
146
143
  const summaryRow = {};
147
144
  let lowest = {
148
145
  pct: 100,
@@ -157,7 +154,7 @@ const getRowData = (rowName, summary, metrics) => {
157
154
  if (typeof percent !== 'number') {
158
155
  return;
159
156
  }
160
- summaryRow[k] = Util.getColorStrByStatus(Util.PSF(percent, 100, 2), s.status);
157
+ summaryRow[k] = Util.getColorStrByStatus(Util.PSF(percent, 100, 2), s.status, color);
161
158
  if (percent < lowest.pct) {
162
159
  lowest = s;
163
160
  }
@@ -167,65 +164,7 @@ const getRowData = (rowName, summary, metrics) => {
167
164
  return summaryRow;
168
165
  };
169
166
 
170
- const getUncoveredLines = (file) => {
171
- const lines = [];
172
-
173
- const dataLines = file.data.lines;
174
-
175
- let startLine;
176
- let endLine;
177
-
178
- const addLines = () => {
179
- if (!startLine) {
180
- return;
181
- }
182
- if (endLine) {
183
- // range
184
- const link = startLine.color === 'yellow' && endLine.color === 'yellow' ? EC.yellow('-') : EC.red('-');
185
- lines.push(EC[startLine.color](startLine.line) + link + EC[endLine.color](endLine.line));
186
- startLine = null;
187
- endLine = null;
188
- } else {
189
- // only start
190
- lines.push(EC[startLine.color](startLine.line));
191
- startLine = null;
192
- }
193
- };
194
-
195
- const setLines = (line, color) => {
196
- if (startLine) {
197
- endLine = {
198
- line,
199
- color
200
- };
201
- return;
202
- }
203
- startLine = {
204
- line,
205
- color
206
- };
207
- };
208
-
209
- Object.keys(dataLines).forEach((line) => {
210
- const count = dataLines[line];
211
- if (count === 0) {
212
- setLines(line, 'red');
213
- return;
214
- }
215
- // 0 < count < 1
216
- if (typeof count === 'string') {
217
- setLines(line, 'yellow');
218
- return;
219
- }
220
- // count >= 1
221
- addLines();
222
- });
223
- addLines();
224
-
225
- return lines.join(',');
226
- };
227
-
228
- const getDetailsRows = (files, metrics, cdOptions) => {
167
+ const getDetailsRows = (files, metrics, cdOptions, color) => {
229
168
 
230
169
  const skipPercent = cdOptions.skipPercent;
231
170
  if (typeof skipPercent === 'number' && skipPercent > 0) {
@@ -250,8 +189,8 @@ const getDetailsRows = (files, metrics, cdOptions) => {
250
189
  }
251
190
 
252
191
  const { sourcePath, summary } = file;
253
- const fileRow = getRowData(sourcePath, summary, metrics);
254
- fileRow.uncoveredLines = getUncoveredLines(file);
192
+ const fileRow = getRowData(sourcePath, summary, metrics, color);
193
+ fileRow.uncoveredLines = Util.getUncoveredLines(file.data.lines, color);
255
194
  flatRows.push(fileRow);
256
195
  });
257
196
 
@@ -280,6 +219,42 @@ const handleCodecovReport = async (reportData, reportOptions, options) => {
280
219
  return Util.relativePath(jsonPath);
281
220
  };
282
221
 
222
+ const handleCodacyReport = async (reportData, reportOptions, options) => {
223
+ const codacyOptions = {
224
+ outputFile: 'codacy.json',
225
+ ... reportOptions
226
+ };
227
+
228
+ const jsonPath = path.resolve(options.outputDir, codacyOptions.outputFile);
229
+
230
+ // https://api.codacy.com/swagger#tocscoveragereport
231
+ const fileReports = [];
232
+ reportData.files.forEach((item) => {
233
+ const { sourcePath, data } = item;
234
+
235
+ const coverage = {};
236
+ for (const [key, value] of Object.entries(data.lines)) {
237
+ if (typeof value === 'number') {
238
+ coverage[key] = value;
239
+ } else {
240
+ // partial coverage not supported?
241
+ coverage[key] = 0;
242
+ }
243
+ }
244
+
245
+ fileReports.push({
246
+ filename: sourcePath,
247
+ coverage
248
+ });
249
+ });
250
+ const codacyData = {
251
+ fileReports
252
+ };
253
+
254
+ await Util.writeFile(jsonPath, JSON.stringify(codacyData));
255
+ return Util.relativePath(jsonPath);
256
+ };
257
+
283
258
  const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
284
259
  const cdOptions = {
285
260
  maxCols: 50,
@@ -296,11 +271,13 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
296
271
  Util.logInfo(EC.cyan(name));
297
272
  }
298
273
 
274
+ const color = 'ansicode';
275
+
299
276
  const metrics = Util.getMetrics(cdOptions.metrics, type);
300
- const rows = getDetailsRows(files, metrics, cdOptions);
301
- const summaryRow = getRowData('Summary', summary, metrics);
277
+ const rows = getDetailsRows(files, metrics, cdOptions, color);
278
+ const summaryRow = getRowData('Summary', summary, metrics, color);
302
279
  if (summaryRow.nameStatus) {
303
- summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus);
280
+ summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus, color);
304
281
  }
305
282
  // no rows if skipped all by skipPercent
306
283
  if (rows.length) {
@@ -335,6 +312,41 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
335
312
  });
336
313
  };
337
314
 
315
+ const getSummaryColumns = (color) => {
316
+
317
+ const columns = [{
318
+ id: 'name',
319
+ name: 'Name'
320
+ }, {
321
+ id: 'pct',
322
+ name: 'Coverage %',
323
+ align: 'right',
324
+ formatter: (v, row, column) => {
325
+ if (typeof v === 'number') {
326
+ return Util.getColorStrByStatus(Util.PSF(v, 100, 2), row.status, color);
327
+ }
328
+ return v;
329
+ }
330
+ }, {
331
+ id: 'covered',
332
+ name: 'Covered',
333
+ align: 'right',
334
+ formatter: nFormatter
335
+ }, {
336
+ id: 'uncovered',
337
+ name: 'Uncovered',
338
+ align: 'right',
339
+ formatter: nFormatter
340
+ }, {
341
+ id: 'total',
342
+ name: 'Total',
343
+ align: 'right',
344
+ formatter: nFormatter
345
+ }];
346
+
347
+ return columns;
348
+ };
349
+
338
350
  const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
339
351
 
340
352
  const csOptions = {
@@ -359,35 +371,136 @@ const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
359
371
  };
360
372
  });
361
373
 
374
+ const columns = getSummaryColumns('ansicode');
375
+
376
+ CG({
377
+ columns,
378
+ rows
379
+ });
380
+ };
381
+
382
+ const handleMarkdownDetailsReport = async (reportData, reportOptions, options) => {
383
+ const mdOptions = {
384
+ baseUrl: '',
385
+ // unicode, html, or empty for no color
386
+ color: 'unicode',
387
+ skipPercent: 0,
388
+ metrics: [],
389
+ outputFile: 'coverage-details.md',
390
+ ... reportOptions
391
+ };
392
+
393
+ const color = Util.normalizeColorType(mdOptions.color);
394
+ const baseUrl = mdOptions.baseUrl;
395
+
396
+ const mdPath = path.resolve(options.outputDir, mdOptions.outputFile);
397
+
398
+ const metrics = Util.getMetrics(mdOptions.metrics, reportData.type);
399
+
400
+ let files = reportData.files;
401
+ const skipPercent = mdOptions.skipPercent;
402
+ if (typeof skipPercent === 'number' && skipPercent > 0) {
403
+ files = files.filter((file) => {
404
+ const { summary } = file;
405
+ for (const k of metrics) {
406
+ const percent = summary[k].pct;
407
+ if (typeof percent === 'number' && percent < skipPercent) {
408
+ return true;
409
+ }
410
+ }
411
+ return false;
412
+ });
413
+ }
414
+
415
+ const rows = [];
416
+ files.forEach((file) => {
417
+
418
+ // do NOT add debug file
419
+ if (file.debug) {
420
+ return;
421
+ }
422
+
423
+ const { sourcePath, summary } = file;
424
+
425
+ const fileRow = getRowData(sourcePath, summary, metrics, color);
426
+ if (fileRow.nameStatus) {
427
+ const nameColor = baseUrl && color === 'tex' ? '' : color;
428
+ fileRow.name = Util.getColorStrByStatus(fileRow.name, fileRow.nameStatus, nameColor);
429
+ }
430
+ if (baseUrl) {
431
+ fileRow.name = `[${fileRow.name}](${baseUrl + sourcePath})`;
432
+ }
433
+
434
+ fileRow.uncoveredLines = Util.getUncoveredLines(file.data.lines, color);
435
+ rows.push(fileRow);
436
+ });
437
+
438
+ const summaryRow = getRowData('Summary', reportData.summary, metrics, color);
439
+ if (summaryRow.nameStatus) {
440
+ summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus, color);
441
+ }
442
+ rows.push(summaryRow);
443
+
362
444
  const columns = [{
363
445
  id: 'name',
364
446
  name: 'Name'
365
- }, {
366
- id: 'pct',
367
- name: 'Coverage %',
368
- align: 'right',
369
- formatter: pFormatter
370
- }, {
371
- id: 'covered',
372
- name: 'Covered',
373
- align: 'right',
374
- formatter: nFormatter
375
- }, {
376
- id: 'uncovered',
377
- name: 'Uncovered',
378
- align: 'right',
379
- formatter: nFormatter
380
- }, {
381
- id: 'total',
382
- name: 'Total',
383
- align: 'right',
384
- formatter: nFormatter
447
+ }, ... metrics.map((m) => {
448
+ return {
449
+ id: m,
450
+ name: Util.capitalizeFirstLetter(m),
451
+ align: 'right'
452
+ };
453
+ }), {
454
+ id: 'uncoveredLines',
455
+ name: 'Uncovered Lines'
385
456
  }];
386
457
 
387
- CG({
458
+ const markdownGrid = generateMarkdownGrid({
459
+ options: {
460
+ name: reportData.name
461
+ },
388
462
  columns,
389
463
  rows
390
464
  });
465
+
466
+ await Util.writeFile(mdPath, markdownGrid);
467
+ return Util.relativePath(mdPath);
468
+ };
469
+
470
+ const handleMarkdownSummaryReport = async (reportData, reportOptions, options) => {
471
+ const msOptions = {
472
+ // unicode, html, or empty for no color
473
+ color: 'unicode',
474
+ metrics: [],
475
+ outputFile: 'coverage-summary.md',
476
+ ... reportOptions
477
+ };
478
+
479
+ const color = Util.normalizeColorType(msOptions.color);
480
+
481
+ const mdPath = path.resolve(options.outputDir, msOptions.outputFile);
482
+
483
+ const metrics = Util.getMetrics(msOptions.metrics, reportData.type);
484
+
485
+ const rows = metrics.map((k) => {
486
+ return {
487
+ ... reportData.summary[k],
488
+ name: Util.capitalizeFirstLetter(k)
489
+ };
490
+ });
491
+
492
+ const columns = getSummaryColumns(color);
493
+
494
+ const markdownGrid = generateMarkdownGrid({
495
+ options: {
496
+ name: reportData.name
497
+ },
498
+ columns,
499
+ rows
500
+ });
501
+
502
+ await Util.writeFile(mdPath, markdownGrid);
503
+ return Util.relativePath(mdPath);
391
504
  };
392
505
 
393
506
  const handleRawReport = (reportData, reportOptions, options) => {
@@ -464,8 +577,11 @@ const generateCoverageReports = async (dataList, sourceCache, options) => {
464
577
 
465
578
  const buildInBothReports = {
466
579
  'codecov': handleCodecovReport,
580
+ 'codacy': handleCodacyReport,
467
581
  'console-details': handleConsoleDetailsReport,
468
582
  'console-summary': handleConsoleSummaryReport,
583
+ 'markdown-details': handleMarkdownDetailsReport,
584
+ 'markdown-summary': handleMarkdownSummaryReport,
469
585
  'raw': handleRawReport
470
586
  };
471
587
 
@@ -854,9 +970,16 @@ const resolveEntrySourceMap = (entry, sourceMapCache) => {
854
970
  }
855
971
  };
856
972
 
857
- const readCoverageData = (item, entryFilter, sourceCache) => {
973
+ const readCoverageData = async (dir, filename, entryFilter, sourceCache) => {
858
974
 
859
- const json = item.json;
975
+ const content = await Util.readFile(path.resolve(dir, filename));
976
+ if (!content) {
977
+ return;
978
+ }
979
+ const json = JSON.parse(content);
980
+ if (!json) {
981
+ return;
982
+ }
860
983
 
861
984
  // raw v8 json
862
985
  let coverageData = json.result;
@@ -886,6 +1009,26 @@ const readCoverageData = (item, entryFilter, sourceCache) => {
886
1009
  return coverageData;
887
1010
  };
888
1011
 
1012
+ const readSourceList = async (dir, sourceList) => {
1013
+ const sourceCache = {};
1014
+
1015
+ for (const filename of sourceList) {
1016
+ const content = await Util.readFile(path.resolve(dir, filename));
1017
+ if (!content) {
1018
+ continue;
1019
+ }
1020
+ const json = JSON.parse(content);
1021
+ if (!json) {
1022
+ continue;
1023
+ }
1024
+ if (json.url) {
1025
+ sourceCache[json.url] = json;
1026
+ }
1027
+ }
1028
+
1029
+ return sourceCache;
1030
+ };
1031
+
889
1032
  const readFromDir = async (mcr, dir) => {
890
1033
 
891
1034
  if (!dir || !fs.existsSync(dir)) {
@@ -894,56 +1037,34 @@ const readFromDir = async (mcr, dir) => {
894
1037
  }
895
1038
 
896
1039
  const files = fs.readdirSync(dir);
897
- const list = [];
1040
+
1041
+ const coverageList = [];
1042
+ const sourceList = [];
1043
+
898
1044
  files.forEach((filename) => {
899
- if (filename.startsWith('coverage-')) {
900
- list.push({
901
- type: 'coverage',
902
- filename
903
- });
904
- return;
905
- }
906
- if (filename.startsWith('source-')) {
907
- list.push({
908
- type: 'source',
909
- filename
910
- });
1045
+ // read all json files
1046
+ if (filename.endsWith('.json')) {
1047
+ // could be source files generated by register hooks
1048
+ if (filename.startsWith('source-')) {
1049
+ sourceList.push(filename);
1050
+ } else {
1051
+ coverageList.push(filename);
1052
+ }
911
1053
  }
912
1054
  });
913
1055
 
914
- if (!list.length) {
1056
+ if (!coverageList.length) {
915
1057
  Util.logInfo(`No coverage files in the dir: ${dir}`);
916
1058
  return;
917
1059
  }
918
1060
 
919
- const contentList = await Promise.all(list.map((it) => Util.readFile(path.resolve(dir, it.filename))));
920
- const sourceCache = {};
921
- const coverageFiles = [];
922
-
923
- contentList.forEach((content, i) => {
924
- if (!content) {
925
- return;
926
- }
927
- const json = JSON.parse(content);
928
- if (!json) {
929
- return;
930
- }
931
- const item = list[i];
932
- if (item.type === 'source') {
933
- if (json.url) {
934
- sourceCache[json.url] = json;
935
- }
936
- } else {
937
- item.json = json;
938
- coverageFiles.push(item);
939
- }
940
- });
1061
+ const sourceCache = await readSourceList(dir, sourceList);
941
1062
 
942
1063
  const entryFilter = mcr.getEntryFilter();
943
1064
 
944
1065
  const results = [];
945
- for (const item of coverageFiles) {
946
- const coverageData = readCoverageData(item, entryFilter, sourceCache);
1066
+ for (const filename of coverageList) {
1067
+ const coverageData = await readCoverageData(dir, filename, entryFilter, sourceCache);
947
1068
  if (coverageData) {
948
1069
  const res = await mcr.add(coverageData);
949
1070
  results.push(res);
package/lib/index.d.ts CHANGED
@@ -44,12 +44,18 @@ declare namespace MCR {
44
44
 
45
45
  export type ReportDescription =
46
46
  ['v8'] | ["v8", {
47
+ /**
48
+ * defaults to `index.html`
49
+ */
47
50
  outputFile?: string;
48
51
  inline?: boolean;
49
52
  assetsPath?: string;
50
53
  metrics?: Array<"bytes" | "statements" | "branches" | "functions" | "lines">;
51
54
  }] |
52
55
  ['v8-json'] | ["v8-json", {
56
+ /**
57
+ * defaults to `coverage-report.json`
58
+ */
53
59
  outputFile?: string;
54
60
  }] |
55
61
  ['clover'] | ['clover', {
@@ -105,6 +111,15 @@ declare namespace MCR {
105
111
  file?: string;
106
112
  }] |
107
113
  ['codecov'] | ["codecov", {
114
+ /**
115
+ * defaults to `codecov.json`
116
+ */
117
+ outputFile?: string;
118
+ }] |
119
+ ['codacy'] | ["codacy", {
120
+ /**
121
+ * defaults to `codacy.json`
122
+ */
108
123
  outputFile?: string;
109
124
  }] |
110
125
  ['console-summary'] | ['console-summary', {
@@ -115,6 +130,24 @@ declare namespace MCR {
115
130
  skipPercent?: number;
116
131
  metrics?: Array<"bytes" | "statements" | "branches" | "functions" | "lines">;
117
132
  }] |
133
+ ['markdown-summary'] | ['markdown-summary', {
134
+ color: 'unicode' | 'html' | 'tex' | string;
135
+ metrics?: Array<"bytes" | "statements" | "branches" | "functions" | "lines">;
136
+ /**
137
+ * defaults to `coverage-summary.md`
138
+ */
139
+ outputFile?: string;
140
+ }] |
141
+ ['markdown-details'] | ['markdown-details', {
142
+ baseUrl?: string;
143
+ color: 'unicode' | 'html' | 'tex' | string;
144
+ skipPercent?: number;
145
+ metrics?: Array<"bytes" | "statements" | "branches" | "functions" | "lines">;
146
+ /**
147
+ * defaults to `coverage-details.md`
148
+ */
149
+ outputFile?: string;
150
+ }] |
118
151
  ['raw'] | ['raw', {
119
152
  outputDir?: string;
120
153
  }] |
@@ -190,10 +223,23 @@ declare namespace MCR {
190
223
  branches?: CoverageRange[];
191
224
  functions?: CoverageRange[];
192
225
  ignores?: IgnoredRange[];
193
- /** codecov json format */
194
- lines: {
226
+ lines?: {
227
+ /**
228
+ * key: line number;
229
+ * number: hits (0 means uncovered);
230
+ * string: partial covered
231
+ */
195
232
  [key: string]: number | string;
196
233
  };
234
+ extras?: {
235
+ /**
236
+ * key: line number;
237
+ * b: blank;
238
+ * c: comment;
239
+ * i: ignored;
240
+ */
241
+ [key: string]: "b" | "c" | "i";
242
+ }
197
243
  }
198
244
  }
199
245
 
@@ -234,6 +280,9 @@ declare namespace MCR {
234
280
  /** {string} base dir for normalizing the relative source path, defaults to cwd */
235
281
  baseDir?: string;
236
282
 
283
+ /** {string} coverage data dir, alternative to method `addFromDir()`, defaults to null */
284
+ dataDir?: string;
285
+
237
286
  /** (V8 only)
238
287
  *
239
288
  * {string} `minimatch` pattern for entry url;
@@ -413,6 +462,7 @@ declare namespace MCR {
413
462
  functions: string;
414
463
  lines: string;
415
464
  uncoveredLines: string;
465
+ extras: string;
416
466
  }
417
467
  }
418
468
  }
package/lib/index.js CHANGED
@@ -136,11 +136,19 @@ class CoverageReport {
136
136
 
137
137
  this.initOptions();
138
138
 
139
+ const dataDir = this.options.dataDir;
140
+ if (dataDir) {
141
+ await this.addFromDir(dataDir);
142
+ }
143
+
139
144
  const inputData = await getInputData(this);
140
145
  if (!inputData) {
141
146
  return;
142
147
  }
143
148
 
149
+ // GC, no OOM
150
+ this.fileCache.clear();
151
+
144
152
  const { dataList, sourceCache } = inputData;
145
153
 
146
154
  // empty output dir except cache dir before generate reports
@@ -161,16 +169,21 @@ class CoverageReport {
161
169
 
162
170
  const coverageResults = await generateCoverageReports(dataList, sourceCache, this.options);
163
171
 
164
- if (this.logging !== 'debug') {
165
- Util.rmSync(this.options.cacheDir);
166
- }
167
-
168
172
  const onEnd = this.options.onEnd;
169
173
  if (typeof onEnd === 'function') {
170
174
  await onEnd.call(this, coverageResults);
171
175
  }
172
176
 
173
- Util.logTime(`generated coverage reports: ${EC.cyan(coverageResults.reportPath)}`, time_start);
177
+ // remove cache dir after finished
178
+ if (!Util.isDebug()) {
179
+ Util.rmSync(this.options.cacheDir);
180
+ }
181
+
182
+ let reportPath = '';
183
+ if (coverageResults.reportPath) {
184
+ reportPath = EC.cyan(coverageResults.reportPath);
185
+ }
186
+ Util.logTime(`generated coverage reports: ${reportPath}`, time_start);
174
187
 
175
188
  return coverageResults;
176
189
  }
@@ -32,12 +32,15 @@ class IstanbulSummary extends ReportBase {
32
32
 
33
33
  const fileCoverage = node.getFileCoverage();
34
34
  const lines = fileCoverage.getLineCoverage();
35
+ // no extras for istanbul
36
+ const extras = {};
35
37
 
36
38
  this.files.push({
37
39
  sourcePath,
38
40
  summary: fileSummary,
39
41
  data: {
40
- lines
42
+ lines,
43
+ extras
41
44
  }
42
45
  });
43
46
  }