1 | const Exclude = require('test-exclude')
|
2 | const furi = require('furi')
|
3 | const libCoverage = require('istanbul-lib-coverage')
|
4 | const libReport = require('istanbul-lib-report')
|
5 | const reports = require('istanbul-reports')
|
6 | const { readdirSync, readFileSync, statSync } = require('fs')
|
7 | const { isAbsolute, resolve, extname } = require('path')
|
8 | const getSourceMapFromFile = require('./source-map-from-file')
|
9 |
|
10 | const v8toIstanbul = require('v8-to-istanbul')
|
11 | const isCjsEsmBridgeCov = require('./is-cjs-esm-bridge')
|
12 |
|
13 | class Report {
|
14 | constructor ({
|
15 | exclude,
|
16 | include,
|
17 | reporter,
|
18 | reportsDirectory,
|
19 | tempDirectory,
|
20 | watermarks,
|
21 | omitRelative,
|
22 | wrapperLength,
|
23 | resolve: resolvePaths,
|
24 | all,
|
25 | src,
|
26 | allowExternal = false
|
27 | }) {
|
28 | this.reporter = reporter
|
29 | this.reportsDirectory = reportsDirectory
|
30 | this.tempDirectory = tempDirectory
|
31 | this.watermarks = watermarks
|
32 | this.resolve = resolvePaths
|
33 | this.exclude = new Exclude({
|
34 | exclude: exclude,
|
35 | include: include,
|
36 | relativePath: !allowExternal
|
37 | })
|
38 | this.omitRelative = omitRelative
|
39 | this.sourceMapCache = {}
|
40 | this.wrapperLength = wrapperLength
|
41 | this.all = all
|
42 | this.src = this._getSrc(src)
|
43 | }
|
44 |
|
45 | _getSrc (src) {
|
46 | if (typeof src === 'string') {
|
47 | return [src]
|
48 | } else if (Array.isArray(src)) {
|
49 | return src
|
50 | } else {
|
51 | return [process.cwd()]
|
52 | }
|
53 | }
|
54 |
|
55 | async run () {
|
56 | var context = libReport.createContext({
|
57 | dir: this.reportsDirectory,
|
58 | watermarks: this.watermarks,
|
59 | coverageMap: await this.getCoverageMapFromAllCoverageFiles()
|
60 | })
|
61 |
|
62 | this.reporter.forEach(function (_reporter) {
|
63 | reports.create(_reporter, {
|
64 | skipEmpty: false,
|
65 | skipFull: false,
|
66 | maxCols: 100
|
67 | }).execute(context)
|
68 | })
|
69 | }
|
70 |
|
71 | async getCoverageMapFromAllCoverageFiles () {
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | if (this._allCoverageFiles) return this._allCoverageFiles
|
77 |
|
78 | const map = libCoverage.createCoverageMap()
|
79 | const v8ProcessCov = this._getMergedProcessCov()
|
80 | const resultCountPerPath = new Map()
|
81 | const possibleCjsEsmBridges = new Map()
|
82 |
|
83 | for (const v8ScriptCov of v8ProcessCov.result) {
|
84 | try {
|
85 | const sources = this._getSourceMap(v8ScriptCov)
|
86 | const path = resolve(this.resolve, v8ScriptCov.url)
|
87 | const converter = v8toIstanbul(path, this.wrapperLength, sources)
|
88 | await converter.load()
|
89 |
|
90 | if (resultCountPerPath.has(path)) {
|
91 | resultCountPerPath.set(path, resultCountPerPath.get(path) + 1)
|
92 | } else {
|
93 | resultCountPerPath.set(path, 0)
|
94 | }
|
95 |
|
96 | if (isCjsEsmBridgeCov(v8ScriptCov)) {
|
97 | possibleCjsEsmBridges.set(converter, {
|
98 | path,
|
99 | functions: v8ScriptCov.functions
|
100 | })
|
101 | } else {
|
102 | converter.applyCoverage(v8ScriptCov.functions)
|
103 | map.merge(converter.toIstanbul())
|
104 | }
|
105 | } catch (err) {
|
106 | console.warn(`file: ${v8ScriptCov.url} error: ${err.stack}`)
|
107 | }
|
108 | }
|
109 |
|
110 | for (const [converter, { path, functions }] of possibleCjsEsmBridges) {
|
111 | if (resultCountPerPath.get(path) <= 1) {
|
112 | converter.applyCoverage(functions)
|
113 | map.merge(converter.toIstanbul())
|
114 | }
|
115 | }
|
116 | this._allCoverageFiles = map
|
117 | return this._allCoverageFiles
|
118 | }
|
119 |
|
120 | |
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | _getSourceMap (v8ScriptCov) {
|
131 | const sources = {}
|
132 | if (this.sourceMapCache[`file://${v8ScriptCov.url}`]) {
|
133 | const sourceMapAndLineLengths = this.sourceMapCache[`file://${v8ScriptCov.url}`]
|
134 |
|
135 | if (!sourceMapAndLineLengths.data) return
|
136 | sources.sourceMap = {
|
137 | sourcemap: sourceMapAndLineLengths.data
|
138 | }
|
139 | if (sourceMapAndLineLengths.lineLengths) {
|
140 | let source = ''
|
141 | sourceMapAndLineLengths.lineLengths.forEach(length => {
|
142 | source += `${''.padEnd(length, '.')}\n`
|
143 | })
|
144 | sources.source = source
|
145 | }
|
146 | }
|
147 | return sources
|
148 | }
|
149 |
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | _getMergedProcessCov () {
|
160 | const { mergeProcessCovs } = require('@bcoe/v8-coverage')
|
161 | const v8ProcessCovs = []
|
162 | const fileIndex = new Set()
|
163 | for (const v8ProcessCov of this._loadReports()) {
|
164 | if (this._isCoverageObject(v8ProcessCov)) {
|
165 | if (v8ProcessCov['source-map-cache']) {
|
166 | Object.assign(this.sourceMapCache, v8ProcessCov['source-map-cache'])
|
167 | }
|
168 | v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
|
169 | }
|
170 | }
|
171 |
|
172 | if (this.all) {
|
173 | const emptyReports = []
|
174 | v8ProcessCovs.unshift({
|
175 | result: emptyReports
|
176 | })
|
177 | const workingDirs = this.src
|
178 | for (const workingDir of workingDirs) {
|
179 | this.exclude.globSync(workingDir).forEach((f) => {
|
180 | const fullPath = resolve(workingDir, f)
|
181 | if (!fileIndex.has(fullPath)) {
|
182 | const ext = extname(fullPath)
|
183 | if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
|
184 | const stat = statSync(fullPath)
|
185 | const sourceMap = getSourceMapFromFile(fullPath)
|
186 | if (sourceMap !== undefined) {
|
187 | this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) }
|
188 | }
|
189 | emptyReports.push({
|
190 | scriptId: 0,
|
191 | url: resolve(fullPath),
|
192 | functions: [{
|
193 | functionName: '(empty-report)',
|
194 | ranges: [{
|
195 | startOffset: 0,
|
196 | endOffset: stat.size,
|
197 | count: 0
|
198 | }],
|
199 | isBlockCoverage: true
|
200 | }]
|
201 | })
|
202 | }
|
203 | }
|
204 | })
|
205 | }
|
206 | }
|
207 |
|
208 | return mergeProcessCovs(v8ProcessCovs)
|
209 | }
|
210 |
|
211 | |
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | _isCoverageObject (maybeV8ProcessCov) {
|
218 | return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result)
|
219 | }
|
220 |
|
221 | |
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | _loadReports () {
|
228 | const files = readdirSync(this.tempDirectory)
|
229 |
|
230 | return files.map((f) => {
|
231 | try {
|
232 | return JSON.parse(readFileSync(
|
233 | resolve(this.tempDirectory, f),
|
234 | 'utf8'
|
235 | ))
|
236 | } catch (err) {
|
237 | console.warn(`${err.stack}`)
|
238 | }
|
239 | })
|
240 | }
|
241 |
|
242 | |
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 | _normalizeProcessCov (v8ProcessCov, fileIndex) {
|
259 | const result = []
|
260 | for (const v8ScriptCov of v8ProcessCov.result) {
|
261 | if (/^file:\/\//.test(v8ScriptCov.url)) {
|
262 | try {
|
263 | v8ScriptCov.url = furi.toSysPath(v8ScriptCov.url)
|
264 | fileIndex.add(v8ScriptCov.url)
|
265 | } catch (err) {
|
266 | console.warn(err)
|
267 | continue
|
268 | }
|
269 | }
|
270 | if (this.exclude.shouldInstrument(v8ScriptCov.url) &&
|
271 | (!this.omitRelative || isAbsolute(v8ScriptCov.url))) {
|
272 | result.push(v8ScriptCov)
|
273 | }
|
274 | }
|
275 | return { result }
|
276 | }
|
277 | }
|
278 |
|
279 | module.exports = function (opts) {
|
280 | return new Report(opts)
|
281 | }
|