1 | 'use strict'
|
2 |
|
3 | const { fork } = require('child_process')
|
4 | const { join } = require('path')
|
5 | const { writeFile, readFile, open, stat, unlink } = require('fs/promises')
|
6 | const { recreateDir, filename, allocPromise } = require('./tools')
|
7 | const { getPageTimeout, pageTimedOut } = require('./timeout')
|
8 | const { getOutput } = require('./output')
|
9 | const { resolvePackage } = require('./npm')
|
10 | const { UTRError } = require('./error')
|
11 | const { $browsers } = require('./symbols')
|
12 |
|
13 | let lastScreenshotId = 0
|
14 | const screenshots = {}
|
15 |
|
16 | async function instantiate (job, config) {
|
17 | const { dir, url } = config
|
18 | await recreateDir(dir)
|
19 | const browserConfig = {
|
20 | capabilities: job.browserCapabilities,
|
21 | modules: job.browserModules,
|
22 | ...config,
|
23 | args: job.browserArgs
|
24 | }
|
25 | const browserConfigPath = join(dir, 'browser.json')
|
26 | await writeFile(browserConfigPath, JSON.stringify(browserConfig, undefined, 2))
|
27 | const stdoutFilename = join(dir, 'stdout.txt')
|
28 | const stderrFilename = join(dir, 'stderr.txt')
|
29 | const stdout = await open(stdoutFilename, 'w')
|
30 | const stderr = await open(stderrFilename, 'w')
|
31 | const childProcess = fork(job.browser, [browserConfigPath], {
|
32 | stdio: [0, stdout, stderr, 'ipc']
|
33 | })
|
34 | const { promise, resolve } = allocPromise()
|
35 | childProcess.on('close', async code => {
|
36 | await stdout.close()
|
37 | await stderr.close()
|
38 | if (code !== 0) {
|
39 | getOutput(job).browserFailed(url, code, dir)
|
40 | }
|
41 | resolve(code)
|
42 | })
|
43 | childProcess.closed = promise
|
44 | childProcess.stdoutFilename = stdoutFilename
|
45 | childProcess.stderrFilename = stderrFilename
|
46 | return childProcess
|
47 | }
|
48 |
|
49 | async function probe (job) {
|
50 | if (job.browserCapabilities) {
|
51 | return
|
52 | }
|
53 | const output = getOutput(job)
|
54 | job.status = 'Probing browser instantiation command'
|
55 |
|
56 | async function execute (folder) {
|
57 | const dir = join(job.reportDir, folder)
|
58 | const capabilities = join(dir, 'capabilities.json')
|
59 | const childProcess = await instantiate(job, {
|
60 | url: 'about:blank',
|
61 | capabilities,
|
62 | dir
|
63 | })
|
64 | const code = await childProcess.closed
|
65 | if (code !== 0) {
|
66 | throw UTRError.BROWSER_PROBE_FAILED(code.toString())
|
67 | }
|
68 | let browserCapabilities
|
69 | try {
|
70 | browserCapabilities = Object.assign({
|
71 | modules: [],
|
72 | screenshot: null,
|
73 | scripts: false,
|
74 | parallel: true,
|
75 | traces: []
|
76 | }, JSON.parse((await readFile(capabilities)).toString()))
|
77 | } catch (e) {
|
78 | throw UTRError.MISSING_OR_INVALID_BROWSER_CAPABILITIES(e.message)
|
79 | }
|
80 | return browserCapabilities
|
81 | }
|
82 |
|
83 | const browserCapabilities = await execute('probe')
|
84 | job.browserCapabilities = browserCapabilities
|
85 |
|
86 | const { modules } = browserCapabilities
|
87 | const resolvedModules = {}
|
88 | if (modules.length) {
|
89 | for await (const name of browserCapabilities.modules) {
|
90 | resolvedModules[name] = await resolvePackage(job, name)
|
91 | }
|
92 | }
|
93 | job.browserModules = resolvedModules
|
94 | if (browserCapabilities['probe-with-modules']) {
|
95 | job.browserCapabilities = await execute('probe/with-modules')
|
96 | }
|
97 |
|
98 | output.browserCapabilities(job.browserCapabilities)
|
99 | }
|
100 |
|
101 | async function start (job, url, scripts = []) {
|
102 | const output = getOutput(job)
|
103 | if (!job[$browsers]) {
|
104 | job[$browsers] = {}
|
105 | }
|
106 | output.browserStart(url)
|
107 | const reportDir = join(job.reportDir, filename(url))
|
108 | const resolvedScripts = []
|
109 | for await (const script of scripts) {
|
110 | if (script.endsWith('.js')) {
|
111 | const scriptFilename = join(__dirname, 'inject', script)
|
112 | const scriptContent = (await readFile(scriptFilename)).toString()
|
113 | resolvedScripts.push(scriptContent)
|
114 | } else {
|
115 | resolvedScripts.push(script)
|
116 | }
|
117 | }
|
118 | if (resolvedScripts.length) {
|
119 | resolvedScripts.unshift(`window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
|
120 | `)
|
121 | }
|
122 | const pageBrowser = {
|
123 | url,
|
124 | reportDir,
|
125 | scripts: resolvedScripts,
|
126 | retry: 0
|
127 | }
|
128 | const { promise, resolve, reject } = allocPromise()
|
129 | pageBrowser.done = value => {
|
130 | delete job[$browsers][url]
|
131 | resolve(value)
|
132 | }
|
133 | pageBrowser.failed = reason => {
|
134 | delete job[$browsers][url]
|
135 | reject(reason)
|
136 | }
|
137 | job[$browsers][url] = pageBrowser
|
138 | await run(job, pageBrowser)
|
139 | await promise
|
140 | output.browserStopped(url)
|
141 | }
|
142 |
|
143 | async function run (job, pageBrowser) {
|
144 | const output = getOutput(job)
|
145 | const { url, retry, reportDir, scripts } = pageBrowser
|
146 | let dir = reportDir
|
147 | if (retry) {
|
148 | output.browserRetry(url, retry)
|
149 | dir = join(dir, retry.toString())
|
150 | if (pageBrowser.console.count) {
|
151 | try {
|
152 | await pageBrowser.console.flush
|
153 | .then(() => unlink(join(reportDir, 'console.jsonl')))
|
154 | } catch (e) {
|
155 |
|
156 | }
|
157 | }
|
158 | }
|
159 | pageBrowser.console = {
|
160 | count: 0,
|
161 | byApi: {},
|
162 | flush: Promise.resolve()
|
163 | }
|
164 | await recreateDir(dir)
|
165 | delete pageBrowser.stopped
|
166 | const childProcess = await instantiate(job, {
|
167 | url,
|
168 | retry,
|
169 | scripts,
|
170 | dir
|
171 | })
|
172 | pageBrowser.childProcess = childProcess
|
173 | const timeout = getPageTimeout(job)
|
174 | if (timeout) {
|
175 | pageBrowser.timeoutId = setTimeout(() => {
|
176 | output.browserTimeout(url, dir)
|
177 | pageTimedOut(job, url)
|
178 | stop(job, url)
|
179 | }, timeout)
|
180 | }
|
181 | childProcess.on('message', message => {
|
182 | if (message.command === 'screenshot') {
|
183 | const { id } = message
|
184 | screenshots[id]()
|
185 | delete screenshots[id]
|
186 | } else if (message.command === 'console') {
|
187 | ++pageBrowser.console.count
|
188 | if (!pageBrowser.console.byApi[message.api]) {
|
189 | pageBrowser.console.byApi[message.api] = 1
|
190 | } else {
|
191 | ++pageBrowser.console.byApi[message.api]
|
192 | }
|
193 | pageBrowser.console.flush = pageBrowser.console.flush
|
194 | .then(() => writeFile(join(reportDir, 'console.jsonl'), JSON.stringify({
|
195 | t: message.t,
|
196 | api: message.api,
|
197 | args: message.args
|
198 | }) + '\n', {
|
199 | flag: 'a+'
|
200 | }))
|
201 | }
|
202 | })
|
203 | childProcess.on('close', async code => {
|
204 | if (!pageBrowser.stopped) {
|
205 | if (code === 0) {
|
206 | output.browserClosed(url, code, dir)
|
207 | }
|
208 | childProcess.closed.then(() => stop(job, url, true))
|
209 | }
|
210 | })
|
211 | }
|
212 |
|
213 | async function screenshot (job, url, filename) {
|
214 | if (!job.browserCapabilities.screenshot) {
|
215 | throw UTRError.BROWSER_SCREENSHOT_NOT_SUPPORTED()
|
216 | }
|
217 | const output = getOutput(job)
|
218 | const id = ++lastScreenshotId
|
219 | try {
|
220 | const { childProcess, reportDir } = job[$browsers][url]
|
221 | const absoluteFilename = join(reportDir, filename + job.browserCapabilities.screenshot)
|
222 | if (childProcess.connected) {
|
223 | output.debug('screenshot', id, url, absoluteFilename)
|
224 | const { promise, resolve, reject } = allocPromise()
|
225 | screenshots[id] = resolve
|
226 | output.debug('screenshot', id, 'sending command')
|
227 | childProcess.send({
|
228 | id,
|
229 | command: 'screenshot',
|
230 | filename: absoluteFilename
|
231 | })
|
232 | const timeoutId = setTimeout(() => {
|
233 | reject(UTRError.BROWSER_SCREENSHOT_TIMEOUT())
|
234 | }, job.screenshotTimeout)
|
235 | output.debug('screenshot', id, 'command sent, waiting for answer')
|
236 | await promise
|
237 | output.debug('screenshot', id, 'answer received')
|
238 | clearTimeout(timeoutId)
|
239 | const result = await stat(absoluteFilename)
|
240 | output.debug('screenshot', id, 'file size :', result.size)
|
241 | if (!result.isFile() || result.size === 0) {
|
242 | throw new Error('File expected')
|
243 | }
|
244 | output.debug('screenshot', id, 'done')
|
245 | return absoluteFilename
|
246 | }
|
247 | } catch (e) {
|
248 | output.debug('screenshot', id, e.message)
|
249 | if (e.code === UTRError.BROWSER_SCREENSHOT_TIMEOUT_CODE) {
|
250 | throw e
|
251 | }
|
252 | throw UTRError.BROWSER_SCREENSHOT_FAILED(e.toString())
|
253 | }
|
254 | }
|
255 |
|
256 | async function stop (job, url, retry = false) {
|
257 | const pageBrowser = job[$browsers][url]
|
258 | if (pageBrowser) {
|
259 | pageBrowser.stopped = true
|
260 | const { childProcess, done, failed, timeoutId } = pageBrowser
|
261 | if (timeoutId) {
|
262 | clearTimeout(timeoutId)
|
263 | }
|
264 | if (childProcess.connected) {
|
265 |
|
266 | if (!job.debugKeepBrowserOpen) {
|
267 | childProcess.send({ command: 'stop' })
|
268 | }
|
269 | const { promise: closeTimeout, resolve } = allocPromise()
|
270 | const timeoutId = setTimeout(resolve, job.browserCloseTimeout)
|
271 | await Promise.race([
|
272 | childProcess.closed,
|
273 | closeTimeout
|
274 | ])
|
275 | clearTimeout(timeoutId)
|
276 | }
|
277 | await pageBrowser.console.flush
|
278 | if (retry) {
|
279 | if (++pageBrowser.retry <= job.browserRetry) {
|
280 | run(job, pageBrowser)
|
281 | } else {
|
282 | failed(UTRError.BROWSER_FAILED())
|
283 | }
|
284 | } else {
|
285 | done()
|
286 | }
|
287 | }
|
288 | }
|
289 |
|
290 | module.exports = { probe, start, screenshot, stop }
|