ui5-test-runner 6.0.0-beta.4 → 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.
- package/README.md +20 -29
- package/dist/Command.js +52 -0
- package/dist/Npm.js +193 -43
- package/dist/browsers/RecoverableBrowserError.js +6 -0
- package/dist/browsers/chromeArguments.js +5 -0
- package/dist/browsers/chromeOptions.js +29 -0
- package/dist/browsers/consoleMessage.js +23 -0
- package/dist/browsers/factory.js +169 -4
- package/dist/browsers/networkResponse.js +15 -0
- package/dist/browsers/playwright.js +104 -0
- package/dist/browsers/puppeteer.js +55 -89
- package/dist/browsers/seleniumWebdriver.js +124 -0
- package/dist/browsers/webdriverio.js +85 -0
- package/dist/cli.js +18 -12
- package/dist/configuration/CommandLine.js +46 -26
- package/dist/configuration/ConfigurationValidator.js +132 -18
- package/dist/configuration/OptionValidationError.js +50 -2
- package/dist/configuration/derivations.js +8 -0
- package/dist/configuration/options.js +392 -23
- package/dist/configuration/validations.js +135 -0
- package/dist/configuration/validators/boolean.js +1 -1
- package/dist/configuration/validators/enumeration.js +7 -0
- package/dist/configuration/validators/fsEntry.js +12 -12
- package/dist/configuration/validators/index.js +8 -2
- package/dist/configuration/validators/integer.js +7 -1
- package/dist/configuration/validators/json.js +23 -0
- package/dist/configuration/validators/libraryMapping.js +28 -0
- package/dist/configuration/validators/percent.js +2 -2
- package/dist/configuration/validators/regexp.js +2 -2
- package/dist/configuration/validators/secret.js +8 -0
- package/dist/configuration/validators/string.js +1 -1
- package/dist/configuration/validators/timeout.js +2 -2
- package/dist/configuration/validators/url.js +3 -3
- package/dist/end.js +58 -0
- package/dist/if.js +22 -0
- package/dist/modes/Modes.js +2 -1
- package/dist/modes/batch/BatchItem.js +1 -0
- package/dist/modes/batch/batchTask.js +159 -0
- package/dist/modes/batch/index.js +98 -0
- package/dist/modes/batch/output.js +22 -0
- package/dist/modes/batch/report.js +45 -0
- package/dist/modes/batch/resolve.js +105 -0
- package/dist/modes/dumpConfig.js +6 -0
- package/dist/modes/execute.js +6 -5
- package/dist/modes/help.js +86 -2
- package/dist/modes/log/LogReader.js +3 -2
- package/dist/modes/log/LogStorage.js +31 -27
- package/dist/modes/log/index.js +39 -7
- package/dist/modes/log/{REserve.js → reserve.js} +25 -2
- package/dist/modes/mcp/IDocument.js +2 -0
- package/dist/modes/mcp/frontmatter.js +142 -0
- package/dist/modes/mcp/index.js +49 -0
- package/dist/modes/mcp/knowledgeBase.js +104 -0
- package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
- package/dist/modes/mcp/reserve.js +175 -0
- package/dist/modes/mcp/tools/getTopic.js +46 -0
- package/dist/modes/mcp/tools/index.js +6 -0
- package/dist/modes/mcp/tools/listTopics.js +14 -0
- package/dist/modes/test/PageContext.js +1 -0
- package/dist/modes/test/Server.js +99 -0
- package/dist/modes/test/browser.js +31 -17
- package/dist/modes/test/browserConfig.js +17 -0
- package/dist/modes/test/coverage/collect.js +38 -0
- package/dist/modes/test/coverage/index.js +3 -0
- package/dist/modes/test/coverage/instrument.js +70 -0
- package/dist/modes/test/coverage/nyc.js +15 -0
- package/dist/modes/test/coverage/report.js +71 -0
- package/dist/modes/test/coverage/settings.js +53 -0
- package/dist/modes/test/index.js +94 -34
- package/dist/modes/test/pageTask.js +293 -98
- package/dist/modes/test/report.js +20 -3
- package/dist/modes/test/reserve.js +76 -0
- package/dist/modes/test/screenshot.js +49 -0
- package/dist/platform/Crypto.js +9 -0
- package/dist/platform/Exit.js +33 -24
- package/dist/platform/FileSystem.js +6 -2
- package/dist/platform/Host.js +9 -1
- package/dist/platform/Http.js +54 -29
- package/dist/platform/Module.js +5 -0
- package/dist/platform/Path.js +5 -1
- package/dist/platform/Process.js +67 -13
- package/dist/platform/Terminal.js +12 -8
- package/dist/platform/Thread.js +1 -1
- package/dist/platform/Url.js +4 -0
- package/dist/platform/assert.js +13 -12
- package/dist/platform/environment.js +9 -18
- package/dist/platform/index.js +3 -0
- package/dist/platform/logger/CompressionContext.js +117 -0
- package/dist/platform/logger/allCompressed.js +8 -7
- package/dist/platform/logger/compress.js +29 -129
- package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
- package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
- package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
- package/dist/platform/logger/output/factory.js +5 -1
- package/dist/platform/logger/output.js +24 -6
- package/dist/platform/logger/toInternalLogAttributes.js +10 -11
- package/dist/platform/logger/traceFileName.js +1 -0
- package/dist/platform/logger.js +52 -21
- package/dist/platform/mock.js +124 -27
- package/dist/platform/version.js +1 -2
- package/dist/platform/workerBootstrap.js +1 -1
- package/dist/reports/initReportBuilder.js +18 -0
- package/dist/reports/junit.js +64 -0
- package/dist/reports/saveReport.js +10 -0
- package/dist/reports/textReport.js +92 -0
- package/dist/reserveLogger.js +22 -0
- package/dist/sendToParentProcess.js +6 -0
- package/dist/start.js +79 -0
- package/dist/types/ISecretValue.js +1 -0
- package/dist/types/SecretValue.js +15 -0
- package/dist/ui/agent.js +4 -2
- package/dist/ui/html-report.js +2 -2
- package/dist/ui/lib.js +1 -1
- package/dist/ui/log-viewer.js +1 -1
- package/dist/utils/node/Folder.js +2 -4
- package/dist/utils/node/FramedStreamReader.js +4 -5
- package/dist/utils/node/FramedStreamWriter.js +6 -5
- package/dist/utils/node/anonymize.js +11 -0
- package/dist/utils/node/timeout.js +11 -0
- package/dist/utils/shared/ProgressBar.js +1 -2
- package/dist/utils/shared/TestReportBuilder.js +11 -7
- package/dist/utils/shared/host.js +11 -0
- package/dist/utils/shared/object.js +12 -3
- package/dist/utils/shared/schema.js +20 -0
- package/dist/utils/shared/string.js +1 -0
- package/dist/utils/shared/toIError.js +3 -7
- package/dist/utils/shared/version.js +52 -0
- package/package.json +2 -78
- package/dist/configuration/validators/browser.js +0 -11
- package/dist/modes/test/REserve.js +0 -31
- package/dist/modes/test/server.js +0 -109
package/dist/platform/logger.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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);
|
package/dist/platform/mock.js
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
import { vi } from 'vitest';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
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
|
+
};
|
|
32
|
+
const mockMethods = (object, members) => {
|
|
33
|
+
for (const member of members) {
|
|
34
|
+
if (typeof object[member] === 'function') {
|
|
35
|
+
object[member] = vi.fn();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const mockStaticMethodsOfExports = (actual) => {
|
|
4
40
|
const mocked = { ...actual };
|
|
5
41
|
for (const exportName in mocked) {
|
|
6
42
|
const exportValue = mocked[exportName];
|
|
7
43
|
if (typeof exportValue === 'function') {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
exportClass[staticName] = vi.fn();
|
|
13
|
-
}
|
|
14
|
-
}
|
|
44
|
+
mockMethods(exportValue, Object.getOwnPropertyNames(exportValue));
|
|
45
|
+
}
|
|
46
|
+
else if (typeof exportValue === 'object' && exportValue !== null) {
|
|
47
|
+
mockMethods(exportValue, Object.keys(exportValue));
|
|
15
48
|
}
|
|
16
49
|
}
|
|
17
50
|
return mocked;
|
|
18
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
|
+
});
|
|
19
58
|
vi.mock(import('./constants.js'), async (importActual) => {
|
|
20
59
|
const mocked = await importActual();
|
|
21
60
|
return {
|
|
@@ -25,8 +64,11 @@ vi.mock(import('./constants.js'), async (importActual) => {
|
|
|
25
64
|
});
|
|
26
65
|
export const __unregisterExitAsyncTask = vi.fn();
|
|
27
66
|
export let __lastRegisteredExitAsyncTask;
|
|
67
|
+
let _ExitShutdownError;
|
|
28
68
|
vi.mock(import('./Exit.js'), async (importActual) => {
|
|
29
|
-
const
|
|
69
|
+
const actual = await importActual();
|
|
70
|
+
_ExitShutdownError = actual.ExitShutdownError;
|
|
71
|
+
const mocked = mockStaticMethodsOfExports(actual);
|
|
30
72
|
const { Exit } = mocked;
|
|
31
73
|
vi.mocked(Exit.registerAsyncTask).mockImplementation((task) => {
|
|
32
74
|
__lastRegisteredExitAsyncTask = task;
|
|
@@ -35,7 +77,8 @@ vi.mock(import('./Exit.js'), async (importActual) => {
|
|
|
35
77
|
return mocked;
|
|
36
78
|
});
|
|
37
79
|
vi.mock(import('./FileSystem.js'), async (importActual) => {
|
|
38
|
-
const
|
|
80
|
+
const actual = await importActual();
|
|
81
|
+
const mocked = mockStaticMethodsOfExports(actual);
|
|
39
82
|
const { FileSystem } = mocked;
|
|
40
83
|
const writeStream = {
|
|
41
84
|
write: vi.fn().mockImplementation((_, callback) => callback()),
|
|
@@ -44,33 +87,75 @@ vi.mock(import('./FileSystem.js'), async (importActual) => {
|
|
|
44
87
|
vi.mocked(FileSystem.createWriteStream).mockReturnValue(writeStream);
|
|
45
88
|
const readStream = {};
|
|
46
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);
|
|
47
113
|
return mocked;
|
|
48
114
|
});
|
|
49
|
-
vi.mock(import('./Host.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
|
|
50
|
-
vi.mock(import('./Http.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
|
|
51
115
|
const logger = {
|
|
52
|
-
start: vi.fn(),
|
|
53
|
-
debug: vi.fn(),
|
|
54
|
-
info: vi.fn(),
|
|
55
|
-
warn: vi.fn(),
|
|
56
|
-
error: vi.fn(),
|
|
57
|
-
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
|
+
}),
|
|
58
130
|
stop: vi.fn()
|
|
59
131
|
};
|
|
60
132
|
vi.mock(import('./logger.js'), () => ({ logger }));
|
|
61
133
|
vi.mock(import('./Path.js'), async (importActual) => {
|
|
62
|
-
const
|
|
134
|
+
const actual = await importActual();
|
|
135
|
+
const mocked = mockStaticMethodsOfExports(actual);
|
|
63
136
|
const { Path } = mocked;
|
|
64
|
-
vi.mocked(Path.
|
|
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('~/'));
|
|
65
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);
|
|
66
152
|
return mocked;
|
|
67
153
|
});
|
|
68
|
-
vi.mock(import('./Process.js'), async (importActual) => mockStaticMethodsOfExportedClasses(await importActual()));
|
|
69
154
|
export let __lastTerminalRawModeCallback = false;
|
|
70
155
|
vi.mock(import('./Terminal.js'), async (importActual) => {
|
|
71
156
|
const actual = await importActual();
|
|
72
157
|
const originalstripVTControlCharacters = actual.Terminal.stripVTControlCharacters;
|
|
73
|
-
const mocked =
|
|
158
|
+
const mocked = mockStaticMethodsOfExports(actual);
|
|
74
159
|
const { Terminal } = mocked;
|
|
75
160
|
vi.mocked(Terminal.setRawMode).mockImplementation((callback) => {
|
|
76
161
|
__lastTerminalRawModeCallback = callback;
|
|
@@ -79,7 +164,7 @@ vi.mock(import('./Terminal.js'), async (importActual) => {
|
|
|
79
164
|
return mocked;
|
|
80
165
|
});
|
|
81
166
|
vi.mock(import('./Thread.js'), async (importActual) => {
|
|
82
|
-
const mocked =
|
|
167
|
+
const mocked = mockStaticMethodsOfExports(await importActual());
|
|
83
168
|
const { Thread } = mocked;
|
|
84
169
|
const channel = {
|
|
85
170
|
postMessage: vi.fn().mockImplementation((data) => {
|
|
@@ -93,12 +178,24 @@ vi.mock(import('./Thread.js'), async (importActual) => {
|
|
|
93
178
|
const worker = eventTarget;
|
|
94
179
|
Object.assign(worker, {
|
|
95
180
|
on: eventTarget.addEventListener.bind(eventTarget),
|
|
96
|
-
postMessage: vi.fn()
|
|
181
|
+
postMessage: vi.fn(),
|
|
182
|
+
unref: vi.fn()
|
|
97
183
|
});
|
|
98
184
|
vi.mocked(Thread.createWorker).mockReturnValue(worker);
|
|
99
185
|
return mocked;
|
|
100
186
|
});
|
|
101
187
|
vi.mock(import('./version.js'), () => ({
|
|
102
|
-
version: vi.fn().mockResolvedValue('ui5-test-runner
|
|
188
|
+
version: vi.fn().mockResolvedValue({ name: 'ui5-test-runner', version: '1.2.3' })
|
|
103
189
|
}));
|
|
104
|
-
vi.mock(import('./ZLib.js'), async (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
|
+
});
|
package/dist/platform/version.js
CHANGED
|
@@ -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
|
-
|
|
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: `
|
|
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) => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[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
|
+
};
|