ui5-test-runner 5.2.0 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/output.js CHANGED
@@ -1,604 +1,608 @@
1
- 'use strict'
2
-
3
- const { readFileSync, writeFileSync } = require('fs')
4
- const { join } = require('path')
5
- const { memoryUsage } = require('process')
6
- const {
7
- $browsers,
8
- $statusProgressCount,
9
- $statusProgressTotal
10
- } = require('./symbols')
11
- const { filename, noop, pad } = require('./tools')
12
-
13
- const inJest = typeof jest !== 'undefined'
14
- const interactive = process.stdout.columns !== undefined && !inJest
15
- const $output = Symbol('output')
16
- const $outputStart = Symbol('output-start')
17
- const $outputProgress = Symbol('output-progress')
18
-
19
- if (!interactive) {
20
- const UTF8_BOM_CODE = '\ufeff'
21
- process.stdout.write(UTF8_BOM_CODE)
22
- }
23
-
24
- let cons
25
- if (inJest) {
26
- cons = {
27
- log: noop,
28
- warn: noop,
29
- error: noop
30
- }
31
- } else {
32
- cons = console
33
- }
34
-
35
- const formatTime = duration => {
36
- duration = Math.ceil(duration / 1000)
37
- const seconds = duration % 60
38
- const minutes = (duration - seconds) / 60
39
- return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0')
40
- }
41
-
42
- const getElapsed = job => formatTime(Date.now() - job[$outputStart])
43
-
44
- function * buildCleanSequence (job) {
45
- const { lines } = job[$output]
46
- if (!lines) {
47
- return
48
- }
49
- yield '\x1b[?12l'
50
- yield `\x1b[${lines.toString()}F`
51
- for (let line = 0; line < lines; ++line) {
52
- if (line > 1) {
53
- yield '\x1b[1E'
54
- }
55
- yield ''.padEnd(process.stdout.columns, ' ')
56
- }
57
- if (lines > 1) {
58
- yield `\x1b[${(lines - 1).toString()}F`
59
- } else {
60
- yield '\x1b[1G'
61
- }
62
- }
63
-
64
- function clean (job) {
65
- process.stdout.write([...buildCleanSequence(job)].join(''))
66
- }
67
-
68
- const BAR_WIDTH = 10
69
-
70
- function * bar (ratio, msg) {
71
- yield '['
72
- if (typeof ratio === 'string') {
73
- if (ratio.length > BAR_WIDTH) {
74
- yield ratio.substring(0, BAR_WIDTH - 3)
75
- yield '...'
76
- } else {
77
- const padded = ratio.padStart(BAR_WIDTH - Math.floor((BAR_WIDTH - ratio.length) / 2), '-').padEnd(BAR_WIDTH, '-')
78
- yield padded
79
- }
80
- yield '] '
81
- } else {
82
- const filled = Math.floor(BAR_WIDTH * Math.min(ratio, 1))
83
- yield ''.padEnd(filled, '\u2588')
84
- yield ''.padEnd(BAR_WIDTH - filled, '\u2591')
85
- yield '] '
86
- yield Math.floor(100 * ratio).toString().padStart(3, ' ').toString()
87
- yield '%'
88
- }
89
- yield ' '
90
- const spaceLeft = process.stdout.columns - BAR_WIDTH - 14
91
- if (msg.length > spaceLeft) {
92
- yield '...'
93
- yield msg.substring(msg.length - spaceLeft - 3)
94
- } else {
95
- yield msg
96
- }
97
- yield '\n'
98
- }
99
-
100
- const TICKS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
101
-
102
- class Progress {
103
- #job = undefined
104
-
105
- constructor (job) {
106
- this.#job = job
107
- if (!job[$outputProgress]) {
108
- job[$outputProgress] = []
109
- }
110
- job[$outputProgress].push(this)
111
- }
112
-
113
- done () {
114
- const pos = this.#job[$outputProgress].indexOf(this)
115
- this.#job[$outputProgress].splice(pos, 1)
116
- }
117
- }
118
-
119
- function progress (job, cleanFirst = true) {
120
- if (process.send) {
121
- process.send({
122
- type: 'progress',
123
- count: job[$statusProgressCount],
124
- total: job[$statusProgressTotal]
125
- })
126
- }
127
- const sequence = []
128
- if (interactive) {
129
- if (cleanFirst) {
130
- sequence.push(...buildCleanSequence(job))
131
- }
132
- } else {
133
- if (job[$browsers]) {
134
- sequence.push(`${getElapsed(job)} │ Progress\n──────┴──────────\n`)
135
- } else {
136
- return
137
- }
138
- }
139
- const output = job[$output]
140
- output.lines = 1
141
- let progressRatio
142
- if (job.debugMemory) {
143
- ++output.lines
144
- const { rss, heapTotal, heapUsed, external, arrayBuffers } = memoryUsage()
145
- const fmt = size => `${(size / (1024 * 1024)).toFixed(2)}M`
146
- sequence.push(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
147
- }
148
- if (job[$outputProgress]) {
149
- output.lines += job[$outputProgress].length
150
- job[$outputProgress].forEach(({ count, total, label }) => {
151
- if (total !== undefined) {
152
- sequence.push(...bar((count || 0) / (total || 1), label))
153
- } else {
154
- sequence.push(...bar('starting', label))
155
- }
156
- })
157
- }
158
- if (job[$statusProgressTotal]) {
159
- progressRatio = (job[$statusProgressCount] || 0) / job[$statusProgressTotal]
160
- }
161
- const status = `${TICKS[++output.lastTick % TICKS.length]} ${job.status}`
162
- if (progressRatio !== undefined) {
163
- sequence.push(...bar(progressRatio, status))
164
- } else {
165
- sequence.push(status, '\n')
166
- }
167
- process.stdout.write(sequence.join(''))
168
- }
169
-
170
- function output (job, ...args) {
171
- writeFileSync(
172
- join(job.reportDir, 'output.txt'),
173
- args.map(arg => {
174
- if (typeof arg === 'object') {
175
- return JSON.stringify(arg, undefined, 2)
176
- }
177
- if (arg === undefined) {
178
- return 'undefined'
179
- }
180
- if (arg === null) {
181
- return 'null'
182
- }
183
- return arg.toString()
184
- }).join(' ') + '\n',
185
- {
186
- encoding: 'utf-8',
187
- flag: 'a'
188
- }
189
- )
190
- }
191
-
192
- function log (job, ...texts) {
193
- cons.log(...texts)
194
- output(job, ...texts)
195
- }
196
-
197
- function warn (job, ...texts) {
198
- cons.warn(...texts)
199
- output(job, ...texts)
200
- }
201
-
202
- function err (job, ...texts) {
203
- cons.error(...texts)
204
- output(job, ...texts)
205
- }
206
-
207
- const p80 = () => pad(process.stdout.columns || 80)
208
-
209
- function browserIssue (job, { type, url, code, dir }) {
210
- const p = p80()
211
- log(job, p`┌──────────${pad.x('─')}┐`)
212
- log(job, p`│ BROWSER ${type.toUpperCase()} ${pad.x(' ')} │`)
213
- log(job, p`├──────┬─${pad.x('─')}──┤`)
214
- log(job, p`│ url │ ${pad.lt(url)} │`)
215
- log(job, p`├──────┼─${pad.x('─')}──┤`)
216
- const unsignedCode = new Uint32Array([code])[0]
217
- log(job, p`│ code │ 0x${unsignedCode.toString(16).toUpperCase()}${pad.x(' ')} │`)
218
- log(job, p`├──────┼─${pad.x('─')}──┤`)
219
- log(job, p`│ dir │ ${pad.lt(dir)} │`)
220
- log(job, p`├──────┴─${pad.x('─')}──┤`)
221
-
222
- const stderr = readFileSync(join(dir, 'stderr.txt')).toString().trim()
223
- if (stderr.length !== 0) {
224
- log(job, p`│ Error output (${stderr.length}) ${pad.x(' ')} │`)
225
- log(job, p`│ ${pad.w(stderr)} │`)
226
- } else {
227
- const stdout = readFileSync(join(dir, 'stdout.txt')).toString()
228
- if (stdout.length !== 0) {
229
- log(job, p`│ Standard output (${stderr.length}), last 10 lines... ${pad.x(' ')} │`)
230
- log(job, p`│ ${pad.w('...')} │`)
231
- log(job, p`│ ${pad.w(stdout.split(/\r?\n/).slice(-10).join('\n'))} │`)
232
- } else {
233
- log(job, p`│ No output ${pad.x(' ')} │`)
234
- }
235
- }
236
- log(job, p`└──────────${pad.x('─')}┘`)
237
- }
238
-
239
- function build (job) {
240
- let wrap
241
- if (interactive) {
242
- wrap = method => function () {
243
- clean(job)
244
- try {
245
- method.call(this, ...arguments)
246
- } finally {
247
- progress(job, false)
248
- }
249
- }
250
- } else {
251
- wrap = method => method
252
- }
253
- job[$outputStart] = Date.now()
254
-
255
- return {
256
- lastTick: 0,
257
- reportIntervalId: undefined,
258
- lines: 0,
259
-
260
- version: () => {
261
- const { name, version } = require(join(__dirname, '../package.json'))
262
- log(job, p80()`${name}@${version}`)
263
- if (job.debugDevMode) {
264
- log(job, p80()`⚠️ Development mode ⚠️`)
265
- }
266
- },
267
-
268
- serving: url => {
269
- log(job, p80()`Server running at ${pad.lt(url)}`)
270
- },
271
-
272
- log: wrap((...texts) => {
273
- log(job, ...texts)
274
- }),
275
-
276
- error: wrap((...texts) => {
277
- err(job, ...texts)
278
- }),
279
-
280
- debug: (moduleSpecifier, ...args) => {
281
- const [mainModule] = moduleSpecifier.split('/')
282
- if (job.debugVerbose && (job.debugVerbose.includes(moduleSpecifier) || job.debugVerbose.includes(mainModule))) {
283
- wrap(() => {
284
- console.log(`🐞${moduleSpecifier}`, ...args)
285
- output(job, `🐞${moduleSpecifier}`, ...args)
286
- })()
287
- }
288
- },
289
-
290
- redirected: wrap(({ method, url, statusCode, timeSpent }) => {
291
- if (url.startsWith('/_/progress')) {
292
- return // avoids pollution
293
- }
294
- let statusText
295
- if (!statusCode) {
296
- statusText = 'N/A'
297
- } else {
298
- statusText = statusCode
299
- }
300
- log(job, p80()`${method.padEnd(7, ' ')} ${pad.lt(url)} ${statusText} ${timeSpent.toString().padStart(4, ' ')}ms`)
301
- }),
302
-
303
- status (status) {
304
- let method
305
- if (interactive) {
306
- method = output
307
- } else {
308
- method = log
309
- }
310
- const text = `${getElapsed(job)} │ ${status}`
311
- method(job, '')
312
- method(job, text)
313
- method(job, '──────┴'.padEnd(text.length, '─'))
314
- delete job[$statusProgressCount]
315
- delete job[$statusProgressTotal]
316
- },
317
-
318
- watching: wrap(path => {
319
- log(job, p80()`Watching changes on ${pad.lt(path)}`)
320
- }),
321
-
322
- changeDetected: wrap((eventType, filename) => {
323
- log(job, p80()`${eventType} ${pad.lt(filename)}`)
324
- }),
325
-
326
- reportOnJobProgress () {
327
- if (interactive) {
328
- this.reportIntervalId = setInterval(progress.bind(null, job), 250)
329
- } else if (job.outputInterval && !inJest) {
330
- this.reportIntervalId = setInterval(progress.bind(null, job), job.outputInterval)
331
- }
332
- },
333
-
334
- browserCapabilities: wrap(capabilities => {
335
- log(job, p80()`Browser capabilities :`)
336
- const { modules } = capabilities
337
- if (modules.length) {
338
- log(job, p80()` ├─ modules`)
339
- modules.forEach((module, index) => {
340
- let prefix
341
- if (index === modules.length - 1) {
342
- prefix = ' │ └─ '
343
- } else {
344
- prefix = ' │ ├─'
345
- }
346
- log(job, p80()`${prefix} ${pad.lt(module)}`)
347
- })
348
- }
349
- Object.keys(capabilities)
350
- .filter(key => key !== 'modules')
351
- .forEach((key, index, keys) => {
352
- let prefix
353
- if (index === keys.length - 1) {
354
- prefix = ' └─'
355
- } else {
356
- prefix = ' ├─'
357
- }
358
- log(job, p80()`${prefix} ${key}: ${JSON.stringify(capabilities[key])}`)
359
- })
360
- }),
361
-
362
- resolvedPackage (name, path, version) {
363
- if (!name.match(/@\d+\.\d+\.\d+$/)) {
364
- name += `@${version}`
365
- }
366
- wrap(() => log(job, p80()`${name} in ${pad.lt(path)}`))()
367
- },
368
-
369
- packageNotLatest (name, latestVersion) {
370
- wrap(() => log(job, `⚠️ [PKGVRS] latest version of ${name} is ${latestVersion}`))()
371
- },
372
-
373
- browserStart (url) {
374
- const text = p80()`${getElapsed(job)} >> ${pad.lt(url)} [${filename(url)}]`
375
- if (interactive) {
376
- output(job, text)
377
- } else {
378
- wrap(() => log(job, text))()
379
- }
380
- },
381
-
382
- browserStopped (url) {
383
- let duration = ''
384
- const page = job.qunitPages && job.qunitPages[url]
385
- if (page) {
386
- duration = ' (' + formatTime(page.end - page.start) + ')'
387
- }
388
- const text = p80()`${getElapsed(job)} << ${pad.lt(url)} ${duration} [${filename(url)}]`
389
- if (interactive) {
390
- output(job, text)
391
- } else {
392
- wrap(() => log(job, text))()
393
- }
394
- },
395
-
396
- browserClosed: wrap((url, code, dir) => {
397
- browserIssue(job, { type: 'unexpected closed', url, code, dir })
398
- }),
399
-
400
- browserRetry (url, retry) {
401
- if (interactive) {
402
- output(job, '>>', url)
403
- } else {
404
- wrap(() => log(job, p80()`>> RETRY ${retry} ${pad.lt(url)}`))()
405
- }
406
- },
407
-
408
- browserTimeout: wrap((url, dir) => {
409
- browserIssue(job, { type: 'timeout', url, code: 0, dir })
410
- }),
411
-
412
- browserFailed: wrap((url, code, dir) => {
413
- browserIssue(job, { type: 'failed', url, code, dir })
414
- }),
415
-
416
- startFailed: wrap((url, error) => {
417
- const p = p80()
418
- log(job, p`┌──────────${pad.x('─')}┐`)
419
- log(job, p`│ UNABLE TO START THE URL ${pad.x(' ')} │`)
420
- log(job, p`├──────┬─${pad.x('─')}──┤`)
421
- log(job, p`│ url │ ${pad.lt(url)} │`)
422
- log(job, p`├──────┴─${pad.x('─')}──┤`)
423
- if (error.stack) {
424
- log(job, p`│ ${pad.w(error.stack)} │`)
425
- } else {
426
- log(job, p`│ ${pad.w(error.toString())} │`)
427
- }
428
- log(job, p`└──────────${pad.x('─')}┘`)
429
- }),
430
-
431
- monitor (childProcess, live = true) {
432
- const defaults = {
433
- stdout: { buffer: [], method: log },
434
- stderr: { buffer: [], method: err }
435
- };
436
- ['stdout', 'stderr'].forEach(channel => {
437
- childProcess[channel].on('data', chunk => {
438
- const { buffer, method } = defaults[channel]
439
- const text = chunk.toString()
440
- if (live) {
441
- if (!text.includes('\n')) {
442
- buffer.push(text)
443
- return
444
- }
445
- const cached = buffer.join('')
446
- const last = text.split('\n').slice(-1)
447
- buffer.length = 0
448
- if (last) {
449
- buffer.push(last)
450
- }
451
- wrap(() => method(job, cached + text.split('\n').slice(0, -1).join('\n')))()
452
- } else {
453
- buffer.push(text)
454
- }
455
- })
456
- })
457
- if (live) {
458
- childProcess.on('close', () => {
459
- ['stdout', 'stderr'].forEach(channel => {
460
- const { buffer, method } = defaults[channel]
461
- if (buffer.length) {
462
- method(job, buffer.join(''))
463
- }
464
- })
465
- })
466
- }
467
- return {
468
- stdout: defaults.stdout.buffer,
469
- stderr: defaults.stderr.buffer
470
- }
471
- },
472
-
473
- nyc: wrap((...args) => {
474
- log(job, p80()`nyc ${args.map(arg => arg.toString()).join(' ')}`)
475
- }),
476
-
477
- instrumentationSkipped: wrap(() => {
478
- log(job, p80()`⚠️ [SKPNYC] Skipping nyc instrumentation (--url)`)
479
- }),
480
-
481
- assumingOneOrigin: wrap(() => {
482
- log(job, p80()`⚠️ [COVORG] Considering only one origin`)
483
- }),
484
-
485
- noInfoForAllCoverage: wrap(() => {
486
- log(job, p80()`⚠️ [COVALL] Unable to process all coverage, report might be incomplete`)
487
- }),
488
-
489
- endpointError: wrap(({ api, url, data, error }) => {
490
- const p = p80()
491
- log(job, p`┌──────────${pad.x('─')}┐`)
492
- log(job, p`│ UNEXPECTED ENDPOINT ERROR ${pad.x(' ')} │`)
493
- log(job, p`├──────┬─${pad.x('─')}──┤`)
494
- log(job, p`│ api │ ${pad.lt(api)} │`)
495
- log(job, p`├──────┼─${pad.x('─')}──┤`)
496
- log(job, p`│ from ${pad.lt(url)} │`)
497
- log(job, p`├──────┴─${pad.x('─')}──┤`)
498
- log(job, p`│ data (${JSON.stringify(data).length}) ${pad.x(' ')} │`)
499
- log(job, p`│ ${pad.w(JSON.stringify(data, undefined, 2))} │`)
500
- log(job, p`├────────${pad.x('─')}──┤`)
501
- if (error.stack) {
502
- log(job, p`│ ${pad.w(error.stack)} │`)
503
- } else {
504
- log(job, p`│ ${pad.w(error.toString())} │`)
505
- }
506
- log(job, p`└──────────${pad.x('─')}┘`)
507
- }),
508
-
509
- serverError: wrap(({ method, url, reason }) => {
510
- const p = p80()
511
- log(job, p`┌──────────${pad.x('─')}┐`)
512
- log(job, p`│ UNEXPECTED SERVER ERROR ${pad.x(' ')} │`)
513
- log(job, p`├──────┬─${pad.x('─')}──┤`)
514
- log(job, p`│ verb │ ${pad.lt(method)} │`)
515
- log(job, p`├──────┼─${pad.x('─')}──┤`)
516
- log(job, p`│ url │ ${pad.lt(url)} │`)
517
- log(job, p`├──────┴─${pad.x('─')}──┤`)
518
- if (reason.stack) {
519
- log(job, p`│ ${pad.w(reason.stack)} │`)
520
- } else {
521
- log(job, p`│ ${pad.w(reason.toString())} │`)
522
- }
523
- log(job, p`└──────────${pad.x('─')}┘`)
524
- }),
525
-
526
- globalTimeout: wrap(url => {
527
- log(job, p80()`!! TIMEOUT ${pad.lt(url)}`)
528
- }),
529
-
530
- failFast: wrap(url => {
531
- log(job, p80()`!! FAILFAST ${pad.lt(url)}`)
532
- }),
533
-
534
- noTestPageFound: wrap(() => {
535
- err(job, p80()`No test page found (or all filtered out)`)
536
- }),
537
-
538
- failedToCacheUI5resource: wrap((path, statusCode) => {
539
- err(job, p80()`Unable to cache '${pad.lt(path)}' (status ${statusCode})`)
540
- }),
541
-
542
- genericError: wrap((error, url) => {
543
- const p = p80()
544
- log(job, p`┌──────────${pad.x('─')}┐`)
545
- log(job, p`│ UNEXPECTED ERROR ${pad.x(' ')} │`)
546
- if (url) {
547
- log(job, p`├──────┬─${pad.x('─')}──┤`)
548
- log(job, p`│ url │ ${pad.lt(url)} │`)
549
- log(job, p`├──────┴─${pad.x('')}──┤`)
550
- } else {
551
- log(job, p`├────────${pad.x('─')}──┤`)
552
- }
553
- if (error.stack) {
554
- log(job, p`│ ${pad.w(error.stack)} │`)
555
- } else {
556
- log(job, p`│ ${pad.w(error.toString())} │`)
557
- }
558
- log(job, p`└──────────${pad.x('─')}┘`)
559
- }),
560
-
561
- unhandled: wrap(() => {
562
- warn(job, p80()`⚠️ [UNHAND] Some requests are not handled properly, check the unhandled.txt report for more info`)
563
- }),
564
-
565
- reportGeneratorFailed: wrap((generator, exitCode, buffers) => {
566
- const p = p80()
567
- log(job, p`┌──────────${pad.x('─')}┐`)
568
- log(job, p`│ REPORT GENERATOR FAILED ${pad.x(' ')} │`)
569
- log(job, p`├───────────┬─${pad.x('─')}──┤`)
570
- log(job, p`│ generator │ ${pad.lt(generator)} │`)
571
- log(job, p`├───────────┼─${pad.x('─')}──┤`)
572
- log(job, p`│ exit code ${pad.lt(exitCode.toString())} │`)
573
- log(job, p`├───────────┴─${pad.x('─')}──┤`)
574
- log(job, p`│ ${pad.w(buffers.stderr.join(''))} │`)
575
- log(job, p`└──────────${pad.x('─')}┘`)
576
- }),
577
-
578
- stop () {
579
- if (this.reportIntervalId) {
580
- clearInterval(this.reportIntervalId)
581
- if (interactive) {
582
- clean(job)
583
- }
584
- }
585
- }
586
- }
587
- }
588
-
589
- module.exports = {
590
- interactive,
591
-
592
- getOutput (job) {
593
- if (!job[$output]) {
594
- job[$output] = build(job)
595
- }
596
- return job[$output]
597
- },
598
-
599
- newProgress (job, label, total, count) {
600
- const progress = new Progress(job)
601
- Object.assign(progress, { label, total, count })
602
- return progress
603
- }
604
- }
1
+ 'use strict'
2
+
3
+ const { readFileSync, writeFileSync } = require('fs')
4
+ const { join } = require('path')
5
+ const { memoryUsage } = require('process')
6
+ const {
7
+ $browsers,
8
+ $statusProgressCount,
9
+ $statusProgressTotal
10
+ } = require('./symbols')
11
+ const { filename, noop, pad } = require('./tools')
12
+
13
+ const inJest = typeof jest !== 'undefined'
14
+ const interactive = process.stdout.columns !== undefined && !inJest
15
+ const $output = Symbol('output')
16
+ const $outputStart = Symbol('output-start')
17
+ const $outputProgress = Symbol('output-progress')
18
+
19
+ if (!interactive) {
20
+ const UTF8_BOM_CODE = '\ufeff'
21
+ process.stdout.write(UTF8_BOM_CODE)
22
+ }
23
+
24
+ let cons
25
+ if (inJest) {
26
+ cons = {
27
+ log: noop,
28
+ warn: noop,
29
+ error: noop
30
+ }
31
+ } else {
32
+ cons = console
33
+ }
34
+
35
+ const formatTime = duration => {
36
+ duration = Math.ceil(duration / 1000)
37
+ const seconds = duration % 60
38
+ const minutes = (duration - seconds) / 60
39
+ return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0')
40
+ }
41
+
42
+ const getElapsed = job => formatTime(Date.now() - job[$outputStart])
43
+
44
+ function * buildCleanSequence (job) {
45
+ const { lines } = job[$output]
46
+ if (!lines) {
47
+ return
48
+ }
49
+ yield '\x1b[?12l'
50
+ yield `\x1b[${lines.toString()}F`
51
+ for (let line = 0; line < lines; ++line) {
52
+ if (line > 1) {
53
+ yield '\x1b[1E'
54
+ }
55
+ yield ''.padEnd(process.stdout.columns, ' ')
56
+ }
57
+ if (lines > 1) {
58
+ yield `\x1b[${(lines - 1).toString()}F`
59
+ } else {
60
+ yield '\x1b[1G'
61
+ }
62
+ }
63
+
64
+ function clean (job) {
65
+ process.stdout.write([...buildCleanSequence(job)].join(''))
66
+ }
67
+
68
+ const BAR_WIDTH = 10
69
+
70
+ function * bar (ratio, msg) {
71
+ yield '['
72
+ if (typeof ratio === 'string') {
73
+ if (ratio.length > BAR_WIDTH) {
74
+ yield ratio.substring(0, BAR_WIDTH - 3)
75
+ yield '...'
76
+ } else {
77
+ const padded = ratio.padStart(BAR_WIDTH - Math.floor((BAR_WIDTH - ratio.length) / 2), '-').padEnd(BAR_WIDTH, '-')
78
+ yield padded
79
+ }
80
+ yield '] '
81
+ } else {
82
+ const filled = Math.floor(BAR_WIDTH * Math.min(ratio, 1))
83
+ yield ''.padEnd(filled, '\u2588')
84
+ yield ''.padEnd(BAR_WIDTH - filled, '\u2591')
85
+ yield '] '
86
+ yield Math.floor(100 * ratio).toString().padStart(3, ' ').toString()
87
+ yield '%'
88
+ }
89
+ yield ' '
90
+ const spaceLeft = process.stdout.columns - BAR_WIDTH - 14
91
+ if (msg.length > spaceLeft) {
92
+ yield '...'
93
+ yield msg.substring(msg.length - spaceLeft - 3)
94
+ } else {
95
+ yield msg
96
+ }
97
+ yield '\n'
98
+ }
99
+
100
+ const TICKS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
101
+
102
+ class Progress {
103
+ #job = undefined
104
+
105
+ constructor (job) {
106
+ this.#job = job
107
+ if (!job[$outputProgress]) {
108
+ job[$outputProgress] = []
109
+ }
110
+ job[$outputProgress].push(this)
111
+ }
112
+
113
+ done () {
114
+ const pos = this.#job[$outputProgress].indexOf(this)
115
+ this.#job[$outputProgress].splice(pos, 1)
116
+ }
117
+ }
118
+
119
+ function progress (job, cleanFirst = true) {
120
+ if (process.send) {
121
+ process.send({
122
+ type: 'progress',
123
+ count: job[$statusProgressCount],
124
+ total: job[$statusProgressTotal]
125
+ })
126
+ }
127
+ const sequence = []
128
+ if (interactive) {
129
+ if (cleanFirst) {
130
+ sequence.push(...buildCleanSequence(job))
131
+ }
132
+ } else {
133
+ if (job[$browsers]) {
134
+ sequence.push(`${getElapsed(job)} │ Progress\n──────┴──────────\n`)
135
+ } else {
136
+ return
137
+ }
138
+ }
139
+ const output = job[$output]
140
+ output.lines = 1
141
+ let progressRatio
142
+ if (job.debugMemory) {
143
+ ++output.lines
144
+ const { rss, heapTotal, heapUsed, external, arrayBuffers } = memoryUsage()
145
+ const fmt = size => `${(size / (1024 * 1024)).toFixed(2)}M`
146
+ sequence.push(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
147
+ }
148
+ if (job[$outputProgress]) {
149
+ output.lines += job[$outputProgress].length
150
+ job[$outputProgress].forEach(({ count, total, label }) => {
151
+ if (total !== undefined) {
152
+ sequence.push(...bar((count || 0) / (total || 1), label))
153
+ } else {
154
+ sequence.push(...bar('starting', label))
155
+ }
156
+ })
157
+ }
158
+ if (job[$statusProgressTotal]) {
159
+ progressRatio = (job[$statusProgressCount] || 0) / job[$statusProgressTotal]
160
+ }
161
+ const status = `${TICKS[++output.lastTick % TICKS.length]} ${job.status}`
162
+ if (progressRatio !== undefined) {
163
+ sequence.push(...bar(progressRatio, status))
164
+ } else {
165
+ sequence.push(status, '\n')
166
+ }
167
+ process.stdout.write(sequence.join(''))
168
+ }
169
+
170
+ function output (job, ...args) {
171
+ writeFileSync(
172
+ join(job.reportDir, 'output.txt'),
173
+ args.map(arg => {
174
+ if (typeof arg === 'object') {
175
+ return JSON.stringify(arg, undefined, 2)
176
+ }
177
+ if (arg === undefined) {
178
+ return 'undefined'
179
+ }
180
+ if (arg === null) {
181
+ return 'null'
182
+ }
183
+ return arg.toString()
184
+ }).join(' ') + '\n',
185
+ {
186
+ encoding: 'utf-8',
187
+ flag: 'a'
188
+ }
189
+ )
190
+ }
191
+
192
+ function log (job, ...texts) {
193
+ cons.log(...texts)
194
+ output(job, ...texts)
195
+ }
196
+
197
+ function warn (job, ...texts) {
198
+ cons.warn(...texts)
199
+ output(job, ...texts)
200
+ }
201
+
202
+ function err (job, ...texts) {
203
+ cons.error(...texts)
204
+ output(job, ...texts)
205
+ }
206
+
207
+ const p80 = () => pad(process.stdout.columns || 80)
208
+
209
+ function browserIssue (job, { type, url, code, dir }) {
210
+ const p = p80()
211
+ log(job, p`┌──────────${pad.x('─')}┐`)
212
+ log(job, p`│ BROWSER ${type.toUpperCase()} ${pad.x(' ')} │`)
213
+ log(job, p`├──────┬─${pad.x('─')}──┤`)
214
+ log(job, p`│ url │ ${pad.lt(url)} │`)
215
+ log(job, p`├──────┼─${pad.x('─')}──┤`)
216
+ const unsignedCode = new Uint32Array([code])[0]
217
+ log(job, p`│ code │ 0x${unsignedCode.toString(16).toUpperCase()}${pad.x(' ')} │`)
218
+ log(job, p`├──────┼─${pad.x('─')}──┤`)
219
+ log(job, p`│ dir │ ${pad.lt(dir)} │`)
220
+ log(job, p`├──────┴─${pad.x('─')}──┤`)
221
+
222
+ const stderr = readFileSync(join(dir, 'stderr.txt')).toString().trim()
223
+ if (stderr.length !== 0) {
224
+ log(job, p`│ Error output (${stderr.length}) ${pad.x(' ')} │`)
225
+ log(job, p`│ ${pad.w(stderr)} │`)
226
+ } else {
227
+ const stdout = readFileSync(join(dir, 'stdout.txt')).toString()
228
+ if (stdout.length !== 0) {
229
+ log(job, p`│ Standard output (${stderr.length}), last 10 lines... ${pad.x(' ')} │`)
230
+ log(job, p`│ ${pad.w('...')} │`)
231
+ log(job, p`│ ${pad.w(stdout.split(/\r?\n/).slice(-10).join('\n'))} │`)
232
+ } else {
233
+ log(job, p`│ No output ${pad.x(' ')} │`)
234
+ }
235
+ }
236
+ log(job, p`└──────────${pad.x('─')}┘`)
237
+ }
238
+
239
+ function build (job) {
240
+ let wrap
241
+ if (interactive) {
242
+ wrap = method => function () {
243
+ clean(job)
244
+ try {
245
+ method.call(this, ...arguments)
246
+ } finally {
247
+ progress(job, false)
248
+ }
249
+ }
250
+ } else {
251
+ wrap = method => method
252
+ }
253
+ job[$outputStart] = Date.now()
254
+
255
+ return {
256
+ lastTick: 0,
257
+ reportIntervalId: undefined,
258
+ lines: 0,
259
+
260
+ version: () => {
261
+ const { name, version } = require(join(__dirname, '../package.json'))
262
+ log(job, p80()`${name}@${version}`)
263
+ if (job.debugDevMode) {
264
+ log(job, p80()`⚠️ Development mode ⚠️`)
265
+ }
266
+ },
267
+
268
+ serving: url => {
269
+ log(job, p80()`Server running at ${pad.lt(url)}`)
270
+ },
271
+
272
+ log: wrap((...texts) => {
273
+ log(job, ...texts)
274
+ }),
275
+
276
+ error: wrap((...texts) => {
277
+ err(job, ...texts)
278
+ }),
279
+
280
+ debug: (moduleSpecifier, ...args) => {
281
+ const [mainModule] = moduleSpecifier.split('/')
282
+ if (job.debugVerbose && (job.debugVerbose.includes(moduleSpecifier) || job.debugVerbose.includes(mainModule))) {
283
+ wrap(() => {
284
+ console.log(`🐞${moduleSpecifier}`, ...args)
285
+ output(job, `🐞${moduleSpecifier}`, ...args)
286
+ })()
287
+ }
288
+ },
289
+
290
+ redirected: wrap(({ method, url, statusCode, timeSpent }) => {
291
+ if (url.startsWith('/_/progress')) {
292
+ return // avoids pollution
293
+ }
294
+ let statusText
295
+ if (!statusCode) {
296
+ statusText = 'N/A'
297
+ } else {
298
+ statusText = statusCode
299
+ }
300
+ log(job, p80()`${method.padEnd(7, ' ')} ${pad.lt(url)} ${statusText} ${timeSpent.toString().padStart(4, ' ')}ms`)
301
+ }),
302
+
303
+ status (status) {
304
+ let method
305
+ if (interactive) {
306
+ method = output
307
+ } else {
308
+ method = log
309
+ }
310
+ const text = `${getElapsed(job)} │ ${status}`
311
+ method(job, '')
312
+ method(job, text)
313
+ method(job, '──────┴'.padEnd(text.length, '─'))
314
+ delete job[$statusProgressCount]
315
+ delete job[$statusProgressTotal]
316
+ },
317
+
318
+ watching: wrap(path => {
319
+ log(job, p80()`Watching changes on ${pad.lt(path)}`)
320
+ }),
321
+
322
+ changeDetected: wrap((eventType, filename) => {
323
+ log(job, p80()`${eventType} ${pad.lt(filename)}`)
324
+ }),
325
+
326
+ reportOnJobProgress () {
327
+ if (interactive) {
328
+ this.reportIntervalId = setInterval(progress.bind(null, job), 250)
329
+ } else if (job.outputInterval && !inJest) {
330
+ this.reportIntervalId = setInterval(progress.bind(null, job), job.outputInterval)
331
+ }
332
+ },
333
+
334
+ browserCapabilities: wrap(capabilities => {
335
+ log(job, p80()`Browser capabilities :`)
336
+ const { modules } = capabilities
337
+ if (modules.length) {
338
+ log(job, p80()` ├─ modules`)
339
+ modules.forEach((module, index) => {
340
+ let prefix
341
+ if (index === modules.length - 1) {
342
+ prefix = ' │ └─ '
343
+ } else {
344
+ prefix = ' │ ├─'
345
+ }
346
+ log(job, p80()`${prefix} ${pad.lt(module)}`)
347
+ })
348
+ }
349
+ Object.keys(capabilities)
350
+ .filter(key => key !== 'modules')
351
+ .forEach((key, index, keys) => {
352
+ let prefix
353
+ if (index === keys.length - 1) {
354
+ prefix = ' └─'
355
+ } else {
356
+ prefix = ' ├─'
357
+ }
358
+ let value = JSON.stringify(capabilities[key])
359
+ if (key === 'scripts' && !capabilities[key] && job.debugCapabilitiesNoScript) {
360
+ value += ' ⚠️ --debug-capabilities-no-script'
361
+ }
362
+ log(job, p80()`${prefix} ${key}: ${value}`)
363
+ })
364
+ }),
365
+
366
+ resolvedPackage (name, path, version) {
367
+ if (!name.match(/@\d+\.\d+\.\d+$/)) {
368
+ name += `@${version}`
369
+ }
370
+ wrap(() => log(job, p80()`${name} in ${pad.lt(path)}`))()
371
+ },
372
+
373
+ packageNotLatest (name, latestVersion) {
374
+ wrap(() => log(job, `⚠️ [PKGVRS] latest version of ${name} is ${latestVersion}`))()
375
+ },
376
+
377
+ browserStart (url) {
378
+ const text = p80()`${getElapsed(job)} >> ${pad.lt(url)} [${filename(url)}]`
379
+ if (interactive) {
380
+ output(job, text)
381
+ } else {
382
+ wrap(() => log(job, text))()
383
+ }
384
+ },
385
+
386
+ browserStopped (url) {
387
+ let duration = ''
388
+ const page = job.qunitPages && job.qunitPages[url]
389
+ if (page) {
390
+ duration = ' (' + formatTime(page.end - page.start) + ')'
391
+ }
392
+ const text = p80()`${getElapsed(job)} << ${pad.lt(url)} ${duration} [${filename(url)}]`
393
+ if (interactive) {
394
+ output(job, text)
395
+ } else {
396
+ wrap(() => log(job, text))()
397
+ }
398
+ },
399
+
400
+ browserClosed: wrap((url, code, dir) => {
401
+ browserIssue(job, { type: 'unexpected closed', url, code, dir })
402
+ }),
403
+
404
+ browserRetry (url, retry) {
405
+ if (interactive) {
406
+ output(job, '>>', url)
407
+ } else {
408
+ wrap(() => log(job, p80()`>> RETRY ${retry} ${pad.lt(url)}`))()
409
+ }
410
+ },
411
+
412
+ browserTimeout: wrap((url, dir) => {
413
+ browserIssue(job, { type: 'timeout', url, code: 0, dir })
414
+ }),
415
+
416
+ browserFailed: wrap((url, code, dir) => {
417
+ browserIssue(job, { type: 'failed', url, code, dir })
418
+ }),
419
+
420
+ startFailed: wrap((url, error) => {
421
+ const p = p80()
422
+ log(job, p`┌──────────${pad.x('─')}┐`)
423
+ log(job, p`│ UNABLE TO START THE URL ${pad.x(' ')} │`)
424
+ log(job, p`├──────┬─${pad.x('─')}──┤`)
425
+ log(job, p`│ url │ ${pad.lt(url)} │`)
426
+ log(job, p`├──────┴─${pad.x('─')}──┤`)
427
+ if (error.stack) {
428
+ log(job, p`│ ${pad.w(error.stack)} │`)
429
+ } else {
430
+ log(job, p`│ ${pad.w(error.toString())} │`)
431
+ }
432
+ log(job, p`└──────────${pad.x('─')}┘`)
433
+ }),
434
+
435
+ monitor (childProcess, live = true) {
436
+ const defaults = {
437
+ stdout: { buffer: [], method: log },
438
+ stderr: { buffer: [], method: err }
439
+ };
440
+ ['stdout', 'stderr'].forEach(channel => {
441
+ childProcess[channel].on('data', chunk => {
442
+ const { buffer, method } = defaults[channel]
443
+ const text = chunk.toString()
444
+ if (live) {
445
+ if (!text.includes('\n')) {
446
+ buffer.push(text)
447
+ return
448
+ }
449
+ const cached = buffer.join('')
450
+ const last = text.split('\n').slice(-1)
451
+ buffer.length = 0
452
+ if (last) {
453
+ buffer.push(last)
454
+ }
455
+ wrap(() => method(job, cached + text.split('\n').slice(0, -1).join('\n')))()
456
+ } else {
457
+ buffer.push(text)
458
+ }
459
+ })
460
+ })
461
+ if (live) {
462
+ childProcess.on('close', () => {
463
+ ['stdout', 'stderr'].forEach(channel => {
464
+ const { buffer, method } = defaults[channel]
465
+ if (buffer.length) {
466
+ method(job, buffer.join(''))
467
+ }
468
+ })
469
+ })
470
+ }
471
+ return {
472
+ stdout: defaults.stdout.buffer,
473
+ stderr: defaults.stderr.buffer
474
+ }
475
+ },
476
+
477
+ nyc: wrap((...args) => {
478
+ log(job, p80()`nyc ${args.map(arg => arg.toString()).join(' ')}`)
479
+ }),
480
+
481
+ instrumentationSkipped: wrap(() => {
482
+ log(job, p80()`⚠️ [SKPNYC] Skipping nyc instrumentation (--url)`)
483
+ }),
484
+
485
+ assumingOneOrigin: wrap(() => {
486
+ log(job, p80()`⚠️ [COVORG] Considering only one origin`)
487
+ }),
488
+
489
+ noInfoForAllCoverage: wrap(() => {
490
+ log(job, p80()`⚠️ [COVALL] Unable to process all coverage, report might be incomplete`)
491
+ }),
492
+
493
+ endpointError: wrap(({ api, url, data, error }) => {
494
+ const p = p80()
495
+ log(job, p`┌──────────${pad.x('─')}┐`)
496
+ log(job, p`│ UNEXPECTED ENDPOINT ERROR ${pad.x(' ')} │`)
497
+ log(job, p`├──────┬─${pad.x('─')}──┤`)
498
+ log(job, p`│ api ${pad.lt(api)} │`)
499
+ log(job, p`├──────┼─${pad.x('─')}──┤`)
500
+ log(job, p`│ from │ ${pad.lt(url)} │`)
501
+ log(job, p`├──────┴─${pad.x('─')}──┤`)
502
+ log(job, p`│ data (${JSON.stringify(data).length}) ${pad.x(' ')} │`)
503
+ log(job, p`│ ${pad.w(JSON.stringify(data, undefined, 2))} │`)
504
+ log(job, p`├────────${pad.x('─')}──┤`)
505
+ if (error.stack) {
506
+ log(job, p`│ ${pad.w(error.stack)} │`)
507
+ } else {
508
+ log(job, p`│ ${pad.w(error.toString())} │`)
509
+ }
510
+ log(job, p`└──────────${pad.x('─')}┘`)
511
+ }),
512
+
513
+ serverError: wrap(({ method, url, reason }) => {
514
+ const p = p80()
515
+ log(job, p`┌──────────${pad.x('─')}┐`)
516
+ log(job, p`│ UNEXPECTED SERVER ERROR ${pad.x(' ')} │`)
517
+ log(job, p`├──────┬─${pad.x('─')}──┤`)
518
+ log(job, p`│ verb │ ${pad.lt(method)} │`)
519
+ log(job, p`├──────┼─${pad.x('─')}──┤`)
520
+ log(job, p`│ url │ ${pad.lt(url)} │`)
521
+ log(job, p`├──────┴─${pad.x('─')}──┤`)
522
+ if (reason.stack) {
523
+ log(job, p`│ ${pad.w(reason.stack)} │`)
524
+ } else {
525
+ log(job, p`│ ${pad.w(reason.toString())} │`)
526
+ }
527
+ log(job, p`└──────────${pad.x('─')}┘`)
528
+ }),
529
+
530
+ globalTimeout: wrap(url => {
531
+ log(job, p80()`!! TIMEOUT ${pad.lt(url)}`)
532
+ }),
533
+
534
+ failFast: wrap(url => {
535
+ log(job, p80()`!! FAILFAST ${pad.lt(url)}`)
536
+ }),
537
+
538
+ noTestPageFound: wrap(() => {
539
+ err(job, p80()`No test page found (or all filtered out)`)
540
+ }),
541
+
542
+ failedToCacheUI5resource: wrap((path, statusCode) => {
543
+ err(job, p80()`Unable to cache '${pad.lt(path)}' (status ${statusCode})`)
544
+ }),
545
+
546
+ genericError: wrap((error, url) => {
547
+ const p = p80()
548
+ log(job, p`┌──────────${pad.x('─')}┐`)
549
+ log(job, p`│ UNEXPECTED ERROR ${pad.x(' ')} │`)
550
+ if (url) {
551
+ log(job, p`├──────┬─${pad.x('─')}──┤`)
552
+ log(job, p`│ url │ ${pad.lt(url)} │`)
553
+ log(job, p`├──────┴─${pad.x('─')}──┤`)
554
+ } else {
555
+ log(job, p`├────────${pad.x('─')}──┤`)
556
+ }
557
+ if (error.stack) {
558
+ log(job, p`│ ${pad.w(error.stack)} │`)
559
+ } else {
560
+ log(job, p`│ ${pad.w(error.toString())} │`)
561
+ }
562
+ log(job, p`└──────────${pad.x('─')}┘`)
563
+ }),
564
+
565
+ unhandled: wrap(() => {
566
+ warn(job, p80()`⚠️ [UNHAND] Some requests are not handled properly, check the unhandled.txt report for more info`)
567
+ }),
568
+
569
+ reportGeneratorFailed: wrap((generator, exitCode, buffers) => {
570
+ const p = p80()
571
+ log(job, p`┌──────────${pad.x('─')}┐`)
572
+ log(job, p`│ REPORT GENERATOR FAILED ${pad.x(' ')} │`)
573
+ log(job, p`├───────────┬─${pad.x('─')}──┤`)
574
+ log(job, p`│ generator │ ${pad.lt(generator)} │`)
575
+ log(job, p`├───────────┼─${pad.x('─')}──┤`)
576
+ log(job, p`│ exit code │ ${pad.lt(exitCode.toString())} │`)
577
+ log(job, p`├───────────┴─${pad.x('─')}──┤`)
578
+ log(job, p`│ ${pad.w(buffers.stderr.join(''))} │`)
579
+ log(job, p`└──────────${pad.x('─')}┘`)
580
+ }),
581
+
582
+ stop () {
583
+ if (this.reportIntervalId) {
584
+ clearInterval(this.reportIntervalId)
585
+ if (interactive) {
586
+ clean(job)
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+
593
+ module.exports = {
594
+ interactive,
595
+
596
+ getOutput (job) {
597
+ if (!job[$output]) {
598
+ job[$output] = build(job)
599
+ }
600
+ return job[$output]
601
+ },
602
+
603
+ newProgress (job, label, total, count) {
604
+ const progress = new Progress(job)
605
+ Object.assign(progress, { label, total, count })
606
+ return progress
607
+ }
608
+ }