ui5-test-runner 6.0.0-beta.5 → 6.0.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.
Files changed (130) hide show
  1. package/README.md +20 -29
  2. package/dist/Command.js +52 -0
  3. package/dist/Npm.js +154 -74
  4. package/dist/browsers/RecoverableBrowserError.js +6 -0
  5. package/dist/browsers/chromeArguments.js +5 -0
  6. package/dist/browsers/chromeOptions.js +29 -0
  7. package/dist/browsers/consoleMessage.js +23 -0
  8. package/dist/browsers/factory.js +168 -3
  9. package/dist/browsers/networkResponse.js +15 -0
  10. package/dist/browsers/playwright.js +104 -0
  11. package/dist/browsers/puppeteer.js +54 -88
  12. package/dist/browsers/seleniumWebdriver.js +124 -0
  13. package/dist/browsers/webdriverio.js +85 -0
  14. package/dist/cli.js +18 -12
  15. package/dist/configuration/CommandLine.js +46 -26
  16. package/dist/configuration/ConfigurationValidator.js +132 -18
  17. package/dist/configuration/OptionValidationError.js +50 -2
  18. package/dist/configuration/derivations.js +8 -0
  19. package/dist/configuration/options.js +380 -23
  20. package/dist/configuration/validations.js +135 -0
  21. package/dist/configuration/validators/boolean.js +1 -1
  22. package/dist/configuration/validators/enumeration.js +7 -0
  23. package/dist/configuration/validators/fsEntry.js +12 -12
  24. package/dist/configuration/validators/index.js +8 -2
  25. package/dist/configuration/validators/integer.js +7 -1
  26. package/dist/configuration/validators/json.js +23 -0
  27. package/dist/configuration/validators/libraryMapping.js +28 -0
  28. package/dist/configuration/validators/percent.js +2 -2
  29. package/dist/configuration/validators/regexp.js +2 -2
  30. package/dist/configuration/validators/secret.js +8 -0
  31. package/dist/configuration/validators/string.js +1 -1
  32. package/dist/configuration/validators/timeout.js +2 -2
  33. package/dist/configuration/validators/url.js +3 -3
  34. package/dist/end.js +58 -0
  35. package/dist/if.js +22 -0
  36. package/dist/modes/Modes.js +2 -1
  37. package/dist/modes/batch/BatchItem.js +1 -0
  38. package/dist/modes/batch/batchTask.js +159 -0
  39. package/dist/modes/batch/index.js +98 -0
  40. package/dist/modes/batch/output.js +22 -0
  41. package/dist/modes/batch/report.js +45 -0
  42. package/dist/modes/batch/resolve.js +105 -0
  43. package/dist/modes/dumpConfig.js +6 -0
  44. package/dist/modes/execute.js +6 -5
  45. package/dist/modes/help.js +86 -2
  46. package/dist/modes/log/LogReader.js +3 -2
  47. package/dist/modes/log/LogStorage.js +31 -27
  48. package/dist/modes/log/index.js +38 -6
  49. package/dist/modes/log/{REserve.js → reserve.js} +25 -2
  50. package/dist/modes/mcp/IDocument.js +2 -0
  51. package/dist/modes/mcp/frontmatter.js +142 -0
  52. package/dist/modes/mcp/index.js +49 -0
  53. package/dist/modes/mcp/knowledgeBase.js +104 -0
  54. package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
  55. package/dist/modes/mcp/reserve.js +175 -0
  56. package/dist/modes/mcp/tools/getTopic.js +46 -0
  57. package/dist/modes/mcp/tools/index.js +6 -0
  58. package/dist/modes/mcp/tools/listTopics.js +14 -0
  59. package/dist/modes/test/PageContext.js +1 -0
  60. package/dist/modes/test/Server.js +99 -0
  61. package/dist/modes/test/browser.js +31 -17
  62. package/dist/modes/test/browserConfig.js +17 -0
  63. package/dist/modes/test/coverage/collect.js +38 -0
  64. package/dist/modes/test/coverage/index.js +3 -0
  65. package/dist/modes/test/coverage/instrument.js +70 -0
  66. package/dist/modes/test/coverage/nyc.js +15 -0
  67. package/dist/modes/test/coverage/report.js +71 -0
  68. package/dist/modes/test/coverage/settings.js +53 -0
  69. package/dist/modes/test/index.js +94 -34
  70. package/dist/modes/test/pageTask.js +293 -98
  71. package/dist/modes/test/report.js +20 -3
  72. package/dist/modes/test/reserve.js +76 -0
  73. package/dist/modes/test/screenshot.js +49 -0
  74. package/dist/platform/Crypto.js +9 -0
  75. package/dist/platform/Exit.js +33 -24
  76. package/dist/platform/FileSystem.js +6 -2
  77. package/dist/platform/Host.js +9 -1
  78. package/dist/platform/Http.js +54 -29
  79. package/dist/platform/Module.js +2 -1
  80. package/dist/platform/Path.js +5 -1
  81. package/dist/platform/Process.js +67 -13
  82. package/dist/platform/Terminal.js +12 -8
  83. package/dist/platform/Thread.js +1 -1
  84. package/dist/platform/assert.js +13 -12
  85. package/dist/platform/environment.js +9 -18
  86. package/dist/platform/index.js +1 -0
  87. package/dist/platform/logger/CompressionContext.js +117 -0
  88. package/dist/platform/logger/allCompressed.js +8 -7
  89. package/dist/platform/logger/compress.js +29 -129
  90. package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
  91. package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
  92. package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
  93. package/dist/platform/logger/output/factory.js +5 -1
  94. package/dist/platform/logger/output.js +24 -6
  95. package/dist/platform/logger/toInternalLogAttributes.js +10 -11
  96. package/dist/platform/logger/traceFileName.js +1 -0
  97. package/dist/platform/logger.js +52 -21
  98. package/dist/platform/mock.js +110 -19
  99. package/dist/platform/version.js +1 -2
  100. package/dist/platform/workerBootstrap.js +1 -1
  101. package/dist/reports/initReportBuilder.js +18 -0
  102. package/dist/reports/junit.js +64 -0
  103. package/dist/reports/saveReport.js +10 -0
  104. package/dist/reports/textReport.js +92 -0
  105. package/dist/reserveLogger.js +22 -0
  106. package/dist/sendToParentProcess.js +6 -0
  107. package/dist/start.js +79 -0
  108. package/dist/types/ISecretValue.js +1 -0
  109. package/dist/types/SecretValue.js +15 -0
  110. package/dist/ui/agent.js +4 -2
  111. package/dist/ui/html-report.js +2 -2
  112. package/dist/ui/lib.js +1 -1
  113. package/dist/ui/log-viewer.js +1 -1
  114. package/dist/utils/node/Folder.js +2 -4
  115. package/dist/utils/node/FramedStreamReader.js +4 -5
  116. package/dist/utils/node/FramedStreamWriter.js +6 -5
  117. package/dist/utils/node/anonymize.js +11 -0
  118. package/dist/utils/node/timeout.js +11 -0
  119. package/dist/utils/shared/ProgressBar.js +1 -2
  120. package/dist/utils/shared/TestReportBuilder.js +11 -7
  121. package/dist/utils/shared/host.js +11 -0
  122. package/dist/utils/shared/object.js +12 -3
  123. package/dist/utils/shared/schema.js +20 -0
  124. package/dist/utils/shared/string.js +1 -0
  125. package/dist/utils/shared/toIError.js +3 -7
  126. package/dist/utils/shared/version.js +52 -0
  127. package/package.json +2 -78
  128. package/dist/configuration/validators/browser.js +0 -11
  129. package/dist/modes/test/REserve.js +0 -31
  130. package/dist/modes/test/server.js +0 -109
