monocart-coverage-reports 1.1.0 → 2.0.1

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
@@ -8,6 +8,11 @@
8
8
 
9
9
  ## Preview Reports
10
10
  - [V8](https://cenfun.github.io/monocart-coverage-reports/v8)
11
+ - [V8 Esbuild](https://cenfun.github.io/monocart-coverage-reports/v8-esbuild)
12
+ - [V8 Rollup](https://cenfun.github.io/monocart-coverage-reports/v8-rollup)
13
+ - [V8 lcov.info](https://cenfun.github.io/monocart-coverage-reports/v8/lcov.info)
14
+ - [V8 Node env](https://cenfun.github.io/monocart-coverage-reports/v8-node-env)
15
+ - [V8 Node Inspector](https://cenfun.github.io/monocart-coverage-reports/v8-node-ins)
11
16
  - [V8 Minify](https://cenfun.github.io/monocart-coverage-reports/v8-minify)
12
17
  - [V8 to Istanbul](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/istanbul)
13
18
  - [Istanbul](https://cenfun.github.io/monocart-coverage-reports/istanbul/)
@@ -26,30 +31,30 @@ await coverageReport.add(coverageData2);
26
31
  const coverageResults = await coverageReport.generate();
27
32
  console.log(coverageResults.summary);
28
33
  ```
29
- - [example v8](https://github.com/cenfun/monocart-coverage-reports/blob/main/tests/test-v8.js)
30
- - [example istanbul](https://github.com/cenfun/monocart-coverage-reports/blob/main/tests/test-istanbul.js)
34
+ - [example v8](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-v8.js)
35
+ - [example istanbul](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-istanbul.js)
31
36
 
32
37
  ## Default Options
33
38
  - [lib/default/options.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/lib/default/options.js)
34
39
 
35
40
  ## Multiple Reports
36
- - v8 report
37
- - v8
38
- - v8-json
39
- - [istanbul reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
40
- - clover
41
- - cobertura
42
- - html
43
- - html-spa
44
- - json
45
- - json-summary
46
- - lcov
47
- - lcovonly
48
- - none
49
- - teamcity
50
- - text
51
- - text-lcov
52
- - text-summary
41
+ ### V8 reports
42
+ - v8 (html)
43
+ - v8-json
44
+ ### [Istanbul reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
45
+ - clover
46
+ - cobertura
47
+ - html
48
+ - html-spa
49
+ - json
50
+ - json-summary
51
+ - lcov
52
+ - lcovonly
53
+ - none
54
+ - teamcity
55
+ - text
56
+ - text-lcov
57
+ - text-summary
53
58
 
54
59
  ```js
55
60
  const CoverageReport = require('monocart-coverage-reports');
@@ -69,7 +74,7 @@ const options = {
69
74
  const coverageReport = new CoverageReport(options);
70
75
  ```
71
76
 
72
- # Multiprocessing Support
77
+ ## Multiprocessing Support
73
78
  The data will be added to `[outputDir]/.cache`, and the cache will be removed after reports generated.
74
79
  - sub process 1
75
80
  ```js
@@ -100,24 +105,29 @@ console.log(coverageResults.summary);
100
105
  ## Compare Reports
101
106
  | | Istanbul | V8 | V8 to Istanbul |
102
107
  | :--------------| :------ | :------ | :---------------------- |
103
- | Input | Istanbul (Object) | V8 (Array) | V8 (Array) |
104
- | Output | [Istanbul HTML report](https://cenfun.github.io/monocart-coverage-reports/istanbul) | [V8 HTML report](https://cenfun.github.io/monocart-coverage-reports/v8) | [Istanbul HTML report](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/istanbul) |
105
- | Indicators | Covered Lines, Branches, Statements and Functions, Execution Counts | Covered Bytes, Lines❔, Execution Counts | Covered Lines, Branches❔, Statements and Functions❔, Execution Counts |
108
+ | Instrumenting code | Required | No | No |
109
+ | Coverage data | Istanbul (Object) | V8 (Array) | V8 (Array) |
110
+ | Output | [Istanbul reports](#istanbul-reports) | [V8 reports](#v8-reports) | [Istanbul reports](#istanbul-reports) |
111
+ | - Bytes | ❌ | ✅ | ❌ |
112
+ | - Statements | ✅ | ❌ | ☑️❔ |
113
+ | - Branches | ✅ | ☑️❔ | ☑️❔ |
114
+ | - Functions | ✅ | ✅ | ✅ |
115
+ | - Lines | ✅ | ✅ | ✅ |
116
+ | - Execution counts | ✅ | ✅ | ✅ |
106
117
  | CSS coverage | ❌ | ✅ | ✅ |
107
- | Minified code | N/A | ✅ | ❌ |
108
- | Code formatting | N/A | ✅ | ❌ |
118
+ | Minified code | ❌ | ✅ | ❌ |
109
119
 
110
120
  ❔ - Partial or conditional support
111
121
 
112
122
  ## Compare Workflows
113
123
  - Istanbul Workflows
114
- - 1, Only for source code: instrumenting code with [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul),see [webpack.config-istanbul.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/mock/webpack.config-istanbul.js), or using [nyc](https://github.com/istanbuljs/nyc).
115
- - 2, Collecting coverage data from browser `window.__coverage__`, see [example](https://github.com/cenfun/monocart-coverage-reports/blob/main/tests/test-istanbul.js).
124
+ - 1, Only for source code: instrumenting code with [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul),see [webpack.config-istanbul.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/webpack.config-istanbul.js), or using [nyc](https://github.com/istanbuljs/nyc).
125
+ - 2, Collecting coverage data from browser `window.__coverage__` or from node `global.__coverage__`, see [example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-istanbul.js).
116
126
  - 3, Generating coverage report.
117
127
 
118
128
  - V8 Workflows
119
- - 1, For any runtime code: nothing to do. For source code: building code with `development` [mode](https://webpack.js.org/configuration/mode/) and [sourcemap](https://webpack.js.org/configuration/devtool/) support, see [webpack.config-v8.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/mock/webpack.config-v8.js).
120
- - 2, Collecting coverage data with [Chromium Coverage API](#chromium-coverage-api), see [example](https://github.com/cenfun/monocart-coverage-reports/blob/main/tests/test-v8.js).
129
+ - 1, For any runtime code: nothing to do. For source code: building code with `development` [mode](https://webpack.js.org/configuration/mode/) and [sourcemap](https://webpack.js.org/configuration/devtool/) support, see [webpack.config-v8.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/webpack.config-v8.js).
130
+ - 2, Collecting coverage data with [Chromium Coverage API](#chromium-coverage-api), see [example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-v8.js).
121
131
  - 3, Generating coverage report.
122
132
 
123
133
  ## Chromium Coverage API
@@ -127,11 +137,88 @@ console.log(coverageResults.summary);
127
137
  - [DevTools Protocol for Coverage](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#method-startPreciseCoverage)
128
138
 
129
139
 
140
+ ## Node.js V8 Coverage Report for Server Side
141
+ There are two ways to collect native V8 coverage data and generate coverage report:
142
+ - Using Node.js env [NODE_V8_COVERAGE](https://nodejs.org/docs/latest/api/cli.html#node_v8_coveragedir)=`dir`
143
+ - 1, Before running your Node.js application, set env `NODE_V8_COVERAGE`=`dir`. After the application runs and exits, the coverage data will be saved to the `dir` directory in JSON file format.
144
+ - 2, Read the json file(s) from the `dir` and generate coverage report. see example:
145
+ > cross-env NODE_V8_COVERAGE=`.temp/v8-coverage` node [./test/test-node-env.js](./test/test-node-env.js) && node [./test/generate-node-report.js](./test/generate-node-report.js)
146
+
147
+ - Using [Inspector](https://nodejs.org/docs/latest/api/inspector.html) API
148
+ - 1, Connecting to the V8 inspector and enable V8 coverage.
149
+ ```js
150
+ const inspector = require('inspector');
151
+ const startV8Coverage = async () => {
152
+ const session = new inspector.Session();
153
+ session.connect();
154
+ await session.post('Profiler.enable');
155
+ await session.post('Profiler.startPreciseCoverage', {
156
+ callCount: true,
157
+ detailed: true
158
+ });
159
+ return session;
160
+ };
161
+ ```
162
+ - 2, Taking coverage data and adding to the report after your application runs.
163
+ ```js
164
+ const takeV8Coverage = (session) => {
165
+ return new Promise((resolve) => {
166
+ session.post('Profiler.takePreciseCoverage', (error, coverage) => {
167
+ if (error) {
168
+ console.log(error);
169
+ resolve();
170
+ return;
171
+ }
172
+ resolve(coverage.result);
173
+ });
174
+ });
175
+ };
176
+ const coverageList = await takeV8Coverage(session);
177
+ // filter node internal files
178
+ coverageList = coverageList.filter((entry) => entry.url && !entry.url.startsWith('node:'));
179
+ await new CoverageReport(coverageOptions).add(coverageList);
180
+ ```
181
+ - 3, Generating coverage report. see example: [./test/test-node-ins.js](./test/test-node-ins.js)
182
+ ```js
183
+ await new CoverageReport(coverageOptions).generate();
184
+ ```
185
+
186
+ ## Using `entryFilter` and `sourceFilter` to filter the results for V8 report
187
+ When you add coverage data with [Chromium Coverage API](#chromium-coverage-api), it actually contains the data of all entry files, for example:
188
+ ```
189
+ 1, dist/main.js
190
+ 2, dist/vendor.js
191
+ 3, dist/something-else.js
192
+ ```
193
+ We can use `entryFilter` to filter the entry files. For example, we should remove `vendor.js` and `something-else.js` if they are not in our coverage scope.
194
+ ```
195
+ 1, dist/main.js
196
+ ```
197
+ When inline or linked sourcemap exists to the entry file, the source files will be extracted from the sourcemap for the entry file, and the entry file will be removed if `logging` is not `debug`.
198
+ ```
199
+ 1, src/index.js
200
+ 2, src/components/app.js
201
+ 3, node_modules/dependency/dist/dependency.js
202
+ ```
203
+ We can use `sourceFilter` to filter the source files. For example, we should remove `dependency.js` if it is not in our coverage scope.
204
+ ```
205
+ 1, src/index.js
206
+ 2, src/components/app.js
207
+ ```
208
+ Example:
209
+ ```js
210
+ const coverageOptions = {
211
+ entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
212
+ sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
213
+ };
214
+ ```
215
+
130
216
  ## Istanbul Coverage
131
217
  - [Istanbul coverage report](https://istanbul.js.org/) - Instrumenting source codes and generating coverage reports
132
218
  - [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul)
133
219
  - [istanbul-reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
134
220
  - [Code Coverage Introduction](https://docs.cypress.io/guides/tooling/code-coverage)
135
221
 
222
+
136
223
  ## Thanks
137
224
  - Special thanks to [@edumserrano](https://github.com/edumserrano)
@@ -0,0 +1,297 @@
1
+ const {
2
+ acorn, acornLoose, parseCss
3
+ } = require('../packages/monocart-coverage-vendor.js');
4
+ const Util = require('../utils/util.js');
5
+
6
+ const {
7
+ createBranches, collectBranches, findInRanges
8
+ } = require('./branches.js');
9
+
10
+ const getFunctionRange = (start, end, state) => {
11
+
12
+ const {
13
+ functionMap, functionNameMap, functionRanges
14
+ } = state;
15
+
16
+ if (!functionRanges.length) {
17
+ return;
18
+ }
19
+
20
+ // exact matched in functionMap
21
+ const range = functionMap.get(start);
22
+ if (range) {
23
+ return range;
24
+ }
25
+ // exact matched in functionNameMap
26
+ const nameRange = functionNameMap.get(start);
27
+ if (nameRange) {
28
+ return nameRange;
29
+ }
30
+
31
+ // find in functionRanges
32
+ return findInRanges(start, end, functionRanges);
33
+ };
34
+
35
+ const collectAstInfo = (ast, state) => {
36
+
37
+ const functions = [];
38
+ const branchMap = new Map();
39
+
40
+ Util.visitAst(ast, {
41
+
42
+ VariableDeclarator(node, parents) {
43
+ // id: { type: 'Identifier', name: '__webpack_modules__' },
44
+ const name = node.id && node.id.name;
45
+ if (name === '__webpack_modules__' && node.init) {
46
+ // mark all as wrap function
47
+ node.init.properties.forEach((it) => {
48
+ it.value._wrapKey = it.key && it.key.value;
49
+ });
50
+ // console.log('==========================', node.type);
51
+ // console.log(name, parents.length);
52
+ }
53
+ },
54
+
55
+ // Function include FunctionDeclaration, ArrowFunctionExpression, FunctionExpression
56
+ Function(node) {
57
+
58
+ const {
59
+ start, end, id
60
+ } = node;
61
+ const functionName = id && id.name;
62
+
63
+ const functionInfo = {
64
+ start,
65
+ end,
66
+ functionName,
67
+ // js default to 1
68
+ count: 1
69
+ };
70
+
71
+ if (node._wrapKey) {
72
+ functionInfo.wrap = true;
73
+ }
74
+
75
+ const range = getFunctionRange(start, end, state);
76
+ if (range) {
77
+ functionInfo.count = range.count;
78
+ functionInfo.range = range;
79
+ // set wrap back to range
80
+ if (functionInfo.wrap) {
81
+ range.wrap = true;
82
+ }
83
+ }
84
+
85
+ functions.push(functionInfo);
86
+
87
+ createBranches(node.body, functionInfo, branchMap);
88
+
89
+ }
90
+
91
+ });
92
+
93
+ const branches = collectBranches(branchMap);
94
+
95
+ return {
96
+ functions,
97
+ branches
98
+ };
99
+
100
+ };
101
+
102
+ const generateCountState = (coverageList) => {
103
+ const functionRanges = [];
104
+ const functionNameMap = new Map();
105
+ coverageList.forEach((block) => {
106
+ const { functionName, ranges } = block;
107
+
108
+ // first one is function coverage info
109
+ const functionRange = ranges[0];
110
+ functionRange.functionName = functionName;
111
+ if (functionName) {
112
+ functionNameMap.set(functionRange.startOffset + functionName.length, functionRange);
113
+ }
114
+
115
+ // blocks
116
+ const len = ranges.length;
117
+ if (len > 1) {
118
+ const blockRanges = [].concat(ranges);
119
+ // remove first one
120
+ blockRanges.shift();
121
+ blockRanges.sort((a, b) => {
122
+ return a.startOffset - b.startOffset;
123
+ });
124
+ const blockMap = new Map();
125
+ blockRanges.forEach((item) => {
126
+ blockMap.set(item.startOffset, item);
127
+ });
128
+ functionRange.blockMap = blockMap;
129
+ functionRange.blockRanges = blockRanges;
130
+ }
131
+
132
+ functionRanges.push(functionRange);
133
+
134
+ });
135
+
136
+ // sort ranges
137
+ functionRanges.sort((a, b) => {
138
+ return a.startOffset - b.startOffset;
139
+ });
140
+
141
+ // keep map index for exact search
142
+ const functionMap = new Map();
143
+ functionRanges.forEach((item) => {
144
+ functionMap.set(item.startOffset, item);
145
+ });
146
+
147
+ return {
148
+ functionMap,
149
+ functionNameMap,
150
+ functionRanges
151
+ };
152
+
153
+ };
154
+
155
+ const getJsAstInfo = (item, coverageList) => {
156
+
157
+ // not for css
158
+ if (!item.js) {
159
+ return {
160
+ functions: [],
161
+ branches: []
162
+ };
163
+ }
164
+
165
+ const { source } = item;
166
+
167
+ const options = {
168
+ ecmaVersion: 'latest',
169
+ // most time for node.js file
170
+ allowReturnOutsideFunction: true,
171
+ allowImportExportEverywhere: true,
172
+ allowSuperOutsideMethod: true,
173
+ // first line: #!/usr/bin/env node
174
+ allowHashBang: true
175
+ };
176
+
177
+ let ast;
178
+ try {
179
+ ast = acorn.parse(source, options);
180
+ } catch (e) {
181
+ Util.logError(`failed to parse source file and fall back to loose: ${item.sourcePath} ${e.message}`);
182
+ // https://github.com/acornjs/acorn/tree/master/acorn-loose
183
+ // It is recommended to always try a parse with the regular acorn parser first,
184
+ // and only fall back to this parser when that one finds syntax errors.
185
+ ast = acornLoose.parse(source, options);
186
+ }
187
+
188
+ const state = generateCountState(coverageList);
189
+ // state.item = item;
190
+
191
+ return collectAstInfo(ast, state);
192
+ };
193
+
194
+ // =========================================================================================================
195
+
196
+ const addCssRules = (rules, coverageList, coveredRanges, locator) => {
197
+ let hasCovered = false;
198
+ // line and line is 1-base
199
+ rules.forEach((rule) => {
200
+
201
+ const { type, position } = rule;
202
+
203
+ if (type === 'comment') {
204
+ return;
205
+ }
206
+
207
+ const { start, end } = position;
208
+
209
+ // both 1-base
210
+ // console.log(start, end);
211
+ const startOffset = locator.locationToOffset({
212
+ line: start.line,
213
+ column: start.column - 1
214
+ });
215
+
216
+ const endOffset = locator.locationToOffset({
217
+ line: end.line,
218
+ column: end.column - 1
219
+ });
220
+
221
+ // start should offset @media
222
+ if (type === 'media') {
223
+ // rules in media
224
+ // console.log('media', rule.media, startOffset, endOffset);
225
+ const childCoverageList = [];
226
+ const hasChildCovered = addCssRules(rule.rules, childCoverageList, coveredRanges, locator);
227
+ if (hasChildCovered) {
228
+ childCoverageList.forEach((it) => {
229
+ coverageList.push(it);
230
+ });
231
+ return;
232
+ }
233
+
234
+ // add whole media as uncovered
235
+ coverageList.push({
236
+ start: startOffset,
237
+ end: endOffset,
238
+ count: 0
239
+ });
240
+ return;
241
+ }
242
+
243
+ let count = 0;
244
+ if (type === 'charset') {
245
+ count = 1;
246
+ } else {
247
+ const coveredRange = findInRanges(startOffset, endOffset, coveredRanges);
248
+ if (coveredRange) {
249
+ count = 1;
250
+ hasCovered = true;
251
+ }
252
+ }
253
+
254
+ coverageList.push({
255
+ start: startOffset,
256
+ end: endOffset,
257
+ count
258
+ });
259
+
260
+ });
261
+
262
+ return hasCovered;
263
+ };
264
+
265
+ const getCssAstInfo = (item, locator) => {
266
+
267
+ const {
268
+ source, sourcePath, ranges
269
+ } = item;
270
+
271
+ // sort covered ranges
272
+ ranges.sort((a, b) => a.start - b.start);
273
+ // to offset covered ranges
274
+ const coveredRanges = ranges.map((it) => {
275
+ return {
276
+ startOffset: it.start,
277
+ endOffset: it.end
278
+ };
279
+ });
280
+
281
+ const time_start_parse = new Date();
282
+ const ast = parseCss(source);
283
+ Util.logTime(`parse css: ${sourcePath}`, time_start_parse);
284
+
285
+ const coverageList = [];
286
+ addCssRules(ast.rules, coverageList, coveredRanges, locator);
287
+
288
+ return {
289
+ coverageList
290
+ };
291
+ };
292
+
293
+
294
+ module.exports = {
295
+ getJsAstInfo,
296
+ getCssAstInfo
297
+ };