-
Notifications
You must be signed in to change notification settings - Fork 0
/
task.js
252 lines (213 loc) · 7.67 KB
/
task.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// @ts-check
const istanbul = require('istanbul-lib-coverage')
const { join, resolve } = require('path')
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs')
const execa = require('execa')
const {
showNycInfo,
resolveRelativePaths,
checkAllPathsNotFound,
tryFindingLocalFiles,
readNycOptions,
includeAllFiles
} = require('./task-utils')
const { fixSourcePaths } = require('./support-utils')
const { removePlaceholders } = require('./common-utils')
const debug = require('debug')('code-coverage')
// these are standard folder and file names used by NYC tools
const processWorkingDirectory = process.cwd()
// there might be custom "nyc" options in the user package.json
// see https://github.com/istanbuljs/nyc#configuring-nyc
// potentially there might be "nyc" options in other configuration files
// it allows, but for now ignore those options
const pkgFilename = join(processWorkingDirectory, 'package.json')
const pkg = existsSync(pkgFilename)
? JSON.parse(readFileSync(pkgFilename, 'utf8'))
: {}
const scripts = pkg.scripts || {}
const DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME = 'coverage:report'
const customNycReportScript = scripts[DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME]
const nycReportOptions = (function getNycOption() {
// https://github.com/istanbuljs/nyc#common-configuration-options
const nycReportOptions = readNycOptions(processWorkingDirectory)
if (nycReportOptions.exclude && !Array.isArray(nycReportOptions.exclude)) {
console.error('NYC options: %o', nycReportOptions)
throw new Error('Expected "exclude" to by an array')
}
if (nycReportOptions['temp-dir']) {
nycReportOptions['temp-dir'] = resolve(nycReportOptions['temp-dir'])
} else {
nycReportOptions['temp-dir'] = join(processWorkingDirectory, '.nyc_output')
}
nycReportOptions.tempDir = nycReportOptions['temp-dir']
if (nycReportOptions['report-dir']) {
nycReportOptions['report-dir'] = resolve(nycReportOptions['report-dir'])
}
// seems nyc API really is using camel cased version
nycReportOptions.reportDir = nycReportOptions['report-dir']
return nycReportOptions
})()
const nycFilename = join(nycReportOptions['temp-dir'], 'out.json')
function saveCoverage(coverage) {
if (!existsSync(nycReportOptions.tempDir)) {
mkdirSync(nycReportOptions.tempDir, { recursive: true })
debug('created folder %s for output coverage', nycReportOptions.tempDir)
}
writeFileSync(nycFilename, JSON.stringify(coverage, null, 2))
}
function maybePrintFinalCoverageFiles(folder) {
const jsonReportFilename = join(folder, 'coverage-final.json')
if (!existsSync(jsonReportFilename)) {
debug('Did not find final coverage file %s', jsonReportFilename)
return
}
debug('Final coverage in %s', jsonReportFilename)
const finalCoverage = JSON.parse(readFileSync(jsonReportFilename, 'utf8'))
const finalCoverageKeys = Object.keys(finalCoverage)
debug(
'There are %d key(s) in %s',
finalCoverageKeys.length,
jsonReportFilename
)
finalCoverageKeys.forEach((key) => {
const s = finalCoverage[key].s || {}
const statements = Object.keys(s)
const totalStatements = statements.length
let coveredStatements = 0
statements.forEach((statementKey) => {
if (s[statementKey]) {
coveredStatements += 1
}
})
const hasStatements = totalStatements > 0
const allCovered = coveredStatements === totalStatements
const coverageStatus = hasStatements ? (allCovered ? '✅' : '⚠️') : '❓'
debug(
'%s %s statements covered %d/%d',
coverageStatus,
key,
coveredStatements,
totalStatements
)
})
}
const tasks = {
/**
* Clears accumulated code coverage information.
*
* Interactive mode with "cypress open"
* - running a single spec or "Run all specs" needs to reset coverage
* Headless mode with "cypress run"
* - runs EACH spec separately, so we cannot reset the coverage
* or we will lose the coverage from previous specs.
*/
resetCoverage({ isInteractive }) {
if (isInteractive) {
debug('reset code coverage in interactive mode')
const coverageMap = istanbul.createCoverageMap({})
saveCoverage(coverageMap)
}
/*
Else:
in headless mode, assume the coverage file was deleted
before the `cypress run` command was called
example: rm -rf .nyc_output || true
*/
return null
},
/**
* Combines coverage information from single test
* with previously collected coverage.
*
* @param {string} sentCoverage Stringified coverage object sent by the test runner
* @returns {null} Nothing is returned from this task
*/
combineCoverage(sentCoverage) {
const coverage = JSON.parse(sentCoverage)
debug('parsed sent coverage')
fixSourcePaths(coverage)
const previousCoverage = existsSync(nycFilename)
? JSON.parse(readFileSync(nycFilename, 'utf8'))
: {}
// previous code coverage object might have placeholder entries
// for files that we have not seen yet,
// but the user expects to include in the coverage report
// the merge function messes up, so we should remove any placeholder entries
// and re-insert them again when creating the report
removePlaceholders(previousCoverage)
const coverageMap = istanbul.createCoverageMap(previousCoverage)
coverageMap.merge(coverage)
saveCoverage(coverageMap)
debug('wrote coverage file %s', nycFilename)
return null
},
/**
* Saves coverage information as a JSON file and calls
* NPM script to generate HTML report
*/
coverageReport() {
if (!existsSync(nycFilename)) {
console.warn('Cannot find coverage file %s', nycFilename)
console.warn('Skipping coverage report')
return null
}
showNycInfo(nycFilename)
const allSourceFilesMissing = checkAllPathsNotFound(nycFilename)
if (allSourceFilesMissing) {
tryFindingLocalFiles(nycFilename)
}
resolveRelativePaths(nycFilename)
if (customNycReportScript) {
debug(
'saving coverage report using script "%s" from package.json, command: %s',
DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME,
customNycReportScript
)
debug('current working directory is %s', process.cwd())
return execa('npm', ['run', DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME], {
stdio: 'inherit'
})
}
if (nycReportOptions.all) {
debug('nyc needs to report on all included files')
includeAllFiles(nycFilename, nycReportOptions)
}
debug('calling NYC reporter with options %o', nycReportOptions)
debug('current working directory is %s', process.cwd())
const NYC = require('nyc')
const nyc = new NYC(nycReportOptions)
const returnReportFolder = () => {
const reportFolder = nycReportOptions['report-dir']
debug(
'after reporting, returning the report folder name %s',
reportFolder
)
maybePrintFinalCoverageFiles(reportFolder)
return reportFolder
}
return nyc.report().then(returnReportFolder)
}
}
/**
* Registers code coverage collection and reporting tasks.
* Sets an environment variable to tell the browser code that it can
* send the coverage.
* @example
```
// your plugins file
module.exports = (on, config) => {
require('cypress/code-coverage/task')(on, config)
// IMPORTANT to return the config object
// with the any changed environment variables
return config
}
```
*/
function registerCodeCoverageTasks(on, config) {
on('task', tasks)
// set a variable to let the hooks running in the browser
// know that they can send coverage commands
config.env.codeCoverageTasksRegistered = true
return config
}
module.exports = registerCodeCoverageTasks