@@ -1,20 +1,27 @@
1
1
  import { Exit, ExitShutdownError } from './Exit.js';
2
2
  import { Host } from './Host.js';
3
3
  import { Terminal } from './Terminal.js';
4
+ import { Path } from './Path.js';
4
5
  import { Thread } from './Thread.js';
6
+ import { performance } from 'node:perf_hooks';
5
7
  import { LogLevel } from './logger/types.js';
6
8
  import { toInternalLogAttributes } from './logger/toInternalLogAttributes.js';
9
+ import { traceFileName } from './logger/traceFileName.js';
7
10
  import assert from 'node:assert/strict';
8
11
  import { toPlainObject } from '../utils/shared/object.js';
12
+ import { redactSecrets } from '../utils/node/anonymize.js';
9
13
  import { toIError } from '../utils/shared/toIError.js';
10
14
  const startedAt = Date.now();
11
15
  let channel;
12
16
  let loggerWorker;
13
17
  let consoleWorker;
18
+ let reportDirectory;
14
19
  const buffer = [];
15
20
  const waitingFor = ['allCompressed', 'output'];
16
- let ready = !Thread.isMainThread;
21
+ let isReady = !Thread.isMainThread;
22
+ const { promise: startPromise, resolve: startResolve } = Promise.withResolvers();
17
23
  let metricsMonitorInterval;
