ui5-test-runner 5.13.1 → 6.0.0-beta.2
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 +3 -2
- package/dist/Npm.js +80 -0
- package/dist/browsers/IBrowser.js +1 -0
- package/dist/browsers/factory.js +9 -0
- package/dist/browsers/puppeteer.js +158 -0
- package/dist/cli.js +20 -0
- package/dist/configuration/CommandLine.js +112 -0
- package/dist/configuration/Configuration.js +1 -0
- package/dist/configuration/ConfigurationValidator.js +79 -0
- package/dist/configuration/Option.js +1 -0
- package/dist/configuration/OptionValidationError.js +15 -0
- package/dist/configuration/indexedOptions.js +13 -0
- package/dist/configuration/options.js +191 -0
- package/dist/configuration/validators/OptionValidator.js +1 -0
- package/dist/configuration/validators/boolean.js +15 -0
- package/dist/configuration/validators/browser.js +11 -0
- package/dist/configuration/validators/fsEntry.js +70 -0
- package/dist/configuration/validators/index.js +20 -0
- package/dist/configuration/validators/integer.js +10 -0
- package/dist/configuration/validators/percent.js +17 -0
- package/dist/configuration/validators/regexp.js +20 -0
- package/dist/configuration/validators/string.js +7 -0
- package/dist/configuration/validators/timeout.js +24 -0
- package/dist/configuration/validators/url.js +8 -0
- package/dist/modes/ModeFunction.js +1 -0
- package/dist/modes/Modes.js +9 -0
- package/dist/modes/execute.js +27 -0
- package/dist/modes/help.js +3 -0
- package/dist/modes/log/ILogStorage.js +1 -0
- package/dist/modes/log/LogMetrics.js +9 -0
- package/dist/modes/log/LogReader.js +37 -0
- package/dist/modes/log/LogStorage.js +68 -0
- package/dist/modes/log/REserve.js +101 -0
- package/dist/modes/log/index.js +58 -0
- package/dist/modes/test/REserve.js +31 -0
- package/dist/modes/test/agent.js +8 -0
- package/dist/modes/test/browser.js +37 -0
- package/dist/modes/test/index.js +66 -0
- package/dist/modes/test/pageTask.js +145 -0
- package/dist/modes/test/report.js +3 -0
- package/dist/modes/test/server.js +109 -0
- package/dist/modes/version.js +11 -0
- package/dist/platform/Exit.js +139 -0
- package/dist/platform/FileSystem.js +13 -0
- package/dist/platform/Host.js +10 -0
- package/dist/platform/Http.js +38 -0
- package/dist/platform/Path.js +5 -0
- package/dist/platform/Process.js +133 -0
- package/dist/platform/Terminal.js +47 -0
- package/dist/platform/Thread.js +43 -0
- package/dist/platform/ZLib.js +7 -0
- package/dist/platform/assert.js +17 -0
- package/dist/platform/constants.js +5 -0
- package/dist/platform/environment.js +28 -0
- package/dist/platform/index.js +13 -0
- package/dist/platform/logger/ILogger.js +1 -0
- package/dist/platform/logger/allCompressed.js +54 -0
- package/dist/platform/logger/compress.js +277 -0
- package/dist/platform/logger/output/BaseLoggerOutput.js +158 -0
- package/dist/platform/logger/output/InteractiveLoggerOutput.js +102 -0
- package/dist/platform/logger/output/StaticLoggerOutput.js +32 -0
- package/dist/platform/logger/output/factory.js +10 -0
- package/dist/platform/logger/output.js +58 -0
- package/dist/platform/logger/proxy.js +6 -0
- package/dist/platform/logger/toInternalLogAttributes.js +22 -0
- package/dist/platform/logger/types.js +7 -0
- package/dist/platform/logger.js +138 -0
- package/dist/platform/mock.js +104 -0
- package/dist/platform/version.js +8 -0
- package/dist/platform/workerBootstrap.js +21 -0
- package/dist/reports/html.js +46 -0
- package/dist/types/AgentState.js +1 -0
- package/dist/types/CommonTestReportFormat.js +50 -0
- package/dist/types/IError.js +1 -0
- package/dist/types/IUserInterfaceController.js +1 -0
- package/dist/types/typeUtilities.js +1 -0
- package/dist/ui/agent.js +3 -0
- package/dist/ui/html-report.js +2 -0
- package/dist/ui/lib.js +1 -0
- package/dist/ui/log-viewer.js +2 -0
- package/dist/utils/node/Folder.js +28 -0
- package/dist/utils/node/FramedStreamReader.js +86 -0
- package/dist/utils/node/FramedStreamWriter.js +27 -0
- package/dist/utils/shared/ProgressBar.js +43 -0
- package/dist/utils/shared/TestReportBuilder.js +48 -0
- package/dist/utils/shared/memoize.js +19 -0
- package/dist/utils/shared/object.js +8 -0
- package/dist/utils/shared/parallelize.js +59 -0
- package/dist/utils/shared/string.js +23 -0
- package/dist/utils/shared/toIError.js +17 -0
- package/package.json +73 -50
- package/.releaserc +0 -5
- package/index.js +0 -175
- package/jest.config.json +0 -31
- package/src/add-test-pages.js +0 -67
- package/src/batch.js +0 -214
- package/src/browsers.js +0 -319
- package/src/capabilities/index.js +0 -204
- package/src/capabilities/tests/basic/iframe.html +0 -8
- package/src/capabilities/tests/basic/index.html +0 -12
- package/src/capabilities/tests/basic/index.js +0 -20
- package/src/capabilities/tests/basic/ui5.html +0 -24
- package/src/capabilities/tests/dynamic-include/index.js +0 -21
- package/src/capabilities/tests/dynamic-include/mix.html +0 -11
- package/src/capabilities/tests/dynamic-include/one.html +0 -11
- package/src/capabilities/tests/dynamic-include/post.js +0 -3
- package/src/capabilities/tests/dynamic-include/test.js +0 -1
- package/src/capabilities/tests/dynamic-include/two.html +0 -11
- package/src/capabilities/tests/index.js +0 -16
- package/src/capabilities/tests/local-storage/index.html +0 -16
- package/src/capabilities/tests/local-storage/index.js +0 -21
- package/src/capabilities/tests/screenshot/index.html +0 -23
- package/src/capabilities/tests/screenshot/index.js +0 -24
- package/src/capabilities/tests/scripts/coverage.html +0 -32
- package/src/capabilities/tests/scripts/iframe.html +0 -18
- package/src/capabilities/tests/scripts/index.js +0 -59
- package/src/capabilities/tests/scripts/qunit.html +0 -22
- package/src/capabilities/tests/scripts/testsuite.html +0 -10
- package/src/capabilities/tests/scripts/testsuite.js +0 -8
- package/src/capabilities/tests/timeout/index.html +0 -21
- package/src/capabilities/tests/timeout/index.js +0 -19
- package/src/capabilities/tests/traces/index.html +0 -18
- package/src/capabilities/tests/traces/index.js +0 -81
- package/src/capabilities/tests/ui5/focus.html +0 -89
- package/src/capabilities/tests/ui5/index.js +0 -39
- package/src/capabilities/tests/ui5/language.html +0 -50
- package/src/capabilities/tests/ui5/timezone.html +0 -27
- package/src/clean.js +0 -22
- package/src/cors.js +0 -21
- package/src/coverage.js +0 -384
- package/src/csv-reader.js +0 -36
- package/src/csv-writer.js +0 -55
- package/src/defaults/.nycrc.json +0 -4
- package/src/defaults/browser.js +0 -217
- package/src/defaults/happy-dom.js +0 -123
- package/src/defaults/jsdom/compatibility.js +0 -163
- package/src/defaults/jsdom/debug.js +0 -23
- package/src/defaults/jsdom/resource-loader.js +0 -44
- package/src/defaults/jsdom/sap.ui.test.matchers.visible.js +0 -39
- package/src/defaults/jsdom.js +0 -95
- package/src/defaults/json-report.js +0 -36
- package/src/defaults/junit-xml-report.js +0 -90
- package/src/defaults/playwright.js +0 -142
- package/src/defaults/puppeteer.js +0 -124
- package/src/defaults/report/common.js +0 -38
- package/src/defaults/report/decompress.js +0 -19
- package/src/defaults/report/default.html +0 -99
- package/src/defaults/report/main.js +0 -69
- package/src/defaults/report/progress.js +0 -60
- package/src/defaults/report/styles.css +0 -66
- package/src/defaults/report.js +0 -91
- package/src/defaults/scan-ui5.js +0 -26
- package/src/defaults/selenium-webdriver/chrome.js +0 -39
- package/src/defaults/selenium-webdriver/edge.js +0 -24
- package/src/defaults/selenium-webdriver/firefox.js +0 -30
- package/src/defaults/selenium-webdriver.js +0 -129
- package/src/defaults/text-report.js +0 -108
- package/src/defaults/webdriverio.js +0 -80
- package/src/end.js +0 -62
- package/src/endpoints.js +0 -219
- package/src/error.js +0 -54
- package/src/get-job-progress.js +0 -78
- package/src/handle.js +0 -43
- package/src/if.js +0 -10
- package/src/inject/jest2qunit.js +0 -289
- package/src/inject/opa-iframe-coverage.js +0 -22
- package/src/inject/post.js +0 -141
- package/src/inject/qunit-hooks.js +0 -107
- package/src/inject/qunit-redirect.js +0 -65
- package/src/inject/ui5-coverage.js +0 -33
- package/src/job-mode.js +0 -65
- package/src/job.js +0 -493
- package/src/npm.js +0 -136
- package/src/options.js +0 -95
- package/src/output.js +0 -739
- package/src/parallelize.js +0 -63
- package/src/qunit-hooks.js +0 -219
- package/src/report.js +0 -89
- package/src/reserve.js +0 -25
- package/src/start.js +0 -133
- package/src/symbols.js +0 -8
- package/src/tests.js +0 -183
- package/src/timeout.js +0 -53
- package/src/tools.js +0 -179
- package/src/ui5.js +0 -199
- package/src/unhandled.js +0 -32
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Host } from './Host.js';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { Exit } from './Exit.js';
|
|
5
|
+
class ProcessStopper {
|
|
6
|
+
_process;
|
|
7
|
+
set process(value) {
|
|
8
|
+
this._process = value;
|
|
9
|
+
}
|
|
10
|
+
async stop() {
|
|
11
|
+
await this._process?.kill();
|
|
12
|
+
return this._process?.closed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class Process {
|
|
16
|
+
static spawn = (command, arguments_, options = {}) => {
|
|
17
|
+
const finalCommand = command === 'node' ? process.argv[0] : command;
|
|
18
|
+
let asyncTask;
|
|
19
|
+
try {
|
|
20
|
+
const stopper = new ProcessStopper();
|
|
21
|
+
asyncTask = Exit.registerAsyncTask({
|
|
22
|
+
name: `Process.spawn(${command},${arguments_.join(',')})`,
|
|
23
|
+
stop: () => stopper.stop()
|
|
24
|
+
});
|
|
25
|
+
const childProcess = spawn(finalCommand, arguments_, options);
|
|
26
|
+
logger.debug({
|
|
27
|
+
source: 'process',
|
|
28
|
+
processId: childProcess.pid,
|
|
29
|
+
message: 'spawned',
|
|
30
|
+
data: { command, arguments: arguments_, options }
|
|
31
|
+
});
|
|
32
|
+
const process = new Process(childProcess, asyncTask);
|
|
33
|
+
stopper.process = process;
|
|
34
|
+
return process;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error({
|
|
38
|
+
source: 'process',
|
|
39
|
+
message: 'spawn failed',
|
|
40
|
+
data: { command, arguments: arguments_, options },
|
|
41
|
+
error
|
|
42
|
+
});
|
|
43
|
+
asyncTask?.[Symbol.dispose]();
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
_stdout = [];
|
|
48
|
+
get stdout() {
|
|
49
|
+
return this._stdout.join('');
|
|
50
|
+
}
|
|
51
|
+
_stderr = [];
|
|
52
|
+
get stderr() {
|
|
53
|
+
return this._stderr.join('');
|
|
54
|
+
}
|
|
55
|
+
_code;
|
|
56
|
+
get code() {
|
|
57
|
+
return this._code;
|
|
58
|
+
}
|
|
59
|
+
_closed;
|
|
60
|
+
get closed() {
|
|
61
|
+
return this._closed;
|
|
62
|
+
}
|
|
63
|
+
_childProcess;
|
|
64
|
+
_asyncTask;
|
|
65
|
+
constructor(childProcess, asyncTask) {
|
|
66
|
+
this._childProcess = childProcess;
|
|
67
|
+
this._asyncTask = asyncTask;
|
|
68
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
69
|
+
this._closed = promise;
|
|
70
|
+
this._childProcess.stdout?.on('data', (buffer) => {
|
|
71
|
+
const message = buffer.toString();
|
|
72
|
+
logger.debug({ source: 'process', processId: this.pid, message, data: { type: 'stdout' } });
|
|
73
|
+
this._stdout.push(message);
|
|
74
|
+
});
|
|
75
|
+
this._childProcess.stderr?.on('data', (buffer) => {
|
|
76
|
+
const message = buffer.toString();
|
|
77
|
+
logger.debug({ source: 'process', processId: this.pid, message, data: { type: 'stderr' } });
|
|
78
|
+
this._stderr.push(message);
|
|
79
|
+
});
|
|
80
|
+
this._childProcess.on('close', (code) => {
|
|
81
|
+
this._code = code ?? 0;
|
|
82
|
+
logger.debug({
|
|
83
|
+
source: 'process',
|
|
84
|
+
processId: this.pid,
|
|
85
|
+
message: 'closed',
|
|
86
|
+
data: { code: this._code }
|
|
87
|
+
});
|
|
88
|
+
this._asyncTask[Symbol.dispose]();
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
get pid() {
|
|
93
|
+
return this._childProcess.pid;
|
|
94
|
+
}
|
|
95
|
+
async kill() {
|
|
96
|
+
logger.debug({
|
|
97
|
+
source: 'process',
|
|
98
|
+
processId: this.pid,
|
|
99
|
+
message: 'kill'
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
if (Host.platform() === 'win32') {
|
|
103
|
+
const killProcess = spawn('taskkill', ['/F', '/T', '/PID', this.pid.toString()], {
|
|
104
|
+
windowsHide: true
|
|
105
|
+
});
|
|
106
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
107
|
+
killProcess.on('close', () => setTimeout(resolve, 0));
|
|
108
|
+
await promise;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(-this.pid);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
process.kill(this.pid);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
logger.debug({
|
|
119
|
+
source: 'process',
|
|
120
|
+
processId: this.pid,
|
|
121
|
+
message: 'killed'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
logger.debug({
|
|
126
|
+
source: 'process',
|
|
127
|
+
processId: this.pid,
|
|
128
|
+
message: 'unable to kill',
|
|
129
|
+
error
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
const ASCII_ESCAPE = '\u001B';
|
|
3
|
+
const CSI = `${ASCII_ESCAPE}[`;
|
|
4
|
+
export class Terminal {
|
|
5
|
+
static isTTY = process.stdout.isTTY;
|
|
6
|
+
static onResize(callback) {
|
|
7
|
+
process.stdout.on('resize', () => callback(Terminal.width));
|
|
8
|
+
}
|
|
9
|
+
static setRawMode(callback) {
|
|
10
|
+
if (callback === false) {
|
|
11
|
+
process.stdin.setRawMode(false);
|
|
12
|
+
process.stdin.pause();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
process.stdin.setRawMode(true);
|
|
16
|
+
process.stdin.on('data', callback);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
static stripVTControlCharacters = stripVTControlCharacters;
|
|
20
|
+
static get width() {
|
|
21
|
+
return process.stdout.columns;
|
|
22
|
+
}
|
|
23
|
+
static write(text) {
|
|
24
|
+
process.stdout.write(text);
|
|
25
|
+
}
|
|
26
|
+
static hideCursor() {
|
|
27
|
+
Terminal.write(`${CSI}?25l`);
|
|
28
|
+
}
|
|
29
|
+
static showCursor() {
|
|
30
|
+
Terminal.write(`${CSI}?25h`);
|
|
31
|
+
}
|
|
32
|
+
static eraseToEnd(lines) {
|
|
33
|
+
if (lines === 0) {
|
|
34
|
+
Terminal.write(`${CSI}0G`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
Terminal.write(`${CSI}0G${CSI}${lines}A${CSI}0J`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
static BLUE = `${CSI}34m`;
|
|
41
|
+
static CYAN = `${CSI}36m`;
|
|
42
|
+
static GREEN = `${CSI}32m`;
|
|
43
|
+
static MAGENTA = `${CSI}35m`;
|
|
44
|
+
static RED = `${CSI}31m`;
|
|
45
|
+
static WHITE = `${CSI}37m`;
|
|
46
|
+
static YELLOW = `${CSI}33m`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { Path } from './Path.js';
|
|
3
|
+
import { BroadcastChannel, Worker, isMainThread, threadId } from 'node:worker_threads';
|
|
4
|
+
import { __sourcesRoot } from './constants.js';
|
|
5
|
+
import { logger } from './logger/proxy.js';
|
|
6
|
+
export class Thread {
|
|
7
|
+
static threadCpuUsage = process.threadCpuUsage.bind(process);
|
|
8
|
+
static createBroadcastChannel = (name) => new BroadcastChannel(name);
|
|
9
|
+
static createWorker = (name, data) => {
|
|
10
|
+
const extension = extname(import.meta.url);
|
|
11
|
+
const workerPath = '../' + name + extension;
|
|
12
|
+
if (process.env['NO_WORKERS']) {
|
|
13
|
+
logger?.debug({ source: 'thread', message: `Creating fiber for ${name}`, data });
|
|
14
|
+
void (async () => {
|
|
15
|
+
const { workerMain } = (await import(workerPath));
|
|
16
|
+
workerMain(data);
|
|
17
|
+
})();
|
|
18
|
+
return {
|
|
19
|
+
on: (event, callback) => {
|
|
20
|
+
if (event === 'exit') {
|
|
21
|
+
callback(0);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
logger?.debug({ source: 'thread', message: `Creating worker for ${name}`, data });
|
|
27
|
+
const bootstrapPath = Path.join(__sourcesRoot, 'platform/workerBootstrap' + extension);
|
|
28
|
+
const js2tsUrl = new URL('js2ts.mjs', import.meta.url).toString();
|
|
29
|
+
const execArgv = extension === '.ts' ? ['--no-warnings', '--import', js2tsUrl] : [];
|
|
30
|
+
const worker = new Worker(bootstrapPath, {
|
|
31
|
+
execArgv,
|
|
32
|
+
workerData: {
|
|
33
|
+
path: workerPath,
|
|
34
|
+
data
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
worker.on('online', () => logger?.debug({ source: 'thread', message: `Worker for ${name} online`, data: { threadId: worker.threadId } }));
|
|
38
|
+
worker.on('exit', () => logger?.debug({ source: 'thread', message: `Worker for ${name} offline`, data: { threadId: worker.threadId } }));
|
|
39
|
+
return worker;
|
|
40
|
+
};
|
|
41
|
+
static isMainThread = isMainThread;
|
|
42
|
+
static threadId = threadId;
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AssertionError } from 'node:assert/strict';
|
|
2
|
+
import { __developmentMode } from './constants.js';
|
|
3
|
+
import { logger } from './logger/proxy.js';
|
|
4
|
+
export const assert = (condition, message = 'Assertion failed') => {
|
|
5
|
+
if (!condition) {
|
|
6
|
+
const error = new AssertionError({ message });
|
|
7
|
+
if (__developmentMode) {
|
|
8
|
+
let { stack } = error;
|
|
9
|
+
stack = stack ? stack.split('\n').slice(1).join('\n') : '';
|
|
10
|
+
logger?.fatal({ source: 'assert', message: (message ?? 'Assertion failed') + stack, error });
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
logger?.fatal({ source: 'assert', message: message ?? 'Assertion failed', error });
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Host } from './Host.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { version } from './version.js';
|
|
4
|
+
export const logEnvironnement = async () => {
|
|
5
|
+
const runnerVersion = await version();
|
|
6
|
+
const now = new Date();
|
|
7
|
+
logger.info({
|
|
8
|
+
source: 'job',
|
|
9
|
+
message: `${runnerVersion} / Node.js ${Host.nodeVersion} / ${now.toISOString()} (${now.getTimezoneOffset()})`
|
|
10
|
+
});
|
|
11
|
+
const cpus = {};
|
|
12
|
+
for (const { model } of Host.cpus()) {
|
|
13
|
+
if (cpus[model]) {
|
|
14
|
+
++cpus[model];
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
cpus[model] = 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const [model, count] of Object.entries(cpus)) {
|
|
21
|
+
if (count === 1) {
|
|
22
|
+
logger.info({ source: 'job', message: `${Host.machine()} / ${model}` });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger.info({ source: 'job', message: `${Host.machine()} / ${count}x ${model}` });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './assert.js';
|
|
2
|
+
export * from './constants.js';
|
|
3
|
+
export * from './environment.js';
|
|
4
|
+
export * from './logger.js';
|
|
5
|
+
export * from './Exit.js';
|
|
6
|
+
export * from './FileSystem.js';
|
|
7
|
+
export * from './Host.js';
|
|
8
|
+
export * from './Http.js';
|
|
9
|
+
export * from './Path.js';
|
|
10
|
+
export * from './Process.js';
|
|
11
|
+
export * from './Terminal.js';
|
|
12
|
+
export * from './Thread.js';
|
|
13
|
+
export * from './ZLib.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Path, Thread, ZLib } from '../index.js';
|
|
2
|
+
import { LogLevel } from './types.js';
|
|
3
|
+
import { toInternalLogAttributes } from './toInternalLogAttributes.js';
|
|
4
|
+
import { createCompressionContext, compress } from './compress.js';
|
|
5
|
+
import { FramedStreamWriter } from '../../utils/node/FramedStreamWriter.js';
|
|
6
|
+
export const MAX_BUFFER_COUNT = 50;
|
|
7
|
+
const FLUSH_INTERVAL_MS = 200;
|
|
8
|
+
const compressionContext = createCompressionContext();
|
|
9
|
+
export const workerMain = ({ configuration }) => {
|
|
10
|
+
const LOG_FILE_NAME = `traces-${new Date().toISOString().slice(0, 19).replaceAll(/[-:]/g, '').replace('T', '-')}.logz`;
|
|
11
|
+
const stream = FramedStreamWriter.create(Path.join(configuration.reportDir, LOG_FILE_NAME));
|
|
12
|
+
const buffer = [];
|
|
13
|
+
let flushTimeout;
|
|
14
|
+
let writePromise = Promise.resolve();
|
|
15
|
+
const flushBuffer = () => {
|
|
16
|
+
if (buffer.length === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const chunk = buffer.join('\n');
|
|
20
|
+
buffer.length = 0;
|
|
21
|
+
clearTimeout(flushTimeout);
|
|
22
|
+
flushTimeout = undefined;
|
|
23
|
+
const compressed = ZLib.deflateRawSync(chunk, { level: ZLib.constants.Z_BEST_COMPRESSION });
|
|
24
|
+
writePromise = writePromise.then(() => stream.write(compressed));
|
|
25
|
+
};
|
|
26
|
+
const log = (attributes) => {
|
|
27
|
+
buffer.push(compress(compressionContext, attributes));
|
|
28
|
+
if (buffer.length >= MAX_BUFFER_COUNT) {
|
|
29
|
+
flushBuffer();
|
|
30
|
+
}
|
|
31
|
+
else if (!flushTimeout) {
|
|
32
|
+
flushTimeout = setTimeout(flushBuffer, FLUSH_INTERVAL_MS);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const _log = (attributes) => log(toInternalLogAttributes(attributes, LogLevel.info));
|
|
36
|
+
const channel = Thread.createBroadcastChannel('logger');
|
|
37
|
+
channel.onmessage = (event) => {
|
|
38
|
+
const { data: message } = event;
|
|
39
|
+
if (message.command === 'terminate') {
|
|
40
|
+
_log({ source: 'logger', message: 'Logger terminating' });
|
|
41
|
+
channel.close();
|
|
42
|
+
flushBuffer();
|
|
43
|
+
void writePromise.then(() => stream.end());
|
|
44
|
+
}
|
|
45
|
+
else if (message.command === 'log') {
|
|
46
|
+
log(message);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
_log({ source: 'logger', message: 'Logger ready' });
|
|
50
|
+
channel.postMessage({
|
|
51
|
+
command: 'ready',
|
|
52
|
+
source: 'allCompressed'
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { LogLevel } from './types.js';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { split } from '../../utils/shared/string.js';
|
|
5
|
+
const ASCII_RECORD_SEPARATOR = '\u001E';
|
|
6
|
+
export const DIGITS = Array.from({ length: 127 - 32 })
|
|
7
|
+
.fill(0)
|
|
8
|
+
.map((_, index) => String.fromCodePoint(32 + index))
|
|
9
|
+
.join('');
|
|
10
|
+
const JSON_VALUE_SEP = ASCII_RECORD_SEPARATOR;
|
|
11
|
+
const MAX_INDEX_DIGITS = 2;
|
|
12
|
+
const CONTEXT_PROCESS_ID = 'p';
|
|
13
|
+
const CONTEXT_SOURCE_ID = 's';
|
|
14
|
+
export const MAX_TIMESTAMP_DIGITS = 7;
|
|
15
|
+
export const MAX_DWORD_DIGITS = 5;
|
|
16
|
+
class Context {
|
|
17
|
+
static compressNumber(value, maxLength) {
|
|
18
|
+
const digits = [];
|
|
19
|
+
while (value > 0) {
|
|
20
|
+
const digit = value % DIGITS.length;
|
|
21
|
+
digits.push(DIGITS[digit]);
|
|
22
|
+
value = (value - digit) / DIGITS.length;
|
|
23
|
+
}
|
|
24
|
+
return digits.join('').padEnd(maxLength, DIGITS[0]);
|
|
25
|
+
}
|
|
26
|
+
static uncompressNumber(value) {
|
|
27
|
+
let result = 0;
|
|
28
|
+
let factor = 1;
|
|
29
|
+
for (const digit of value) {
|
|
30
|
+
const index = DIGITS.indexOf(digit);
|
|
31
|
+
result += index * factor;
|
|
32
|
+
factor *= DIGITS.length;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
fail(message) {
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
_compressWithList({ array, value, compress }) {
|
|
40
|
+
const index = array.findIndex((candidate) => isDeepStrictEqual(candidate, value));
|
|
41
|
+
if (index !== -1) {
|
|
42
|
+
const compressed = Context.compressNumber(index, MAX_INDEX_DIGITS);
|
|
43
|
+
return {
|
|
44
|
+
context: '',
|
|
45
|
+
compressed
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const lastIndex = array.length;
|
|
49
|
+
array.push(value);
|
|
50
|
+
return {
|
|
51
|
+
context: compress(value).join(''),
|
|
52
|
+
compressed: Context.compressNumber(lastIndex, MAX_INDEX_DIGITS)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
_uncompressFromList({ type, array, compressed }) {
|
|
56
|
+
const index = Context.uncompressNumber(compressed);
|
|
57
|
+
return array[index] ?? this.fail(`Invalid ${type} index ${index} (length: ${array.length})`);
|
|
58
|
+
}
|
|
59
|
+
_processes = [];
|
|
60
|
+
compressProcess(value) {
|
|
61
|
+
return this._compressWithList({
|
|
62
|
+
array: this._processes,
|
|
63
|
+
value,
|
|
64
|
+
compress: ({ processId, threadId, isMainThread }) => {
|
|
65
|
+
if (threadId === -1) {
|
|
66
|
+
return [CONTEXT_PROCESS_ID, Context.compressNumber(processId, MAX_DWORD_DIGITS)];
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
CONTEXT_PROCESS_ID,
|
|
70
|
+
Context.compressNumber(processId, MAX_DWORD_DIGITS),
|
|
71
|
+
Context.compressNumber(threadId, MAX_DWORD_DIGITS),
|
|
72
|
+
isMainThread ? '!' : ''
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
addProcess(compressed) {
|
|
78
|
+
const [, cProcessId, cThreadId, cIsMainThread] = split(compressed, 1, MAX_DWORD_DIGITS, MAX_DWORD_DIGITS, 1);
|
|
79
|
+
if (cThreadId) {
|
|
80
|
+
this._processes.push({
|
|
81
|
+
processId: Context.uncompressNumber(cProcessId),
|
|
82
|
+
threadId: Context.uncompressNumber(cThreadId),
|
|
83
|
+
isMainThread: cIsMainThread === '!'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this._processes.push({
|
|
88
|
+
processId: Context.uncompressNumber(cProcessId),
|
|
89
|
+
threadId: -1,
|
|
90
|
+
isMainThread: false
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
uncompressProcess(compressed) {
|
|
95
|
+
return this._uncompressFromList({
|
|
96
|
+
type: 'process',
|
|
97
|
+
array: this._processes,
|
|
98
|
+
compressed
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
_sources = [];
|
|
102
|
+
compressSource(value) {
|
|
103
|
+
return this._compressWithList({
|
|
104
|
+
array: this._sources,
|
|
105
|
+
value,
|
|
106
|
+
compress: (value) => [CONTEXT_SOURCE_ID, value]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
addSource(compressed) {
|
|
110
|
+
this._sources.push(compressed.slice(1));
|
|
111
|
+
}
|
|
112
|
+
uncompressSource(compressed) {
|
|
113
|
+
return this._uncompressFromList({
|
|
114
|
+
type: 'source',
|
|
115
|
+
array: this._sources,
|
|
116
|
+
compressed
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export const createCompressionContext = () => new Context();
|
|
121
|
+
const LEVEL_MAPPING = {
|
|
122
|
+
[LogLevel.debug]: 'D',
|
|
123
|
+
[LogLevel.info]: 'I',
|
|
124
|
+
[LogLevel.warn]: 'W',
|
|
125
|
+
[LogLevel.error]: 'E',
|
|
126
|
+
[LogLevel.fatal]: 'F'
|
|
127
|
+
};
|
|
128
|
+
const LEVELS = Object.values(LEVEL_MAPPING).join('');
|
|
129
|
+
const levelSlot = {
|
|
130
|
+
width: 1,
|
|
131
|
+
compress(_, { level }) {
|
|
132
|
+
return { compressed: LEVEL_MAPPING[level] };
|
|
133
|
+
},
|
|
134
|
+
uncompress(_, compressed) {
|
|
135
|
+
return { level: LEVELS.indexOf(compressed) };
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const timestampSlot = {
|
|
139
|
+
width: MAX_TIMESTAMP_DIGITS,
|
|
140
|
+
compress(_, { timestamp }) {
|
|
141
|
+
return { compressed: Context.compressNumber(timestamp, MAX_TIMESTAMP_DIGITS) };
|
|
142
|
+
},
|
|
143
|
+
uncompress(_, compressed) {
|
|
144
|
+
return { timestamp: Context.uncompressNumber(compressed) };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const processSlot = {
|
|
148
|
+
width: MAX_INDEX_DIGITS,
|
|
149
|
+
compress(context, { processId, threadId, isMainThread }) {
|
|
150
|
+
const { context: contextLine, compressed } = context.compressProcess({ processId, threadId, isMainThread });
|
|
151
|
+
return { contextLine: contextLine || undefined, compressed };
|
|
152
|
+
},
|
|
153
|
+
uncompress(context, compressed) {
|
|
154
|
+
return context.uncompressProcess(compressed);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const sourceSlot = {
|
|
158
|
+
width: MAX_INDEX_DIGITS,
|
|
159
|
+
compress(context, { source }) {
|
|
160
|
+
const { context: contextLine, compressed } = context.compressSource(source);
|
|
161
|
+
return { contextLine: contextLine || undefined, compressed };
|
|
162
|
+
},
|
|
163
|
+
uncompress(context, compressed) {
|
|
164
|
+
return { source: context.uncompressSource(compressed) };
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const pageIdSlot = {
|
|
168
|
+
width: MAX_INDEX_DIGITS,
|
|
169
|
+
compress(_, { pageId }) {
|
|
170
|
+
return { compressed: Context.compressNumber(pageId === undefined ? 0 : pageId + 1, MAX_INDEX_DIGITS) };
|
|
171
|
+
},
|
|
172
|
+
uncompress(_, compressed) {
|
|
173
|
+
const value = Context.uncompressNumber(compressed);
|
|
174
|
+
return value > 0 ? { pageId: value - 1 } : {};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const messageAndExtraSlot = {
|
|
178
|
+
width: 0,
|
|
179
|
+
compress(_, { message, data, error }) {
|
|
180
|
+
const parts = [message.replaceAll(/\r?\n/g, '\r')];
|
|
181
|
+
if (data || error) {
|
|
182
|
+
parts.push(JSON_VALUE_SEP, JSON.stringify([data ?? 0, error ?? 0]).replaceAll(/"(\w+)":/g, (_, name) => `${name}${JSON_VALUE_SEP}`));
|
|
183
|
+
}
|
|
184
|
+
return { compressed: parts.join('') };
|
|
185
|
+
},
|
|
186
|
+
uncompress(_, compressed) {
|
|
187
|
+
const startOfJson = compressed.indexOf(JSON_VALUE_SEP);
|
|
188
|
+
if (startOfJson === -1) {
|
|
189
|
+
return { message: compressed.replaceAll('\r', '\n') };
|
|
190
|
+
}
|
|
191
|
+
const message = compressed.slice(0, startOfJson).replaceAll('\r', '\n');
|
|
192
|
+
const json = compressed
|
|
193
|
+
.slice(startOfJson + 1)
|
|
194
|
+
.replaceAll(new RegExp(String.raw `(\w+)${JSON_VALUE_SEP}`, 'g'), (_, name) => `"${name}":`);
|
|
195
|
+
const [data, error] = JSON.parse(json);
|
|
196
|
+
return {
|
|
197
|
+
message,
|
|
198
|
+
...(data ? { data } : {}),
|
|
199
|
+
...(error ? { error } : {})
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const DATA_LINE_SLOTS = [
|
|
204
|
+
levelSlot,
|
|
205
|
+
timestampSlot,
|
|
206
|
+
processSlot,
|
|
207
|
+
sourceSlot,
|
|
208
|
+
pageIdSlot,
|
|
209
|
+
messageAndExtraSlot
|
|
210
|
+
];
|
|
211
|
+
const FIXED_SLOTS = DATA_LINE_SLOTS.filter((s) => s.width > 0);
|
|
212
|
+
const FIXED_SLOT_WIDTHS = FIXED_SLOTS.map((s) => s.width);
|
|
213
|
+
const VARIABLE_SLOTS = DATA_LINE_SLOTS.filter((s) => s.width === 0);
|
|
214
|
+
assert.ok(VARIABLE_SLOTS.length === 1, 'DATA_LINE_SLOTS must contain exactly one variable-width slot');
|
|
215
|
+
const VARIABLE_SLOT = VARIABLE_SLOTS[0];
|
|
216
|
+
export const _ALL_LOG_ATTRIBUTES_ARE_HANDLED = {
|
|
217
|
+
level: levelSlot,
|
|
218
|
+
timestamp: timestampSlot,
|
|
219
|
+
processId: processSlot,
|
|
220
|
+
threadId: processSlot,
|
|
221
|
+
isMainThread: processSlot,
|
|
222
|
+
source: sourceSlot,
|
|
223
|
+
pageId: pageIdSlot,
|
|
224
|
+
message: messageAndExtraSlot,
|
|
225
|
+
data: messageAndExtraSlot,
|
|
226
|
+
error: messageAndExtraSlot
|
|
227
|
+
};
|
|
228
|
+
export const compress = (context, attributes) => {
|
|
229
|
+
assert.ok(context instanceof Context);
|
|
230
|
+
const contextLines = [];
|
|
231
|
+
const parts = [];
|
|
232
|
+
for (const slot of DATA_LINE_SLOTS) {
|
|
233
|
+
const { contextLine, compressed } = slot.compress(context, attributes);
|
|
234
|
+
if (contextLine)
|
|
235
|
+
contextLines.push(contextLine);
|
|
236
|
+
parts.push(compressed);
|
|
237
|
+
}
|
|
238
|
+
return [...contextLines, parts.join('')].join('\n') + '\n';
|
|
239
|
+
};
|
|
240
|
+
const augmentContext = (context, line) => {
|
|
241
|
+
const firstChar = line.charAt(0);
|
|
242
|
+
if (firstChar === CONTEXT_PROCESS_ID) {
|
|
243
|
+
context.addProcess(line);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
assert.ok(firstChar === CONTEXT_SOURCE_ID, `unexpected context operator ${firstChar}`);
|
|
247
|
+
context.addSource(line);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const uncompressLine = (context, line) => {
|
|
251
|
+
const firstChar = line.charAt(0);
|
|
252
|
+
if (!LEVELS.includes(firstChar)) {
|
|
253
|
+
augmentContext(context, line);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const parts = split(line, ...FIXED_SLOT_WIDTHS);
|
|
257
|
+
const attributes = {};
|
|
258
|
+
for (const [index, fixedSlot] of FIXED_SLOTS.entries()) {
|
|
259
|
+
Object.assign(attributes, fixedSlot.uncompress(context, parts[index]));
|
|
260
|
+
}
|
|
261
|
+
Object.assign(attributes, VARIABLE_SLOT.uncompress(context, parts[FIXED_SLOTS.length]));
|
|
262
|
+
return attributes;
|
|
263
|
+
};
|
|
264
|
+
export const uncompress = (context, compressed) => {
|
|
265
|
+
assert.ok(context instanceof Context);
|
|
266
|
+
const result = [];
|
|
267
|
+
for (const line of compressed.split('\n')) {
|
|
268
|
+
if (!line) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const attributes = uncompressLine(context, line);
|
|
272
|
+
if (attributes) {
|
|
273
|
+
result.push(attributes);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
};
|