ui5-test-runner 5.13.1 → 6.0.0-beta.1
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 +17 -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,58 @@
|
|
|
1
|
+
import { __developmentMode, Exit } from '../../platform/index.js';
|
|
2
|
+
import { LogReader } from './LogReader.js';
|
|
3
|
+
import { serve } from 'reserve';
|
|
4
|
+
import { LogStorage } from './LogStorage.js';
|
|
5
|
+
import { buildREserveConfiguration } from './REserve.js';
|
|
6
|
+
import { BrowserFactory } from '../../browsers/factory.js';
|
|
7
|
+
import { getInitialLogMetrics } from './LogMetrics.js';
|
|
8
|
+
export const log = async (configuration) => {
|
|
9
|
+
const logFileName = configuration.log;
|
|
10
|
+
let stopped = false;
|
|
11
|
+
const metrics = getInitialLogMetrics();
|
|
12
|
+
const storage = LogStorage.create();
|
|
13
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
14
|
+
const abortController = new AbortController();
|
|
15
|
+
const abortSignal = abortController.signal;
|
|
16
|
+
const server = serve(buildREserveConfiguration(storage, metrics, abortController));
|
|
17
|
+
const stop = async () => {
|
|
18
|
+
stopped = true;
|
|
19
|
+
await server?.close();
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
abortSignal.addEventListener('abort', () => {
|
|
23
|
+
void stop();
|
|
24
|
+
});
|
|
25
|
+
Exit.registerAsyncTask({
|
|
26
|
+
name: 'log',
|
|
27
|
+
stop: stop
|
|
28
|
+
});
|
|
29
|
+
const browser = await BrowserFactory.build('puppeteer');
|
|
30
|
+
const browserReady = browser.setup({
|
|
31
|
+
visible: true
|
|
32
|
+
});
|
|
33
|
+
server.on('ready', ({ url, port }) => {
|
|
34
|
+
console.log(url);
|
|
35
|
+
void browserReady
|
|
36
|
+
.then(() => browser.newWindow({
|
|
37
|
+
pageId: 0,
|
|
38
|
+
url: `http://localhost:${port}/`,
|
|
39
|
+
scripts: []
|
|
40
|
+
}))
|
|
41
|
+
.then(() => {
|
|
42
|
+
console.log('Use CTRL+C to exit');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
for await (const item of LogReader.read(logFileName, abortSignal)) {
|
|
46
|
+
if (stopped) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const { type, ...attributes } = item;
|
|
50
|
+
if (type === 'log') {
|
|
51
|
+
storage.add(attributes);
|
|
52
|
+
}
|
|
53
|
+
else if (__developmentMode) {
|
|
54
|
+
Object.assign(metrics, attributes);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await promise;
|
|
58
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { logger } from '../../platform/index.js';
|
|
2
|
+
export const buildREserveConfiguration = (configuration) => {
|
|
3
|
+
const match = /\/((?:test-)?resources\/.*)/;
|
|
4
|
+
let { ui5 } = configuration;
|
|
5
|
+
if (!ui5.endsWith('/')) {
|
|
6
|
+
ui5 += '/';
|
|
7
|
+
}
|
|
8
|
+
const mappingUrl = new URL('$1', ui5).toString();
|
|
9
|
+
return {
|
|
10
|
+
port: configuration.port ?? 0,
|
|
11
|
+
mappings: [
|
|
12
|
+
{
|
|
13
|
+
method: 'GET,HEAD',
|
|
14
|
+
match,
|
|
15
|
+
url: mappingUrl,
|
|
16
|
+
'ignore-unverifiable-certificate': true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
match: /^\/(.*)/,
|
|
20
|
+
cwd: configuration.webapp,
|
|
21
|
+
file: '$1'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
custom: (request) => logger.debug({ source: 'server', message: 'Unhandled request', data: { url: request.url } })
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
status: 404
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { __developmentMode, __sourcesRoot, Path, FileSystem } from '../../platform/index.js';
|
|
2
|
+
import { memoize } from '../../utils/shared/memoize.js';
|
|
3
|
+
export const getAgentSource = memoize(async () => {
|
|
4
|
+
const path = __developmentMode
|
|
5
|
+
? Path.join(__sourcesRoot, '../dist/ui', 'agent.js')
|
|
6
|
+
: Path.join(__sourcesRoot, 'ui/agent.js');
|
|
7
|
+
return FileSystem.readFile(path, 'utf8');
|
|
8
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BrowserFactory } from '../../browsers/factory.js';
|
|
2
|
+
import { Exit, assert, logger } from '../../platform/index.js';
|
|
3
|
+
let browser;
|
|
4
|
+
export const setupBrowser = async (configuration) => {
|
|
5
|
+
assert(configuration.browser === 'puppeteer');
|
|
6
|
+
browser = await BrowserFactory.build('puppeteer');
|
|
7
|
+
const { debugKeepBrowserOpen } = configuration;
|
|
8
|
+
const settings = {
|
|
9
|
+
visible: debugKeepBrowserOpen
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
await browser.setup(settings);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
logger.fatal({ source: 'job', message: 'Unable to setup browser', error });
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
if (debugKeepBrowserOpen) {
|
|
19
|
+
const { newWindow } = browser;
|
|
20
|
+
browser.newWindow = async (settings) => {
|
|
21
|
+
const window = await newWindow.call(browser, settings);
|
|
22
|
+
window.close = async () => { };
|
|
23
|
+
return window;
|
|
24
|
+
};
|
|
25
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
26
|
+
Exit.registerAsyncTask({
|
|
27
|
+
name: 'debugKeepBrowserOpen',
|
|
28
|
+
stop: () => resolve()
|
|
29
|
+
});
|
|
30
|
+
browser.shutdown = () => {
|
|
31
|
+
logger.warn({ source: 'job', message: 'Browser will remain open, use CTRL+C to end command' });
|
|
32
|
+
return promise;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return browser;
|
|
36
|
+
};
|
|
37
|
+
export const getBrowser = () => browser;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { logger, logEnvironnement, Exit, FileSystem, Path, Http } from '../../platform/index.js';
|
|
2
|
+
import { defaults } from '../../configuration/options.js';
|
|
3
|
+
import { parallelize } from '../../utils/shared/parallelize.js';
|
|
4
|
+
import { getAgentSource } from './agent.js';
|
|
5
|
+
import { setupBrowser } from './browser.js';
|
|
6
|
+
import { pageTask } from './pageTask.js';
|
|
7
|
+
import { reportBuilder } from './report.js';
|
|
8
|
+
import { generateHtmlReport } from '../../reports/html.js';
|
|
9
|
+
import { Folder } from '../../utils/node/Folder.js';
|
|
10
|
+
import { server } from './server.js';
|
|
11
|
+
import { formatDuration } from '../../utils/shared/string.js';
|
|
12
|
+
export const test = async (configuration) => {
|
|
13
|
+
await Folder.create(configuration.reportDir);
|
|
14
|
+
logger.start(configuration);
|
|
15
|
+
logger.debug({ source: 'job', message: 'Configuration', data: { defaults, configuration } });
|
|
16
|
+
await logEnvironnement();
|
|
17
|
+
await getAgentSource();
|
|
18
|
+
let browser;
|
|
19
|
+
try {
|
|
20
|
+
const port = await server.start(configuration);
|
|
21
|
+
const version = JSON.parse(await Http.get(`http://localhost:${port}/resources/sap-ui-version.json`));
|
|
22
|
+
const { version: coreVersion } = version.libraries.find(({ name }) => name === 'sap.ui.core') ?? {
|
|
23
|
+
version: 'unknown'
|
|
24
|
+
};
|
|
25
|
+
logger.info({ source: 'job', message: `UI5 version used by the local server: ${coreVersion}` });
|
|
26
|
+
if (!configuration.url) {
|
|
27
|
+
configuration.url = [new URL(configuration.testsuite, `http://localhost:0`).toString()];
|
|
28
|
+
}
|
|
29
|
+
const urls = [...configuration.url.map((url) => url.replace(':0/', `:${port}/`))];
|
|
30
|
+
browser = await setupBrowser(configuration);
|
|
31
|
+
logger.info({ source: 'progress', message: 'Executing pages', pageId: undefined, data: { value: 0, max: 0 } });
|
|
32
|
+
let completed = 0;
|
|
33
|
+
await parallelize(pageTask, urls, {
|
|
34
|
+
parallel: configuration.parallel,
|
|
35
|
+
on: (event) => {
|
|
36
|
+
if (event.type === 'failed') {
|
|
37
|
+
logger.error({ source: 'job', message: 'page failed', error: event.error, data: { url: event.input } });
|
|
38
|
+
Exit.code = -1;
|
|
39
|
+
}
|
|
40
|
+
if (event.type === 'completed') {
|
|
41
|
+
++completed;
|
|
42
|
+
}
|
|
43
|
+
logger.info({
|
|
44
|
+
source: 'progress',
|
|
45
|
+
message: 'Executing pages',
|
|
46
|
+
pageId: undefined,
|
|
47
|
+
data: { value: completed, max: urls.length }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
reportBuilder.finalize();
|
|
52
|
+
FileSystem.writeFileSync(Path.join(configuration.reportDir, 'report.json'), JSON.stringify(reportBuilder.report, undefined, 2), 'utf8');
|
|
53
|
+
await generateHtmlReport(configuration, reportBuilder.report);
|
|
54
|
+
const { duration } = reportBuilder.report.results.summary;
|
|
55
|
+
if (duration) {
|
|
56
|
+
logger.info({ source: 'job', message: `Tests duration: ${formatDuration(duration)}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.error({ source: 'job', message: 'An error occurred', error });
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await browser?.shutdown();
|
|
64
|
+
await server.stop();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { assert, logger } from '../../platform/index.js';
|
|
2
|
+
import { getAgentSource } from './agent.js';
|
|
3
|
+
import { getBrowser } from './browser.js';
|
|
4
|
+
import { Exit, ExitShutdownError } from '../../platform/Exit.js';
|
|
5
|
+
import { setTimeout } from 'node:timers/promises';
|
|
6
|
+
import { reportBuilder } from './report.js';
|
|
7
|
+
let lastPageId = 0;
|
|
8
|
+
const reportQunitProgress = (context, agentState) => {
|
|
9
|
+
if (agentState.isOpa) {
|
|
10
|
+
context.loopDelay = 1000;
|
|
11
|
+
}
|
|
12
|
+
if (agentState.total > 0) {
|
|
13
|
+
const { executed, total, errors } = agentState;
|
|
14
|
+
if (executed !== context.lastExecuted || total !== context.lastTotal) {
|
|
15
|
+
const type = agentState.isOpa ? 'opa' : 'qunit';
|
|
16
|
+
context.lastExecuted = executed;
|
|
17
|
+
context.lastTotal = total;
|
|
18
|
+
context.errors = errors;
|
|
19
|
+
context.type = type;
|
|
20
|
+
logger.info({
|
|
21
|
+
source: 'progress',
|
|
22
|
+
message: context.url,
|
|
23
|
+
pageId: context.pageId,
|
|
24
|
+
data: { max: agentState.total, value: agentState.executed, type, errors }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const queryAgentState = async (context) => {
|
|
30
|
+
const agentState = (await context.page.eval("window['ui5-test-runner'].state"));
|
|
31
|
+
logger.debug({ source: 'page', message: 'agent state', data: { state: agentState }, pageId: context.pageId });
|
|
32
|
+
if (agentState.done) {
|
|
33
|
+
if (agentState.type === 'suite') {
|
|
34
|
+
for (const page of agentState.pages) {
|
|
35
|
+
const pageUrl = new URL(page, context.url).toString();
|
|
36
|
+
context.urls.push(pageUrl);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (agentState.type === 'unknown') {
|
|
40
|
+
logger.fatal({
|
|
41
|
+
source: 'page',
|
|
42
|
+
message: 'Unable to detect page type',
|
|
43
|
+
pageId: context.pageId,
|
|
44
|
+
data: { state: agentState }
|
|
45
|
+
});
|
|
46
|
+
throw new Error('Unable to detect page type');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
assert(agentState.type === 'QUnit');
|
|
50
|
+
reportQunitProgress(context, agentState);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (agentState.type === 'QUnit') {
|
|
55
|
+
reportQunitProgress(context, agentState);
|
|
56
|
+
if (agentState.uncaughtErrors?.length) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
export const pageTask = async function (url, index, urls) {
|
|
63
|
+
const pageId = ++lastPageId;
|
|
64
|
+
logger.debug({ source: 'page', message: 'new page task', pageId, data: { url } });
|
|
65
|
+
logger.info({
|
|
66
|
+
source: 'progress',
|
|
67
|
+
message: url,
|
|
68
|
+
pageId,
|
|
69
|
+
data: { max: 0, value: 1, type: 'unknown', errors: 0 }
|
|
70
|
+
});
|
|
71
|
+
const { promise: taskStopped, resolve: setTaskAsStopped } = Promise.withResolvers();
|
|
72
|
+
using _ = Exit.registerAsyncTask({
|
|
73
|
+
name: url,
|
|
74
|
+
stop: async () => {
|
|
75
|
+
try {
|
|
76
|
+
this.stop(new ExitShutdownError());
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await taskStopped;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
let page;
|
|
86
|
+
let context;
|
|
87
|
+
try {
|
|
88
|
+
const agentSource = await getAgentSource();
|
|
89
|
+
const browser = getBrowser();
|
|
90
|
+
page = await browser.newWindow({
|
|
91
|
+
pageId,
|
|
92
|
+
scripts: [agentSource],
|
|
93
|
+
url
|
|
94
|
+
});
|
|
95
|
+
context = {
|
|
96
|
+
pageId,
|
|
97
|
+
urls,
|
|
98
|
+
url,
|
|
99
|
+
page,
|
|
100
|
+
loopDelay: 250,
|
|
101
|
+
type: 'unknown',
|
|
102
|
+
lastExecuted: 0,
|
|
103
|
+
errors: 0,
|
|
104
|
+
lastTotal: 0
|
|
105
|
+
};
|
|
106
|
+
while (!this.stopRequested) {
|
|
107
|
+
try {
|
|
108
|
+
await setTimeout(context.loopDelay);
|
|
109
|
+
if (await queryAgentState(context)) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger.error({ source: 'page', message: 'An error occurred', error, pageId, data: {} });
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const testResults = (await page.eval("window['ui5-test-runner'].results"));
|
|
119
|
+
logger.debug({ source: 'page', message: 'test results', pageId, data: { results: testResults } });
|
|
120
|
+
reportBuilder.merge(url, testResults);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (context !== undefined) {
|
|
124
|
+
logger.info({
|
|
125
|
+
source: 'progress',
|
|
126
|
+
message: url,
|
|
127
|
+
pageId,
|
|
128
|
+
data: {
|
|
129
|
+
max: context.lastTotal,
|
|
130
|
+
value: context.lastExecuted,
|
|
131
|
+
type: context.type,
|
|
132
|
+
errors: context.errors,
|
|
133
|
+
remove: true
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await page?.close();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.error({ source: 'page', message: 'page.close failed', error, pageId, data: {} });
|
|
142
|
+
}
|
|
143
|
+
setTaskAsStopped();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { assert, Exit, logger, Thread } from '../../platform/index.js';
|
|
2
|
+
import { serve } from 'reserve';
|
|
3
|
+
import { toPlainObject } from '../../utils/shared/object.js';
|
|
4
|
+
import { buildREserveConfiguration } from './REserve.js';
|
|
5
|
+
let channel;
|
|
6
|
+
let serverWorker;
|
|
7
|
+
let stopping = false;
|
|
8
|
+
export const server = {
|
|
9
|
+
async start(configuration) {
|
|
10
|
+
assert(serverWorker === undefined);
|
|
11
|
+
channel = Thread.createBroadcastChannel('server');
|
|
12
|
+
Exit.registerAsyncTask({
|
|
13
|
+
name: 'server',
|
|
14
|
+
stop: () => server.stop()
|
|
15
|
+
});
|
|
16
|
+
logger.debug({ source: 'server', message: 'Starting server' });
|
|
17
|
+
serverWorker = Thread.createWorker('modes/test/server', toPlainObject(configuration));
|
|
18
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
19
|
+
channel.onmessage = ({ data: message }) => {
|
|
20
|
+
if (message.command === 'ready') {
|
|
21
|
+
resolve(message.port);
|
|
22
|
+
}
|
|
23
|
+
else if (message.command === 'error') {
|
|
24
|
+
reject(new Error('failed to start'));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
assert(false, 'unexpected');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return promise;
|
|
31
|
+
},
|
|
32
|
+
async stop() {
|
|
33
|
+
if (stopping) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
stopping = true;
|
|
37
|
+
try {
|
|
38
|
+
assert(serverWorker !== undefined);
|
|
39
|
+
logger.debug({ source: 'server', message: 'Stopping server' });
|
|
40
|
+
channel.postMessage({
|
|
41
|
+
command: 'terminate'
|
|
42
|
+
});
|
|
43
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
44
|
+
channel.onmessage = ({ data: message }) => {
|
|
45
|
+
if (message.command === 'terminated') {
|
|
46
|
+
channel.close();
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
await promise;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
channel.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const workerMain = (configuration) => {
|
|
58
|
+
logger.debug({ source: 'server', message: 'Starting server...' });
|
|
59
|
+
channel = Thread.createBroadcastChannel('server');
|
|
60
|
+
channel.onmessage = ({ data: message }) => {
|
|
61
|
+
if (message.command === 'terminate') {
|
|
62
|
+
logger.debug({ source: 'server', message: 'Stopping server...' });
|
|
63
|
+
void server.close().finally(() => {
|
|
64
|
+
logger.debug({ source: 'server', message: 'Server stopped.' });
|
|
65
|
+
channel.postMessage({
|
|
66
|
+
command: 'terminated'
|
|
67
|
+
});
|
|
68
|
+
channel.close();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
let server;
|
|
73
|
+
try {
|
|
74
|
+
server = serve(buildREserveConfiguration(configuration));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.error({ source: 'server', message: 'An error occurred while configuring', error });
|
|
78
|
+
channel.postMessage({
|
|
79
|
+
command: 'error'
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
server.on('created', () => {
|
|
84
|
+
logger.debug({ source: 'reserve', message: 'created', data: {} });
|
|
85
|
+
});
|
|
86
|
+
for (const eventName of ['incoming', 'redirecting', 'redirected', 'aborted', 'closed']) {
|
|
87
|
+
server.on(eventName, (event) => {
|
|
88
|
+
const { eventName: message, ...data } = event;
|
|
89
|
+
logger.debug({ source: 'reserve', message, data });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
server
|
|
93
|
+
.on('ready', (event) => {
|
|
94
|
+
const { eventName: message, ...data } = event;
|
|
95
|
+
logger.debug({ source: 'reserve', message, data });
|
|
96
|
+
logger.info({ source: 'server', message: `Server listening on: ${event.url}` });
|
|
97
|
+
channel.postMessage({
|
|
98
|
+
command: 'ready',
|
|
99
|
+
port: event.port
|
|
100
|
+
});
|
|
101
|
+
})
|
|
102
|
+
.on('error', (event) => {
|
|
103
|
+
const { eventName: message, reason: error, ...data } = event;
|
|
104
|
+
logger.debug({ source: 'reserve', message, data, error });
|
|
105
|
+
channel.postMessage({
|
|
106
|
+
command: 'error'
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FileSystem } from '../platform/index.js';
|
|
2
|
+
import { Npm } from '../Npm.js';
|
|
3
|
+
export const version = async () => {
|
|
4
|
+
const packageFile = await FileSystem.readFile('package.json', 'utf8');
|
|
5
|
+
const { name, version: installedVersion } = JSON.parse(packageFile);
|
|
6
|
+
console.log(`${name}@${installedVersion}`);
|
|
7
|
+
const latestVersion = await Npm.getLatestVersion(name);
|
|
8
|
+
if (latestVersion !== installedVersion) {
|
|
9
|
+
console.log(`Latest version of ${name} is ${latestVersion}`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ServerResponse, ClientRequest } from 'node:http';
|
|
2
|
+
import { Thread } from './Thread.js';
|
|
3
|
+
import { assert } from './assert.js';
|
|
4
|
+
import { logger } from './logger/proxy.js';
|
|
5
|
+
export class ExitShutdownError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('Exiting application');
|
|
8
|
+
this.name = 'ExitShutdownError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const socketHandleDescriptor = (handle) => {
|
|
12
|
+
if (handle._httpMessage instanceof ServerResponse) {
|
|
13
|
+
const { method, url } = handle._httpMessage.req;
|
|
14
|
+
return `IncomingRequest ${method} ${url}`;
|
|
15
|
+
}
|
|
16
|
+
if (handle._httpMessage instanceof ClientRequest) {
|
|
17
|
+
const { path, method, host, protocol } = handle._httpMessage;
|
|
18
|
+
return `ClientRequest ${method} ${protocol}//${host}${path}`;
|
|
19
|
+
}
|
|
20
|
+
if (handle.localAddress) {
|
|
21
|
+
const { localAddress, localPort, remoteAddress, remotePort } = handle;
|
|
22
|
+
return remoteAddress === undefined
|
|
23
|
+
? `local ${localAddress}:${localPort}`
|
|
24
|
+
: `local ${localAddress}:${localPort} <-> remote ${remoteAddress}:${remotePort}`;
|
|
25
|
+
}
|
|
26
|
+
if (handle._handle) {
|
|
27
|
+
const underlyingHandle = handle._handle;
|
|
28
|
+
const underlyingClassName = underlyingHandle && underlyingHandle.constructor && underlyingHandle.constructor.name;
|
|
29
|
+
return `${underlyingClassName || 'handle unknown'}`;
|
|
30
|
+
}
|
|
31
|
+
return 'unknown';
|
|
32
|
+
};
|
|
33
|
+
const handleDescriptors = {
|
|
34
|
+
ChildProcess: (handle) => {
|
|
35
|
+
return (`pid: ${handle.pid}` +
|
|
36
|
+
(handle.spawnargs ? ` ${handle.spawnargs.map((value) => ('' + value).replaceAll(' ', '␣'))}` : ' unknown'));
|
|
37
|
+
},
|
|
38
|
+
ReadStream: (handle) => (handle.fd === 0 ? `stdin isTTY: ${handle.isTTY}` : `fd: ${handle.fd}`),
|
|
39
|
+
Server: (handle) => `connections: ${handle._connections} events: ${handle._eventsCount}`,
|
|
40
|
+
Socket: socketHandleDescriptor,
|
|
41
|
+
TLSSocket: socketHandleDescriptor,
|
|
42
|
+
WriteStream: (handle) => handle.fd < 3
|
|
43
|
+
? `${[0, 'stdout', 'stderr'][handle.fd]} ${handle.columns}x${handle.rows} isTTY: ${handle.isTTY}`
|
|
44
|
+
: `fd: ${handle.fd}`
|
|
45
|
+
};
|
|
46
|
+
const isStdStream = (handle) => 'fd' in handle && handle.fd >= 0 && handle.fd < 3;
|
|
47
|
+
const unknownHandleDescriptor = () => 'unknown';
|
|
48
|
+
const describeHandle = (handle) => {
|
|
49
|
+
const className = handle && handle.constructor && handle.constructor.name;
|
|
50
|
+
return { className, label: (handleDescriptors[className] ?? unknownHandleDescriptor)(handle) };
|
|
51
|
+
};
|
|
52
|
+
export class Exit {
|
|
53
|
+
static _asyncTaskId = 0;
|
|
54
|
+
static _asyncTasks = [];
|
|
55
|
+
static _checkForHandlesLeak() {
|
|
56
|
+
const undocumentedProcess = process;
|
|
57
|
+
if (!undocumentedProcess._getActiveHandles) {
|
|
58
|
+
logger?.warn({ source: 'exit/handle', message: 'Missing process._getActiveHandles' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const activeHandles = undocumentedProcess._getActiveHandles();
|
|
62
|
+
let messagePortFound = false;
|
|
63
|
+
for (const handle of activeHandles) {
|
|
64
|
+
const { className, label } = describeHandle(handle);
|
|
65
|
+
if (isStdStream(handle)) {
|
|
66
|
+
logger?.debug({ source: 'exit/handle', message: `${className} ${label}` });
|
|
67
|
+
}
|
|
68
|
+
else if (className === 'MessagePort' && !messagePortFound) {
|
|
69
|
+
messagePortFound = true;
|
|
70
|
+
logger?.debug({ source: 'exit/handle', message: `${className} ${label}` });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
logger?.warn({ source: 'exit/handle', message: `possible leak ${className} ${label}` });
|
|
74
|
+
if (className === 'TLSSocket' || className === 'Socket') {
|
|
75
|
+
handle.destroy();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
static set code(code) {
|
|
81
|
+
assert(Thread.isMainThread, 'Exit.code can be set only on main thread');
|
|
82
|
+
process.exitCode = code;
|
|
83
|
+
}
|
|
84
|
+
static registerAsyncTask(task) {
|
|
85
|
+
assert(Thread.isMainThread, 'Exit.registerAsyncTask can be called only on main thread');
|
|
86
|
+
if (Exit._enteringShutdown) {
|
|
87
|
+
throw new ExitShutdownError();
|
|
88
|
+
}
|
|
89
|
+
const id = ++Exit._asyncTaskId;
|
|
90
|
+
this._asyncTasks.push({
|
|
91
|
+
id,
|
|
92
|
+
...task
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
[Symbol.dispose]() {
|
|
96
|
+
const index = Exit._asyncTasks.findIndex((task) => task.id === id);
|
|
97
|
+
try {
|
|
98
|
+
assert(index !== -1, 'unable to find Exit async task to unregister');
|
|
99
|
+
Exit._asyncTasks.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
static _enteringShutdown = false;
|
|
107
|
+
static _logLevel = 'debug';
|
|
108
|
+
static async shutdown() {
|
|
109
|
+
assert(Thread.isMainThread, 'Exit.shutdown can be called only on main thread');
|
|
110
|
+
Exit._enteringShutdown = true;
|
|
111
|
+
const logLevel = Exit._logLevel;
|
|
112
|
+
while (Exit._asyncTasks.length > 0) {
|
|
113
|
+
const task = Exit._asyncTasks.at(-1);
|
|
114
|
+
try {
|
|
115
|
+
logger?.[logLevel]({ source: 'exit', message: `Stopping ${task.name}...` });
|
|
116
|
+
await task.stop();
|
|
117
|
+
logger?.[logLevel]({ source: 'exit', message: `${task.name} stopped.` });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger?.[logLevel]({ source: 'exit', message: `Failed while stopping ${task.name}...`, error });
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (task === Exit._asyncTasks.at(-1)) {
|
|
124
|
+
Exit._asyncTasks.pop();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
Exit._checkForHandlesLeak();
|
|
129
|
+
logger?.[logLevel]({ source: 'exit', message: `Stopping logger...` });
|
|
130
|
+
await logger?.stop();
|
|
131
|
+
}
|
|
132
|
+
static sigInt() {
|
|
133
|
+
Exit._logLevel = 'info';
|
|
134
|
+
void Exit.shutdown();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (Thread.isMainThread) {
|
|
138
|
+
process.on('SIGINT', Exit.sigInt);
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { access, stat, constants, readFile, mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { createReadStream, createWriteStream, writeFileSync } from 'node:fs';
|
|
3
|
+
export class FileSystem {
|
|
4
|
+
static access = access;
|
|
5
|
+
static constants = constants;
|
|
6
|
+
static createReadStream = createReadStream;
|
|
7
|
+
static createWriteStream = createWriteStream;
|
|
8
|
+
static mkdir = mkdir;
|
|
9
|
+
static readFile = readFile;
|
|
10
|
+
static rm = rm;
|
|
11
|
+
static stat = stat;
|
|
12
|
+
static writeFileSync = writeFileSync;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { machine, cpus, platform } from 'node:os';
|
|
2
|
+
export class Host {
|
|
3
|
+
static cpus = cpus;
|
|
4
|
+
static cwd = process.cwd.bind(process);
|
|
5
|
+
static machine = machine;
|
|
6
|
+
static memoryUsage = process.memoryUsage.bind(process);
|
|
7
|
+
static nodeVersion = process.version;
|
|
8
|
+
static pid = process.pid;
|
|
9
|
+
static platform = platform;
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
import { Exit } from './Exit.js';
|
|
3
|
+
let lastRequestId = 0;
|
|
4
|
+
export const Http = {
|
|
5
|
+
async get(url) {
|
|
6
|
+
const requestId = ++lastRequestId;
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
using _ = Exit.registerAsyncTask({
|
|
9
|
+
name: `http.get#${requestId}`,
|
|
10
|
+
stop: () => controller.abort()
|
|
11
|
+
});
|
|
12
|
+
logger.debug({ source: 'http', message: `GET ${url}`, data: { requestId } });
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
signal: controller.signal
|
|
16
|
+
});
|
|
17
|
+
const headers = {};
|
|
18
|
+
for (const [name, value] of response.headers) {
|
|
19
|
+
headers[name] = value;
|
|
20
|
+
}
|
|
21
|
+
logger.debug({
|
|
22
|
+
source: 'http',
|
|
23
|
+
message: `${response.status} ${response.statusText}`,
|
|
24
|
+
data: { requestId, status: response.status, headers }
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error('HTTP request failed');
|
|
28
|
+
}
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
logger.debug({ source: 'http', message: text, data: { requestId } });
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger.debug({ source: 'http', message: 'error caught', data: { requestId }, error });
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|