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,7 @@
|
|
|
1
|
+
import { OptionValidationError } from '../OptionValidationError.js';
|
|
2
|
+
export const enumeration = (option, value) => {
|
|
3
|
+
if (typeof value !== 'string' || !option.typeModifiers?.has(value)) {
|
|
4
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
import { FileSystem, Host, Path } from '../../platform/index.js';
|
|
2
2
|
import { OptionValidationError } from '../OptionValidationError.js';
|
|
3
3
|
const fsCheckAccess = async (option, path) => {
|
|
4
|
-
const
|
|
4
|
+
const isOverwrite = option.typeModifiers?.has('overwrite') ?? false;
|
|
5
5
|
try {
|
|
6
|
-
const mode = FileSystem.constants.R_OK | (
|
|
6
|
+
const mode = FileSystem.constants.R_OK | (isOverwrite ? FileSystem.constants.W_OK : 0);
|
|
7
7
|
await FileSystem.access(path, mode);
|
|
8
8
|
}
|
|
9
9
|
catch (error) {
|
|
10
|
-
if (
|
|
10
|
+
if (isOverwrite) {
|
|
11
11
|
const code = typeof error === 'object' && error && 'code' in error && error.code;
|
|
12
12
|
if (code === 'EACCES') {
|
|
13
|
-
throw
|
|
13
|
+
throw OptionValidationError.createFsAccessError(option, error);
|
|
14
14
|
}
|
|
15
15
|
if (code === 'ENOENT') {
|
|
16
16
|
return path;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
|
-
throw
|
|
20
|
+
throw OptionValidationError.createFsAccessError(option, error);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
return undefined;
|
|
24
24
|
};
|
|
25
25
|
const fsCheckStat = async (option, path) => {
|
|
26
|
-
const
|
|
26
|
+
const isFile = option.typeModifiers?.has('file') ?? false;
|
|
27
27
|
try {
|
|
28
28
|
const stat = await FileSystem.stat(path);
|
|
29
|
-
if (!
|
|
30
|
-
throw
|
|
29
|
+
if (!isFile && !stat.isDirectory()) {
|
|
30
|
+
throw OptionValidationError.createFsNotFolder(option);
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
33
|
-
throw
|
|
32
|
+
if (isFile && !stat.isFile()) {
|
|
33
|
+
throw OptionValidationError.createFsNotFile(option);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
|
-
throw
|
|
37
|
+
throw OptionValidationError.createFsCheckTypeError(option, error);
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
export const fsOption = async (option, value, configuration) => {
|
|
41
41
|
if (typeof value !== 'string') {
|
|
42
|
-
throw
|
|
42
|
+
throw OptionValidationError.createFsExpectedString(option);
|
|
43
43
|
}
|
|
44
44
|
let path;
|
|
45
45
|
if (Path.isAbsolute(value)) {
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { boolean } from './boolean.js';
|
|
2
|
-
import {
|
|
2
|
+
import { enumeration } from './enumeration.js';
|
|
3
3
|
import { fsEntry } from './fsEntry.js';
|
|
4
4
|
import { integer } from './integer.js';
|
|
5
|
+
import { json } from './json.js';
|
|
6
|
+
import { lib } from './libraryMapping.js';
|
|
5
7
|
import { percent } from './percent.js';
|
|
6
8
|
import { regexp } from './regexp.js';
|
|
9
|
+
import { secret } from './secret.js';
|
|
7
10
|
import { string } from './string.js';
|
|
8
11
|
import { timeout } from './timeout.js';
|
|
9
12
|
import { url } from './url.js';
|
|
10
13
|
export const validators = {
|
|
11
14
|
boolean,
|
|
12
|
-
|
|
15
|
+
enumeration,
|
|
13
16
|
'fs-entry': fsEntry,
|
|
14
17
|
integer,
|
|
18
|
+
json,
|
|
19
|
+
'library-mapping': lib,
|
|
15
20
|
percent,
|
|
16
21
|
regexp,
|
|
22
|
+
secret,
|
|
17
23
|
string,
|
|
18
24
|
timeout,
|
|
19
25
|
url
|
|
@@ -4,7 +4,13 @@ export const integer = (option, value) => {
|
|
|
4
4
|
value = Number.parseInt(value);
|
|
5
5
|
}
|
|
6
6
|
if (typeof value !== 'number' || value % 1 !== 0) {
|
|
7
|
-
throw
|
|
7
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
8
|
+
}
|
|
9
|
+
if (value < 0 && option.typeModifiers?.has('positive')) {
|
|
10
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
11
|
+
}
|
|
12
|
+
if (value === 0 && option.typeModifiers?.has('non-zero')) {
|
|
13
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
8
14
|
}
|
|
9
15
|
return value;
|
|
10
16
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { OptionValidationError } from '../OptionValidationError.js';
|
|
2
|
+
const isJsonObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
export const json = (option, value) => {
|
|
4
|
+
if (isJsonObject(value)) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
if (typeof value !== 'string') {
|
|
8
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(value);
|
|
12
|
+
if (!isJsonObject(parsed)) {
|
|
13
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof OptionValidationError) {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
throw OptionValidationError.createConfigInvalidJson(option, value, error);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { OptionValidationError } from '../OptionValidationError.js';
|
|
2
|
+
import { validate } from '../../utils/shared/schema.js';
|
|
3
|
+
import { fsOption } from './fsEntry.js';
|
|
4
|
+
const libraryMappingSchema = {
|
|
5
|
+
resourcesSubFolder: 'string',
|
|
6
|
+
sourceFolder: 'string'
|
|
7
|
+
};
|
|
8
|
+
export const lib = async (option, value, configuration) => {
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
const [resourcesSubFolder, sourceFolder, extra] = value.split('=', 3);
|
|
11
|
+
if (extra) {
|
|
12
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
13
|
+
}
|
|
14
|
+
value = {
|
|
15
|
+
resourcesSubFolder,
|
|
16
|
+
sourceFolder: sourceFolder ?? resourcesSubFolder
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
validate(value, libraryMappingSchema);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
24
|
+
}
|
|
25
|
+
const { resourcesSubFolder, sourceFolder } = value;
|
|
26
|
+
const validatedSourceFolder = await fsOption(option, sourceFolder, configuration);
|
|
27
|
+
return { resourcesSubFolder, sourceFolder: validatedSourceFolder };
|
|
28
|
+
};
|
|
@@ -4,7 +4,7 @@ export const percent = (option, value) => {
|
|
|
4
4
|
const match = /^(\d{0,3}(:?\.\d{0,3})?)%$/.exec(value);
|
|
5
5
|
if (match) {
|
|
6
6
|
const [, digits = '0'] = match;
|
|
7
|
-
const float = Number
|
|
7
|
+
const float = Number(digits);
|
|
8
8
|
if (float >= 0 && float <= 100) {
|
|
9
9
|
value = float;
|
|
10
10
|
}
|
|
@@ -13,5 +13,5 @@ export const percent = (option, value) => {
|
|
|
13
13
|
if (typeof value === 'number' && !Number.isNaN(value) && value >= 0 && value <= 100) {
|
|
14
14
|
return value;
|
|
15
15
|
}
|
|
16
|
-
throw
|
|
16
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
17
17
|
};
|
|
@@ -4,7 +4,7 @@ export const regexp = (option, value) => {
|
|
|
4
4
|
return value;
|
|
5
5
|
}
|
|
6
6
|
if (typeof value !== 'string' || !value) {
|
|
7
|
-
throw
|
|
7
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
8
8
|
}
|
|
9
9
|
try {
|
|
10
10
|
const match = /^\/(.+)\/(\w*)$/.exec(value);
|
|
@@ -15,6 +15,6 @@ export const regexp = (option, value) => {
|
|
|
15
15
|
return new RegExp(value);
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
-
throw
|
|
18
|
+
throw OptionValidationError.createInvalidRegexp(option, error);
|
|
19
19
|
}
|
|
20
20
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SecretValue } from '../../types/SecretValue.js';
|
|
2
|
+
import { OptionValidationError } from '../OptionValidationError.js';
|
|
3
|
+
export const secret = (option, value) => {
|
|
4
|
+
if (typeof value !== 'string' || !value) {
|
|
5
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
6
|
+
}
|
|
7
|
+
return new SecretValue(value);
|
|
8
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OptionValidationError } from '../OptionValidationError.js';
|
|
2
2
|
export const string = (option, value) => {
|
|
3
3
|
if (typeof value !== 'string' || !value) {
|
|
4
|
-
throw
|
|
4
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
5
5
|
}
|
|
6
6
|
return value;
|
|
7
7
|
};
|
|
@@ -9,7 +9,7 @@ export const timeout = (option, value) => {
|
|
|
9
9
|
const [, digits = '0', specifier = ''] = match;
|
|
10
10
|
let int = Number.parseInt(digits);
|
|
11
11
|
if (['s', 'sec'].includes(specifier)) {
|
|
12
|
-
int
|
|
12
|
+
int *= 1000;
|
|
13
13
|
}
|
|
14
14
|
if (['m', 'min'].includes(specifier)) {
|
|
15
15
|
int = int * 60 * 1000;
|
|
@@ -20,5 +20,5 @@ export const timeout = (option, value) => {
|
|
|
20
20
|
if (typeof value === 'number' && !Number.isNaN(value) && value >= 0 && value < Number.POSITIVE_INFINITY) {
|
|
21
21
|
return value;
|
|
22
22
|
}
|
|
23
|
-
throw
|
|
23
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
24
24
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { OptionValidationError } from '../OptionValidationError.js';
|
|
2
|
-
export const
|
|
2
|
+
export const isLikeAnUrl = (value) => !!/^https?:\/\/[^ "]+$/.test(value);
|
|
3
3
|
export const url = (option, value) => {
|
|
4
|
-
if (typeof value === 'string' &&
|
|
4
|
+
if (typeof value === 'string' && isLikeAnUrl(value)) {
|
|
5
5
|
return value;
|
|
6
6
|
}
|
|
7
|
-
throw
|
|
7
|
+
throw OptionValidationError.createInvalidValue(option);
|
|
8
8
|
};
|
package/dist/end.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { logger, Exit, Process, Host } from './platform/index.js';
|
|
2
|
+
import { Command } from './Command.js';
|
|
3
|
+
import { formatDuration } from './utils/shared/string.js';
|
|
4
|
+
export const end = async (configuration) => {
|
|
5
|
+
if (!configuration.end) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
logger.info({ source: 'job', message: 'executing end command...' });
|
|
10
|
+
logger.info({ source: 'progress', message: 'executing end command', pageId: undefined, data: { value: 0, max: 0 } });
|
|
11
|
+
const [command, arguments_, environment] = await Command.parse(configuration, configuration.end, {
|
|
12
|
+
exitCode: String(Exit.code)
|
|
13
|
+
});
|
|
14
|
+
const process = Process.spawn(command, arguments_, {
|
|
15
|
+
cwd: configuration.cwd,
|
|
16
|
+
env: { ...Host.env, ...environment },
|
|
17
|
+
forceRender: true
|
|
18
|
+
});
|
|
19
|
+
let code;
|
|
20
|
+
if (configuration.endTimeout) {
|
|
21
|
+
let didTimeout = false;
|
|
22
|
+
const timeout = new Promise((resolve) => setTimeout(() => {
|
|
23
|
+
didTimeout = true;
|
|
24
|
+
resolve();
|
|
25
|
+
}, configuration.endTimeout));
|
|
26
|
+
await Promise.race([process.closed, timeout]);
|
|
27
|
+
if (didTimeout) {
|
|
28
|
+
logger.error({ source: 'job', message: 'end command timed out, killing' });
|
|
29
|
+
await process.kill();
|
|
30
|
+
code = -1;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
code = process.code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
await process.closed;
|
|
38
|
+
code = process.code;
|
|
39
|
+
}
|
|
40
|
+
if (code !== undefined) {
|
|
41
|
+
const before = Exit.code;
|
|
42
|
+
if (before === 0 && code !== 0) {
|
|
43
|
+
logger.warn({
|
|
44
|
+
source: 'job',
|
|
45
|
+
message: 'status changed to error by end command'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (before !== 0 && code === 0) {
|
|
49
|
+
logger.warn({
|
|
50
|
+
source: 'job',
|
|
51
|
+
message: 'status changed to success by end command'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
Exit.code = code;
|
|
55
|
+
}
|
|
56
|
+
const duration = formatDuration(Date.now() - start);
|
|
57
|
+
logger.info({ source: 'job', message: `End command executed (${duration})` });
|
|
58
|
+
};
|
package/dist/if.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { punyexpr } from 'punyexpr';
|
|
2
|
+
import { Host } from './platform/index.js';
|
|
3
|
+
import { version } from './platform/version.js';
|
|
4
|
+
import { compareVersions } from './utils/shared/version.js';
|
|
5
|
+
import { options } from './configuration/options.js';
|
|
6
|
+
export const isImplemented = (option) => Object.values(options).some(({ name }) => name === option);
|
|
7
|
+
export const isIfEvaluatedAsTrue = async ({ if: condition }) => {
|
|
8
|
+
if (!condition) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
const [nodeMajorVersion] = Host.version.slice(1).split('.', 1);
|
|
12
|
+
const { name: runnerName, version: runnerVersion } = await version();
|
|
13
|
+
return !!punyexpr(condition)({
|
|
14
|
+
...Host.env,
|
|
15
|
+
UI5TR_NAME: runnerName,
|
|
16
|
+
UI5TR_VERSION: runnerVersion,
|
|
17
|
+
NODE_VERSION: Host.version,
|
|
18
|
+
NODE_MAJOR_VERSION: Number(nodeMajorVersion),
|
|
19
|
+
compareVersions,
|
|
20
|
+
implemented: isImplemented
|
|
21
|
+
});
|
|
22
|
+
};
|
package/dist/modes/Modes.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { logger, Path, Process, Host, __sourcesRoot, FileSystem } from '../../platform/index.js';
|
|
2
|
+
import { options } from '../../configuration/options.js';
|
|
3
|
+
import { toKebabCase, formatDuration } from '../../utils/shared/string.js';
|
|
4
|
+
import { getEffectiveTimeout, isGloballyTimedOut } from '../../utils/node/timeout.js';
|
|
5
|
+
const getBatchTimeout = (configuration, startTime) => getEffectiveTimeout(configuration.batchTimeout, configuration.globalTimeout, startTime);
|
|
6
|
+
const buildRunnerCommand = () => {
|
|
7
|
+
const extension = Path.extname(import.meta.url);
|
|
8
|
+
const cliPath = Path.join(__sourcesRoot, 'cli' + extension);
|
|
9
|
+
const js2tsUrl = Path.join(__sourcesRoot, 'platform/js2ts.mjs');
|
|
10
|
+
return extension === '.ts' ? ['--no-warnings', '--import', js2tsUrl, cliPath] : [cliPath];
|
|
11
|
+
};
|
|
12
|
+
const forwardedOptions = options.filter((o) => 'batchForwarded' in o && o.batchForwarded);
|
|
13
|
+
const buildForwardedParameters = (configuration) => {
|
|
14
|
+
const parameters = [];
|
|
15
|
+
for (const option of forwardedOptions) {
|
|
16
|
+
const name = option.name;
|
|
17
|
+
if (configuration.sources[name] !== 'cli') {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const flag = `--${toKebabCase(option.name)}`;
|
|
21
|
+
const value = configuration[name];
|
|
22
|
+
if (option.type === 'boolean') {
|
|
23
|
+
parameters.push(flag, String(value));
|
|
24
|
+
}
|
|
25
|
+
else if (option.type === 'json') {
|
|
26
|
+
parameters.push(flag, JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
else if ('multiple' in option && option.multiple) {
|
|
29
|
+
for (const item of value) {
|
|
30
|
+
parameters.push(flag, item);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
parameters.push(flag, `${value}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return parameters;
|
|
38
|
+
};
|
|
39
|
+
const MAX_MESSAGE_LENGTH = 120;
|
|
40
|
+
const buildFailureMessage = async (reportDirectory) => {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await FileSystem.readFile(Path.join(reportDirectory, 'report.json'), 'utf8');
|
|
43
|
+
const report = JSON.parse(raw);
|
|
44
|
+
const failed = report.results.tests.filter((t) => t.status === 'failed');
|
|
45
|
+
if (failed.length === 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const prefix = `${failed.length} test${failed.length > 1 ? 's' : ''} failed: `;
|
|
49
|
+
let names = failed.map((t) => t.name).join(', ');
|
|
50
|
+
if (prefix.length + names.length > MAX_MESSAGE_LENGTH) {
|
|
51
|
+
names = names.slice(0, MAX_MESSAGE_LENGTH - prefix.length - 1) + '…';
|
|
52
|
+
}
|
|
53
|
+
return prefix + names;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
let lastPageId = 0;
|
|
60
|
+
export const batchTask = async (configuration, batchItem, startTime) => {
|
|
61
|
+
const pageId = ++lastPageId;
|
|
62
|
+
const label = `${batchItem.label} (${batchItem.id})`;
|
|
63
|
+
logger.info({
|
|
64
|
+
source: 'progress',
|
|
65
|
+
message: label,
|
|
66
|
+
pageId,
|
|
67
|
+
data: { max: 0, value: 1, type: 'unknown', errors: 0 }
|
|
68
|
+
});
|
|
69
|
+
if (isGloballyTimedOut(configuration.globalTimeout, startTime)) {
|
|
70
|
+
logger.warn({ source: 'page', message: 'global timeout reached, skipping batch item', pageId, data: { label } });
|
|
71
|
+
batchItem.skipped = true;
|
|
72
|
+
batchItem.message = 'batch item not executed (process was interrupted)';
|
|
73
|
+
return batchItem;
|
|
74
|
+
}
|
|
75
|
+
batchItem.start = new Date();
|
|
76
|
+
const reportDirectory = Path.join(configuration.reportDir, batchItem.id);
|
|
77
|
+
batchItem.reportDir = reportDirectory;
|
|
78
|
+
const parameters = [
|
|
79
|
+
...buildRunnerCommand(),
|
|
80
|
+
'--no-banner',
|
|
81
|
+
...batchItem.args,
|
|
82
|
+
...buildForwardedParameters(configuration)
|
|
83
|
+
];
|
|
84
|
+
const childProcess = Process.spawn('node', parameters, {
|
|
85
|
+
env: { ...Host.env, UI5TR_BATCH_MODE: '1', UI5TR_BATCH_REPORT_DIR: reportDirectory },
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
onMessage: (data) => {
|
|
88
|
+
const message = data;
|
|
89
|
+
if (message?.type === 'progress') {
|
|
90
|
+
logger.info({
|
|
91
|
+
source: 'progress',
|
|
92
|
+
message: label,
|
|
93
|
+
pageId,
|
|
94
|
+
data: { value: message.count, max: message.total, errors: 0, type: 'unknown' }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (message?.type === 'skip') {
|
|
98
|
+
batchItem.skipped = true;
|
|
99
|
+
}
|
|
100
|
+
else if (message?.type === 'report-dir') {
|
|
101
|
+
batchItem.reportDir = message.reportDir;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
logger.debug({
|
|
106
|
+
source: 'page',
|
|
107
|
+
message: 'new batch task',
|
|
108
|
+
pageId,
|
|
109
|
+
data: { ...batchItem, processId: childProcess.pid }
|
|
110
|
+
});
|
|
111
|
+
const batchTimeoutMs = getBatchTimeout(configuration, startTime);
|
|
112
|
+
if (batchTimeoutMs > 0) {
|
|
113
|
+
const timeoutHandle = setTimeout(() => {
|
|
114
|
+
logger.warn({ source: 'page', message: 'batch item timed out', pageId, data: { label } });
|
|
115
|
+
batchItem.timedOut = true;
|
|
116
|
+
batchItem.message = 'batch item timed out, check the logs';
|
|
117
|
+
void childProcess.kill();
|
|
118
|
+
}, batchTimeoutMs);
|
|
119
|
+
await childProcess.closed;
|
|
120
|
+
clearTimeout(timeoutHandle);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await childProcess.closed;
|
|
124
|
+
}
|
|
125
|
+
batchItem.end = new Date();
|
|
126
|
+
batchItem.statusCode = childProcess.code;
|
|
127
|
+
const duration = formatDuration(batchItem.end.getTime() - batchItem.start.getTime());
|
|
128
|
+
if (batchItem.skipped) {
|
|
129
|
+
logger.warn({ source: 'page', pageId, message: `skipped ${label} (${duration})` });
|
|
130
|
+
}
|
|
131
|
+
else if (batchItem.statusCode === 0 && !batchItem.timedOut) {
|
|
132
|
+
logger.info({
|
|
133
|
+
source: 'page',
|
|
134
|
+
pageId,
|
|
135
|
+
message: `succeeded ${label} (${duration})`
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
logger.error({
|
|
140
|
+
source: 'page',
|
|
141
|
+
pageId,
|
|
142
|
+
message: `failed ${label} (${duration})`
|
|
143
|
+
});
|
|
144
|
+
batchItem.message = await buildFailureMessage(batchItem.reportDir ?? reportDirectory);
|
|
145
|
+
}
|
|
146
|
+
logger.debug({
|
|
147
|
+
source: 'page',
|
|
148
|
+
message: 'batch task closed',
|
|
149
|
+
pageId,
|
|
150
|
+
data: { ...batchItem, processId: childProcess.pid }
|
|
151
|
+
});
|
|
152
|
+
logger.info({
|
|
153
|
+
source: 'progress',
|
|
154
|
+
message: label,
|
|
155
|
+
pageId,
|
|
156
|
+
data: { max: 0, value: 0, type: 'unknown', errors: 0, remove: true }
|
|
157
|
+
});
|
|
158
|
+
return batchItem;
|
|
159
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { logger, Exit, logEnvironnement } from '../../platform/index.js';
|
|
2
|
+
import { parallelize } from '../../utils/shared/parallelize.js';
|
|
3
|
+
import { start } from '../../start.js';
|
|
4
|
+
import { resolve } from './resolve.js';
|
|
5
|
+
import { batchTask } from './batchTask.js';
|
|
6
|
+
import { buildBatchReport } from './report.js';
|
|
7
|
+
import { defaults } from '../../configuration/options.js';
|
|
8
|
+
import { redactSecrets } from '../../utils/node/anonymize.js';
|
|
9
|
+
import { toPlainObject } from '../../utils/shared/object.js';
|
|
10
|
+
import { Folder } from '../../utils/node/Folder.js';
|
|
11
|
+
import { saveReport } from '../../reports/saveReport.js';
|
|
12
|
+
import { formatDuration } from '../../utils/shared/string.js';
|
|
13
|
+
import { end } from '../../end.js';
|
|
14
|
+
import { printBatchOutput } from './output.js';
|
|
15
|
+
export const batch = async (configuration) => {
|
|
16
|
+
const batchStart = Date.now();
|
|
17
|
+
await Folder.create(configuration.reportDir);
|
|
18
|
+
await logger.start(configuration);
|
|
19
|
+
logger.debug({
|
|
20
|
+
source: 'job',
|
|
21
|
+
message: 'configuration',
|
|
22
|
+
data: { defaults, configuration: redactSecrets(toPlainObject(configuration)) }
|
|
23
|
+
});
|
|
24
|
+
if (!configuration.noBanner) {
|
|
25
|
+
await logEnvironnement();
|
|
26
|
+
}
|
|
27
|
+
logger.info({ source: 'progress', message: 'resolving batch items', pageId: undefined, data: { value: 0, max: 0 } });
|
|
28
|
+
const items = await resolve(configuration);
|
|
29
|
+
if (items.length === 0) {
|
|
30
|
+
logger.warn({ source: 'job', message: 'no batch items found' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
logger.info({ source: 'job', message: `Found ${items.length} batch items` });
|
|
34
|
+
const startProcess = await start(configuration);
|
|
35
|
+
let failed = 0;
|
|
36
|
+
logger.info({ source: 'progress', message: 'executing batch items', pageId: undefined, data: { value: 0, max: 0 } });
|
|
37
|
+
let completed = 0;
|
|
38
|
+
let lastLoggedCompleted;
|
|
39
|
+
const started = new Set();
|
|
40
|
+
try {
|
|
41
|
+
await parallelize(async function (item) {
|
|
42
|
+
const result = await batchTask(configuration, item, batchStart);
|
|
43
|
+
if (configuration.failFast && (result.statusCode !== 0 || result.timedOut)) {
|
|
44
|
+
for (const [queuedIndex, queuedItem] of items.entries()) {
|
|
45
|
+
if (started.has(queuedIndex)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
queuedItem.skipped = true;
|
|
49
|
+
queuedItem.message = 'batch item not executed (fail-fast)';
|
|
50
|
+
}
|
|
51
|
+
this.stop(new Error(`batch item failed: ${result.id}`));
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}, items, {
|
|
55
|
+
parallel: configuration.parallel,
|
|
56
|
+
on: (event) => {
|
|
57
|
+
if (event.type === 'started') {
|
|
58
|
+
started.add(event.index);
|
|
59
|
+
}
|
|
60
|
+
else if (event.type === 'completed') {
|
|
61
|
+
++completed;
|
|
62
|
+
if (event.input.statusCode !== 0 || event.input.timedOut) {
|
|
63
|
+
++failed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (event.type === 'failed') {
|
|
67
|
+
++failed;
|
|
68
|
+
++completed;
|
|
69
|
+
}
|
|
70
|
+
if (lastLoggedCompleted === completed) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
lastLoggedCompleted = completed;
|
|
74
|
+
logger.info({
|
|
75
|
+
source: 'progress',
|
|
76
|
+
message: 'executing batch items',
|
|
77
|
+
pageId: undefined,
|
|
78
|
+
data: { value: completed, max: items.length }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await startProcess?.kill();
|
|
85
|
+
}
|
|
86
|
+
await saveReport(configuration, await buildBatchReport(items, configuration));
|
|
87
|
+
if (configuration.batchOutput) {
|
|
88
|
+
await printBatchOutput(items);
|
|
89
|
+
}
|
|
90
|
+
logger.info({
|
|
91
|
+
source: 'job',
|
|
92
|
+
message: `Batch items completed: passed=${completed - failed} failed=${failed} count=${completed} (${formatDuration(Date.now() - batchStart)})`
|
|
93
|
+
});
|
|
94
|
+
if (failed > 0) {
|
|
95
|
+
Exit.code = -1;
|
|
96
|
+
}
|
|
97
|
+
await end(configuration);
|
|
98
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FileSystem, Path, logger } from '../../platform/index.js';
|
|
2
|
+
export const printBatchOutput = async (items) => {
|
|
3
|
+
const sortedItems = [...items].toSorted((left, right) => left.id.localeCompare(right.id));
|
|
4
|
+
for (const item of sortedItems) {
|
|
5
|
+
try {
|
|
6
|
+
if (!item.reportDir) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const output = await FileSystem.readFile(Path.join(item.reportDir, 'output.txt'), 'utf8');
|
|
10
|
+
logger.info({
|
|
11
|
+
source: 'job',
|
|
12
|
+
message: `Batch output: ${item.label} (${item.id})\n${output}`
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
logger.warn({
|
|
17
|
+
source: 'job',
|
|
18
|
+
message: `No batch output: ${item.label} (${item.id})`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|