monocart-coverage-reports 2.8.7 → 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/README.md CHANGED
@@ -139,22 +139,27 @@ For more information, see [Command Line](#command-line)
139
139
 
140
140
  > Other build-in reports (both V8 and Istanbul data):
141
141
 
142
- - `codecov`
143
- - coverage data for [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format), see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
142
+ - `codecov` Save coverage data to a json file with [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format) format (defaults to `codecov.json`), see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports).
144
143
 
145
- - `codacy`
146
- - coverage data for [Codacy](https://api.codacy.com/swagger#tocscoveragereport) API
144
+ - `codacy` Save coverage data to a json file with [Codacy API](https://api.codacy.com/swagger#tocscoveragereport) format (defaults to `codacy.json`).
147
145
 
148
- - `console-summary` shows coverage summary in the console
146
+ - `console-summary` shows coverage summary in the console.
149
147
 
150
148
  ![](./assets/console-summary.png)
151
149
 
152
- - `console-details` Show file coverage and uncovered lines in the console. Like `text`, but for V8. For Github actions, we can enforce color with env: `FORCE_COLOR: true`.
150
+ - `console-details` Show coverage details in the console. Like `text`, but for V8. For Github actions, we can enforce color with env: `FORCE_COLOR: true`.
153
151
 
154
152
  ![](./assets/console-details.png)
155
153
 
156
- - `raw` only keep all original data, which can be used for other reports input with `inputDir`
157
- - see [Merge Coverage Reports](#merge-coverage-reports)
154
+ - `markdown-summary` Save coverage summary to a markdown file (defaults to `coverage-summary.md`). For Github actions, we can show the markdown content to [a job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary)
155
+ ```sh
156
+ cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
157
+ ```
158
+ ![](./assets/markdown-summary.png)
159
+
160
+ - `markdown-details` Save coverage details to a markdown file (defaults to `coverage-details.md`).
161
+
162
+ - `raw` only keep all original data, which can be used for other reports input with `inputDir`. see [Merge Coverage Reports](#merge-coverage-reports)
158
163
 
159
164
  - Custom Reporter
160
165
  ```js
package/README.zh-Hans.md CHANGED
@@ -139,11 +139,9 @@ mcr node my-app.js -r v8,console-details
139
139
 
140
140
  > 其他内置报告 (V8和Istanbul格式数据都支持):
141
141
 
142
- - `codecov`
143
- - 专属支持[Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format)的报告, see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
142
+ - `codecov` 保存覆盖率数据到 [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format) 专属的json文件 (默认是`codecov.json`), 见[例子](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
144
143
 
145
- - `codacy`
146
- - 专属支持[Codacy](https://api.codacy.com/swagger#tocscoveragereport) API的报告
144
+ - `codacy` 保存覆盖率数据到 [Codacy](https://api.codacy.com/swagger#tocscoveragereport) 专属的json文件 (默认是`codacy.json`)
147
145
 
148
146
  - `console-summary` 在控制台显示覆盖率概要
149
147
 
@@ -153,8 +151,15 @@ mcr node my-app.js -r v8,console-details
153
151
 
154
152
  ![](./assets/console-details.png)
155
153
 
156
- - `raw` 只是保存原始覆盖率数据, 用于使用`inputDir`参数来导入多个原始数据进行合并报告
157
- - 参见 [合并覆盖率报告](#merge-coverage-reports)
154
+ - `markdown-summary` 保存概要信息到markdown文件 (默认是`coverage-summary.md`)。 如果是Github actions, 可以把markdown的内容添加到[a job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary)
155
+ ```sh
156
+ cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
157
+ ```
158
+ ![](./assets/markdown-summary.png)
159
+
160
+ - `markdown-details` 保存覆盖率详情到markdown文件 (默认是 `coverage-details.md`)
161
+
162
+ - `raw` 只是保存原始覆盖率数据, 用于使用`inputDir`参数来导入多个原始数据进行合并报告。参见 [合并覆盖率报告](#merge-coverage-reports)
158
163
 
159
164
  - 自定义报告
160
165
  ```js
@@ -115,24 +115,30 @@ const handleLinesCoverage = (bytes, locator, ignoredRanges) => {
115
115
  // init lines
116
116
  let blankCount = 0;
117
117
  let commentCount = 0;
118
+ const dataExtras = {};
118
119
  // line 1 based
119
120
  const lineMap = new Map();
120
121
  locator.lines.forEach((lineItem) => {
122
+ // line 1-base
123
+ const line = lineItem.line + 1;
124
+
121
125
  // exclude blank and comment
122
126
  if (lineItem.blank) {
123
127
  blankCount += 1;
128
+ dataExtras[line] = 'b';
124
129
  return;
125
130
  }
126
131
  if (lineItem.comment) {
127
132
  commentCount += 1;
133
+ dataExtras[line] = 'c';
128
134
  return;
129
135
  }
130
136
  const ignored = Util.findInRanges(lineItem.start, lineItem.end, ignoredRanges);
131
137
  if (ignored) {
138
+ dataExtras[line] = 'i';
132
139
  return;
133
140
  }
134
- // line 1-base
135
- const line = lineItem.line + 1;
141
+
136
142
  lineItem.coveredCount = 1;
137
143
  lineItem.uncoveredEntire = null;
138
144
  lineItem.uncoveredPieces = [];
@@ -164,6 +170,7 @@ const handleLinesCoverage = (bytes, locator, ignoredRanges) => {
164
170
 
165
171
  return {
166
172
  dataLines,
173
+ dataExtras,
167
174
  summaryLines
168
175
  };
169
176
  };
@@ -262,9 +269,12 @@ const collectFileCoverage = (v8Data, state, options) => {
262
269
  // ==========================================
263
270
  // lines
264
271
  // after bytes with ignored, before calculateV8Lines
265
- const { dataLines, summaryLines } = handleLinesCoverage(data.bytes, locator, ignoredRanges);
272
+ const {
273
+ dataLines, dataExtras, summaryLines
274
+ } = handleLinesCoverage(data.bytes, locator, ignoredRanges);
266
275
 
267
276
  data.lines = dataLines;
277
+ data.extras = dataExtras;
268
278
 
269
279
  // console.log('statements', state.sourcePath, statements.length);
270
280
 
@@ -27,6 +27,9 @@ module.exports = {
27
27
  // {string} base dir for normalizing the relative source path, defaults to cwd
28
28
  baseDir: null,
29
29
 
30
+ // {string} coverage data dir, alternative to method `addFromDir()`, defaults to null
31
+ dataDir: null,
32
+
30
33
  // (V8 only) {function} A filter function to execute for each element in the V8 list.
31
34
  // entryFilter: (entry) => {
32
35
  // if (entry.url.indexOf('googleapis.com') !== -1) {
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
 
@@ -93,6 +94,8 @@ const getReportGroup = (reports, lcov, dataType) => {
93
94
  'codacy': 'both',
94
95
  'console-details': 'both',
95
96
  'console-summary': 'both',
97
+ 'markdown-details': 'both',
98
+ 'markdown-summary': 'both',
96
99
  'raw': 'both'
97
100
  };
98
101
 
@@ -129,13 +132,6 @@ const getReportGroup = (reports, lcov, dataType) => {
129
132
 
130
133
  // ========================================================================================================
131
134
 
132
- const pFormatter = (v, row, column) => {
133
- if (typeof v === 'number') {
134
- return Util.getColorStrByStatus(Util.PSF(v, 100, 2), row.status);
135
- }
136
- return v;
137
- };
138
-
139
135
  const nFormatter = (v) => {
140
136
  if (typeof v === 'number') {
141
137
  return Util.NF(v);
@@ -143,7 +139,7 @@ const nFormatter = (v) => {
143
139
  return v;
144
140
  };
145
141
 
146
- const getRowData = (rowName, summary, metrics) => {
142
+ const getRowData = (rowName, summary, metrics, color) => {
147
143
  const summaryRow = {};
148
144
  let lowest = {
149
145
  pct: 100,
@@ -158,7 +154,7 @@ const getRowData = (rowName, summary, metrics) => {
158
154
  if (typeof percent !== 'number') {
159
155
  return;
160
156
  }
161
- summaryRow[k] = Util.getColorStrByStatus(Util.PSF(percent, 100, 2), s.status);
157
+ summaryRow[k] = Util.getColorStrByStatus(Util.PSF(percent, 100, 2), s.status, color);
162
158
  if (percent < lowest.pct) {
163
159
  lowest = s;
164
160
  }
@@ -168,65 +164,7 @@ const getRowData = (rowName, summary, metrics) => {
168
164
  return summaryRow;
169
165
  };
170
166
 
171
- const getUncoveredLines = (file) => {
172
- const lines = [];
173
-
174
- const dataLines = file.data.lines;
175
-
176
- let startLine;
177
- let endLine;
178
-
179
- const addLines = () => {
180
- if (!startLine) {
181
- return;
182
- }
183
- if (endLine) {
184
- // range
185
- const link = startLine.color === 'yellow' && endLine.color === 'yellow' ? EC.yellow('-') : EC.red('-');
186
- lines.push(EC[startLine.color](startLine.line) + link + EC[endLine.color](endLine.line));
187
- startLine = null;
188
- endLine = null;
189
- } else {
190
- // only start
191
- lines.push(EC[startLine.color](startLine.line));
192
- startLine = null;
193
- }
194
- };
195
-
196
- const setLines = (line, color) => {
197
- if (startLine) {
198
- endLine = {
199
- line,
200
- color
201
- };
202
- return;
203
- }
204
- startLine = {
205
- line,
206
- color
207
- };
208
- };
209
-
210
- Object.keys(dataLines).forEach((line) => {
211
- const count = dataLines[line];
212
- if (count === 0) {
213
- setLines(line, 'red');
214
- return;
215
- }
216
- // 0 < count < 1
217
- if (typeof count === 'string') {
218
- setLines(line, 'yellow');
219
- return;
220
- }
221
- // count >= 1
222
- addLines();
223
- });
224
- addLines();
225
-
226
- return lines.join(',');
227
- };
228
-
229
- const getDetailsRows = (files, metrics, cdOptions) => {
167
+ const getDetailsRows = (files, metrics, cdOptions, color) => {
230
168
 
231
169
  const skipPercent = cdOptions.skipPercent;
232
170
  if (typeof skipPercent === 'number' && skipPercent > 0) {
@@ -251,8 +189,8 @@ const getDetailsRows = (files, metrics, cdOptions) => {
251
189
  }
252
190
 
253
191
  const { sourcePath, summary } = file;
254
- const fileRow = getRowData(sourcePath, summary, metrics);
255
- fileRow.uncoveredLines = getUncoveredLines(file);
192
+ const fileRow = getRowData(sourcePath, summary, metrics, color);
193
+ fileRow.uncoveredLines = Util.getUncoveredLines(file.data.lines, color);
256
194
  flatRows.push(fileRow);
257
195
  });
258
196
 
@@ -333,11 +271,13 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
333
271
  Util.logInfo(EC.cyan(name));
334
272
  }
335
273
 
274
+ const color = 'ansicode';
275
+
336
276
  const metrics = Util.getMetrics(cdOptions.metrics, type);
337
- const rows = getDetailsRows(files, metrics, cdOptions);
338
- const summaryRow = getRowData('Summary', summary, metrics);
277
+ const rows = getDetailsRows(files, metrics, cdOptions, color);
278
+ const summaryRow = getRowData('Summary', summary, metrics, color);
339
279
  if (summaryRow.nameStatus) {
340
- summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus);
280
+ summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus, color);
341
281
  }
342
282
  // no rows if skipped all by skipPercent
343
283
  if (rows.length) {
@@ -372,6 +312,41 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
372
312
  });
373
313
  };
374
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
+
375
350
  const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
376
351
 
377
352
  const csOptions = {
@@ -396,35 +371,136 @@ const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
396
371
  };
397
372
  });
398
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
+
399
444
  const columns = [{
400
445
  id: 'name',
401
446
  name: 'Name'
402
- }, {
403
- id: 'pct',
404
- name: 'Coverage %',
405
- align: 'right',
406
- formatter: pFormatter
407
- }, {
408
- id: 'covered',
409
- name: 'Covered',
410
- align: 'right',
411
- formatter: nFormatter
412
- }, {
413
- id: 'uncovered',
414
- name: 'Uncovered',
415
- align: 'right',
416
- formatter: nFormatter
417
- }, {
418
- id: 'total',
419
- name: 'Total',
420
- align: 'right',
421
- 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'
422
456
  }];
423
457
 
424
- CG({
458
+ const markdownGrid = generateMarkdownGrid({
459
+ options: {
460
+ name: reportData.name
461
+ },
425
462
  columns,
426
463
  rows
427
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);
428
504
  };
429
505
 
430
506
  const handleRawReport = (reportData, reportOptions, options) => {
@@ -504,6 +580,8 @@ const generateCoverageReports = async (dataList, sourceCache, options) => {
504
580
  'codacy': handleCodacyReport,
505
581
  'console-details': handleConsoleDetailsReport,
506
582
  'console-summary': handleConsoleSummaryReport,
583
+ 'markdown-details': handleMarkdownDetailsReport,
584
+ 'markdown-summary': handleMarkdownSummaryReport,
507
585
  'raw': handleRawReport
508
586
  };
509
587
 
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,9 +111,15 @@ declare namespace MCR {
105
111
  file?: string;
106
112
  }] |
107
113
  ['codecov'] | ["codecov", {
114
+ /**
115
+ * defaults to `codecov.json`
116
+ */
108
117
  outputFile?: string;
109
118
  }] |
110
119
  ['codacy'] | ["codacy", {
120
+ /**
121
+ * defaults to `codacy.json`
122
+ */
111
123
  outputFile?: string;
112
124
  }] |
113
125
  ['console-summary'] | ['console-summary', {
@@ -118,6 +130,24 @@ declare namespace MCR {
118
130
  skipPercent?: number;
119
131
  metrics?: Array<"bytes" | "statements" | "branches" | "functions" | "lines">;
120
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
+ }] |
121
151
  ['raw'] | ['raw', {
122
152
  outputDir?: string;
123
153
  }] |
@@ -193,10 +223,23 @@ declare namespace MCR {
193
223
  branches?: CoverageRange[];
194
224
  functions?: CoverageRange[];
195
225
  ignores?: IgnoredRange[];
196
- /** common json format */
197
- lines: {
226
+ lines?: {
227
+ /**
228
+ * key: line number;
229
+ * number: hits (0 means uncovered);
230
+ * string: partial covered
231
+ */
198
232
  [key: string]: number | string;
199
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
+ }
200
243
  }
201
244
  }
202
245
 
@@ -237,6 +280,9 @@ declare namespace MCR {
237
280
  /** {string} base dir for normalizing the relative source path, defaults to cwd */
238
281
  baseDir?: string;
239
282
 
283
+ /** {string} coverage data dir, alternative to method `addFromDir()`, defaults to null */
284
+ dataDir?: string;
285
+
240
286
  /** (V8 only)
241
287
  *
242
288
  * {string} `minimatch` pattern for entry url;
@@ -416,6 +462,7 @@ declare namespace MCR {
416
462
  functions: string;
417
463
  lines: string;
418
464
  uncoveredLines: string;
465
+ extras: string;
419
466
  }
420
467
  }
421
468
  }
package/lib/index.js CHANGED
@@ -136,6 +136,11 @@ 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;
@@ -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
  }