24
+ let lastELU = performance.eventLoopUtilization();
18
25
  const terminalResized = (width) => {
19
26
  channel?.postMessage({
20
27
  command: 'terminal-resized',
@@ -24,13 +31,17 @@ const terminalResized = (width) => {
24
31
  const start = () => {
25
32
  channel = Thread.createBroadcastChannel('logger');
26
33
  metricsMonitorInterval = setInterval(() => {
34
+ const cpu = Thread.threadCpuUsage();
35
+ const mem = Host.memoryUsage();
36
+ const temporaryELU = performance.eventLoopUtilization();
37
+ const elu = performance.eventLoopUtilization(temporaryELU, lastELU);
38
+ lastELU = temporaryELU;
39
+ const heapUsedMB = Math.round(mem.heapUsed / 1024 / 1024);
40
+ const cpuUserMs = Math.round(cpu.user / 1000);
27
41
  logger.debug({
28
42
  source: 'metric',
29
- message: '',
30
- data: {
31
- cpu: Thread.threadCpuUsage(),
32
- mem: Host.memoryUsage()
33
- }
43
+ message: `heapUsed=${heapUsedMB}MB cpu=${cpuUserMs}ms elu=${elu.utilization.toFixed(3)}`,
44
+ data: { cpu, mem, elu }
34
45
  });
35
46
  }, 1000);
36
47
  channel.onmessage = (event) => {
@@ -39,17 +50,23 @@ const start = () => {
39
50
  terminalResized(Terminal.width);
40
51
  const index = waitingFor.indexOf(message.source);
41
52
  waitingFor.splice(index, 1);
42
- ready = waitingFor.length === 0;
43
- if (ready && buffer.length > 0) {
44
- for (const buffered of buffer) {
45
- channel.postMessage({
46
- command: 'log',
47
- ...buffered
48
- });
53
+ isReady = waitingFor.length === 0;
54
+ if (isReady) {
55
+ if (buffer.length > 0) {
56
+ for (const buffered of buffer) {
57
+ channel.postMessage({
58
+ command: 'log',
59
+ ...buffered
60
+ });
61
+ }
62
+ buffer.length = 0;
49
63
  }
50
- buffer.length = 0;
64
+ startResolve();
51
65
  }
52
66
  }
67
+ else if (message.command === 'fatal') {
68
+ void Exit.shutdown();
69
+ }
53
70
  else if (message.command === 'terminate') {
54
71
  clearInterval(metricsMonitorInterval);
55
72
  channel.close();
@@ -61,7 +78,7 @@ const log = (level, attributes) => {
61
78
  if (attributes.error) {
62
79
  allAttributes.error = toIError(attributes.error);
63
80
  }
64
- if (ready && !stopping) {
81
+ if (isReady && !stopping) {
65
82
  channel.postMessage({
66
83
  command: 'log',
67
84
  ...allAttributes
@@ -76,26 +93,30 @@ if (!Thread.isMainThread) {
76
93
  }
77
94
  let stopping;
78
95
  export const logger = {
79
- start(configuration) {
96
+ async start(configuration) {
80
97
  assert.ok(Thread.isMainThread, 'Call logger.start only in main thread');
81
98
  assert.ok(!loggerWorker && !consoleWorker, 'Call logger.start only once');
99
+ reportDirectory = configuration.reportDir;
82
100
  start();
83
101
  loggerWorker = Thread.createWorker('platform/logger/allCompressed', {
84
- configuration: toPlainObject(configuration)
102
+ configuration: redactSecrets(toPlainObject(configuration)),
103
+ startedAt
85
104
  });
86
105
  consoleWorker = Thread.createWorker('platform/logger/output', {
87
- configuration: toPlainObject(configuration),
106
+ configuration: redactSecrets(toPlainObject(configuration)),
88
107
  startedAt
89
108
  });
90
109
  if (!configuration.ci) {
91
110
  Terminal.setRawMode((data) => {
92
- if (data.length === 1 && data[0] === 3) {
93
- logger.warn({ source: 'job', message: 'User requested interruption' });
94
- Exit.sigInt();
111
+ if (!(data.length === 1 && data[0] === 3)) {
112
+ return;
95
113
  }
114
+ logger.warn({ source: 'job', message: 'user requested interruption' });
115
+ Exit.sigInt();
96
116
  });
97
117
  Terminal.onResize(terminalResized);
98
118
  }
119
+ return startPromise;
99
120
  },
100
121
  debug(attributes) {
101
122
  log(LogLevel.debug, attributes);
@@ -111,12 +132,22 @@ export const logger = {
111
132
  },
112
133
  fatal(attributes) {
113
134
  log(LogLevel.fatal, attributes);
135
+ if (Thread.isMainThread) {
136
+ void Exit.shutdown();
137
+ }
138
+ else {
139
+ channel.postMessage({ command: 'fatal' });
140
+ }
141
+ throw new ExitShutdownError();
114
142
  },
115
143
  async stop() {
116
144
  assert.ok(Thread.isMainThread, 'Call logger.stop only in main thread');
117
145
  if (stopping) {
118
146
  return await stopping;
119
147
  }
148
+ if (reportDirectory !== undefined) {
149
+ logger.info({ source: 'job', message: `trace file: ${Path.join(reportDirectory, traceFileName(startedAt))}` });
150
+ }
120
151
  const { promise, resolve } = Promise.withResolvers();
121
152
  stopping = promise;
122
153
  clearInterval(metricsMonitorInterval);
@@ -1,5 +1,34 @@
1
1
  import { vi } from 'vitest';
2
- import { join } from 'node:path';
2
+ import { basename, extname, join, relative } from 'node:path';
3
+ import { setTimeout } from 'node:timers/promises';
4
+ import { options, defaults } from '../configuration/options.js';
5
+ import assert from 'node:assert';
6
+ export const MOCK_CWD = '~/ui5-test-runner/vitest';
7
+ assert.ok(options[0].name === 'cwd');
8
+ Object.assign(options[0], { default: MOCK_CWD });
9
+ Object.assign(defaults, { cwd: MOCK_CWD });
10
+ const _registry = new WeakMap();
11
+ const functionMembers = (object) => {
12
+ const out = {};
13
+ for (const key of Object.getOwnPropertyNames(object)) {
14
+ if (typeof object[key] === 'function') {
15
+ out[key] = object[key];
16
+ }
17
+ }
18
+ return out;
19
+ };
20
+ const registerUnmock = (mocked, real) => {
21
+ _registry.set(mocked, { real: functionMembers(real), mocked: functionMembers(mocked) });
22
+ };
23
+ export const unmock = (target) => {
24
+ const entry = _registry.get(target);
25
+ assert.ok(entry, 'unmock: object was not registered as a mocked platform export');
26
+ Object.assign(target, entry.real);
27
+ const restore = () => {
28
+ Object.assign(target, entry.mocked);
29
+ };
30
+ return { restore, [Symbol.dispose]: restore };
31
+ };
3
32
  const mockMethods = (object, members) => {
4
33
  for (const member of members) {
5
34
  if (typeof object[member] === 'function') {
@@ -20,6 +49,12 @@ const mockStaticMethodsOfExports = (actual) => {
20
49
  }
21
50
  return mocked;
22
51
  };
52
+ vi.mock(import('./Crypto.js'), async (importActual) => {
53
+ const actual = await importActual();
54
+ const mocked = mockStaticMethodsOfExports(actual);
55
+ registerUnmock(mocked.Crypto, actual.Crypto);
56
+ return mocked;
57
+ });
23
58
  vi.mock(import('./constants.js'), async (importActual) => {
24
59
  const mocked = await importActual();
25
60
  return {
@@ -29,8 +64,11 @@ vi.mock(import('./constants.js'), async (importActual) => {
29
64
  });
30
65
  export const __unregisterExitAsyncTask = vi.fn();
31
66
  export let __lastRegisteredExitAsyncTask;
67
+ let _ExitShutdownError;
32
68
  vi.mock(import('./Exit.js'), async (importActual) => {
33
- const mocked = mockStaticMethodsOfExports(await importActual());
69
+ const actual = await importActual();
70
+ _ExitShutdownError = actual.ExitShutdownError;
71
+ const mocked = mockStaticMethodsOfExports(actual);
34
72
  const { Exit } = mocked;
35
73
  vi.mocked(Exit.registerAsyncTask).mockImplementation((task) => {
36
74
  __lastRegisteredExitAsyncTask = task;
@@ -39,7 +77,8 @@ vi.mock(import('./Exit.js'), async (importActual) => {
39
77
  return mocked;
40
78
  });
41
79
  vi.mock(import('./FileSystem.js'), async (importActual) => {
42
- const mocked = mockStaticMethodsOfExports(await importActual());
80
+ const actual = await importActual();
81
+ const mocked = mockStaticMethodsOfExports(actual);
43
82
  const { FileSystem } = mocked;
44
83
  const writeStream = {
45
84
  write: vi.fn().mockImplementation((_, callback) => callback()),
@@ -48,29 +87,70 @@ vi.mock(import('./FileSystem.js'), async (importActual) => {
48
87
  vi.mocked(FileSystem.createWriteStream).mockReturnValue(writeStream);
49
88
  const readStream = {};
50
89
  vi.mocked(FileSystem.createReadStream).mockReturnValue(readStream);
90
+ registerUnmock(FileSystem, actual.FileSystem);
91
+ return mocked;
92
+ });
93
+ vi.mock(import('./Host.js'), async (importActual) => {
94
+ const actual = await importActual();
95
+ const mocked = mockStaticMethodsOfExports(actual);
96
+ const { Host } = mocked;
97
+ vi.mocked(Host.cpus).mockReturnValue([]);
98
+ vi.mocked(Host.cwd).mockReturnValue(MOCK_CWD);
99
+ vi.mocked(Host.tmpdir).mockReturnValue('/tmp');
100
+ registerUnmock(Host, actual.Host);
101
+ return mocked;
102
+ });
103
+ vi.mock(import('./Http.js'), async (importActual) => {
104
+ const actual = await importActual();
105
+ const mocked = mockStaticMethodsOfExports(actual);
106
+ registerUnmock(mocked.Http, actual.Http);
107
+ return mocked;
108
+ });
109
+ vi.mock(import('./Module.js'), async (importActual) => {
110
+ const actual = await importActual();
111
+ const mocked = mockStaticMethodsOfExports(actual);
112
+ registerUnmock(mocked.Module, actual.Module);
51
113
  return mocked;
52
114
  });
53
- vi.mock(import('./Host.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
54
- vi.mock(import('./Http.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
55
- vi.mock(import('./Module.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
56
115
  const logger = {
57
- start: vi.fn(),
58
- debug: vi.fn(),
59
- info: vi.fn(),
60
- warn: vi.fn(),
61
- error: vi.fn(),
62
- fatal: vi.fn(),
116
+ start: vi.fn(() => Promise.resolve()),
117
+ debug: vi.fn().mockImplementation((attributes) => console.debug(attributes)),
118
+ info: vi.fn().mockImplementation((attributes) => console.log(attributes)),
119
+ warn: vi.fn().mockImplementation((attributes) => console.warn(attributes)),
120
+ error: vi.fn().mockImplementation((attributes) => console.error(attributes)),
121
+ fatal: vi.fn().mockImplementation((attributes) => {
122
+ console.error(attributes);
123
+ const error = new _ExitShutdownError();
124
+ error.message = `logger.fatal [${attributes.source}] ${attributes.message}`;
125
+ if (attributes.error !== undefined) {
126
+ error.cause = attributes.error;
127
+ }
128
+ throw error;
129
+ }),
63
130
  stop: vi.fn()
64
131
  };
65
132
  vi.mock(import('./logger.js'), () => ({ logger }));
66
133
  vi.mock(import('./Path.js'), async (importActual) => {
67
- const mocked = mockStaticMethodsOfExports(await importActual());
134
+ const actual = await importActual();
135
+ const mocked = mockStaticMethodsOfExports(actual);
68
136
  const { Path } = mocked;
69
- vi.mocked(Path.isAbsolute).mockImplementation((path) => path.startsWith('/'));
137
+ vi.mocked(Path.basename).mockImplementation((path) => basename(path));
138
+ vi.mocked(Path.dirname).mockImplementation((path) => join(path, '..').replaceAll('\\', '/'));
139
+ vi.mocked(Path.extname).mockImplementation((path) => extname(path));
140
+ vi.mocked(Path.isAbsolute).mockImplementation((path) => path.startsWith('/') || path.startsWith('~/'));
70
141
  vi.mocked(Path.join).mockImplementation((...arguments_) => join(...arguments_).replaceAll('\\', '/'));
142
+ vi.mocked(Path.relative).mockImplementation((from, to) => relative(from, to).replaceAll('\\', '/'));
143
+ registerUnmock(Path, actual.Path);
144
+ return mocked;
145
+ });
146
+ vi.mock(import('./Process.js'), async (importActual) => {
147
+ const actual = await importActual();
148
+ const mocked = mockStaticMethodsOfExports(actual);
149
+ const { Process } = mocked;
150
+ vi.mocked(Process.sleep).mockImplementation((ms) => setTimeout(ms));
151
+ registerUnmock(Process, actual.Process);
71
152
  return mocked;
72
153
  });
73
- vi.mock(import('./Process.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
74
154
  export let __lastTerminalRawModeCallback = false;
75
155
  vi.mock(import('./Terminal.js'), async (importActual) => {
76
156
  const actual = await importActual();
@@ -98,13 +178,24 @@ vi.mock(import('./Thread.js'), async (importActual) => {
98
178
  const worker = eventTarget;
99
179
  Object.assign(worker, {
100
180
  on: eventTarget.addEventListener.bind(eventTarget),
101
- postMessage: vi.fn()
181
+ postMessage: vi.fn(),
182
+ unref: vi.fn()
102
183
  });
103
184
  vi.mocked(Thread.createWorker).mockReturnValue(worker);
104
185
  return mocked;
105
186
  });
106
187
  vi.mock(import('./version.js'), () => ({
107
- version: vi.fn().mockResolvedValue('ui5-test-runner@1.2.3')
188
+ version: vi.fn().mockResolvedValue({ name: 'ui5-test-runner', version: '1.2.3' })
108
189
  }));
109
- vi.mock(import('./ZLib.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
110
- vi.mock(import('./Url.js'), async (importActual) => mockStaticMethodsOfExports(await importActual()));
190
+ vi.mock(import('./ZLib.js'), async (importActual) => {
191
+ const actual = await importActual();
192
+ const mocked = mockStaticMethodsOfExports(actual);
193
+ registerUnmock(mocked.ZLib, actual.ZLib);
194
+ return mocked;
195
+ });
196
+ vi.mock(import('./Url.js'), async (importActual) => {
197
+ const actual = await importActual();
198
+ const mocked = mockStaticMethodsOfExports(actual);
199
+ registerUnmock(mocked.Url, actual.Url);
200
+ return mocked;
201
+ });
@@ -3,6 +3,5 @@ import { __sourcesRoot } from './constants.js';
3
3
  import { Path } from './Path.js';
4
4
  import { memoize } from '../utils/shared/memoize.js';
5
5
  export const version = memoize(async () => {
6
- const { name, version } = JSON.parse(await FileSystem.readFile(Path.join(__sourcesRoot, '../package.json'), 'utf8'));
7
- return `${name}@${version}`;
6
+ return JSON.parse(await FileSystem.readFile(Path.join(__sourcesRoot, '../package.json'), 'utf8'));
8
7
  });
@@ -8,7 +8,7 @@ const main = async () => {
8
8
  await workerMain(data);
9
9
  }
10
10
  catch (error) {
11
- logger.fatal({ source: 'thread', message: `An error occurred`, error, data: { path } });
11
+ logger.fatal({ source: 'thread', message: `an error occurred`, error, data: { path } });
12
12
  }
13
13
  finally {
14
14
  logger.debug({
@@ -0,0 +1,18 @@
1
+ import { version } from '../platform/version.js';
2
+ import { Host } from '../platform/index.js';
3
+ import { toPlainObject } from '../utils/shared/object.js';
4
+ import { anonymize } from '../utils/node/anonymize.js';
5
+ import { TestReportBuilder } from '../utils/shared/TestReportBuilder.js';
6
+ export async function initReportBuilder(configuration) {
7
+ const { name: toolName, version: toolVersion } = await version();
8
+ const builder = new TestReportBuilder(crypto.randomUUID(), `${toolName}@${toolVersion}`);
9
+ builder.report.results.tool = { name: toolName, version: toolVersion };
10
+ builder.report.results.environment = {
11
+ osPlatform: Host.platform(),
12
+ osRelease: Host.osRelease(),
13
+ osVersion: Host.osVersion(),
14
+ extra: { machine: Host.machine(), cpus: Host.cpus() }
15
+ };
16
+ builder.report.extra = { configuration: anonymize(toPlainObject(configuration)) };
17
+ return builder;
18
+ }
@@ -0,0 +1,64 @@
1
+ import { FileSystem, Path } from '../platform/index.js';
2
+ function xmlEscape(text) {
3
+ return text.replaceAll(/[<>&"]/g, (match) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[match]);
4
+ }
5
+ function groupBySuite(tests) {
6
+ const suitesByName = new Map();
7
+ for (const test of tests) {
8
+ const suiteName = test.suite?.[0] ?? '(root)';
9
+ let bucket = suitesByName.get(suiteName);
10
+ if (bucket === undefined) {
11
+ bucket = [];
12
+ suitesByName.set(suiteName, bucket);
13
+ }
14
+ bucket.push(test);
15
+ }
16
+ return suitesByName;
17
+ }
18
+ function testcaseLines(test, suiteName) {
19
+ const classname = test.suite && test.suite.length > 1 ? test.suite.slice(1).join('.') : suiteName;
20
+ const lines = [
21
+ ` <testcase name="${xmlEscape(test.name)}" classname="${xmlEscape(classname)}" time="${(test.duration / 1000).toFixed(3)}">`
22
+ ];
23
+ if (test.status === 'skipped' || test.status === 'pending') {
24
+ lines.push(' <skipped/>');
25
+ }
26
+ else if (test.status === 'failed') {
27
+ const message = test.message ? ` message="${xmlEscape(test.message)}"` : '';
28
+ lines.push(` <failure${message}>`);
29
+ if (test.trace) {
30
+ lines.push(xmlEscape(test.trace));
31
+ }
32
+ lines.push(' </failure>');
33
+ }
34
+ const attachments = test.attachments ?? [];
35
+ for (const attachment of attachments) {
36
+ lines.push(` <system-out>[[ATTACHMENT|${attachment.path}]]</system-out>`);
37
+ }
38
+ lines.push(' </testcase>');
39
+ return lines;
40
+ }
41
+ function suiteLines(suiteName, suiteTests) {
42
+ const failures = suiteTests.filter((t) => t.status === 'failed').length;
43
+ const skipped = suiteTests.filter((t) => t.status === 'skipped' || t.status === 'pending').length;
44
+ const time = (suiteTests.reduce((accumulator, t) => accumulator + t.duration, 0) / 1000).toFixed(3);
45
+ return [
46
+ ` <testsuite name="${xmlEscape(suiteName)}" tests="${suiteTests.length}" failures="${failures}" skipped="${skipped}" time="${time}">`,
47
+ ...suiteTests.flatMap((test) => testcaseLines(test, suiteName)),
48
+ ' </testsuite>'
49
+ ];
50
+ }
51
+ export function generateJUnitReport(configuration, report) {
52
+ if (!configuration.junitXmlFilename) {
53
+ return;
54
+ }
55
+ const { summary, tests } = report.results;
56
+ const time = ((summary.stop - summary.start) / 1000).toFixed(3);
57
+ const lines = [
58
+ '<?xml version="1.0" encoding="UTF-8"?>',
59
+ `<testsuites name="ui5-test-runner" tests="${summary.tests}" failures="${summary.failed}" skipped="${summary.skipped}" time="${time}">`,
60
+ ...[...groupBySuite(tests)].flatMap(([name, suiteTests]) => suiteLines(name, suiteTests)),
61
+ '</testsuites>'
62
+ ];
63
+ FileSystem.writeFileSync(Path.join(configuration.reportDir, configuration.junitXmlFilename), lines.join('\n'), 'utf8');
64
+ }
@@ -0,0 +1,10 @@
1
+ import { FileSystem, Path } from '../platform/index.js';
2
+ import { generateHtmlReport } from './html.js';
3
+ import { generateJUnitReport } from './junit.js';
4
+ import { generateTextReport } from './textReport.js';
5
+ export async function saveReport(configuration, report) {
6
+ FileSystem.writeFileSync(Path.join(configuration.reportDir, 'report.json'), JSON.stringify(report, undefined, 2), 'utf8');
7
+ generateTextReport(configuration, report);
8
+ await generateHtmlReport(configuration, report);
9
+ generateJUnitReport(configuration, report);
10
+ }
@@ -0,0 +1,92 @@
1
+ import { logger } from '../platform/index.js';
2
+ const PASSED_COUNT_WIDTH = 3;
3
+ const pageUrlOf = (test) => {
4
+ const suite = test.suite ?? [];
5
+ for (let index = suite.length - 1; index >= 0; --index) {
6
+ const entry = suite[index];
7
+ try {
8
+ const { protocol } = new URL(entry);
9
+ if (protocol === 'http:' || protocol === 'https:') {
10
+ return entry;
11
+ }
12
+ }
13
+ catch {
14
+ }
15
+ }
16
+ return suite[0] ?? test.name;
17
+ };
18
+ const moduleOf = (test, pageUrl) => {
19
+ const suite = test.suite ?? [];
20
+ const pageIndex = suite.lastIndexOf(pageUrl);
21
+ const tail = pageIndex === -1 ? suite : suite.slice(pageIndex + 1);
22
+ return tail.join(' ▶ ');
23
+ };
24
+ const groupByPage = (tests) => {
25
+ const pages = new Map();
26
+ for (const test of tests) {
27
+ const url = pageUrlOf(test);
28
+ let page = pages.get(url);
29
+ if (page === undefined) {
30
+ page = { url, passed: 0, skipped: 0, total: 0, isOpa: false, failures: [] };
31
+ pages.set(url, page);
32
+ }
33
+ ++page.total;
34
+ if (test.status === 'passed') {
35
+ ++page.passed;
36
+ }
37
+ else if (test.status === 'failed') {
38
+ page.failures.push(test);
39
+ }
40
+ else if (test.status === 'skipped' || test.status === 'pending') {
41
+ ++page.skipped;
42
+ }
43
+ if (test.extra?.type === 'opa') {
44
+ page.isOpa = true;
45
+ }
46
+ }
47
+ return pages.values().toArray();
48
+ };
49
+ const reportFailures = (page) => {
50
+ const shown = page.isOpa ? page.failures.slice(0, 1) : page.failures;
51
+ for (const test of shown) {
52
+ const module = moduleOf(test, page.url);
53
+ let message = `[${page.url}]\n` + (module ? `${module} ▶ ${test.name}` : test.name);
54
+ if (test.message) {
55
+ message += `\n ${test.message}`;
56
+ }
57
+ if (test.trace) {
58
+ message += `\n ${test.trace}`;
59
+ }
60
+ logger.error({ source: 'job', message });
61
+ }
62
+ if (page.isOpa && page.failures.length > 1) {
63
+ logger.warn({
64
+ source: 'job',
65
+ message: `[${page.url}]\n${page.failures.length} failures; showing first OPA error only`
66
+ });
67
+ }
68
+ };
69
+ const statusOf = (page) => {
70
+ if (page.failures.length > 0) {
71
+ return 'FAIL';
72
+ }
73
+ return page.passed === 0 && page.skipped > 0 ? 'SKIP' : 'PASS';
74
+ };
75
+ export function generateTextReport(_configuration, report) {
76
+ const pages = groupByPage(report.results.tests);
77
+ if (pages.length === 0) {
78
+ return;
79
+ }
80
+ logger.info({ source: 'job', message: 'Test report:' });
81
+ for (const page of pages) {
82
+ const ratio = `${page.passed.toString().padStart(PASSED_COUNT_WIDTH, ' ')}/${page.total}`;
83
+ logger.info({ source: 'job', message: `${statusOf(page)} ${ratio} ${page.url}` });
84
+ }
85
+ if (report.results.summary.failed > 0) {
86
+ for (const page of pages) {
87
+ if (page.failures.length > 0) {
88
+ reportFailures(page);
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,22 @@
1
+ import { logger } from './platform/index.js';
2
+ export const logReserve = (server) => {
3
+ server
4
+ .on('created', () => {
5
+ logger.debug({ source: 'reserve', message: 'created', data: {} });
6
+ })
7
+ .on('ready', (event) => {
8
+ const { eventName: message, ...data } = event;
9
+ logger.debug({ source: 'reserve', message, data });
10
+ logger.info({ source: 'server', message: `server listening on: ${event.url}` });
11
+ })
12
+ .on('error', (event) => {
13
+ const { eventName: message, reason: error, ...data } = event;
14
+ logger.debug({ source: 'reserve', message, data, error });
15
+ });
16
+ for (const eventName of ['incoming', 'redirecting', 'redirected', 'aborted', 'closed']) {
17
+ server.on(eventName, (event) => {
18
+ const { eventName: message, ...data } = event;
19
+ logger.debug({ source: 'reserve', message, data });
20
+ });
21
+ }
22
+ };
@@ -0,0 +1,6 @@
1
+ import { Host, Process } from './platform/index.js';
2
+ export const sendToParentProcess = (message) => {
3
+ if (Host.env['UI5TR_BATCH_MODE']) {
4
+ Process.sendToParent(message);
5
+ }
6
+ };
package/dist/start.js ADDED
@@ -0,0 +1,79 @@
1
+ import { logger, Http, Process, Host, assert } from './platform/index.js';
2
+ import { Command } from './Command.js';
3
+ const POLL_INTERVAL = 250;
4
+ const waitForStart = async (configuration, startProcess) => {
5
+ assert(configuration.startWaitUrl !== undefined);
6
+ const { startWaitUrl: url, startWaitMethod: method, startTimeout: timeout } = configuration;
7
+ logger.info({ source: 'job', message: 'waiting for start URL to be reachable', data: { url } });
8
+ logger.info({
9
+ source: 'progress',
10
+ message: 'waiting for start URL to be reachable',
11
+ pageId: undefined,
12
+ data: { value: 0, max: 0 }
13
+ });
14
+ const controller = new AbortController();
15
+ let isTimedOut = false;
16
+ const timeoutHandle = setTimeout(() => {
17
+ isTimedOut = true;
18
+ controller.abort();
19
+ }, timeout);
20
+ using _ = {
21
+ [Symbol.dispose]: () => {
22
+ clearTimeout(timeoutHandle);
23
+ }
24
+ };
25
+ let numberOfAttempts = 0;
26
+ let lastStatus;
27
+ while (!isTimedOut && startProcess.code === undefined) {
28
+ try {
29
+ ++numberOfAttempts;
30
+ const fetchResult = await Http.fetch(url, { method, signal: controller.signal });
31
+ if (fetchResult.ok) {
32
+ return;
33
+ }
34
+ lastStatus = fetchResult.statusText;
35
+ }
36
+ catch {
37
+ lastStatus = 'Unreachable';
38
+ }
39
+ if (isTimedOut || startProcess.code !== undefined) {
40
+ continue;
41
+ }
42
+ logger.info({
43
+ source: 'progress',
44
+ message: `waiting for start URL to be reachable (${numberOfAttempts}:${lastStatus})`,
45
+ pageId: undefined,
46
+ data: { value: 0, max: 0 }
47
+ });
48
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
49
+ }
50
+ if (startProcess.code === undefined) {
51
+ await startProcess.kill();
52
+ throw new Error(`Timeout while waiting for ${url}`);
53
+ }
54
+ throw new Error(`Start command failed with exit code ${startProcess.code}`);
55
+ };
56
+ export const start = async (configuration) => {
57
+ if (!configuration.start) {
58
+ return undefined;
59
+ }
60
+ logger.info({ source: 'job', message: 'executing start command...' });
61
+ logger.info({
62
+ source: 'progress',
63
+ message: 'executing start command',
64
+ pageId: undefined,
65
+ data: { value: 0, max: 0 }
66
+ });
67
+ const [command, arguments_, environment] = await Command.parse(configuration, configuration.start);
68
+ const startProcess = Process.spawn(command, arguments_, {
69
+ cwd: configuration.cwd,
70
+ windowsHide: true,
71
+ detached: true,
72
+ env: { ...Host.env, ...environment },
73
+ forceRender: true
74
+ });
75
+ if (configuration.startWaitUrl) {
76
+ await waitForStart(configuration, startProcess);
77
+ }
78
+ return startProcess;
79
+ };
@@ -0,0 +1 @@
1
+ export const isSecretValue = (value) => typeof value?.reveal === 'function';
@@ -0,0 +1,15 @@
1
+ export class SecretValue {
2
+ #value;
3
+ constructor(value) {
4
+ this.#value = value;
5
+ }
6
+ reveal() {
7
+ return this.#value;
8
+ }
9
+ toJSON() {
10
+ return '***';
11
+ }
12
+ toString() {
13
+ return '***';
14
+ }
15
+ }