UNPKG

8.63 kBJavaScriptView Raw
1'use strict'
2
3const { join, dirname, isAbsolute } = require('path')
4const { fork } = require('child_process')
5const { cleanDir, createDir, filename, download, allocPromise } = require('./tools')
6const { readdir, readFile, stat, writeFile, access, constants } = require('fs').promises
7const { Readable } = require('stream')
8const { getOutput } = require('./output')
9const { resolvePackage } = require('./npm')
10const { promisify } = require('util')
11const { UTRError } = require('./error')
12const { $remoteOnLegacy } = require('./symbols')
13
14const $nycSettingsPath = Symbol('nycSettingsPath')
15const $coverageFileIndex = Symbol('coverageFileIndex')
16const $coverageRemote = Symbol('coverageRemote')
17
18let nycInstallationPath
19let nycScript
20
21async function setupNyc (job) {
22 if (!nycInstallationPath) {
23 nycInstallationPath = resolvePackage(job, 'nyc')
24 }
25 nycScript = join(await nycInstallationPath, 'bin/nyc.js')
26}
27
28async function nyc (job, ...args) {
29 const output = getOutput(job)
30 output.nyc(...args)
31 const childProcess = fork(nycScript, args, { stdio: 'pipe' })
32 output.monitor(childProcess)
33 const { promise, resolve, reject } = allocPromise()
34 childProcess.on('close', async code => {
35 if (code !== 0) {
36 reject(UTRError.NYC_FAILED(`Return code ${code}`))
37 }
38 resolve()
39 })
40 return promise
41}
42
43const globalContextSearch = 'var global=new Function("return this")();'
44const globalContextReplace = 'var global=window.top;'
45
46const customFileSystem = {
47 stat: path => stat(path)
48 .then(stats => {
49 stats.size -= globalContextSearch.length + globalContextReplace.length
50 return stats
51 }),
52 readdir,
53 createReadStream: async (path) => {
54 const buffer = (await readFile(path))
55 .toString()
56 .replace(globalContextSearch, globalContextReplace)
57 return Readable.from(buffer)
58 }
59}
60
61async function instrument (job) {
62 await setupNyc(job)
63 job[$nycSettingsPath] = join(job.coverageTempDir, 'settings/nyc.json')
64 await cleanDir(job.coverageTempDir)
65 await createDir(join(job.coverageTempDir, 'settings'))
66 const settings = JSON.parse((await readFile(job.coverageSettings)).toString())
67 settings.cwd = job.cwd
68 if (!settings.exclude) {
69 settings.exclude = []
70 }
71 settings.exclude.push(join(job.coverageTempDir, '**'))
72 if (job.cache) {
73 settings.exclude.push(join(job.cache, '**'))
74 }
75 settings.exclude.push(join(job.reportDir, '**'))
76 settings.exclude.push(join(job.coverageReportDir, '**'))
77 await writeFile(job[$nycSettingsPath], JSON.stringify(settings))
78 if (job.mode === 'url') {
79 if (!job[$remoteOnLegacy]) {
80 job[$coverageRemote] = true
81 getOutput(job).instrumentationSkipped()
82 return
83 }
84 }
85 job.status = 'Instrumenting'
86 await nyc(job, 'instrument', job.webapp, join(job.coverageTempDir, 'instrumented'), '--nycrc-path', job[$nycSettingsPath])
87}
88
89async function getReadableSource (job, pathOrUrl) {
90 if (isAbsolute(pathOrUrl)) {
91 try {
92 await access(pathOrUrl, constants.R_OK)
93 return pathOrUrl
94 } catch (e) {}
95 }
96 try {
97 const filePath = join(job.webapp, pathOrUrl)
98 await access(filePath, constants.R_OK)
99 return filePath
100 } catch (e) {}
101 try {
102 // Assuming all files are coming from the same server
103 const { origin } = new URL(job.testPageUrls[0])
104 const filePath = join(job.coverageTempDir, 'sources', pathOrUrl)
105 await download(origin + pathOrUrl, filePath)
106 return filePath
107 } catch (e) {}
108}
109
110async function generateCoverageReport (job) {
111 job.status = 'Generating coverage report'
112 const output = getOutput(job)
113 output.debug('coverage', 'Generating coverage report...')
114 await cleanDir(job.coverageReportDir)
115 const coverageMergedDir = join(job.coverageTempDir, 'merged')
116 await createDir(coverageMergedDir)
117 const coverageFilename = join(coverageMergedDir, 'coverage.json')
118 await nyc(job, 'merge', job.coverageTempDir, coverageFilename)
119 if (job[$coverageRemote] && !job.coverageProxy) {
120 job.status = 'Checking remote source files'
121 output.debug('coverage', 'Checking remote source files...')
122 const coverageData = require(coverageFilename)
123 const filenames = Object.keys(coverageData)
124 let changes = 0
125 for (const filename of filenames) {
126 const fileData = coverageData[filename]
127 const filePath = await getReadableSource(job, fileData.path)
128 if (filePath && filePath !== fileData.path) {
129 fileData.path = filePath
130 ++changes
131 }
132 }
133 if (changes > 0) {
134 await writeFile(coverageFilename, JSON.stringify(coverageData))
135 }
136 }
137 const reporters = job.coverageReporters.map(reporter => `--reporter=${reporter}`)
138 if (!job.coverageReporters.includes('text')) {
139 reporters.push('--reporter=text')
140 }
141 const checks = []
142 if (job.coverageCheckBranches || job.coverageCheckFunctions || job.coverageCheckLines || job.coverageCheckStatements) {
143 if (!job.coverageReporters.includes('lcov')) {
144 reporters.push('--reporter=lcov')
145 }
146 checks.push(
147 `--branches=${job.coverageCheckBranches}`,
148 `--functions=${job.coverageCheckFunctions}`,
149 `--lines=${job.coverageCheckLines}`,
150 `--statements=${job.coverageCheckStatements}`,
151 '--check-coverage'
152 )
153 }
154 await nyc(job, 'report', ...reporters, ...checks, '--temp-dir', coverageMergedDir, '--report-dir', job.coverageReportDir, '--nycrc-path', job[$nycSettingsPath])
155 if (checks.length) {
156 // The checks are not triggered if the coverage is empty
157 const lcov = await stat(join(job.coverageReportDir, 'lcov.info'))
158 if (lcov.size === 0) {
159 throw UTRError.NYC_FAILED('No coverage information extracted')
160 }
161 }
162}
163
164module.exports = {
165 instrument: job => job.coverage && instrument(job),
166 async collect (job, url, coverageData) {
167 job[$coverageFileIndex] = (job[$coverageFileIndex] || 0) + 1
168 const coverageFileName = join(job.coverageTempDir, `${filename(url)}_${job[$coverageFileIndex]}.json`)
169 getOutput(job).debug('coverage', `saved coverage in '${coverageFileName}'`)
170 await writeFile(coverageFileName, JSON.stringify(coverageData))
171 },
172 generateCoverageReport: job => job.coverage ? generateCoverageReport(job) : Promise.resolve(),
173 mappings: async job => {
174 if (!job.coverage) {
175 return []
176 }
177 const instrumentedBasePath = join(job.coverageTempDir, 'instrumented')
178 const instrumentedMapping = {
179 match: /(.*\.js)(\?.*)?$/,
180 file: join(instrumentedBasePath, '$1'),
181 'ignore-if-not-found': true
182 }
183 if (job.mode === 'legacy' || job[$remoteOnLegacy]) {
184 return [{
185 ...instrumentedMapping,
186 'custom-file-system': job.debugCoverageNoCustomFs ? undefined : customFileSystem
187 }]
188 }
189 if (job.mode === 'url' && job.coverageProxy) {
190 await setupNyc(job)
191 // Assuming all files are coming from the same server
192 const { origin } = new URL(job.url[0])
193 const { createInstrumenter } = require(join(await nycInstallationPath, 'node_modules/istanbul-lib-instrument'))
194 const instrumenter = createInstrumenter({
195 produceSourceMap: true,
196 coverageGlobalScope: 'window.top',
197 coverageGlobalScopeFunc: false
198 })
199 const instrument = promisify(instrumenter.instrument.bind(instrumenter))
200 const sources = {}
201 return [{
202 match: /(.*\.js)(\?.*)?$/,
203 custom: async (request, response, url) => {
204 if (!url.match(job.coverageProxyInclude) || url.match(/\bresources\b/) || url.match(job.coverageProxyExclude)) {
205 getOutput(job).debug('coverage', 'coverage_proxy ignore', url)
206 return
207 }
208 const instrumentedSourcePath = join(instrumentedBasePath, url)
209 try {
210 await access(instrumentedSourcePath, constants.R_OK)
211 return
212 } catch (e) {}
213 const instrumenting = sources[url]
214 if (instrumenting) {
215 await instrumenting
216 return // ok
217 }
218 sources[url] = (async () => {
219 const sourcePath = await getReadableSource(job, url)
220 getOutput(job).debug('coverage', 'coverage_proxy instrument', url, sourcePath)
221 if (sourcePath) {
222 const source = (await readFile(sourcePath)).toString()
223 const instrumentedSource = await instrument(source, sourcePath)
224 await createDir(dirname(instrumentedSourcePath))
225 await writeFile(instrumentedSourcePath, instrumentedSource)
226 }
227 })()
228 }
229 },
230 instrumentedMapping,
231 {
232 match: /(.*)$/,
233 url: `${origin}$1`
234 }]
235 }
236 return []
237 }
238}