ui5-test-runner 5.9.0 → 5.10.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/src/start.js CHANGED
@@ -2,9 +2,7 @@ const { exec } = require('child_process')
2
2
  const { stat, readFile } = require('fs/promises')
3
3
  const { join } = require('path')
4
4
  const { getOutput } = require('./output')
5
- const psTreeNodeCb = require('ps-tree')
6
- const { promisify } = require('util')
7
- const psTree = promisify(psTreeNodeCb)
5
+ const pidtree = require('pidtree')
8
6
 
9
7
  async function start (job) {
10
8
  const { startWaitUrl: url, startWaitMethod: method } = job
@@ -40,23 +38,23 @@ async function start (job) {
40
38
  }
41
39
  }
42
40
 
43
- let childProcessExited = false
41
+ let startProcessExited = false
44
42
  output.debug('start', 'Starting command :', start)
45
- const childProcess = exec(start, {
43
+ const startProcess = exec(start, {
46
44
  cwd: job.cwd,
47
45
  windowsHide: true
48
46
  })
49
- childProcess.on('close', () => {
50
- output.debug('start', 'start command process exited')
51
- childProcessExited = true
47
+ startProcess.on('close', () => {
48
+ output.debug('start', 'Start command process exited')
49
+ startProcessExited = true
52
50
  })
53
- output.monitor(childProcess)
51
+ output.monitor(startProcess)
54
52
 
55
53
  job.status = 'Waiting for URL to be reachable'
56
54
 
57
55
  const begin = Date.now()
58
56
  // eslint-disable-next-line no-unmodified-loop-condition
59
- while (!childProcessExited && Date.now() - begin <= job.startTimeout) {
57
+ while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
60
58
  try {
61
59
  const response = await fetch(url, { method })
62
60
  output.debug('start', url, response.status)
@@ -69,20 +67,60 @@ async function start (job) {
69
67
  }
70
68
  }
71
69
 
72
- if (childProcessExited) {
73
- throw new Error(`Start command failed with exit code ${childProcess.exitCode}`)
70
+ if (startProcessExited) {
71
+ throw new Error(`Start command failed with exit code ${startProcess.exitCode}`)
74
72
  }
75
73
 
76
74
  const stop = async () => {
77
- output.debug('start', 'Getting child processes...')
78
- const childProcesses = await psTree(childProcess.pid)
79
- for (const child of childProcesses) {
80
- output.debug('start', 'Terminating process', child.PID)
81
- try {
82
- process.kill(child.PID, 'SIGKILL')
83
- } catch (e) {
84
- output.debug('start', 'Failed to terminate process', child.PID, ':', e)
75
+ job.status = 'Terminating start command'
76
+ const begin = new Date()
77
+ // eslint-disable-next-line no-unmodified-loop-condition
78
+ while (!startProcessExited && Date.now() - begin <= job.startTimeout) {
79
+ output.debug('start', `Getting start command ${startProcess.pid} child processes...`)
80
+ const childProcesses = await pidtree(startProcess.pid, { advanced: true })
81
+ output.debug('start', 'Child processes', JSON.stringify(childProcesses))
82
+ if (childProcesses.length === 0) {
83
+ try {
84
+ output.debug('start', 'Terminating start command')
85
+ process.kill(startProcess.pid, 'SIGKILL')
86
+ } catch (e) {
87
+ output.debug('start', 'Failed to terminate start command', startProcess.pid, ':', e)
88
+ }
89
+ } else {
90
+ const depth = {}
91
+ let deepest = 1
92
+ let deepless = childProcesses.length
93
+ while (deepless > 0) {
94
+ for (const { ppid, pid } of childProcesses) {
95
+ if (ppid === startProcess.pid) {
96
+ depth[pid] = 1
97
+ --deepless
98
+ } else {
99
+ const parentDepth = depth[ppid]
100
+ if (parentDepth !== undefined) {
101
+ depth[pid] = parentDepth + 1
102
+ deepest = Math.max(deepest, parentDepth + 1)
103
+ --deepless
104
+ }
105
+ }
106
+ }
107
+ }
108
+ output.debug('start', 'Child processes', JSON.stringify(depth), 'terminating', deepest)
109
+ for (const { pid } of childProcesses) {
110
+ if (depth[pid] === deepest) {
111
+ output.debug('start', 'Terminating start child process', pid)
112
+ try {
113
+ process.kill(pid, 'SIGKILL')
114
+ } catch (e) {
115
+ output.debug('start', 'Failed to terminate start child process', pid, ':', e)
116
+ }
117
+ }
118
+ }
85
119
  }
120
+ await new Promise(resolve => setTimeout(resolve, 250))
121
+ }
122
+ if (!startProcessExited) {
123
+ output.failedToTerminateStartCommand()
86
124
  }
87
125
  }
88
126
 
package/src/tests.js CHANGED
@@ -48,9 +48,13 @@ async function probeUrl (job, url) {
48
48
  let scripts
49
49
  if (job.browserCapabilities.scripts) {
50
50
  scripts = [
51
+ '(function () { window[\'ui5-test-runner/probe\'] = true }())',
51
52
  'post.js',
52
53
  'qunit-redirect.js'
53
54
  ]
55
+ if (job.jest) {
56
+ scripts.push('jest2qunit.js')
57
+ }
54
58
  }
55
59
  await start(job, url, scripts)
56
60
  } catch (error) {
@@ -64,10 +68,19 @@ async function runTestPage (job, url) {
64
68
  try {
65
69
  let scripts
66
70
  if (job.browserCapabilities.scripts) {
67
- scripts = [
71
+ scripts = []
72
+ if (job.qunitBatchSize) {
73
+ scripts.push(
74
+ `(function () { window['ui5-test-runner/batch'] = ${job.qunitBatchSize} }())`
75
+ )
76
+ }
77
+ scripts.push(
68
78
  'post.js',
69
79
  'qunit-hooks.js'
70
- ]
80
+ )
81
+ if (job.jest) {
82
+ scripts.push('jest2qunit.js')
83
+ }
71
84
  if (job.coverage && !job.coverageProxy) {
72
85
  scripts.push(
73
86
  'opa-iframe-coverage.js',
@@ -102,13 +115,42 @@ async function process (job) {
102
115
  await save(job)
103
116
  job.testPageUrls = []
104
117
 
118
+ let probingRound = 0
119
+ const parallel = job.probeParallel || job.parallel
120
+ const confirmedTestPageUrls = []
105
121
  job.status = 'Probing urls'
106
- try {
107
- await parallelize(task(job, probeUrl), job.url, job.parallel)
108
- } catch (e) {
109
- output.genericError(e)
110
- job.failed = true
111
- }
122
+ do {
123
+ ++probingRound
124
+ if (probingRound >= 2) {
125
+ if (job.testPageUrls.length === 0) {
126
+ break
127
+ }
128
+ job.status = `Probing urls (${probingRound})`
129
+ job.url = job.testPageUrls.filter(url => !confirmedTestPageUrls.includes(url))
130
+ if (job.url.length) {
131
+ job.testPageUrls = []
132
+ } else {
133
+ job.testPageUrls = confirmedTestPageUrls
134
+ break
135
+ }
136
+ }
137
+ try {
138
+ await parallelize(task(job, probeUrl), job.url, parallel)
139
+ } catch (e) {
140
+ output.genericError(e)
141
+ job.failed = true
142
+ break
143
+ }
144
+ job.testPageUrls.forEach(url => {
145
+ if ((job.url.includes(url) && !confirmedTestPageUrls.includes(url)) ||
146
+ (url.includes('/resources/sap/ui/test/starter/Test.qunit.html?testsuite=') && url.includes('&test='))
147
+ ) {
148
+ confirmedTestPageUrls.push(url)
149
+ getOutput(job).debug('probe', 'confirmed:', url)
150
+ }
151
+ })
152
+ getOutput(job).debug('probe', 'from', job.url.length, 'to', job.testPageUrls.length, 'confirmed', confirmedTestPageUrls.length)
153
+ } while (job.deepProbe)
112
154
 
113
155
  /* istanbul ignore else */
114
156
  if (!job.debugProbeOnly && !job.failed) {
package/src/ui5.js CHANGED
@@ -16,6 +16,109 @@ const buildCacheBase = job => {
16
16
  return join(job.cache || '', hostName.replace(':', '_'), version || '')
17
17
  }
18
18
 
19
+ const ui5mappings = job => {
20
+ const cacheBase = buildCacheBase(job)
21
+ const match = /\/((?:test-)?resources\/.*)/
22
+ const ifCacheEnabled = (request, url, match) => job.cache
23
+ const uncachable = {}
24
+ const cachingInProgress = {}
25
+
26
+ const mappings = [{
27
+ /* Prevent caching issues :
28
+ * - Caching was not possible (99% URL does not exist)
29
+ * - Caching is in progress (must wait for the end of the writing stream)
30
+ */
31
+ match,
32
+ 'if-match': ifCacheEnabled,
33
+ custom: async (request, response, path) => {
34
+ if (uncachable[path]) {
35
+ response.writeHead(404)
36
+ response.end()
37
+ return
38
+ }
39
+ const cachingPromise = cachingInProgress[path]
40
+ /* istanbul ignore next */ // Hard to reproduce
41
+ if (cachingPromise) {
42
+ await cachingPromise
43
+ }
44
+ }
45
+ }, {
46
+ // UI5 from cache
47
+ match,
48
+ 'if-match': ifCacheEnabled,
49
+ cwd: cacheBase,
50
+ file: '$1',
51
+ static: !job.debugDevMode
52
+ }, {
53
+ // UI5 caching
54
+ method: 'GET',
55
+ match,
56
+ 'if-match': ifCacheEnabled,
57
+ custom: async (request, response, path) => {
58
+ const filePath = /([^?#]+)/.exec(unescape(path))[1] // filter URL parameters & hash (assuming resources are static)
59
+ const cachePath = join(cacheBase, filePath)
60
+ const cacheFolder = dirname(cachePath)
61
+ await mkdir(cacheFolder, { recursive: true })
62
+ if (cachingInProgress[path]) {
63
+ return request.url // loop back to use cached result
64
+ }
65
+ const file = createWriteStream(cachePath)
66
+ cachingInProgress[path] = capture(response, file)
67
+ .catch(reason => {
68
+ file.end()
69
+ uncachable[path] = true
70
+ if (response.statusCode !== 404) {
71
+ getOutput(job).failedToCacheUI5resource(path, response.statusCode)
72
+ }
73
+ return unlink(cachePath)
74
+ })
75
+ .then(() => {
76
+ delete cachingInProgress[path]
77
+ })
78
+ }
79
+ }, {
80
+ // UI5 from url
81
+ method: ['GET', 'HEAD'],
82
+ match,
83
+ url: `${job.ui5}/$1`,
84
+ 'ignore-unverifiable-certificate': true
85
+ }]
86
+
87
+ for (let { relative, source } of job.libs) {
88
+ if (source.endsWith('/') || source.endsWith('\\')) {
89
+ source = source.substring(0, source.length - 1)
90
+ }
91
+ const relativeUrl = relative.replace(/\//g, '\\/')
92
+ if (source.startsWith(job.webapp)) {
93
+ const relativeAbsoluteUrl = '/' + relativePath(job.webapp, source).replace(/\\/g, '/')
94
+ getOutput(job).debug('libs', `${relative} maps to webapp sub directory, use internal redirection to ${relativeAbsoluteUrl}`)
95
+ mappings.unshift({
96
+ match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
97
+ custom: (request, response, $1) => `${relativeAbsoluteUrl}${$1}`
98
+ })
99
+ } else {
100
+ mappings.unshift({
101
+ match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
102
+ cwd: source,
103
+ file: '$1',
104
+ static: !job.watch && !job.debugDevMode
105
+ }, {
106
+ match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
107
+ custom: (request, response, $1) => {
108
+ if ($1 === undefined) {
109
+ getOutput(job).debug('libs', `Unable to map ${relative} : $1 is undefined`)
110
+ } else {
111
+ getOutput(job).debug('libs', `Unable to map ${relative}/${$1} to ${join(source, $1)}`)
112
+ }
113
+ return 404
114
+ }
115
+ })
116
+ }
117
+ }
118
+
119
+ return mappings
120
+ }
121
+
19
122
  module.exports = {
20
123
  preload: async job => {
21
124
  const cacheBase = buildCacheBase(job)
@@ -57,110 +160,10 @@ module.exports = {
57
160
  progress.done()
58
161
  },
59
162
 
60
- mappings: job => {
163
+ mappings: async job => {
61
164
  if (job.disableUi5) {
62
165
  return []
63
166
  }
64
-
65
- const cacheBase = buildCacheBase(job)
66
- const match = /\/((?:test-)?resources\/.*)/
67
- const ifCacheEnabled = (request, url, match) => job.cache
68
- const uncachable = {}
69
- const cachingInProgress = {}
70
-
71
- const mappings = [{
72
- /* Prevent caching issues :
73
- * - Caching was not possible (99% URL does not exist)
74
- * - Caching is in progress (must wait for the end of the writing stream)
75
- */
76
- match,
77
- 'if-match': ifCacheEnabled,
78
- custom: async (request, response, path) => {
79
- if (uncachable[path]) {
80
- response.writeHead(404)
81
- response.end()
82
- return
83
- }
84
- const cachingPromise = cachingInProgress[path]
85
- /* istanbul ignore next */ // Hard to reproduce
86
- if (cachingPromise) {
87
- await cachingPromise
88
- }
89
- }
90
- }, {
91
- // UI5 from cache
92
- match,
93
- 'if-match': ifCacheEnabled,
94
- cwd: cacheBase,
95
- file: '$1',
96
- static: !job.debugDevMode
97
- }, {
98
- // UI5 caching
99
- method: 'GET',
100
- match,
101
- 'if-match': ifCacheEnabled,
102
- custom: async (request, response, path) => {
103
- const filePath = /([^?#]+)/.exec(unescape(path))[1] // filter URL parameters & hash (assuming resources are static)
104
- const cachePath = join(cacheBase, filePath)
105
- const cacheFolder = dirname(cachePath)
106
- await mkdir(cacheFolder, { recursive: true })
107
- if (cachingInProgress[path]) {
108
- return request.url // loop back to use cached result
109
- }
110
- const file = createWriteStream(cachePath)
111
- cachingInProgress[path] = capture(response, file)
112
- .catch(reason => {
113
- file.end()
114
- uncachable[path] = true
115
- if (response.statusCode !== 404) {
116
- getOutput(job).failedToCacheUI5resource(path, response.statusCode)
117
- }
118
- return unlink(cachePath)
119
- })
120
- .then(() => {
121
- delete cachingInProgress[path]
122
- })
123
- }
124
- }, {
125
- // UI5 from url
126
- method: ['GET', 'HEAD'],
127
- match,
128
- url: `${job.ui5}/$1`,
129
- 'ignore-unverifiable-certificate': true
130
- }]
131
-
132
- for (let { relative, source } of job.libs) {
133
- if (source.endsWith('/') || source.endsWith('\\')) {
134
- source = source.substring(0, source.length - 1)
135
- }
136
- const relativeUrl = relative.replace(/\//g, '\\/')
137
- if (source.startsWith(job.webapp)) {
138
- const relativeAbsoluteUrl = '/' + relativePath(job.webapp, source).replace(/\\/g, '/')
139
- getOutput(job).debug('libs', `${relative} maps to webapp sub directory, use internal redirection to ${relativeAbsoluteUrl}`)
140
- mappings.unshift({
141
- match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
142
- custom: (request, response, $1) => `${relativeAbsoluteUrl}${$1}`
143
- })
144
- } else {
145
- mappings.unshift({
146
- match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
147
- cwd: source,
148
- file: '$1',
149
- static: !job.watch && !job.debugDevMode
150
- }, {
151
- match: new RegExp(`\\/resources\\/${relativeUrl}(.*)`),
152
- custom: (request, response, $1) => {
153
- if ($1 === undefined) {
154
- getOutput(job).debug('libs', `Unable to map ${relative} : $1 is undefined`)
155
- } else {
156
- getOutput(job).debug('libs', `Unable to map ${relative}/${$1} to ${join(source, $1)}`)
157
- }
158
- return 404
159
- }
160
- })
161
- }
162
- }
163
-
164
- return mappings
167
+ return ui5mappings(job)
165
168
  }
166
169
  }