ui5-test-runner 6.0.0-beta.5 → 6.0.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 +20 -29
- package/dist/Command.js +52 -0
- package/dist/Npm.js +154 -74
- 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 +168 -3
- package/dist/browsers/networkResponse.js +15 -0
- package/dist/browsers/playwright.js +104 -0
- package/dist/browsers/puppeteer.js +54 -88
- 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 +380 -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 +87 -2
- package/dist/modes/log/LogReader.js +3 -2
- package/dist/modes/log/LogStorage.js +31 -27
- package/dist/modes/log/index.js +38 -6
- 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 +2 -1
- 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/assert.js +13 -12
- package/dist/platform/environment.js +9 -18
- package/dist/platform/index.js +1 -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 +110 -19
- 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 +3 -79
- package/dist/configuration/validators/browser.js +0 -11
- package/dist/modes/test/REserve.js +0 -31
- package/dist/modes/test/server.js +0 -109
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { logger, Path } from '../platform/index.js';
|
|
2
|
+
import { Npm } from '../Npm.js';
|
|
3
|
+
import { handleConsoleMessage } from './consoleMessage.js';
|
|
4
|
+
import { handleNetworkResponse } from './networkResponse.js';
|
|
5
|
+
import { buildChromeLaunchOptions } from './chromeOptions.js';
|
|
6
|
+
export const descriptor = {
|
|
7
|
+
supportedBrowsers: ['chrome', 'firefox', 'edge', 'safari'],
|
|
8
|
+
defaultBrowser: 'chrome',
|
|
9
|
+
screenshotFormat: '.png'
|
|
10
|
+
};
|
|
11
|
+
const headerValue = (value) => typeof value === 'string' ? value : (value?.value ?? '');
|
|
12
|
+
const headersToObject = (headers) => Object.fromEntries((headers ?? []).map(({ name, value }) => [name.toLowerCase(), headerValue(value)]));
|
|
13
|
+
export const factory = async (configuration) => {
|
|
14
|
+
const webdriverio = await Npm.import(configuration, 'webdriverio');
|
|
15
|
+
const { remote } = webdriverio;
|
|
16
|
+
let browser;
|
|
17
|
+
const contextToPageId = new Map();
|
|
18
|
+
return {
|
|
19
|
+
async setup(settings) {
|
|
20
|
+
logger.debug({ source: 'webdriverio', message: 'launching browser' });
|
|
21
|
+
process.env['WDIO_LOG_PATH'] = Path.join(configuration.reportDir, 'wdio.log');
|
|
22
|
+
browser = await remote({
|
|
23
|
+
capabilities: {
|
|
24
|
+
browserName: 'chrome',
|
|
25
|
+
webSocketUrl: true,
|
|
26
|
+
'goog:chromeOptions': buildChromeLaunchOptions(settings.options, [
|
|
27
|
+
...(settings.visible ? [] : ['--headless']),
|
|
28
|
+
'--start-maximized',
|
|
29
|
+
...(settings.viewport ? [`--window-size=${settings.viewport.width},${settings.viewport.height}`] : [])
|
|
30
|
+
])
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
browser.on('log.entryAdded', (entry) => {
|
|
34
|
+
const event = entry;
|
|
35
|
+
if (event.type !== 'console') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const pageId = (event.source?.context === undefined ? undefined : contextToPageId.get(event.source.context)) ?? -1;
|
|
39
|
+
handleConsoleMessage(event.method ?? 'log', event.text ?? '', pageId);
|
|
40
|
+
});
|
|
41
|
+
await browser.sessionSubscribe({ events: ['network.responseStarted'] });
|
|
42
|
+
browser.on('network.responseStarted', (entry) => {
|
|
43
|
+
const event = entry;
|
|
44
|
+
const pageId = (event.context === null ? undefined : contextToPageId.get(event.context)) ?? -1;
|
|
45
|
+
handleNetworkResponse(pageId, event.request.url, event.request.method, event.response.status, headersToObject(event.request.headers), headersToObject(event.response.headers));
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
browserName: browser.capabilities.browserName ?? 'chrome',
|
|
49
|
+
browserVersion: browser.capabilities.browserVersion ?? ''
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async newWindow(settings) {
|
|
53
|
+
const { pageId, scripts, url } = settings;
|
|
54
|
+
const { context } = await browser.browsingContextCreate({ type: 'tab', background: true });
|
|
55
|
+
contextToPageId.set(context, pageId);
|
|
56
|
+
for (const script of scripts) {
|
|
57
|
+
await browser.scriptAddPreloadScript({
|
|
58
|
+
functionDeclaration: `() => { ${script} }`,
|
|
59
|
+
contexts: [context]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
await browser.browsingContextNavigate({ context, url, wait: 'interactive' });
|
|
63
|
+
const switchTo = () => browser.switchToWindow(context);
|
|
64
|
+
return {
|
|
65
|
+
async eval(script) {
|
|
66
|
+
await switchTo();
|
|
67
|
+
return browser.execute(`return (${script})`);
|
|
68
|
+
},
|
|
69
|
+
async screenshot(path) {
|
|
70
|
+
await switchTo();
|
|
71
|
+
await browser.saveScreenshot(path);
|
|
72
|
+
},
|
|
73
|
+
async close() {
|
|
74
|
+
contextToPageId.delete(context);
|
|
75
|
+
await browser.browsingContextClose({ context });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
async shutdown() {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
81
|
+
await browser?.deleteSession();
|
|
82
|
+
delete process.env['WDIO_LOG_PATH'];
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -3,18 +3,24 @@ import { Exit, Host } from './platform/index.js';
|
|
|
3
3
|
import { version } from './platform/version.js';
|
|
4
4
|
import { CommandLine } from './configuration/CommandLine.js';
|
|
5
5
|
import { execute } from './modes/execute.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
await
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
import { isIfEvaluatedAsTrue } from './if.js';
|
|
7
|
+
import { sendToParentProcess } from './sendToParentProcess.js';
|
|
8
|
+
try {
|
|
9
|
+
const { name: cliName } = await version();
|
|
10
|
+
const indexOfCli = Host.argv.findIndex((value) => /[\\/]cli(\.[tj]s)?$/.exec(value) || value.endsWith(cliName));
|
|
11
|
+
const configuration = await CommandLine.buildConfigurationFrom(Host.cwd(), indexOfCli === -1 ? [] : Host.argv.slice(indexOfCli + 1));
|
|
12
|
+
if (await isIfEvaluatedAsTrue(configuration)) {
|
|
13
|
+
await execute(configuration);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(String.raw `/!\ Skipping execution (--if)`);
|
|
17
|
+
sendToParentProcess({ type: 'skip' });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
15
21
|
console.error(error);
|
|
16
22
|
Exit.code = -1;
|
|
17
|
-
}
|
|
18
|
-
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
19
25
|
await Exit.shutdown();
|
|
20
|
-
}
|
|
26
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { indexedOptions } from './indexedOptions.js';
|
|
2
2
|
import { ConfigurationValidator } from './ConfigurationValidator.js';
|
|
3
3
|
import { OptionValidationError } from './OptionValidationError.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isLikeAnUrl } from './validators/url.js';
|
|
5
|
+
import { assert, Host } from '../platform/index.js';
|
|
5
6
|
const setOption = (configuration, option, value) => {
|
|
6
7
|
const name = option.name;
|
|
7
8
|
if (value === undefined) {
|
|
@@ -11,12 +12,12 @@ const setOption = (configuration, option, value) => {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
else if (option.multiple !== true) {
|
|
14
|
-
configuration.errors.push(
|
|
15
|
+
configuration.errors.push(OptionValidationError.createMissingValue(option));
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
else {
|
|
18
19
|
if (option.multiple) {
|
|
19
|
-
if (!(option.name
|
|
20
|
+
if (!Object.hasOwn(configuration, option.name)) {
|
|
20
21
|
Object.assign(configuration, {
|
|
21
22
|
[name]: []
|
|
22
23
|
});
|
|
@@ -35,10 +36,13 @@ const switchOption = (configuration, currentOption, name) => {
|
|
|
35
36
|
setOption(configuration, currentOption);
|
|
36
37
|
}
|
|
37
38
|
const option = indexedOptions[name];
|
|
38
|
-
if (
|
|
39
|
-
|
|
39
|
+
if (option) {
|
|
40
|
+
return option;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
if (Host.env['IGNORE_UNKNOWN_OPTION']) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
throw OptionValidationError.createUnknown(name);
|
|
42
46
|
};
|
|
43
47
|
const positionalOption = {
|
|
44
48
|
name: 'positional',
|
|
@@ -46,29 +50,38 @@ const positionalOption = {
|
|
|
46
50
|
type: 'string'
|
|
47
51
|
};
|
|
48
52
|
const handlePositional = (configuration, value) => {
|
|
49
|
-
if (
|
|
53
|
+
if (isLikeAnUrl(value)) {
|
|
50
54
|
setOption(configuration, indexedOptions.url, value);
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
|
-
const shortcuts = ['
|
|
57
|
+
const shortcuts = ['version', 'help'];
|
|
54
58
|
for (const shortcut of shortcuts) {
|
|
55
59
|
if (value === shortcut) {
|
|
56
60
|
setOption(configuration, indexedOptions[shortcut]);
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
configuration.errors.push(
|
|
64
|
+
configuration.errors.push(OptionValidationError.createUnprocessable(positionalOption, value));
|
|
65
|
+
};
|
|
66
|
+
const NEGATIVE_NUMBER = /^-\d+$|^-\d*\.\d+$/;
|
|
67
|
+
const switchOptionFromToken = (configuration, currentOption, argument) => {
|
|
68
|
+
if (argument.startsWith('--')) {
|
|
69
|
+
return { option: switchOption(configuration, currentOption, argument.slice(2)) };
|
|
70
|
+
}
|
|
71
|
+
return argument.startsWith('-')
|
|
72
|
+
? { option: switchOption(configuration, currentOption, argument.slice(1)) }
|
|
73
|
+
: undefined;
|
|
61
74
|
};
|
|
62
75
|
const traverseArguments = (configuration, argv) => {
|
|
63
76
|
let currentOption;
|
|
64
77
|
for (const argument of argv) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const isValueForCurrentOption = currentOption !== undefined && NEGATIVE_NUMBER.test(argument);
|
|
79
|
+
if (!isValueForCurrentOption) {
|
|
80
|
+
const switched = switchOptionFromToken(configuration, currentOption, argument);
|
|
81
|
+
if (switched) {
|
|
82
|
+
currentOption = switched.option;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
72
85
|
}
|
|
73
86
|
if (currentOption === undefined) {
|
|
74
87
|
handlePositional(configuration, argument);
|
|
@@ -86,26 +99,33 @@ const traverseArguments = (configuration, argv) => {
|
|
|
86
99
|
};
|
|
87
100
|
export const CommandLine = {
|
|
88
101
|
async buildConfigurationFrom(cwd, argv) {
|
|
89
|
-
const configuration = { cwd, errors: [] };
|
|
102
|
+
const configuration = Object.create({ cwd, errors: [] });
|
|
90
103
|
traverseArguments(configuration, argv);
|
|
91
|
-
const
|
|
104
|
+
const cliKeys = new Set(Object.keys(configuration));
|
|
92
105
|
let validatedConfiguration;
|
|
93
106
|
try {
|
|
94
|
-
validatedConfiguration = await ConfigurationValidator.validate(
|
|
107
|
+
validatedConfiguration = await ConfigurationValidator.validate(configuration);
|
|
95
108
|
}
|
|
96
109
|
catch (error) {
|
|
97
|
-
if (errors.length > 0 && error instanceof AggregateError) {
|
|
98
|
-
errors.push(...error.errors);
|
|
110
|
+
if (configuration.errors.length > 0 && error instanceof AggregateError) {
|
|
111
|
+
configuration.errors.push(...error.errors);
|
|
99
112
|
}
|
|
100
113
|
else {
|
|
101
|
-
errors.push(error);
|
|
114
|
+
configuration.errors.push(error);
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
|
-
if (errors.length === 1) {
|
|
105
|
-
throw errors[0];
|
|
117
|
+
if (configuration.errors.length === 1) {
|
|
118
|
+
throw configuration.errors[0];
|
|
119
|
+
}
|
|
120
|
+
if (configuration.errors.length > 0) {
|
|
121
|
+
throw new AggregateError(configuration.errors, 'Multiple errors occurred');
|
|
106
122
|
}
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
assert(validatedConfiguration !== undefined);
|
|
124
|
+
validatedConfiguration.sources = {
|
|
125
|
+
...validatedConfiguration.sources
|
|
126
|
+
};
|
|
127
|
+
for (const key of cliKeys) {
|
|
128
|
+
validatedConfiguration.sources[key] = 'cli';
|
|
109
129
|
}
|
|
110
130
|
return validatedConfiguration;
|
|
111
131
|
}
|
|
@@ -2,20 +2,24 @@ import { options, defaults } from './options.js';
|
|
|
2
2
|
import { indexedOptions } from './indexedOptions.js';
|
|
3
3
|
import { validators } from './validators/index.js';
|
|
4
4
|
import { OptionValidationError } from './OptionValidationError.js';
|
|
5
|
+
import { validations } from './validations.js';
|
|
6
|
+
import { derivations } from './derivations.js';
|
|
5
7
|
import { Modes } from '../modes/Modes.js';
|
|
8
|
+
import { assert, FileSystem, Host, Path } from '../platform/index.js';
|
|
9
|
+
const MAX_CONFIG_DEPTH = 10;
|
|
6
10
|
const assertIfConfiguration = (value) => {
|
|
7
11
|
const errors = [];
|
|
8
12
|
for (const key of Object.keys(value)) {
|
|
9
13
|
const option = indexedOptions[key];
|
|
10
14
|
if (option) {
|
|
11
15
|
if (key === option.short) {
|
|
12
|
-
errors.push(
|
|
16
|
+
errors.push(OptionValidationError.createShortName(option));
|
|
13
17
|
}
|
|
14
18
|
else if (key !== option.name) {
|
|
15
|
-
errors.push(
|
|
19
|
+
errors.push(OptionValidationError.createKebabCase(option));
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
|
-
else {
|
|
22
|
+
else if (!Host.env['IGNORE_UNKNOWN_OPTION']) {
|
|
19
23
|
errors.push(OptionValidationError.createUnknown(key));
|
|
20
24
|
}
|
|
21
25
|
}
|
|
@@ -27,25 +31,123 @@ const assertIfConfiguration = (value) => {
|
|
|
27
31
|
throw error;
|
|
28
32
|
}
|
|
29
33
|
};
|
|
34
|
+
const callValidator = (option, value, configuration) => validators[option.type](option, value, configuration);
|
|
30
35
|
const validateValue = async (option, configuration) => {
|
|
31
36
|
const value = configuration[option.name];
|
|
32
37
|
if ('multiple' in option) {
|
|
33
38
|
const validatedValues = [];
|
|
34
39
|
if (Array.isArray(value)) {
|
|
35
40
|
for (const valueItem of value) {
|
|
36
|
-
validatedValues.push(await
|
|
41
|
+
validatedValues.push(await callValidator(option, valueItem, configuration));
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
else {
|
|
40
|
-
validatedValues.push(await
|
|
45
|
+
validatedValues.push(await callValidator(option, value, configuration));
|
|
41
46
|
}
|
|
42
47
|
return validatedValues;
|
|
43
48
|
}
|
|
44
|
-
return await
|
|
49
|
+
return await callValidator(option, value, configuration);
|
|
50
|
+
};
|
|
51
|
+
const loadConfigFile = async (configPath) => {
|
|
52
|
+
let content;
|
|
53
|
+
try {
|
|
54
|
+
content = await FileSystem.readFile(configPath, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw OptionValidationError.createConfigReadError(indexedOptions.config, configPath, error);
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(content);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw OptionValidationError.createConfigInvalidJson(indexedOptions.config, configPath, error);
|
|
65
|
+
}
|
|
66
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
67
|
+
throw OptionValidationError.createConfigNotObject(indexedOptions.config, configPath);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
};
|
|
71
|
+
const extractConfigEntries = (parsed, configDirectory) => {
|
|
72
|
+
const forcedKeys = new Set();
|
|
73
|
+
const configFileKeys = new Set();
|
|
74
|
+
const configFileObject = {};
|
|
75
|
+
for (const [rawKey, value] of Object.entries(parsed)) {
|
|
76
|
+
const key = rawKey.startsWith('!') ? rawKey.slice(1) : rawKey;
|
|
77
|
+
if (rawKey.startsWith('!')) {
|
|
78
|
+
forcedKeys.add(key);
|
|
79
|
+
}
|
|
80
|
+
configFileObject[key] = value;
|
|
81
|
+
configFileKeys.add(key);
|
|
82
|
+
}
|
|
83
|
+
if (configFileKeys.has('cwd') && typeof configFileObject['cwd'] === 'string') {
|
|
84
|
+
const rawCwd = configFileObject['cwd'];
|
|
85
|
+
if (!Path.isAbsolute(rawCwd)) {
|
|
86
|
+
configFileObject['cwd'] = Path.join(configDirectory, rawCwd);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
configFileObject['cwd'] = configDirectory;
|
|
91
|
+
}
|
|
92
|
+
return { forcedKeys, configFileKeys, configFileObject };
|
|
93
|
+
};
|
|
94
|
+
const finalizeConfiguration = async (merged) => {
|
|
95
|
+
const hasExplicitReportDirectory = Object.hasOwn(merged, 'reportDir');
|
|
96
|
+
for (const option of options) {
|
|
97
|
+
if (!Object.hasOwn(merged, option.name) && merged[option.name] && option.type === 'fs-entry') {
|
|
98
|
+
Object.assign(merged, {
|
|
99
|
+
[option.name]: await validateValue(option, merged)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const batchReportDirectory = Host.env['UI5TR_BATCH_REPORT_DIR'];
|
|
104
|
+
if (batchReportDirectory && !hasExplicitReportDirectory) {
|
|
105
|
+
const reportDirectoryOption = options.find((option) => option.name === 'reportDir');
|
|
106
|
+
assert(reportDirectoryOption !== undefined);
|
|
107
|
+
Object.assign(merged, {
|
|
108
|
+
reportDir: await validateValue(reportDirectoryOption, { ...merged, reportDir: batchReportDirectory })
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
for (const option of options) {
|
|
112
|
+
if (!(option.type === 'fs-entry' &&
|
|
113
|
+
typeof merged[option.name] === 'string' &&
|
|
114
|
+
merged[option.name].includes('{{reportDir}}'))) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const substituted = Path.join(merged.reportDir, merged[option.name].split('{{reportDir}}', 2)[1]);
|
|
118
|
+
Object.assign(merged, { [option.name]: await validateValue(option, { ...merged, [option.name]: substituted }) });
|
|
119
|
+
}
|
|
120
|
+
for (const derive of derivations) {
|
|
121
|
+
derive(merged);
|
|
122
|
+
}
|
|
123
|
+
merged.mode = ConfigurationValidator.computeMode(merged);
|
|
124
|
+
for (const validate of validations) {
|
|
125
|
+
validate(merged);
|
|
126
|
+
}
|
|
45
127
|
};
|
|
46
128
|
export const ConfigurationValidator = {
|
|
47
|
-
merge(configuration) {
|
|
48
|
-
|
|
129
|
+
async merge(configuration, explicitKeys, depth) {
|
|
130
|
+
if (!configuration.config || !Path.isAbsolute(configuration.config)) {
|
|
131
|
+
return configuration;
|
|
132
|
+
}
|
|
133
|
+
if (depth >= MAX_CONFIG_DEPTH) {
|
|
134
|
+
throw OptionValidationError.createConfigNestingDepth(indexedOptions.config, MAX_CONFIG_DEPTH);
|
|
135
|
+
}
|
|
136
|
+
const parsed = await loadConfigFile(configuration.config);
|
|
137
|
+
const configDirectory = Path.dirname(configuration.config);
|
|
138
|
+
const { forcedKeys, configFileKeys, configFileObject } = extractConfigEntries(parsed, configDirectory);
|
|
139
|
+
const fileConfig = await this.validate(configFileObject, depth + 1);
|
|
140
|
+
for (const [key, value] of Object.entries(fileConfig)) {
|
|
141
|
+
if (key === 'mode' ||
|
|
142
|
+
key === 'config' ||
|
|
143
|
+
(key === 'cwd' && !configFileKeys.has('cwd')) ||
|
|
144
|
+
(!forcedKeys.has(key) && explicitKeys.has(key))) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
Object.assign(configuration, { [key]: value });
|
|
148
|
+
configuration.sources[key] = 'config';
|
|
149
|
+
}
|
|
150
|
+
return configuration;
|
|
49
151
|
},
|
|
50
152
|
computeMode(configuration) {
|
|
51
153
|
if (configuration.help) {
|
|
@@ -54,26 +156,38 @@ export const ConfigurationValidator = {
|
|
|
54
156
|
if (configuration.version) {
|
|
55
157
|
return Modes.version;
|
|
56
158
|
}
|
|
159
|
+
if (configuration.dumpConfig) {
|
|
160
|
+
return Modes.dumpConfig;
|
|
161
|
+
}
|
|
162
|
+
if (configuration.mcp) {
|
|
163
|
+
return Modes.mcp;
|
|
164
|
+
}
|
|
165
|
+
if (configuration.batch) {
|
|
166
|
+
return Modes.batch;
|
|
167
|
+
}
|
|
57
168
|
if (configuration.log) {
|
|
58
169
|
return Modes.log;
|
|
59
170
|
}
|
|
60
|
-
|
|
61
|
-
return Modes.remote;
|
|
62
|
-
}
|
|
63
|
-
return Modes.legacy;
|
|
171
|
+
return configuration.url ? Modes.remote : Modes.legacy;
|
|
64
172
|
},
|
|
65
|
-
async validate(configuration) {
|
|
173
|
+
async validate(configuration, depth = 0) {
|
|
66
174
|
const withDefaults = Object.assign(Object.create(defaults), configuration);
|
|
67
175
|
assertIfConfiguration(withDefaults);
|
|
68
|
-
const
|
|
69
|
-
merged.mode = this.computeMode(merged);
|
|
176
|
+
const explicitKeys = new Set(Object.keys(withDefaults));
|
|
70
177
|
for (const option of options) {
|
|
71
|
-
if (Object.
|
|
72
|
-
Object.assign(
|
|
73
|
-
[option.name]: await validateValue(option,
|
|
178
|
+
if (Object.hasOwn(withDefaults, option.name)) {
|
|
179
|
+
Object.assign(withDefaults, {
|
|
180
|
+
[option.name]: await validateValue(option, withDefaults)
|
|
74
181
|
});
|
|
75
182
|
}
|
|
76
183
|
}
|
|
184
|
+
if (!Object.hasOwn(withDefaults, 'sources')) {
|
|
185
|
+
Object.assign(withDefaults, { sources: {} });
|
|
186
|
+
}
|
|
187
|
+
const merged = await this.merge(withDefaults, explicitKeys, depth);
|
|
188
|
+
if (depth === 0) {
|
|
189
|
+
await finalizeConfiguration(merged);
|
|
190
|
+
}
|
|
77
191
|
return merged;
|
|
78
192
|
}
|
|
79
193
|
};
|
|
@@ -1,9 +1,57 @@
|
|
|
1
1
|
export class OptionValidationError extends Error {
|
|
2
|
+
static createInvalidValue(option) {
|
|
3
|
+
return new OptionValidationError(option, `Invalid value for ${option.name}`);
|
|
4
|
+
}
|
|
2
5
|
static createUnknown(name) {
|
|
3
|
-
return new OptionValidationError({ name, type: 'string', description: 'unknown' },
|
|
6
|
+
return new OptionValidationError({ name, type: 'string', description: 'unknown' }, `Unknown option ${name}`);
|
|
7
|
+
}
|
|
8
|
+
static createShortName(option) {
|
|
9
|
+
return new OptionValidationError(option, 'Do not use short name');
|
|
10
|
+
}
|
|
11
|
+
static createKebabCase(option) {
|
|
12
|
+
return new OptionValidationError(option, 'Do not use kebab-case');
|
|
13
|
+
}
|
|
14
|
+
static createMissingValue(option) {
|
|
15
|
+
return new OptionValidationError(option, `Missing value for ${option.name}`);
|
|
16
|
+
}
|
|
17
|
+
static createUnprocessable(option, value) {
|
|
18
|
+
return new OptionValidationError(option, `Unable to process: ${value}`);
|
|
19
|
+
}
|
|
20
|
+
static createInvalidRegexp(option, cause) {
|
|
21
|
+
return new OptionValidationError(option, `Invalid regexp set to ${option.name}`, cause);
|
|
22
|
+
}
|
|
23
|
+
static createFsAccessError(option, cause) {
|
|
24
|
+
return new OptionValidationError(option, 'Unable to access file system entry', cause);
|
|
25
|
+
}
|
|
26
|
+
static createFsNotFolder(option) {
|
|
27
|
+
return new OptionValidationError(option, 'Not a folder');
|
|
28
|
+
}
|
|
29
|
+
static createFsNotFile(option) {
|
|
30
|
+
return new OptionValidationError(option, 'Not a file');
|
|
31
|
+
}
|
|
32
|
+
static createFsCheckTypeError(option, cause) {
|
|
33
|
+
return new OptionValidationError(option, 'Unable to check type', cause);
|
|
34
|
+
}
|
|
35
|
+
static createFsExpectedString(option) {
|
|
36
|
+
return new OptionValidationError(option, 'Expected string');
|
|
37
|
+
}
|
|
38
|
+
static createConfigNestingDepth(option, maxDepth) {
|
|
39
|
+
return new OptionValidationError(option, `config file nesting exceeded maximum depth of ${maxDepth}`);
|
|
40
|
+
}
|
|
41
|
+
static createConfigReadError(option, path, cause) {
|
|
42
|
+
return new OptionValidationError(option, `cannot read config file ${path}`, cause);
|
|
43
|
+
}
|
|
44
|
+
static createConfigInvalidJson(option, path, cause) {
|
|
45
|
+
return new OptionValidationError(option, `config file ${path} is not valid JSON`, cause);
|
|
46
|
+
}
|
|
47
|
+
static createConfigNotObject(option, path) {
|
|
48
|
+
return new OptionValidationError(option, `config file ${path} must be a JSON object`);
|
|
49
|
+
}
|
|
50
|
+
static createValidationError(option, message) {
|
|
51
|
+
return new OptionValidationError(option, `invalid use of ${option.name}: ${message}`);
|
|
4
52
|
}
|
|
5
53
|
_option;
|
|
6
|
-
constructor(option, message
|
|
54
|
+
constructor(option, message, cause) {
|
|
7
55
|
super(message);
|
|
8
56
|
this._option = option;
|
|
9
57
|
this.name = 'OptionValidationError';
|