ui5-test-runner 4.2.0 → 4.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -66,19 +66,19 @@
66
66
  "mime": "^3.0.0",
67
67
  "punybind": "^1.2.1",
68
68
  "punyexpr": "^1.0.4",
69
- "reserve": "^1.15.6"
69
+ "reserve": "^1.15.8"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@openui5/types": "^1.121.0",
73
73
  "@ui5/cli": "^3.9.1",
74
74
  "@ui5/middleware-code-coverage": "^1.1.1",
75
75
  "jest": "^29.7.0",
76
- "nock": "^13.5.3",
76
+ "nock": "^13.5.4",
77
77
  "nyc": "^15.1.0",
78
78
  "standard": "^17.1.0",
79
79
  "start-server-and-test": "^2.0.3",
80
- "typescript": "^5.3.3",
81
- "ui5-tooling-transpile": "^3.3.5"
80
+ "typescript": "^5.4.2",
81
+ "ui5-tooling-transpile": "^3.3.7"
82
82
  },
83
83
  "optionalDependencies": {
84
84
  "fsevents": "^2.3.3"
@@ -2,9 +2,11 @@
2
2
 
3
3
  const { stop } = require('./browsers')
4
4
  const { URL } = require('url')
5
+ const { getOutput } = require('./output')
5
6
 
6
7
  module.exports = {
7
8
  async addTestPages (job, url, pages) {
9
+ getOutput(job).debug('probe', `addTestPages from ${url}`, pages)
8
10
  let testPageUrls
9
11
  pages = pages.map(relativeUrl => {
10
12
  const absoluteUrl = new URL(relativeUrl, url)
package/src/browsers.js CHANGED
@@ -5,7 +5,7 @@ const { join } = require('path')
5
5
  const { writeFile, readFile, open, stat, unlink } = require('fs/promises')
6
6
  const { recreateDir, filename, allocPromise } = require('./tools')
7
7
  const { getPageTimeout, pageTimedOut } = require('./timeout')
8
- const { getOutput } = require('./output')
8
+ const { getOutput, newProgress } = require('./output')
9
9
  const { resolvePackage } = require('./npm')
10
10
  const { UTRError } = require('./error')
11
11
  const { $browsers } = require('./symbols')
@@ -119,11 +119,14 @@ async function start (job, url, scripts = []) {
119
119
  resolvedScripts.unshift(`window['ui5-test-runner/base-host'] = 'http://localhost:${job.port}'
120
120
  `)
121
121
  }
122
+ const progress = newProgress(job)
123
+ progress.label = url
122
124
  const pageBrowser = {
123
125
  url,
124
126
  reportDir,
125
127
  scripts: resolvedScripts,
126
- retry: 0
128
+ retry: 0,
129
+ progress
127
130
  }
128
131
  const { promise, resolve, reject } = allocPromise()
129
132
  pageBrowser.done = value => {
@@ -136,8 +139,12 @@ async function start (job, url, scripts = []) {
136
139
  }
137
140
  job[$browsers][url] = pageBrowser
138
141
  await run(job, pageBrowser)
139
- await promise
140
- output.browserStopped(url)
142
+ try {
143
+ await promise
144
+ } finally {
145
+ progress.done()
146
+ output.browserStopped(url)
147
+ }
141
148
  }
142
149
 
143
150
  async function run (job, pageBrowser) {
package/src/job.js CHANGED
@@ -112,6 +112,7 @@ function getCommand (cwd) {
112
112
  .option('--webapp <path>', '[💻🔗] Base folder of the web application (relative to cwd)', 'webapp')
113
113
  .option('-pf, --page-filter <regexp>', '[💻🔗] Filter out pages not matching the regexp')
114
114
  .option('-pp, --page-params <params>', '[💻🔗] Add parameters to page URL')
115
+ .option('--page-close-timeout <timeout>', '[💻🔗] Maximum waiting time for page close', timeout, 250)
115
116
  .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)
116
117
  .option('--screenshot [flag]', '[💻🔗] Take screenshots during the tests execution (if supported by the browser)', boolean, true)
117
118
  .option('--no-screenshot', '[💻🔗] Disable screenshots')
@@ -137,6 +138,7 @@ function getCommand (cwd) {
137
138
  .option('--libs <lib...>', '[💻] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
138
139
  .option('--mappings <mapping...>', '[💻] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
139
140
  .option('--cache <path>', '[💻] Cache UI5 resources locally in the given folder (empty to disable)')
141
+ .option('--preload <library...>', '[💻] Preload UI5 libraries in the cache folder (only if --cache is used)', arrayOf(string))
140
142
  .option('--testsuite <path>', '[💻] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
141
143
  .option('-w, --watch [flag]', '[💻] Monitor the webapp folder and re-execute tests on change', boolean, false)
142
144
 
@@ -224,6 +226,8 @@ function finalize (job) {
224
226
  .forEach(setting => updateToAbsolute(setting))
225
227
  if (job.cache) {
226
228
  updateToAbsolute('cache')
229
+ } else if (job.preload) {
230
+ throw new Error('--preload cannot be used without --cache')
227
231
  }
228
232
  if (job.alternateNpmPath) {
229
233
  checkAccess({ path: job.alternateNpmPath, label: 'Alternate NPM path' })
package/src/output.js CHANGED
@@ -5,9 +5,8 @@ const { join } = require('path')
5
5
  const { memoryUsage } = require('process')
6
6
  const {
7
7
  $browsers,
8
- $probeUrlsStarted,
9
- $probeUrlsCompleted,
10
- $testPagesCompleted
8
+ $statusProgressCount,
9
+ $statusProgressTotal
11
10
  } = require('./symbols')
12
11
  const { filename, noop, pad } = require('./tools')
13
12
 
@@ -15,6 +14,7 @@ const inJest = typeof jest !== 'undefined'
15
14
  const interactive = process.stdout.columns !== undefined && !inJest
16
15
  const $output = Symbol('output')
17
16
  const $outputStart = Symbol('output-start')
17
+ const $outputProgress = Symbol('output-progress')
18
18
 
19
19
  if (!interactive) {
20
20
  const UTF8_BOM_CODE = '\ufeff'
@@ -41,66 +41,90 @@ const formatTime = duration => {
41
41
 
42
42
  const getElapsed = job => formatTime(Date.now() - job[$outputStart])
43
43
 
44
- const write = (...parts) => parts.forEach(part => process.stdout.write(part))
45
-
46
- function clean (job) {
44
+ function * buildCleanSequence (job) {
47
45
  const { lines } = job[$output]
48
46
  if (!lines) {
49
47
  return
50
48
  }
51
- write('\x1b[?12l')
52
- write(`\x1b[${lines.toString()}F`)
49
+ yield '\x1b[?12l'
50
+ yield `\x1b[${lines.toString()}F`
53
51
  for (let line = 0; line < lines; ++line) {
54
52
  if (line > 1) {
55
- write('\x1b[1E')
53
+ yield '\x1b[1E'
56
54
  }
57
- write(''.padEnd(process.stdout.columns, ' '))
55
+ yield ''.padEnd(process.stdout.columns, ' ')
58
56
  }
59
57
  if (lines > 1) {
60
- write(`\x1b[${(lines - 1).toString()}F`)
58
+ yield `\x1b[${(lines - 1).toString()}F`
61
59
  } else {
62
- write('\x1b[1G')
60
+ yield '\x1b[1G'
63
61
  }
64
62
  }
65
63
 
64
+ function clean (job) {
65
+ process.stdout.write([...buildCleanSequence(job)].join(''))
66
+ }
67
+
66
68
  const BAR_WIDTH = 10
67
69
 
68
- function bar (ratio, msg) {
69
- write('[')
70
+ function * bar (ratio, msg) {
71
+ yield '['
70
72
  if (typeof ratio === 'string') {
71
73
  if (ratio.length > BAR_WIDTH) {
72
- write(ratio.substring(0, BAR_WIDTH - 3), '...')
74
+ yield ratio.substring(0, BAR_WIDTH - 3)
75
+ yield '...'
73
76
  } else {
74
77
  const padded = ratio.padStart(BAR_WIDTH - Math.floor((BAR_WIDTH - ratio.length) / 2), '-').padEnd(BAR_WIDTH, '-')
75
- write(padded)
78
+ yield padded
76
79
  }
77
- write('] ')
80
+ yield '] '
78
81
  } else {
79
82
  const filled = Math.floor(BAR_WIDTH * ratio)
80
- write(''.padEnd(filled, '\u2588'), ''.padEnd(BAR_WIDTH - filled, '\u2591'))
81
- const percent = Math.floor(100 * ratio).toString().padStart(3, ' ')
82
- write('] ', percent, '%')
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 '%'
83
88
  }
84
- write(' ')
89
+ yield ' '
85
90
  const spaceLeft = process.stdout.columns - BAR_WIDTH - 14
86
91
  if (msg.length > spaceLeft) {
87
- write('...', msg.substring(msg.length - spaceLeft - 3))
92
+ yield '...'
93
+ yield msg.substring(msg.length - spaceLeft - 3)
88
94
  } else {
89
- write(msg)
95
+ yield msg
90
96
  }
91
- write('\n')
97
+ yield '\n'
92
98
  }
93
99
 
94
100
  const TICKS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
95
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
+
96
119
  function progress (job, cleanFirst = true) {
120
+ const sequence = []
97
121
  if (interactive) {
98
122
  if (cleanFirst) {
99
- clean(job)
123
+ sequence.push(...buildCleanSequence(job))
100
124
  }
101
125
  } else {
102
126
  if (job[$browsers]) {
103
- write(`${getElapsed(job)} │ Progress\n──────┴──────────\n`)
127
+ sequence.push(`${getElapsed(job)} │ Progress\n──────┴──────────\n`)
104
128
  } else {
105
129
  return
106
130
  }
@@ -112,41 +136,28 @@ function progress (job, cleanFirst = true) {
112
136
  ++output.lines
113
137
  const { rss, heapTotal, heapUsed, external, arrayBuffers } = memoryUsage()
114
138
  const fmt = size => `${(size / (1024 * 1024)).toFixed(2)}M`
115
- write(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
116
- }
117
- if (job[$probeUrlsStarted]) {
118
- const total = job.url.length + job.testPageUrls.length
119
- if (job[$testPagesCompleted] !== total) {
120
- progressRatio = (job[$probeUrlsCompleted] + (job[$testPagesCompleted] || 0)) / total
121
- }
139
+ sequence.push(`MEM r:${fmt(rss)}, h:${fmt(heapUsed)}/${fmt(heapTotal)}, x:${fmt(external)}, a:${fmt(arrayBuffers)}\n`)
122
140
  }
123
- if (job[$browsers]) {
124
- const runningPages = Object.keys(job[$browsers])
125
- output.lines += runningPages.length
126
- runningPages.forEach(pageUrl => {
127
- let starting = true
128
- if (job.qunitPages) {
129
- const page = job.qunitPages[pageUrl]
130
- if (page) {
131
- const { count, passed, failed } = page
132
- if (count) {
133
- const progress = passed + failed
134
- bar(progress / count, pageUrl)
135
- starting = false
136
- }
137
- }
138
- }
139
- if (starting) {
140
- bar('starting', pageUrl)
141
+ if (job[$outputProgress]) {
142
+ output.lines += job[$outputProgress].length
143
+ job[$outputProgress].forEach(({ count, total, label }) => {
144
+ if (total !== undefined) {
145
+ sequence.push(...bar((count || 0) / (total || 1), label))
146
+ } else {
147
+ sequence.push(...bar('starting', label))
141
148
  }
142
149
  })
143
150
  }
151
+ if (job[$statusProgressTotal]) {
152
+ progressRatio = (job[$statusProgressCount] || 0) / job[$statusProgressTotal]
153
+ }
144
154
  const status = `${TICKS[++output.lastTick % TICKS.length]} ${job.status}`
145
155
  if (progressRatio !== undefined) {
146
- bar(progressRatio, status)
156
+ sequence.push(...bar(progressRatio, status))
147
157
  } else {
148
- write(status, '\n')
158
+ sequence.push(status, '\n')
149
159
  }
160
+ process.stdout.write(sequence.join(''))
150
161
  }
151
162
 
152
163
  function output (job, ...args) {
@@ -250,12 +261,15 @@ function build (job) {
250
261
  log(job, p80()`Server running at ${pad.lt(url)}`)
251
262
  },
252
263
 
253
- debug: wrap((module, ...args) => {
254
- if (job.debugVerbose && job.debugVerbose.includes(module)) {
255
- console.log(`🐞${module}`, ...args)
256
- output(job, `🐞${module}`, ...args)
264
+ debug: (moduleSpecifier, ...args) => {
265
+ const [mainModule] = moduleSpecifier.split('/')
266
+ if (job.debugVerbose && (job.debugVerbose.includes(moduleSpecifier) || job.debugVerbose.includes(mainModule))) {
267
+ wrap(() => {
268
+ console.log(`🐞${moduleSpecifier}`, ...args)
269
+ output(job, `🐞${moduleSpecifier}`, ...args)
270
+ })()
257
271
  }
258
- }),
272
+ },
259
273
 
260
274
  redirected: wrap(({ method, url, statusCode, timeSpent }) => {
261
275
  if (url.startsWith('/_/progress')) {
@@ -281,6 +295,8 @@ function build (job) {
281
295
  method(job, '')
282
296
  method(job, text)
283
297
  method(job, '──────┴'.padEnd(text.length, '─'))
298
+ delete job[$statusProgressCount]
299
+ delete job[$statusProgressTotal]
284
300
  },
285
301
 
286
302
  watching: wrap(path => {
@@ -552,5 +568,9 @@ module.exports = {
552
568
  job[$output] = build(job)
553
569
  }
554
570
  return job[$output]
571
+ },
572
+
573
+ newProgress (job) {
574
+ return new Progress(job)
555
575
  }
556
576
  }
@@ -5,7 +5,10 @@ const { collect } = require('./coverage')
5
5
  const { UTRError } = require('./error')
6
6
  const { getOutput } = require('./output')
7
7
  const { basename } = require('path')
8
- const { filename, stripUrlHash } = require('./tools')
8
+ const { filename, stripUrlHash, allocPromise } = require('./tools')
9
+ const { $browsers } = require('./symbols')
10
+ const $doneResolve = Symbol('doneResolve')
11
+ const $doneTimeout = Symbol('doneTimeout')
9
12
 
10
13
  function error (job, url, details = '') {
11
14
  stop(job, url)
@@ -40,11 +43,12 @@ function merge (targetModules, modules) {
40
43
  function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
41
44
  const url = stripUrlHash(urlWithHash)
42
45
  const page = job.qunitPages && job.qunitPages[url]
46
+ const progress = (job[$browsers] && job[$browsers][url] && job[$browsers][url].progress) || { total: 0, count: 0 }
43
47
  if (!page) {
44
48
  error(job, url, `No QUnit page found for ${urlWithHash}`)
45
49
  }
46
50
  if (modules && modules.length) {
47
- page.count = merge(page.modules, modules)
51
+ progress.total = page.count = merge(page.modules, modules)
48
52
  }
49
53
  if (!page.isOpa && isOpa) {
50
54
  page.isOpa = true
@@ -65,7 +69,7 @@ function get (job, urlWithHash, { testId, modules, isOpa } = {}) {
65
69
  invalidTestId(job, url, testId)
66
70
  }
67
71
  }
68
- return { url, page, testModule, test }
72
+ return { url, page, testModule, test, progress }
69
73
  }
70
74
 
71
75
  async function done (job, urlWithHash, report) {
@@ -73,27 +77,33 @@ async function done (job, urlWithHash, report) {
73
77
  if (page.count === 0) {
74
78
  return // wait
75
79
  }
76
- if (job.browserCapabilities.screenshot) {
77
- try {
78
- await screenshot(job, url, 'done')
79
- } catch (error) {
80
- getOutput(job).genericError(error, url)
80
+ const { promise, resolve } = allocPromise()
81
+ page[$doneResolve] = resolve
82
+ page[$doneTimeout] = setTimeout(async () => {
83
+ if (job.browserCapabilities.screenshot) {
84
+ try {
85
+ await screenshot(job, url, 'done')
86
+ } catch (error) {
87
+ getOutput(job).genericError(error, url)
88
+ }
81
89
  }
82
- }
83
- page.end = new Date()
84
- if (report.__coverage__) {
85
- await collect(job, url, report.__coverage__)
86
- delete report.__coverage__
87
- }
88
- page.report = report
89
- stop(job, url)
90
+ page.end = new Date()
91
+ if (report.__coverage__) {
92
+ await collect(job, url, report.__coverage__)
93
+ delete report.__coverage__
94
+ }
95
+ page.report = report
96
+ stop(job, url)
97
+ resolve()
98
+ }, job.pageCloseTimeout)
99
+ return promise
90
100
  }
91
101
 
92
102
  module.exports = {
93
103
  get,
94
104
 
95
105
  async begin (job, urlWithHash, details) {
96
- getOutput(job).debug('qunit', 'begin', urlWithHash, details)
106
+ getOutput(job).debug('qunit/begin', 'begin', urlWithHash, details)
97
107
  const { isOpa, totalTests, modules } = details
98
108
  const url = stripUrlHash(urlWithHash)
99
109
  if (!job.qunitPages) {
@@ -109,16 +119,24 @@ module.exports = {
109
119
  modules
110
120
  }
111
121
  job.qunitPages[url] = qunitPage
122
+ const { progress } = get(job, url)
123
+ progress.count = 0
124
+ progress.total = totalTests
112
125
  },
113
126
 
114
127
  async testStart (job, urlWithHash, details) {
115
- getOutput(job).debug('qunit', 'testStart', urlWithHash, details)
116
- const { test } = get(job, urlWithHash, details)
128
+ getOutput(job).debug('qunit/testStart', 'testStart', urlWithHash, details)
129
+ const { page, test } = get(job, urlWithHash, details)
130
+ const { [$doneTimeout]: doneTimeout, [$doneResolve]: doneResolve } = page
131
+ if (doneTimeout) {
132
+ clearTimeout(doneTimeout)
133
+ doneResolve()
134
+ }
117
135
  test.start = new Date()
118
136
  },
119
137
 
120
138
  async log (job, urlWithHash, details) {
121
- getOutput(job).debug('qunit', 'log', urlWithHash, details)
139
+ getOutput(job).debug('qunit/log', 'log', urlWithHash, details)
122
140
  const { url, page, test } = get(job, urlWithHash, details)
123
141
  const { isOpa, modules, module, name, testId, ...log } = details
124
142
  if (!test) {
@@ -139,13 +157,14 @@ module.exports = {
139
157
  },
140
158
 
141
159
  async testDone (job, urlWithHash, details) {
142
- getOutput(job).debug('qunit', 'testDone', urlWithHash, details)
160
+ getOutput(job).debug('qunit/testDone', 'testDone', urlWithHash, details)
143
161
  const { name, module, testId, assertions, ...report } = details
144
162
  const { failed } = report
145
- const { url, page, test } = get(job, urlWithHash, { testId })
163
+ const { url, page, test, progress } = get(job, urlWithHash, { testId })
146
164
  if (!test) {
147
165
  invalidTestId(job, url, testId)
148
166
  }
167
+ ++progress.count
149
168
  if (failed) {
150
169
  if (job.browserCapabilities.screenshot) {
151
170
  try {
@@ -180,5 +199,8 @@ module.exports = {
180
199
  }
181
200
  },
182
201
 
183
- done
202
+ async done (job, urlWithHash, report) {
203
+ getOutput(job).debug('qunit/done', 'done', urlWithHash, report)
204
+ await done(job, urlWithHash, report)
205
+ }
184
206
  }
package/src/reserve.js CHANGED
@@ -2,7 +2,7 @@ const { join } = require('path')
2
2
  const cors = require('./cors')
3
3
  const endpoints = require('./endpoints')
4
4
  const { mappings: coverage } = require('./coverage')
5
- const ui5 = require('./ui5')
5
+ const { mappings: ui5 } = require('./ui5')
6
6
  const { check } = require('reserve')
7
7
  const unhandled = require('./unhandled')
8
8
 
package/src/symbols.js CHANGED
@@ -6,5 +6,7 @@ module.exports = {
6
6
  $testPagesCompleted: Symbol('testPagesCompleted'),
7
7
  $valueSources: Symbol('valueSources'),
8
8
  $remoteOnLegacy: Symbol('remoteOnLegacy'),
9
- $proxifiedUrls: Symbol('proxifiedUrls')
9
+ $proxifiedUrls: Symbol('proxifiedUrls'),
10
+ $statusProgressCount: Symbol('statusProgressCount'),
11
+ $statusProgressTotal: Symbol('statusProgressTotal')
10
12
  }
package/src/tests.js CHANGED
@@ -10,10 +10,13 @@ const {
10
10
  $probeUrlsStarted,
11
11
  $probeUrlsCompleted,
12
12
  $testPagesStarted,
13
- $testPagesCompleted
13
+ $testPagesCompleted,
14
+ $statusProgressTotal,
15
+ $statusProgressCount
14
16
  } = require('./symbols')
15
17
  const { UTRError } = require('./error')
16
18
  const { $proxifiedUrls } = require('./symbols')
19
+ const { preload } = require('./ui5')
17
20
 
18
21
  async function run (task, job) {
19
22
  const {
@@ -25,6 +28,10 @@ async function run (task, job) {
25
28
  const output = getOutput(job)
26
29
  const urls = job[urlsMember]
27
30
  const { length } = urls
31
+ if (job[$statusProgressTotal] === undefined) {
32
+ job[$statusProgressTotal] = length
33
+ job[$statusProgressCount] = 0
34
+ }
28
35
  if (job[completedMember] === length) {
29
36
  return
30
37
  }
@@ -47,6 +54,7 @@ async function run (task, job) {
47
54
  }
48
55
  }
49
56
  ++job[completedMember]
57
+ ++job[$statusProgressCount]
50
58
  return run(task, job)
51
59
  }
52
60
 
@@ -158,6 +166,9 @@ module.exports = {
158
166
  async execute (job) {
159
167
  await recreateDir(job.reportDir)
160
168
  getOutput(job).version()
169
+ if (job.preload) {
170
+ await preload(job)
171
+ }
161
172
  await probe(job)
162
173
  if (job.mode !== 'url') {
163
174
  job.url = [`http://localhost:${job.port}/${job.testsuite}`]
package/src/tools.js CHANGED
@@ -127,6 +127,7 @@ function allocPromise () {
127
127
 
128
128
  async function download (url, filename) {
129
129
  const { hostname, port, origin } = new URL(url)
130
+ const error = reason => new Error(`Error downloading ${url} to ${filename}, ${reason}`)
130
131
  const options = {
131
132
  hostname,
132
133
  port,
@@ -139,16 +140,27 @@ async function download (url, filename) {
139
140
  const { promise, resolve, reject } = allocPromise()
140
141
  const request = protocol.request(options, async response => {
141
142
  if (response.statusCode !== 200) {
142
- reject(response.statusCode)
143
+ reject(error(`server responded with ${response.statusCode}`))
143
144
  output.end()
144
- await unlink(filename)
145
+ try {
146
+ await unlink(filename)
147
+ } catch (e) {
148
+ // ignore
149
+ }
145
150
  return
146
151
  }
147
- response.on('error', reject)
148
- response.on('end', resolve)
149
- response.pipe(output)
152
+ response.on('error', reason => {
153
+ reject(error(`response failed : ${reason.toString()}`))
154
+ })
155
+ response
156
+ .pipe(output)
157
+ .on('finish', () => {
158
+ resolve(filename)
159
+ })
160
+ })
161
+ request.on('error', reason => {
162
+ reject(error(`request failed : ${reason.toString()}`))
150
163
  })
151
- request.on('error', reject)
152
164
  request.end()
153
165
  return promise
154
166
  }
package/src/ui5.js CHANGED
@@ -2,90 +2,158 @@
2
2
 
3
3
  const { dirname, join } = require('path')
4
4
  const { createWriteStream } = require('fs')
5
- const { mkdir, unlink } = require('fs').promises
5
+ const { mkdir, unlink, stat } = require('fs').promises
6
6
  const { capture } = require('reserve')
7
- const { getOutput } = require('./output')
8
-
9
- module.exports = job => {
10
- if (job.disableUi5) {
11
- return []
12
- }
7
+ const { getOutput, newProgress } = require('./output')
8
+ const { download, allocPromise } = require('./tools')
9
+ const { $statusProgressCount, $statusProgressTotal } = require('./symbols')
13
10
 
11
+ const buildCacheBase = job => {
14
12
  const [, hostName] = /https?:\/\/([^/]*)/.exec(job.ui5)
15
13
  const [, version] = /(\d+\.\d+\.\d+)?$/.exec(job.ui5)
16
- const cacheBase = join(job.cache || '', hostName.replace(':', '_'), version || '')
17
- const match = /\/((?:test-)?resources\/.*)/
18
- const ifCacheEnabled = (request, url, match) => job.cache ? match : false
19
- const uncachable = {}
20
- const cachingInProgress = {}
14
+ return join(job.cache || '', hostName.replace(':', '_'), version || '')
15
+ }
21
16
 
22
- const mappings = [{
23
- /* Prevent caching issues :
24
- * - Caching was not possible (99% URL does not exist)
25
- * - Caching is in progress (must wait for the end of the writing stream)
26
- */
27
- match,
28
- 'if-match': ifCacheEnabled,
29
- custom: async (request, response, path) => {
30
- if (uncachable[path]) {
31
- response.writeHead(404)
32
- response.end()
33
- return
34
- }
35
- const cachingPromise = cachingInProgress[path]
36
- /* istanbul ignore next */ // Hard to reproduce
37
- if (cachingPromise) {
38
- await cachingPromise
17
+ module.exports = {
18
+ preload: async job => {
19
+ const cacheBase = buildCacheBase(job)
20
+
21
+ const get = async (path, expectedSize) => {
22
+ const filePath = join(cacheBase, 'resources/' + path)
23
+ try {
24
+ const info = await stat(filePath)
25
+ if (expectedSize !== undefined && info.isFile() && info.size === expectedSize) {
26
+ return filePath
27
+ }
28
+ } catch (e) {
29
+ // ignore
39
30
  }
31
+ return download((new URL('resources/' + path, job.ui5)).toString(), filePath)
40
32
  }
41
- }, {
42
- // UI5 from cache
43
- match,
44
- 'if-match': ifCacheEnabled,
45
- file: join(cacheBase, '$1'),
46
- 'ignore-if-not-found': true
47
- }, {
48
- // UI5 caching
49
- method: 'GET',
50
- match,
51
- 'if-match': ifCacheEnabled,
52
- custom: async (request, response, path) => {
53
- const filePath = /([^?#]+)/.exec(unescape(path))[1] // filter URL parameters & hash (assuming resources are static)
54
- const cachePath = join(cacheBase, filePath)
55
- const cacheFolder = dirname(cachePath)
56
- await mkdir(cacheFolder, { recursive: true })
57
- if (cachingInProgress[path]) {
58
- return request.url // loop back to use cached result
33
+
34
+ const lib = async name => {
35
+ progress.label = name
36
+ progress.count = 0
37
+ const { promise, resolve/*, reject */ } = allocPromise()
38
+ const libPath = name.replace(/\./g, '/') + '/'
39
+ const { resources } = require(await get(libPath + 'resources.json'))
40
+ progress.total = resources.length
41
+ let index = 0
42
+ let active = 0
43
+
44
+ const task = async () => {
45
+ ++active
46
+ const { name, size } = resources[index++]
47
+ await get(libPath + name, size)
48
+ ++progress.count
49
+ if (index < resources.length) {
50
+ task()
51
+ }
52
+ if (--active === 0) {
53
+ resolve()
54
+ }
59
55
  }
60
- const file = createWriteStream(cachePath)
61
- cachingInProgress[path] = capture(response, file)
62
- .catch(reason => {
63
- file.end()
64
- uncachable[path] = true
65
- if (response.statusCode !== 404) {
66
- getOutput(job).failedToCacheUI5resource(path, response.statusCode)
67
- }
68
- return unlink(cachePath)
69
- })
70
- .then(() => {
71
- delete cachingInProgress[path]
72
- })
56
+
57
+ for (let parallel = 0; parallel < 8; ++parallel) {
58
+ task()
59
+ }
60
+
61
+ return promise
73
62
  }
74
- }, {
75
- // UI5 from url
76
- method: ['GET', 'HEAD'],
77
- match,
78
- url: `${job.ui5}/$1`,
79
- 'ignore-unverifiable-certificate': true
80
- }]
81
63
 
82
- job.libs.forEach(({ relative, source }) => {
83
- mappings.unshift({
84
- match: new RegExp(`\\/resources\\/${relative.replace(/\//g, '\\/')}(.*)`),
85
- file: join(source, '$1'),
64
+ job.status = 'Preloading UI5'
65
+ job[$statusProgressCount] = 0
66
+ job[$statusProgressTotal] = job.preload.length + 1
67
+ await get('sap-ui-version.json')
68
+ await get('sap-ui-core.js')
69
+ const progress = newProgress(job)
70
+ await lib('sap.ui.core')
71
+ for (const name of job.preload) {
72
+ ++job[$statusProgressCount]
73
+ await lib(name)
74
+ }
75
+ progress.done()
76
+ },
77
+
78
+ mappings: job => {
79
+ if (job.disableUi5) {
80
+ return []
81
+ }
82
+
83
+ const cacheBase = buildCacheBase(job)
84
+ const match = /\/((?:test-)?resources\/.*)/
85
+ const ifCacheEnabled = (request, url, match) => job.cache ? match : false
86
+ const uncachable = {}
87
+ const cachingInProgress = {}
88
+
89
+ const mappings = [{
90
+ /* Prevent caching issues :
91
+ * - Caching was not possible (99% URL does not exist)
92
+ * - Caching is in progress (must wait for the end of the writing stream)
93
+ */
94
+ match,
95
+ 'if-match': ifCacheEnabled,
96
+ custom: async (request, response, path) => {
97
+ if (uncachable[path]) {
98
+ response.writeHead(404)
99
+ response.end()
100
+ return
101
+ }
102
+ const cachingPromise = cachingInProgress[path]
103
+ /* istanbul ignore next */ // Hard to reproduce
104
+ if (cachingPromise) {
105
+ await cachingPromise
106
+ }
107
+ }
108
+ }, {
109
+ // UI5 from cache
110
+ match,
111
+ 'if-match': ifCacheEnabled,
112
+ file: join(cacheBase, '$1'),
86
113
  'ignore-if-not-found': true
114
+ }, {
115
+ // UI5 caching
116
+ method: 'GET',
117
+ match,
118
+ 'if-match': ifCacheEnabled,
119
+ custom: async (request, response, path) => {
120
+ const filePath = /([^?#]+)/.exec(unescape(path))[1] // filter URL parameters & hash (assuming resources are static)
121
+ const cachePath = join(cacheBase, filePath)
122
+ const cacheFolder = dirname(cachePath)
123
+ await mkdir(cacheFolder, { recursive: true })
124
+ if (cachingInProgress[path]) {
125
+ return request.url // loop back to use cached result
126
+ }
127
+ const file = createWriteStream(cachePath)
128
+ cachingInProgress[path] = capture(response, file)
129
+ .catch(reason => {
130
+ file.end()
131
+ uncachable[path] = true
132
+ if (response.statusCode !== 404) {
133
+ getOutput(job).failedToCacheUI5resource(path, response.statusCode)
134
+ }
135
+ return unlink(cachePath)
136
+ })
137
+ .then(() => {
138
+ delete cachingInProgress[path]
139
+ })
140
+ }
141
+ }, {
142
+ // UI5 from url
143
+ method: ['GET', 'HEAD'],
144
+ match,
145
+ url: `${job.ui5}/$1`,
146
+ 'ignore-unverifiable-certificate': true
147
+ }]
148
+
149
+ job.libs.forEach(({ relative, source }) => {
150
+ mappings.unshift({
151
+ match: new RegExp(`\\/resources\\/${relative.replace(/\//g, '\\/')}(.*)`),
152
+ file: join(source, '$1'),
153
+ 'ignore-if-not-found': true
154
+ })
87
155
  })
88
- })
89
156
 
90
- return mappings
157
+ return mappings
158
+ }
91
159
  }