UNPKG

14.5 kBJavaScriptView Raw
1'use strict'
2
3const { Command, Option, InvalidArgumentError } = require('commander')
4const { statSync, accessSync, constants } = require('fs')
5const { dirname, join, isAbsolute } = require('path')
6const { name, description, version } = require(join(__dirname, '../package.json'))
7const { getOutput } = require('./output')
8const { $valueSources, $remoteOnLegacy } = require('./symbols')
9const { buildAndCheckMode } = require('./job-mode')
10const { boolean, integer, timeout, url, arrayOf, regex, percent, string } = require('./options')
11
12const $status = Symbol('status')
13
14function toLongName (name) {
15 return name.replace(/([A-Z])([a-z]+)/g, (match, firstLetter, reminder) => `-${firstLetter.toLowerCase()}${reminder}`)
16}
17
18function buildArgs (parameters) {
19 const before = []
20 const after = []
21 let browser = []
22 Object.keys(parameters).forEach(name => {
23 if (name === '--') {
24 return
25 }
26 const value = parameters[name]
27 let args
28 if (name.startsWith('!')) {
29 args = after
30 name = name.substring(1)
31 } else {
32 args = before
33 }
34 args.push(`--${toLongName(name)}`)
35 if (value !== null) {
36 if (Array.isArray(value)) {
37 args.push(...value)
38 } else {
39 args.push(value)
40 }
41 }
42 })
43 if (parameters['--']) {
44 browser = parameters['--']
45 }
46 const stringify = args => args.map(value => value.toString())
47 return {
48 before: stringify(before),
49 after: stringify(after),
50 browser: stringify(browser)
51 }
52}
53
54function lib (value) {
55 if (value.includes('=')) {
56 const [relative, source] = value.split('=')
57 return { relative, source }
58 } else {
59 return { relative: '', source: value }
60 }
61}
62
63function mapping (value) {
64 try {
65 const [, match, handler, mapping] = /([^=]*)=(file|url)\((.*)\)/.exec(value)
66 return {
67 match,
68 [handler]: mapping
69 }
70 } catch (e) {
71 throw new InvalidArgumentError('Invalid mapping')
72 }
73}
74
75function getCommand (cwd) {
76 const command = new Command()
77 command.exitOverride()
78
79 const DEBUG_OPTION = '(๐Ÿž for debugging purpose)'
80 const EXPERIMENTAL_OPTION = '[โš ๏ธ experimental]'
81
82 command
83 .name(name)
84 .description(description)
85 .version(version)
86
87 .option('--capabilities', '๐Ÿงช Capabilities tester for browser')
88 .option('-u, --url <url...>', '๐Ÿ”— URL of the testsuite / page to test', arrayOf(url))
89
90 // Common to all modes
91 .addOption(
92 new Option('-c, --cwd <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Set working directory')
93 .default(cwd, 'current working directory')
94 )
95 .option('--port <port>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Port to use (0 to use any free one)', integer, 0)
96 .option('-r, --report-dir <path>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Directory to output test reports (relative to cwd)', 'report')
97 .option('-pt, --page-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Limit the page execution time, fails the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
98 .option('-f, --fail-fast [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Stop the execution after the first failing page', boolean, false)
99 .option('-fo, --fail-opa-fast [flag]', '[๐Ÿ’ป๐Ÿ”—] Stop the OPA page execution after the first failing test', boolean, false)
100 .option('-k, --keep-alive [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Keep the server alive', boolean, false)
101 .option('-l, --log-server [flag]', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Log inner server traces', boolean, false)
102 .option('-p, --parallel <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Number of parallel tests executions', 2)
103 .option('-b, --browser <command>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command (relative to cwd or use $/ for provided ones)', '$/puppeteer.js')
104 .option('--browser-args <argument...>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation command parameters (use -- instead)')
105 .option('--no-npm-install', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Prevent any NPM install (execution may fail if a dependency is missing)')
106 .option('-bt, --browser-close-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Maximum waiting time for browser close', timeout, 2000)
107 .option('-br, --browser-retry <count>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
108 .option('-oi, --output-interval <interval>', '[๐Ÿ’ป๐Ÿ”—๐Ÿงช] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
109
110 // Common to legacy and url
111 .option('--webapp <path>', '[๐Ÿ’ป๐Ÿ”—] Base folder of the web application (relative to cwd)', 'webapp')
112 .option('-pf, --page-filter <regexp>', '[๐Ÿ’ป๐Ÿ”—] Filter out pages not matching the regexp')
113 .option('-pp, --page-params <params>', '[๐Ÿ’ป๐Ÿ”—] Add parameters to page URL')
114 .option('-t, --global-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Limit the pages execution time, fail the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
115 .option('--screenshot [flag]', '[๐Ÿ’ป๐Ÿ”—] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
116 .option('--no-screenshot', '[๐Ÿ’ป๐Ÿ”—] Disable screenshots')
117 .option('-st, --screenshot-timeout <timeout>', '[๐Ÿ’ป๐Ÿ”—] Maximum waiting time for browser screenshot', timeout, 5000)
118 .option('-rg, --report-generator <path...>', '[๐Ÿ’ป๐Ÿ”—] Report generator paths (relative to cwd or use $/ for provided ones)', ['$/report.js'])
119 .option('--progress-page <path>', '[๐Ÿ’ป๐Ÿ”—] progress page path (relative to cwd or use $/ for provided ones)', '$/report/default.html')
120
121 .option('--coverage [flag]', '[๐Ÿ’ป๐Ÿ”—] Enable or disable code coverage', boolean)
122 .option('--no-coverage', '[๐Ÿ’ป๐Ÿ”—] Disable code coverage')
123 .option('-cs, --coverage-settings <path>', '[๐Ÿ’ป๐Ÿ”—] Path to a custom nyc.json file providing settings for instrumentation (relative to cwd or use $/ for provided ones)', '$/nyc.json')
124 .option('-ctd, --coverage-temp-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to output raw coverage information to (relative to cwd)', '.nyc_output')
125 .option('-crd, --coverage-report-dir <path>', '[๐Ÿ’ป๐Ÿ”—] Directory to store the coverage report files (relative to cwd)', 'coverage')
126 .option('-cr, --coverage-reporters <reporter...>', '[๐Ÿ’ป๐Ÿ”—] List of nyc reporters to use (text is always used)', ['lcov', 'cobertura'])
127 .option('-ccb, --coverage-check-branches <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of branches must be covered', percent, 0)
128 .option('-ccf, --coverage-check-functions <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of functions must be covered', percent, 0)
129 .option('-ccl, --coverage-check-lines <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of lines must be covered', percent, 0)
130 .option('-ccs, --coverage-check-statements <percent>', '[๐Ÿ’ป๐Ÿ”—] What % of statements must be covered', percent, 0)
131 .option('-s, --serve-only [flag]', '[๐Ÿ’ป๐Ÿ”—] Serve only', boolean, false)
132
133 // Specific to legacy (and might be used with url if pointing to local project)
134 .option('--ui5 <url>', '[๐Ÿ’ป] UI5 url', url, 'https://ui5.sap.com')
135 .option('--disable-ui5', '[๐Ÿ’ป] Disable UI5 mapping (also disable libs)', boolean, false)
136 .option('--libs <lib...>', '[๐Ÿ’ป] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
137 .option('--mappings <mapping...>', '[๐Ÿ’ป] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
138 .option('--cache <path>', '[๐Ÿ’ป] Cache UI5 resources locally in the given folder (empty to disable)')
139 .option('--testsuite <path>', '[๐Ÿ’ป] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
140 .option('-w, --watch [flag]', '[๐Ÿ’ป] Monitor the webapp folder and re-execute tests on change', boolean, false)
141
142 // Specific to coverage in url mode (experimental)
143 .option('-cp, --coverage-proxy [flag]', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} use internal proxy to instrument remote files`, boolean, false)
144 .option('-cpi, --coverage-proxy-include <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to instrument for coverage`, regex, regex('.*'))
145 .option('-cpe, --coverage-proxy-exclude <regexp>', `[๐Ÿ”—] ${EXPERIMENTAL_OPTION} urls to ignore for coverage`, regex, regex('/((test-)?resources|tests?)/.*'))
146
147 .addOption(new Option('--debug-probe-only', DEBUG_OPTION, boolean).hideHelp())
148 .addOption(new Option('--debug-keep-browser-open', DEBUG_OPTION, boolean).hideHelp())
149 .addOption(new Option('--debug-memory', DEBUG_OPTION, boolean).hideHelp())
150 .addOption(new Option('--debug-keep-report', DEBUG_OPTION, boolean).hideHelp())
151 .addOption(new Option('--debug-capabilities-test <name>', DEBUG_OPTION).hideHelp())
152 .addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
153 .addOption(new Option('--debug-coverage-no-custom-fs', DEBUG_OPTION, boolean).hideHelp())
154 .addOption(new Option('--debug-verbose <module...>', DEBUG_OPTION, arrayOf(string), []).hideHelp())
155
156 return command
157}
158
159function parse (cwd, args) {
160 const command = getCommand(cwd)
161
162 command.parse(args, { from: 'user' })
163 const options = command.opts()
164
165 return Object.assign({
166 initialCwd: cwd,
167 browserArgs: command.args,
168 [$valueSources]: Object.keys(options).reduce((valueSources, name) => {
169 if (name !== 'browserArgs') {
170 valueSources[name] = command.getOptionValueSource(name)
171 }
172 return valueSources
173 }, {})
174 }, options)
175}
176
177function checkAccess ({ path, label, file /*, write */ }) {
178 try {
179 const mode = constants.R_OK
180 // if (write) {
181 // mode |= constants.W_OK
182 // }
183 accessSync(path, mode)
184 } catch (error) {
185 throw new Error(`Unable to access ${label}, check your settings`)
186 }
187 const stat = statSync(path)
188 if (file) {
189 if (!stat.isFile()) {
190 throw new Error(`Unable to access ${label}, file expected`)
191 }
192 } else {
193 if (!stat.isDirectory()) {
194 throw new Error(`Unable to access ${label}, folder expected`)
195 }
196 }
197}
198
199function finalize (job) {
200 function toAbsolute (path, from = job.cwd) {
201 if (!isAbsolute(path)) {
202 return join(from, path)
203 }
204 return path
205 }
206
207 function checkDefault (path) {
208 if (path.startsWith('$/')) {
209 return join(__dirname, './defaults', path.replace('$/', ''))
210 }
211 return path
212 }
213
214 function updateToAbsolute (member, from = job.cwd) {
215 job[member] = toAbsolute(job[member], from)
216 }
217 'browser,coverageSettings,progressPage'
218 .split(',')
219 .forEach(setting => { job[setting] = checkDefault(job[setting]) })
220 updateToAbsolute('cwd', job.initialCwd)
221 'webapp,browser,reportDir,coverageSettings,coverageTempDir,coverageReportDir'
222 .split(',')
223 .forEach(setting => updateToAbsolute(setting))
224 if (job.cache) {
225 updateToAbsolute('cache')
226 }
227 job.mode = buildAndCheckMode(job)
228 if (job.mode === 'legacy') {
229 checkAccess({ path: job.webapp, label: 'webapp folder' })
230
231 const [, testsuiteFile] = job.testsuite.match(/([^?]*)(\?.*)?$/)
232 const testsuitePath = toAbsolute(testsuiteFile, job.webapp)
233 checkAccess({ path: testsuitePath, label: 'testsuite', file: true })
234 } else if (job.mode === 'url') {
235 if (job[$valueSources].coverage !== 'cli') {
236 job.coverage = false
237 }
238 }
239 checkAccess({ path: job.browser, label: 'browser command', file: true })
240 job.reportGenerator = job.reportGenerator.map(setting => {
241 const path = toAbsolute(checkDefault(setting), job.cwd)
242 checkAccess({ path, label: 'report generator', file: true })
243 return path
244 })
245
246 if (!job.libs) {
247 job.libs = []
248 } else {
249 job.libs.forEach(libMapping => {
250 libMapping.source = toAbsolute(libMapping.source)
251 let description
252 if (libMapping.relative) {
253 description = `lib mapping of ${libMapping.relative}`
254 } else {
255 description = 'generic lib mapping'
256 }
257 checkAccess({ path: libMapping.source, label: `${description} (${libMapping.source})` })
258 })
259 }
260
261 const output = getOutput(job)
262
263 if (job.coverage) {
264 function overrideIfNotSet (option, valueFromSettings) {
265 if (valueFromSettings && job[$valueSources][option] !== 'cli') {
266 output.debug('coverage', `${option} extracted from nyc settings : ${valueFromSettings}`)
267 job[option] = valueFromSettings
268 }
269 }
270
271 function overrideDirIfNotSet (option, valueFromSettings) {
272 if (valueFromSettings && !isAbsolute(valueFromSettings)) {
273 valueFromSettings = join(dirname(job.coverageSettings), valueFromSettings)
274 }
275 overrideIfNotSet(option, valueFromSettings)
276 }
277
278 checkAccess({ path: job.coverageSettings, file: true, label: 'coverage settings' })
279
280 let settings
281 try {
282 settings = require(job.coverageSettings)
283 } catch (e) {
284 throw new Error(`Unable to read ${job.coverageSettings} as JSON`)
285 }
286 overrideDirIfNotSet('coverageReportDir', settings['report-dir'])
287 overrideDirIfNotSet('coverageTempDir', settings['temp-dir'])
288 overrideIfNotSet('coverageReporters', settings.reporter)
289 }
290
291 if (job.mode === 'url') {
292 const port = job.port.toString()
293 job[$remoteOnLegacy] = job.url.every(url => {
294 // ignore host name since the machine might be exposed with any name
295 const parsedUrl = new URL(url)
296 return parsedUrl.port === port
297 })
298 }
299
300 job[$status] = 'Starting'
301 Object.defineProperty(job, 'status', {
302 get () {
303 return job[$status]
304 },
305 set (value) {
306 job[$status] = value
307 output.status(value)
308 },
309 enumerable: false,
310 configurable: false
311 })
312}
313
314function fromCmdLine (cwd, args) {
315 let job = parse(cwd, args)
316
317 let defaultPath = join(job.cwd, 'ui5-test-runner.json')
318 if (!isAbsolute(defaultPath)) {
319 defaultPath = join(job.initialCwd, defaultPath)
320 }
321 let hasDefaultSettings = false
322 try {
323 checkAccess({ path: defaultPath, file: true })
324 hasDefaultSettings = true
325 } catch (e) {
326 // ignore
327 }
328 if (hasDefaultSettings) {
329 const defaults = require(defaultPath)
330 const { before, after, browser } = buildArgs(defaults)
331 const sep = args.indexOf('--')
332 if (sep === -1) {
333 args = [...before, ...args, ...after, '--', ...browser]
334 } else {
335 args = [...before, ...args.slice(0, sep), ...after, '--', ...browser, ...args.slice(sep + 1)]
336 }
337 job = parse(cwd, args)
338 }
339
340 finalize(job)
341 return job
342}
343
344function fromObject (cwd, parameters) {
345 const { before, browser } = buildArgs(parameters)
346 if (browser.length) {
347 return fromCmdLine(cwd, [...before, '--', ...browser])
348 }
349 return fromCmdLine(cwd, [...before])
350}
351
352module.exports = {
353 getCommand,
354 fromCmdLine,
355 fromObject
356}